Spaces:
Sleeping
A newer version of the Gradio SDK is available: 6.22.0
title: Invoice & Receipt Extractor
emoji: π§Ύ
colorFrom: blue
colorTo: indigo
sdk: gradio
sdk_version: 5.9.1
app_file: app.py
pinned: false
Invoice & Receipt Extractor (Gemma 3 4B, ZeroGPU)
Upload an invoice/receipt image, get structured JSON back.
Why 4B and not 1B?
unsloth/gemma-3-1b-it-qat is text-only β Gemma 3's SigLIP vision encoder was
only added to the 4B, 12B, and 27B sizes. This Space uses
unsloth/gemma-3-4b-it-qat instead, which is still small/fast enough to run
comfortably on a ZeroGPU slot.
Deploying
- Create a new Space on huggingface.co: SDK = Gradio.
- You need a HF PRO subscription (or an Enterprise Hub org) for the ZeroGPU hardware option to appear β select it in the Space's Settings.
- Push these three files (
app.py,requirements.txt, thisREADME.md) to the Space repo (viagit push, the web UI, orhuggingface_hub'supload_folder). - Accept the Gemma license on the google/gemma-3-4b-it model page with the same account/org the Space runs as (unsloth's repo mirrors the same gated license).
- First build will take a few minutes to download the model weights.
Calling it via API
Gradio auto-generates an API for any Space with an api_name set (extract
here). Two easy ways to call it:
Python, via gradio_client:
from gradio_client import Client, handle_file
client = Client("your-username/invoice-extractor-space")
result = client.predict(
image=handle_file("receipt.jpg"),
high_accuracy=False, # set True only for long/narrow thermal-paper receipts
api_name="/extract",
)
print(result) # JSON string β see "Output schema & tax/item edge cases" below
Image size handling
app.py downscales any incoming image to a 1568px long edge before it reaches
the model β Gemma's vision encoder resizes everything to a fixed 896x896
square internally anyway (256 tokens/pass), so nothing above that buys extra
accuracy. This guard protects against 12MP phone photos costing you upload
time and Space RAM for zero benefit.
If you're calling from a mobile client or somewhere bandwidth is tight, it's still worth compressing/resizing client-side before upload (e.g. JPEG quality ~85, long edge ~1600px) β the server-side guard only kicks in after the full file has already been transferred.
The high_accuracy checkbox turns on Gemma's pan_and_scan, which crops the
image into extra tiles instead of squashing it into one square β useful for
long thermal-paper receipts where line items near the top/bottom would
otherwise get compressed away. It costs roughly +256 tokens per extra tile,
so leave it off unless you're seeing missed line items.
Raw HTTP, if you'd rather not add the gradio_client dependency β click
"Use via API" at the bottom of your Space's page once it's live; it gives you
the exact POST endpoint and payload shape for your Space (Gradio's queueing
API requires a submit + poll call pair, which gradio_client handles for
you β that's the easier route for most use cases).
If the Space is private, pass hf_token="hf_..." to Client(...).
Output schema & tax/item edge cases
app.py's prompt asks for a schema built to handle the messy realities of
real receipts, not just the happy path:
- Multiple tax lines (GST+PST, state+county, a VAT rate table) go in a
taxes: [{label, rate_percent, amount}]array;taxstays as a convenience sum. A receipt with one combined tax figure just usestaxand leavestaxesempty. tax_inclusiveflags whether listed prices already include tax (common outside the US) β matters if you're recomputing anything downstream.- Discounts and refunds are captured as a positive
discountamount (meant to be subtracted) at the document level, and as a negativeamounton the specific line item if it's an itemized discount/return. service_chargevstipare kept separate β a mandatory service fee isn't the same as a voluntary gratuity, and conflating them breaks downstream accounting.- Non-receipt images (or unreadable ones) get
document_type: "unknown", everything elsenull, and a reason innotesβ instead of the model forcing garbage into the schema. validationis appended after parsing, not part of what the model generates: it recomputessubtotalfromline_items, sumstaxes, addsservice_charge/tip, subtractsdiscount, and compares against the statedtotal(Β±$0.02 tolerance). Usematches_stated_total: falseas a signal to flag a document for manual review β it usually means either the model misread a digit or the receipt itself doesn't add up.- If generation hits the token limit before finishing,
validation.warningsays so β treat line items near the end of the list as unverified rather than silently trusting a cut-off list.
This is inherently a known-limitations tool, not a guarantee: a 4B model
can still misread a smudged digit that happens to produce internally
consistent (but wrong) totals β validation only catches inconsistency,
not every possible misread. It also currently assumes one document per
image (a photo containing two separate receipts side by side isn't
handled) and doesn't support multi-page PDFs in a single call.
Notes / tuning
@spaces.GPU(duration=90)caps each call at 90s of GPU time β bumped up from 60s sinceMAX_NEW_TOKENSwas raised to 1536 to fit long itemized receipts. Raise further if you still see timeouts on dense invoices.MAX_NEW_TOKENS = 1536inapp.pyβ the ceiling on how long a generated JSON response can be. Grocery-length receipts (40+ line items) can get close to this; the truncation check (validation.warning) tells you when a response likely got cut off so you know to raise it further.do_sample=False(greedy decoding) is used for repeatability; extraction tasks don't benefit from sampling.- JSON parsing has two fallback layers before giving up: stripping stray
prose/fences around the
{...}, then repairing trailing commas β the two most common small-model output quirks. - The prompt in
app.pydefines the JSON schema. Edit it directly if you need extra fields (e.g.po_number,tax_id). - For stricter production reliability, consider validating the model's JSON
output against a
pydanticschema and retrying once on failure/mismatch before falling back to returning the raw output for manual handling.