Course EN
← back to chapter

Parser de recibos

Vision se encuentra con salida estructurada. Pega la URL de una foto de un recibo

Showcase — Parser de recibos

Vision se encuentra con salida estructurada. Pega la URL de una foto de un recibo o factura y recibe de vuelta JSON limpio — comercio, fecha, partidas, total — el tipo de registro que insertas directo en una base de datos. El modelo lee los pixeles; el prompt fija la forma (Gemini además usa JSON response mode).

Córrelo

bash bootstrap-secrets.sh              # reads ../../../../.env, writes secrets/
docker compose up --build              # default: PROVIDER=openai

Abre http://localhost:3000. Gemini: PROVIDER=gemini docker compose up --build.

Qué hay aquí

  • backend/ai_openai.py / backend/ai_gemini.py — un prompt de extracción de JSON sobre la imagen del recibo.
  • frontend/app/page.tsx — caja de URL + JSON extraído.

Detenlo

docker compose down

Ejecútalo en tu máquina

Descarga el proyecto como ZIP y córrelo con Docker. Levanta un backend FastAPI y un frontend Next.js en localhost:3000.

Descargar receipt-parser.zip

unzip receipt-parser.zip
cd receipt-parser
bash bootstrap-secrets.sh   # one-time: pulls API keys into ./secrets
docker compose up --build   # default provider: openai
# or:  PROVIDER=gemini docker compose up --build

Escribe algo, elige un proveedor y ejecuta el mismo código de Código contra la API real. Requiere iniciar sesión.


  

Los mismos módulos que ejecuta el botón Run. El proyecto completo (frontend, Dockerfile, compose) está en el ZIP, pestaña README.

backend/ai_openai.py

"""Showcase 3 (OpenAI): reading a receipt into structured JSON.

Vision meets structured output (week 4). Paste a photo URL of a receipt or
invoice and get back clean JSON — merchant, date, line items, total — the kind
of thing you'd insert straight into a database. The model reads the pixels; the
prompt pins the shape.
"""
from openai import OpenAI

_client = OpenAI()

_MODEL = "gpt-5.4-nano"

_PROMPT = (
    "Read this receipt or invoice image and extract it as JSON with exactly these "
    "keys: merchant (string), date (string or null), items (list of objects with "
    "name and price), total (number or null). Output ONLY the JSON, no prose. Use "
    "null for anything you can't read."
)


def run(image_url: str) -> str:
    url = image_url.strip()
    if not url.startswith("http"):
        return "Paste a public image URL of a receipt or invoice (http/https)."
    response = _client.responses.create(
        model=_MODEL,
        input=[{
            "role": "user",
            "content": [
                {"type": "input_text", "text": _PROMPT},
                {"type": "input_image", "image_url": url},
            ],
        }],
    )
    return response.output_text

backend/ai_gemini.py

"""Showcase 3 (Gemini): reading a receipt into structured JSON.

Same extraction schema; Gemini takes the image as bytes and is asked for JSON.
"""
import os
import urllib.request

from google import genai
from google.genai import types

_client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])

_MODEL = "gemini-3.1-flash-lite"

_PROMPT = (
    "Read this receipt or invoice image and extract it as JSON with exactly these "
    "keys: merchant (string), date (string or null), items (list of objects with "
    "name and price), total (number or null). Output ONLY the JSON, no prose. Use "
    "null for anything you can't read."
)


def _mime(url: str) -> str:
    u = url.lower()
    if u.endswith(".png"):
        return "image/png"
    if u.endswith(".webp"):
        return "image/webp"
    return "image/jpeg"


def run(image_url: str) -> str:
    url = image_url.strip()
    if not url.startswith("http"):
        return "Paste a public image URL of a receipt or invoice (http/https)."
    image_bytes = urllib.request.urlopen(urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})).read()
    response = _client.models.generate_content(
        model=_MODEL,
        contents=[types.Part.from_bytes(data=image_bytes, mime_type=_mime(url)), _PROMPT],
        config=types.GenerateContentConfig(response_mime_type="application/json"),
    )
    return response.text or ""

Archivos del proyecto

  • .gitignore
  • README.es.md
  • README.md
  • backend/Dockerfile
  • backend/ai_gemini.py
  • backend/ai_openai.py
  • backend/main.py
  • backend/requirements.txt
  • bootstrap-secrets.sh
  • docker-compose.yml
  • frontend/Dockerfile
  • frontend/app/layout.tsx
  • frontend/app/page.tsx
  • frontend/next.config.ts
  • frontend/package.json
  • frontend/tsconfig.json