| import json |
| import os |
| import torch |
| from PIL import Image |
| from tqdm import tqdm |
|
|
|
|
|
|
| DEFAULT_SAVE_DIR = "./data/processed" |
|
|
|
|
| def clean_price(price_str: str) -> str: |
|
|
| return str(price_str).strip().rstrip('.') |
|
|
|
|
| def parse_ground_truth(gt_string: str) -> dict: |
|
|
| gt = json.loads(gt_string) |
| parsed = gt.get("gt_parse", {}) |
|
|
| items = [] |
| menu = parsed.get("menu", []) |
|
|
| if not isinstance(menu, list): |
| menu = [] |
|
|
| for item in menu: |
| if not isinstance(item, dict): |
| continue |
| items.append({ |
| "name" : str(item.get("nm", "")).strip(), |
| "count": str(item.get("cnt", "")).strip(), |
| "price": clean_price(item.get("price", "0")), |
| }) |
|
|
| sub = parsed.get("sub_total", {}) |
| if not isinstance(sub, dict): |
| sub = {} |
|
|
| total_obj = parsed.get("total", {}) |
| if not isinstance(total_obj, dict): |
| total_obj = {} |
|
|
| return { |
| "vendor" : "unknown", |
| "items" : items, |
| "subtotal": clean_price(sub.get("subtotal_price", "0")), |
| "tax" : clean_price(sub.get("tax_price", "0")), |
| "service" : clean_price(sub.get("service_price", "0")), |
| "total" : clean_price(total_obj.get("total_price", "0")), |
| } |
|
|
|
|
| def format_instruction(sample: dict) -> dict: |
| img = sample["image"].convert("RGB") |
| parsed = parse_ground_truth(sample["ground_truth"]) |
|
|
| prompt = ( |
| "Extract the receipt information and return ONLY valid JSON.\n" |
| 'Schema: {"vendor":"","items":[{"name":"","count":"","price":""}],"subtotal":"","tax":"","service":"","total":""}' |
| ) |
|
|
| target = json.dumps(parsed, ensure_ascii=False) |
|
|
| return { |
| "image" : img, |
| "prompt": prompt, |
| "target": target, |
| } |
|
|
|
|
|
|
| def save_splits(splits: dict, save_dir: str = DEFAULT_SAVE_DIR) -> None: |
| os.makedirs(save_dir, exist_ok=True) |
|
|
| for split, data in splits.items(): |
| path = os.path.join(save_dir, f"{split}.pt") |
| torch.save(data, path) |
| print(f" Saved {len(data):>5} samples → {path}") |
|
|
|
|
| def load_splits(save_dir: str = DEFAULT_SAVE_DIR) -> dict: |
| splits = {} |
|
|
| for split in ["train", "validation", "test"]: |
| path = os.path.join(save_dir, f"{split}.pt") |
| if not os.path.exists(path): |
| raise FileNotFoundError( |
| f"Expected '{path}' — run the pipeline first to generate it." |
| ) |
| splits[split] = torch.load(path) |
| print(f" Loaded {len(splits[split]):>5} samples ← {path}") |
|
|
| return splits |
|
|
|
|
|
|
| def preprocess_dataset(dataset: dict, save_dir: str = None) -> dict: |
| splits = {} |
|
|
| for split in ["train", "validation", "test"]: |
| processed = [] |
| skipped = 0 |
|
|
| for sample in tqdm(dataset[split], desc=f" {split:<12}", unit="sample"): |
| try: |
| formatted = format_instruction(sample) |
| processed.append(formatted) |
| except Exception as e: |
| skipped += 1 |
| tqdm.write(f" [skip] {split} sample: {e}") |
| continue |
|
|
| if skipped: |
| print(f" {split}: skipped {skipped} samples") |
|
|
| splits[split] = processed |
|
|
| if save_dir is not None: |
| print("\nSaving processed splits...") |
| save_splits(splits, save_dir) |
|
|
| return splits |