aditya0103's picture
v4: fine-tuning pipeline complete + bug fixes
d95323c
Raw
History Blame Contribute Delete
4.19 kB
"""SROIE dataset loader + normalizer.
SROIE (ICDAR 2019 Robust Reading Challenge on Scanned Receipts Information
Extraction) has ~1000 scanned Singapore-region receipts with ground truth for
four fields: company, address, date, total.
Datasets change over time β€” we default to `darentang/sroie` on Hugging Face,
which mirrors the ICDAR test set. If that ID has moved by the time you run
this, pass `--dataset-id` to override.
Reference: https://rrc.cvc.uab.es/?ch=13
"""
from __future__ import annotations
from collections.abc import Iterator
from typing import Any
from src.data_prep.parsers import clean_text, parse_date, parse_money
from src.schemas import Address, Receipt
from src.utils.logging import logger
# Default Hugging Face dataset ID + fallbacks. Updated 2026-07 β€” override
# via the CLI if it has moved again.
DEFAULT_DATASET_IDS = (
"darentang/sroie",
"mychen76/invoices-and-receipts_ocr_v1",
)
def load_sroie_split(split: str = "test", dataset_id: str | None = None):
"""Load a SROIE split from Hugging Face. Requires `datasets` package.
Returns a HuggingFace Dataset object β€” iterable of dicts.
"""
from datasets import load_dataset
ids_to_try = (dataset_id,) if dataset_id else DEFAULT_DATASET_IDS
last_err: Exception | None = None
for ds_id in ids_to_try:
try:
logger.info(f"Loading SROIE split={split!r} from Hugging Face id={ds_id!r}")
return load_dataset(ds_id, split=split)
except Exception as e:
logger.warning(f"Failed to load {ds_id}: {e}")
last_err = e
raise RuntimeError(
f"Could not load any SROIE dataset. Last error: {last_err}. "
f"Try passing --dataset-id explicitly. Check huggingface.co for a working ID."
) from last_err
def normalize_sroie_record(record: dict[str, Any]) -> Receipt | None:
"""Convert a raw SROIE record into our Receipt schema.
Handles both common shapes:
A) flat: {"company": ..., "date": ..., "address": ..., "total": ...}
B) nested: {"parsed_data": {"company": ..., ...}}
Returns None if we can't extract the minimum required fields (merchant + total).
"""
# Unwrap common nesting patterns. HF datasets sometimes store the annotation
# as a JSON *string* (parquet doesn\'t love nested dicts), so try to parse
# string values before treating them as opaque.
import json as _json
_raw = record.get("parsed_data") or record.get("ground_truth") or record
if isinstance(_raw, str):
try:
_raw = _json.loads(_raw)
except Exception:
return None # unparseable β€” skip this record rather than crash
if not isinstance(_raw, dict):
return None
src = _raw
company = clean_text(src.get("company") or src.get("merchant"))
address = clean_text(src.get("address"))
date_str = src.get("date")
total_str = src.get("total") or src.get("amount")
if not company:
logger.debug(f"SROIE record missing company field; skipping. record keys={list(src.keys())}")
return None
total = parse_money(total_str)
if total is None:
logger.debug(f"SROIE record {company!r} missing parseable total ({total_str!r}); skipping.")
return None
try:
return Receipt(
merchant=company,
merchant_address=Address(line1=address) if address else None,
transaction_date=parse_date(date_str),
total=total,
# SROIE is largely Singapore/Malaysia β€” SGD is the safer default than USD.
currency="SGD",
)
except Exception as e:
logger.warning(f"Failed to build Receipt from SROIE record {company!r}: {e}")
return None
def iter_normalized(dataset) -> Iterator[tuple[str, Receipt]]:
"""Iterate a HF Dataset, yielding (record_id, Receipt) pairs.
Skips records that fail normalization.
"""
for idx, rec in enumerate(dataset):
record_id = str(rec.get("id") or rec.get("image_id") or f"sroie_{idx:05d}")
normalized = normalize_sroie_record(rec)
if normalized is not None:
yield record_id, normalized