trishpurkait commited on
Commit
af1c030
·
1 Parent(s): 0ff2e7d

Add BillStructAI demo

Browse files
Files changed (2) hide show
  1. app.py +153 -0
  2. requirements.txt +6 -0
app.py ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ BillStructAI demo — paste noisy OCR text from an invoice/receipt and get
3
+ back structured JSON, using a Qwen2.5-1.5B-Instruct base model fine-tuned
4
+ with QLoRA for this task.
5
+
6
+ Deploy: push this file + requirements.txt to a Hugging Face Space
7
+ (Gradio SDK). Set ADAPTER_REPO below to your uploaded LoRA adapter repo.
8
+ """
9
+
10
+ import json
11
+ import re
12
+
13
+ import gradio as gr
14
+ import torch
15
+ from peft import PeftModel
16
+ from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
17
+
18
+ BASE_MODEL_NAME = "Qwen/Qwen2.5-1.5B-Instruct"
19
+ ADAPTER_REPO = "trishpurkait/billstructai-qwen-lora" # <-- update after uploading adapter
20
+
21
+ SYSTEM_PROMPT = """
22
+ You are a precise invoice information extraction assistant.
23
+ Your task is to extract structured invoice data from noisy OCR text.
24
+ Return only valid JSON.
25
+ Do not explain.
26
+ Do not add markdown.
27
+ Use null for missing fields.
28
+ Do not hallucinate values that are not present in the OCR text.
29
+ """.strip()
30
+
31
+ EXAMPLE_OCR = """RECEIPT
32
+ ORLANDO INTERNATIONAL AIRPORT
33
+ 2001207734
34
+ EXIT: SEQUENCE /LINE/CASHIER/ DATE / TIME / FEE /COST LD.
35
+ 3493 11 108 18DE 0008 012.00 FLFRC927 R01 67212 2. 14DE 0817"""
36
+
37
+ _model = None
38
+ _tokenizer = None
39
+
40
+
41
+ def load_model():
42
+ """Lazily load base model + LoRA adapter (4-bit) on first request."""
43
+ global _model, _tokenizer
44
+ if _model is not None:
45
+ return _model, _tokenizer
46
+
47
+ bnb_config = BitsAndBytesConfig(
48
+ load_in_4bit=True,
49
+ bnb_4bit_quant_type="nf4",
50
+ bnb_4bit_compute_dtype=torch.float16,
51
+ bnb_4bit_use_double_quant=True,
52
+ )
53
+
54
+ tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL_NAME, trust_remote_code=True)
55
+ if tokenizer.pad_token is None:
56
+ tokenizer.pad_token = tokenizer.eos_token
57
+
58
+ base_model = AutoModelForCausalLM.from_pretrained(
59
+ BASE_MODEL_NAME,
60
+ quantization_config=bnb_config,
61
+ device_map="auto",
62
+ trust_remote_code=True,
63
+ )
64
+ model = PeftModel.from_pretrained(base_model, ADAPTER_REPO)
65
+ model.eval()
66
+
67
+ _model, _tokenizer = model, tokenizer
68
+ return model, tokenizer
69
+
70
+
71
+ def extract_json_from_text(text: str):
72
+ """Best-effort JSON extraction from model output (handles markdown fences)."""
73
+ if not text:
74
+ return None
75
+ text = text.strip()
76
+ try:
77
+ return json.loads(text)
78
+ except Exception:
79
+ pass
80
+ text = text.replace("```json", "").replace("```", "").strip()
81
+ try:
82
+ return json.loads(text)
83
+ except Exception:
84
+ pass
85
+ match = re.search(r"\{.*\}", text, re.DOTALL)
86
+ if match:
87
+ try:
88
+ return json.loads(match.group(0))
89
+ except Exception:
90
+ return None
91
+ return None
92
+
93
+
94
+ def run_extraction(ocr_text: str) -> str:
95
+ if not ocr_text or not ocr_text.strip():
96
+ return "Paste some OCR text first."
97
+
98
+ model, tokenizer = load_model()
99
+
100
+ messages = [
101
+ {"role": "system", "content": SYSTEM_PROMPT},
102
+ {
103
+ "role": "user",
104
+ "content": f"Extract invoice information from the OCR text below and "
105
+ f"return only valid JSON.\n\nOCR_TEXT:\n{ocr_text.strip()}",
106
+ },
107
+ ]
108
+ prompt = tokenizer.apply_chat_template(
109
+ messages, tokenize=False, add_generation_prompt=True
110
+ )
111
+ inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
112
+
113
+ with torch.no_grad():
114
+ output_ids = model.generate(
115
+ **inputs,
116
+ max_new_tokens=768,
117
+ do_sample=False,
118
+ pad_token_id=tokenizer.pad_token_id,
119
+ )
120
+
121
+ generated = tokenizer.decode(
122
+ output_ids[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True
123
+ )
124
+
125
+ parsed = extract_json_from_text(generated)
126
+ if parsed is not None:
127
+ return json.dumps(parsed, indent=2, ensure_ascii=False)
128
+ return generated # fall back to raw output if parsing fails
129
+
130
+
131
+ with gr.Blocks(title="BillStructAI — Invoice OCR to JSON") as demo:
132
+ gr.Markdown(
133
+ "# BillStructAI\n"
134
+ "Paste noisy OCR text from an invoice or receipt below. "
135
+ "A Qwen2.5-1.5B-Instruct model fine-tuned with QLoRA extracts it into "
136
+ "structured JSON. [See the full writeup and evaluation results](https://github.com/your-username/billstructai)."
137
+ )
138
+ with gr.Row():
139
+ with gr.Column():
140
+ ocr_input = gr.Textbox(
141
+ label="OCR text",
142
+ lines=12,
143
+ value=EXAMPLE_OCR,
144
+ placeholder="Paste raw OCR text here...",
145
+ )
146
+ submit_btn = gr.Button("Extract", variant="primary")
147
+ with gr.Column():
148
+ json_output = gr.Code(label="Extracted JSON", language="json", lines=20)
149
+
150
+ submit_btn.click(fn=run_extraction, inputs=ocr_input, outputs=json_output)
151
+
152
+ if __name__ == "__main__":
153
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ gradio>=4.0.0
2
+ torch
3
+ transformers
4
+ accelerate
5
+ bitsandbytes
6
+ peft