devZenaight commited on
Commit
63f1f4e
Β·
1 Parent(s): 0739539

two endpoints

Browse files
Files changed (2) hide show
  1. .gitignore +1 -0
  2. app.py +130 -39
.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ .env
app.py CHANGED
@@ -7,10 +7,22 @@ from PIL import Image
7
  import uvicorn
8
  import os
9
  import json
 
10
  import pytesseract # βœ… OCR
11
 
 
 
 
 
 
 
 
12
  from langchain_openai import ChatOpenAI
13
  from langchain.prompts import ChatPromptTemplate
 
 
 
 
14
 
15
  # Init FastAPI
16
  app = FastAPI()
@@ -22,13 +34,25 @@ os.makedirs("receiptimages", exist_ok=True)
22
  model = YOLO("receipt-scanner/models/best.pt")
23
 
24
  # Init LangChain with OpenAI
25
- llm = ChatOpenAI(
26
- model="gpt-5-mini", # βœ… text-only model, fine since we feed text now
 
 
 
 
 
 
 
 
 
 
 
 
27
  temperature=0,
28
  )
29
 
30
  # Define the prompt for categorization
31
- prompt = ChatPromptTemplate.from_template("""
32
  You are an expert at analyzing receipt text extracted with OCR.
33
  The text may contain noise or extra characters, but focus only on useful parts.
34
 
@@ -77,7 +101,7 @@ def run_langchain_with_text(receipt_text: str, image_path: str):
77
  """Send OCR text to GPT for structured JSON, with logs."""
78
  print(f"πŸ“œ OCR extracted text from {image_path}:\n{receipt_text}")
79
 
80
- chain = prompt | llm
81
  result = chain.invoke({"ocr_text": receipt_text}) # βœ… match prompt var
82
 
83
  # πŸ”₯ Debug logging
@@ -86,6 +110,38 @@ def run_langchain_with_text(receipt_text: str, image_path: str):
86
 
87
  return result.content
88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  @app.post("/process-receipt")
90
  async def process_receipt(file: UploadFile = File(...)):
91
  """
@@ -95,47 +151,82 @@ async def process_receipt(file: UploadFile = File(...)):
95
  image = Image.open(io.BytesIO(contents)).convert("RGB")
96
  img_array = np.array(image)
97
 
98
- # Run YOLO detection
99
- results = model(img_array)
100
-
101
  outputs = []
102
- for r in results:
103
- if len(r.boxes.xyxy) == 0:
104
- print("⚠️ No receipts detected, falling back to full image")
105
- cropped = img_array
106
- else:
107
- # βœ… Pick the largest bounding box
108
- boxes = r.boxes.xyxy
109
- i = max(range(len(boxes)), key=lambda j: (boxes[j][2]-boxes[j][0]) * (boxes[j][3]-boxes[j][1]))
110
- x1, y1, x2, y2 = map(int, boxes[i].tolist())
111
- cropped = img_array[y1:y2, x1:x2]
112
-
113
- # Save cropped or fallback image
114
- save_path = f"receiptimages/receipt_main.jpg"
115
- cv2.imwrite(save_path, cv2.cvtColor(cropped, cv2.COLOR_RGB2BGR))
116
- print(f"βœ… Saved {save_path}")
117
-
118
- # OCR step
119
- ocr_text = pytesseract.image_to_string(Image.fromarray(cropped))
120
-
121
- # βœ… Fallback to full image if OCR is empty
122
- if not ocr_text.strip():
123
- print("⚠️ Empty OCR text, retrying with full image")
124
- ocr_text = pytesseract.image_to_string(image)
125
-
126
- # Send OCR text to GPT
127
- llm_result = run_langchain_with_text(ocr_text, save_path)
128
-
129
- outputs.append({
130
- "cropped_file": save_path,
131
- "ocr_text": ocr_text.strip(),
132
- "extracted": llm_result
133
- })
134
 
135
  # πŸ”₯ Log the full outputs for debugging
136
  print("πŸ“¦ Final API response:", json.dumps(outputs, indent=2))
137
 
138
  return {"status": "success", "receipts": outputs}
139
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  if __name__ == "__main__":
141
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
7
  import uvicorn
8
  import os
9
  import json
10
+ import base64
11
  import pytesseract # βœ… OCR
12
 
13
+ try:
14
+ from dotenv import load_dotenv # type: ignore
15
+ load_dotenv()
16
+ except Exception:
17
+ # optional in production containers
18
+ pass
19
+
20
  from langchain_openai import ChatOpenAI
21
  from langchain.prompts import ChatPromptTemplate
22
+ try:
23
+ from langchain_core.messages import HumanMessage
24
+ except Exception: # Fallback for older langchain
25
+ from langchain.schema import HumanMessage # type: ignore
26
 
27
  # Init FastAPI
28
  app = FastAPI()
 
34
  model = YOLO("receipt-scanner/models/best.pt")
35
 
36
  # Init LangChain with OpenAI
37
+ openai_api_key = os.getenv("OPENAI_API_KEY")
38
+ if not openai_api_key:
39
+ print("⚠️ OPENAI_API_KEY is not set; LangChain calls will fail until configured.")
40
+
41
+ TEXT_LLM_MODEL = os.getenv("TEXT_LLM_MODEL", "gpt-5-mini")
42
+ VISION_LLM_MODEL = os.getenv("VISION_LLM_MODEL", "gpt-4o")
43
+
44
+ llm_text = ChatOpenAI(
45
+ model=TEXT_LLM_MODEL,
46
+ temperature=0,
47
+ )
48
+
49
+ llm_vision = ChatOpenAI(
50
+ model=VISION_LLM_MODEL,
51
  temperature=0,
52
  )
53
 
54
  # Define the prompt for categorization
55
+ text_prompt = ChatPromptTemplate.from_template("""
56
  You are an expert at analyzing receipt text extracted with OCR.
57
  The text may contain noise or extra characters, but focus only on useful parts.
58
 
 
101
  """Send OCR text to GPT for structured JSON, with logs."""
102
  print(f"πŸ“œ OCR extracted text from {image_path}:\n{receipt_text}")
103
 
104
+ chain = text_prompt | llm_text
105
  result = chain.invoke({"ocr_text": receipt_text}) # βœ… match prompt var
106
 
107
  # πŸ”₯ Debug logging
 
110
 
111
  return result.content
112
 
113
+
114
+ def detect_and_crop_largest_receipt(img_array: np.ndarray):
115
+ """Detect the largest bounding box via YOLO and return crop and bbox.
116
+
117
+ Returns (cropped_array, bbox_dict)
118
+ bbox_dict = {"x1": int, "y1": int, "x2": int, "y2": int}
119
+ """
120
+ results = model(img_array)
121
+ # Default fallback to full image
122
+ cropped = img_array
123
+ bbox = None
124
+ for r in results:
125
+ if len(r.boxes.xyxy) == 0:
126
+ continue
127
+ boxes = r.boxes.xyxy
128
+ i = max(range(len(boxes)), key=lambda j: (boxes[j][2]-boxes[j][0]) * (boxes[j][3]-boxes[j][1]))
129
+ x1, y1, x2, y2 = map(int, boxes[i].tolist())
130
+ cropped = img_array[y1:y2, x1:x2]
131
+ bbox = {"x1": x1, "y1": y1, "x2": x2, "y2": y2}
132
+ break
133
+ return cropped, bbox
134
+
135
+
136
+ def encode_image_to_data_url(img_array: np.ndarray, format: str = "JPEG") -> str:
137
+ """Encode an RGB image array to a data URL suitable for GPT-4o image input."""
138
+ pil_img = Image.fromarray(img_array)
139
+ buffer = io.BytesIO()
140
+ pil_img.save(buffer, format=format)
141
+ b64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
142
+ mime = "image/jpeg" if format.upper() == "JPEG" else "image/png"
143
+ return f"data:{mime};base64,{b64}"
144
+
145
  @app.post("/process-receipt")
146
  async def process_receipt(file: UploadFile = File(...)):
147
  """
 
151
  image = Image.open(io.BytesIO(contents)).convert("RGB")
152
  img_array = np.array(image)
153
 
 
 
 
154
  outputs = []
155
+ # Detect and crop once per image (YOLO returns one result for input image)
156
+ cropped, bbox = detect_and_crop_largest_receipt(img_array)
157
+
158
+ # Save cropped or fallback image
159
+ save_path = f"receiptimages/receipt_main.jpg"
160
+ cv2.imwrite(save_path, cv2.cvtColor(cropped, cv2.COLOR_RGB2BGR))
161
+ print(f"βœ… Saved {save_path}")
162
+
163
+ # OCR step
164
+ ocr_text = pytesseract.image_to_string(Image.fromarray(cropped))
165
+
166
+ # βœ… Fallback to full image if OCR is empty
167
+ if not ocr_text.strip():
168
+ print("⚠️ Empty OCR text, retrying with full image")
169
+ ocr_text = pytesseract.image_to_string(image)
170
+
171
+ # Send OCR text to GPT
172
+ llm_result = run_langchain_with_text(ocr_text, save_path)
173
+
174
+ outputs.append({
175
+ "cropped_file": save_path,
176
+ "bbox": bbox,
177
+ "ocr_text": ocr_text.strip(),
178
+ "extracted": llm_result
179
+ })
 
 
 
 
 
 
 
180
 
181
  # πŸ”₯ Log the full outputs for debugging
182
  print("πŸ“¦ Final API response:", json.dumps(outputs, indent=2))
183
 
184
  return {"status": "success", "receipts": outputs}
185
 
186
+
187
+ @app.post("/process-receipt-vision")
188
+ async def process_receipt_vision(file: UploadFile = File(...)):
189
+ """
190
+ Upload receipt image -> detect largest -> crop -> send image to GPT-4o -> JSON result
191
+ No OCR is performed; the model reads directly from the image.
192
+ """
193
+ contents = await file.read()
194
+ image = Image.open(io.BytesIO(contents)).convert("RGB")
195
+ img_array = np.array(image)
196
+
197
+ cropped, bbox = detect_and_crop_largest_receipt(img_array)
198
+
199
+ save_path = f"receiptimages/receipt_vision.jpg"
200
+ cv2.imwrite(save_path, cv2.cvtColor(cropped, cv2.COLOR_RGB2BGR))
201
+ print(f"βœ… Saved {save_path}")
202
+
203
+ data_url = encode_image_to_data_url(cropped, format="JPEG")
204
+
205
+ vision_instructions = (
206
+ "You are an expert at reading receipts from an image. "
207
+ "Extract these fields as strict JSON: {\\n"
208
+ " \"venue\": string or null,\\n"
209
+ " \"date\": string or null,\\n"
210
+ " \"total\": string or number, include currency if shown,\\n"
211
+ " \"category\": string or null\\n"
212
+ "}. Do not include any extra commentary."
213
+ )
214
+
215
+ message = HumanMessage(
216
+ content=[
217
+ {"type": "text", "text": vision_instructions},
218
+ {"type": "image_url", "image_url": {"url": data_url}},
219
+ ]
220
+ )
221
+
222
+ result = llm_vision.invoke([message])
223
+
224
+ return {
225
+ "status": "success",
226
+ "cropped_file": save_path,
227
+ "bbox": bbox,
228
+ "extracted": getattr(result, "content", str(result)),
229
+ }
230
+
231
  if __name__ == "__main__":
232
  uvicorn.run(app, host="0.0.0.0", port=7860)