LogicGoInfotechSpaces commited on
Commit
72b7090
·
verified ·
1 Parent(s): 27d080d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +229 -168
app.py CHANGED
@@ -1,3 +1,4 @@
 
1
  # app.py
2
  import uvicorn
3
  import numpy as np
@@ -5,10 +6,13 @@ import cv2
5
  import boto3
6
  import os
7
  import json
 
8
  import requests
9
- from fastapi import FastAPI, UploadFile, File, HTTPException
 
10
  from rapidocr_onnxruntime import RapidOCR
11
  from openai import OpenAI
 
12
 
13
  # ---------------- ENV CONFIG ----------------
14
  DO_KEY_ID = os.getenv("DO_SPACES_KEY_ID")
@@ -20,16 +24,16 @@ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
20
 
21
  FOLDER = "OCR_Images"
22
 
 
 
 
23
  if not OPENAI_API_KEY:
24
  raise RuntimeError("OPENAI_API_KEY missing!")
25
 
 
26
  client = OpenAI(api_key=OPENAI_API_KEY)
27
 
28
-
29
- CATEGORY_API_URL = os.getenv("CATEGORY_API_URL")
30
- NOTES_CATEGORIZER_URL = os.getenv("NOTES_CATEGORIZER_URL")
31
-
32
- # S3 client
33
  s3 = boto3.client(
34
  "s3",
35
  region_name=DO_REGION,
@@ -38,15 +42,50 @@ s3 = boto3.client(
38
  aws_secret_access_key=DO_SECRET_KEY,
39
  )
40
 
 
 
 
 
 
 
 
41
  app = FastAPI()
42
  ocr_engine = RapidOCR()
43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  # ---------------- ROUTES ----------------
45
  @app.get("/health")
46
  async def health():
47
  return {"status": "ok"}
48
 
49
-
50
  @app.post("/upload")
51
  async def upload_image(file: UploadFile = File(...)):
52
  try:
@@ -61,113 +100,102 @@ async def upload_image(file: UploadFile = File(...)):
61
  ACL="private"
62
  )
63
 
64
- # Also return a local path (if available) for debugging / local testing.
65
- # Developer note: we include a local container path at /mnt/data/image.png when applicable.
66
- return {"image_id": image_key, "message": "Uploaded successfully", "local_path": "/mnt/data/image.png"}
 
 
 
67
 
68
  except Exception as e:
69
  raise HTTPException(status_code=500, detail=str(e))
70
 
71
-
72
  @app.post("/generate/{image_id:path}")
73
- async def generate(image_id: str):
 
 
 
 
74
 
75
- # -------- Download image --------
76
  try:
77
- obj = s3.get_object(Bucket=DO_BUCKET, Key=image_id)
78
- raw_bytes = obj["Body"].read()
79
- except Exception:
80
- # Fallback: try to load from local path if exists (useful for local testing)
81
- local_path = "/mnt/data/image.png"
82
- if os.path.exists(local_path):
83
- with open(local_path, "rb") as f:
84
- raw_bytes = f.read()
85
- else:
86
- raise HTTPException(status_code=404, detail="Image not found")
87
-
88
- img_array = np.frombuffer(raw_bytes, np.uint8)
89
- img = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
90
- if img is None:
91
- raise HTTPException(status_code=400, detail="Unable to decode image")
92
-
93
- # -------- OCR --------
94
- result, _ = ocr_engine(img)
95
- if not result:
96
- raise HTTPException(status_code=500, detail="OCR returned empty result")
97
-
98
- full_text = "\n".join([text for _, text, _ in result])
99
-
100
- # -------- CONFIDENCE SCORE --------
101
- confidences = [conf for _, _, conf in result if isinstance(conf, (int, float))]
102
- avg_confidence = sum(confidences) / len(confidences) if confidences else 0
103
-
104
- if avg_confidence < 0.70:
105
- return {
106
- "image_id": image_id,
107
- "raw_text": full_text,
108
- "confidence": round(avg_confidence, 3),
109
- "message": "Upload image with more clarity or enter manually.",
110
- "source_image_path": "/mnt/data/image.png"
111
- }
112
-
113
- # -------- JSON SCHEMA FOR GPT --------
114
- schema = {
115
- "name": "extract_expense_details",
116
- "schema": {
117
- "type": "object",
118
- "properties": {
119
- "total_amount": {"type": "number"},
120
- "label": {"type": "string"},
121
- "date": {"type": "string"},
122
- "time": {"type": "string"},
123
- "payment_type": {
124
- "type": "string",
125
- "enum": ["cash", "card", "upi", "unknown"]
 
 
 
 
 
 
 
 
 
 
 
126
  },
127
- "notes": {"type": "string"}
128
- },
129
- "required": ["total_amount", "label"]
130
  }
131
- }
132
-
133
- # -------- PROMPT --------
134
- prompt = f"""
135
- You are an expense extraction AI.
136
 
137
- Extract expense details from the OCR text below:
 
138
 
139
  \"\"\"
140
  {full_text}
141
  \"\"\"
142
 
143
- ### STRICT INFORMATION RULES:
144
- - Do NOT create or guess any information that does not exist in the extracted text.
145
- - If any field (date, time, payment_type, total_amount) is not clearly present in the text, set its value to "unknown".
146
- - Only infer the label category (Restaurant, Store, etc.) based on business name and item types.
147
-
148
- ### Labeling Rules:
149
- 1. Detect the business/merchant name from the text (e.g., KFC, Starbucks, Ying Thai Kitchen).
150
- 2. If items are food or restaurant-related → label must be: "<Business Name> Restaurant".
151
- 3. If it's a store/retail → "<Business Name> Store".
152
- 4. If unclear, infer the closest meaningful category.
153
- 5. If business name is not found → label = "unknown".
154
-
155
- ### Notes Format:
156
- Always generate notes EXACTLY in this format:
157
  "Spent <total_amount> on <label> on <date>."
158
-
159
- ### Required Output:
160
- Return structured JSON (via schema) with:
161
- - total_amount
162
- - label
163
- - date
164
- - time
165
- - payment_type
166
- - notes
167
  """
168
 
169
- # -------- CALL GPT --------
170
- try:
171
  response = client.chat.completions.create(
172
  model="gpt-4o-mini",
173
  response_format={"type": "json_schema", "json_schema": schema},
@@ -178,81 +206,70 @@ Return structured JSON (via schema) with:
178
  temperature=0.1
179
  )
180
 
181
- # The SDK may return the json directly in a field depending on version;
182
- # fall back to extracting message content.
183
- raw_content = None
 
 
 
 
 
184
  try:
185
- raw_content = response.choices[0].message.content
186
- parsed = json.loads(raw_content)
 
 
 
 
 
 
 
 
 
 
 
 
187
  except Exception:
188
- # try another path if SDK embeds the json directly
189
- try:
190
- parsed = response.choices[0].message.json # hypothetical
191
- except Exception:
192
- raise
193
 
194
- except Exception as e:
195
- raise HTTPException(status_code=500, detail=f"OpenAI Error: {str(e)}")
196
 
197
- # Ensure required keys exist and enforce strict defaults
198
- parsed.setdefault("total_amount", 0)
199
- parsed.setdefault("label", "unknown")
200
- parsed.setdefault("date", "unknown")
201
- parsed.setdefault("time", "unknown")
202
- parsed.setdefault("payment_type", "unknown")
203
- parsed.setdefault("notes", "unknown")
204
 
205
- # -------- CATEGORY API CALL (USING NOTES INSTEAD OF LABEL) --------
206
- # Use the notes text to derive a category/subcategory via the notes categorizer.
207
- notes_text = parsed.get("notes", "")
 
 
 
 
 
208
 
209
- try:
210
- cat_response = requests.post(
211
- NOTES_CATEGORIZER_URL,
212
- json={"notes": notes_text},
213
- timeout=10
 
 
 
214
  )
215
 
216
- if cat_response.status_code == 200:
217
- cat_data = cat_response.json()
218
- # category should be filled with the subcategory field from the notes API
219
- parsed["category"] = cat_data.get("subcategory", "unknown")
220
- # keep label unchanged
221
- parsed["label"] = parsed.get("label", "unknown")
222
- # also provide the top-level title for convenience
223
- parsed["category_title"] = cat_data.get("title", None)
224
- else:
225
- parsed["category"] = "unknown"
226
- parsed["category_title"] = None
227
 
228
- except Exception:
229
- parsed["category"] = "unknown"
230
- parsed["category_title"] = None
231
-
232
- # -------- FINAL RESPONSE --------
233
- return {
234
- "image_id": image_id,
235
- "raw_text": full_text,
236
- "confidence": round(avg_confidence, 3),
237
- "parsed": parsed,
238
- # Developer/test helper: include local path (will be transformed if necessary)
239
- "source_image_path": "/mnt/data/image.png"
240
- }
241
-
242
  @app.get("/ping")
243
  def ping():
244
  return {"status": "alive"}
245
 
246
-
247
  if __name__ == "__main__":
248
  uvicorn.run("app:app", host="0.0.0.0", port=7860)
249
 
250
-
251
-
252
-
253
-
254
-
255
-
256
  # # app.py
257
  # import uvicorn
258
  # import numpy as np
@@ -280,7 +297,9 @@ if __name__ == "__main__":
280
 
281
  # client = OpenAI(api_key=OPENAI_API_KEY)
282
 
283
- # # Category API URL
 
 
284
 
285
  # # S3 client
286
  # s3 = boto3.client(
@@ -314,7 +333,9 @@ if __name__ == "__main__":
314
  # ACL="private"
315
  # )
316
 
317
- # return {"image_id": image_key, "message": "Uploaded successfully"}
 
 
318
 
319
  # except Exception as e:
320
  # raise HTTPException(status_code=500, detail=str(e))
@@ -327,8 +348,14 @@ if __name__ == "__main__":
327
  # try:
328
  # obj = s3.get_object(Bucket=DO_BUCKET, Key=image_id)
329
  # raw_bytes = obj["Body"].read()
330
- # except:
331
- # raise HTTPException(status_code=404, detail="Image not found")
 
 
 
 
 
 
332
 
333
  # img_array = np.frombuffer(raw_bytes, np.uint8)
334
  # img = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
@@ -351,7 +378,8 @@ if __name__ == "__main__":
351
  # "image_id": image_id,
352
  # "raw_text": full_text,
353
  # "confidence": round(avg_confidence, 3),
354
- # "message": "Upload image with more clarity or enter manually."
 
355
  # }
356
 
357
  # # -------- JSON SCHEMA FOR GPT --------
@@ -422,38 +450,71 @@ if __name__ == "__main__":
422
  # temperature=0.1
423
  # )
424
 
425
- # parsed = json.loads(response.choices[0].message.content)
 
 
 
 
 
 
 
 
 
 
 
426
 
427
  # except Exception as e:
428
  # raise HTTPException(status_code=500, detail=f"OpenAI Error: {str(e)}")
429
 
430
- # # -------- CATEGORY API CALL --------
431
- # extracted_label = parsed.get("label", "unknown")
 
 
 
 
 
 
 
 
 
432
 
433
  # try:
434
  # cat_response = requests.post(
435
- # CATEGORY_API_URL,
436
- # json={"label": extracted_label},
437
  # timeout=10
438
  # )
439
 
440
  # if cat_response.status_code == 200:
441
  # cat_data = cat_response.json()
442
- # parsed["category"] = cat_data.get("category", "unknown")
 
 
 
 
 
443
  # else:
444
  # parsed["category"] = "unknown"
 
445
 
446
  # except Exception:
447
  # parsed["category"] = "unknown"
 
448
 
449
  # # -------- FINAL RESPONSE --------
450
  # return {
451
  # "image_id": image_id,
452
  # "raw_text": full_text,
453
  # "confidence": round(avg_confidence, 3),
454
- # "parsed": parsed
 
 
455
  # }
 
 
 
 
456
 
457
 
458
  # if __name__ == "__main__":
459
- # uvicorn.run("app:app", host="0.0.0.0", port=7860)
 
1
+
2
  # app.py
3
  import uvicorn
4
  import numpy as np
 
6
  import boto3
7
  import os
8
  import json
9
+ import time
10
  import requests
11
+ from datetime import datetime
12
+ from fastapi import FastAPI, UploadFile, File, HTTPException, Header
13
  from rapidocr_onnxruntime import RapidOCR
14
  from openai import OpenAI
15
+ from pymongo import MongoClient
16
 
17
  # ---------------- ENV CONFIG ----------------
18
  DO_KEY_ID = os.getenv("DO_SPACES_KEY_ID")
 
24
 
25
  FOLDER = "OCR_Images"
26
 
27
+ CATEGORY_API_URL = os.getenv("CATEGORY_API_URL")
28
+ NOTES_CATEGORIZER_URL = os.getenv("NOTES_CATEGORIZER_URL")
29
+
30
  if not OPENAI_API_KEY:
31
  raise RuntimeError("OPENAI_API_KEY missing!")
32
 
33
+ # ---------------- OPENAI ----------------
34
  client = OpenAI(api_key=OPENAI_API_KEY)
35
 
36
+ # ---------------- S3 ----------------
 
 
 
 
37
  s3 = boto3.client(
38
  "s3",
39
  region_name=DO_REGION,
 
42
  aws_secret_access_key=DO_SECRET_KEY,
43
  )
44
 
45
+ # ---------------- MONGODB ----------------
46
+ MONGO_URI = os.getenv("MONGO_URI")
47
+ mongo_client = MongoClient(MONGO_URI)
48
+ mongo_db = mongo_client["expense"]
49
+ api_logs_col = mongo_db["api_logs"]
50
+
51
+ # ---------------- APP ----------------
52
  app = FastAPI()
53
  ocr_engine = RapidOCR()
54
 
55
+ # ---------------- HELPERS ----------------
56
+ def ist_now():
57
+ return datetime.now().strftime("%d-%m-%Y %H:%M:%S:IST")
58
+
59
+ def log_api_event(
60
+ *,
61
+ status: str,
62
+ response_time: float,
63
+ user_id: str | None,
64
+ error_message: str | None = None
65
+ ):
66
+ payload = {
67
+ "name": "Receipt Scanner",
68
+ "status": status,
69
+ "date": ist_now(),
70
+ "response_time": round(response_time, 3),
71
+ }
72
+
73
+ if user_id:
74
+ payload["user_id"] = user_id
75
+
76
+ if error_message:
77
+ payload["error_message"] = error_message
78
+
79
+ try:
80
+ api_logs_col.insert_one(payload)
81
+ except Exception:
82
+ pass # never break API because of logging failure
83
+
84
  # ---------------- ROUTES ----------------
85
  @app.get("/health")
86
  async def health():
87
  return {"status": "ok"}
88
 
 
89
  @app.post("/upload")
90
  async def upload_image(file: UploadFile = File(...)):
91
  try:
 
100
  ACL="private"
101
  )
102
 
103
+ return {
104
+ "status": "success",
105
+ "message": "Uploaded successfully",
106
+ "image_id": image_key,
107
+ "local_path": "/mnt/data/image.png"
108
+ }
109
 
110
  except Exception as e:
111
  raise HTTPException(status_code=500, detail=str(e))
112
 
 
113
  @app.post("/generate/{image_id:path}")
114
+ async def generate(
115
+ image_id: str,
116
+ user_id: str | None = Header(default=None)
117
+ ):
118
+ start_time = time.time()
119
 
 
120
  try:
121
+ # -------- DOWNLOAD IMAGE --------
122
+ try:
123
+ obj = s3.get_object(Bucket=DO_BUCKET, Key=image_id)
124
+ raw_bytes = obj["Body"].read()
125
+ except Exception:
126
+ local_path = "/mnt/data/image.png"
127
+ if os.path.exists(local_path):
128
+ with open(local_path, "rb") as f:
129
+ raw_bytes = f.read()
130
+ else:
131
+ raise HTTPException(status_code=404, detail="Image not found")
132
+
133
+ img_array = np.frombuffer(raw_bytes, np.uint8)
134
+ img = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
135
+
136
+ if img is None:
137
+ raise HTTPException(status_code=400, detail="Unable to decode image")
138
+
139
+ # -------- OCR --------
140
+ result, _ = ocr_engine(img)
141
+ if not result:
142
+ raise RuntimeError("OCR returned empty result")
143
+
144
+ full_text = "\n".join([text for _, text, _ in result])
145
+
146
+ confidences = [conf for _, _, conf in result if isinstance(conf, (int, float))]
147
+ avg_confidence = sum(confidences) / len(confidences) if confidences else 0
148
+
149
+ if avg_confidence < 0.70:
150
+ response_time = time.time() - start_time
151
+ log_api_event(
152
+ status="fail",
153
+ response_time=response_time,
154
+ user_id=user_id,
155
+ error_message="Low OCR confidence"
156
+ )
157
+
158
+ return {
159
+ "status": "fail",
160
+ "message": "Upload image with more clarity or enter manually.",
161
+ "image_id": image_id,
162
+ "raw_text": full_text,
163
+ "confidence": round(avg_confidence, 3),
164
+ }
165
+
166
+ # -------- GPT SCHEMA --------
167
+ schema = {
168
+ "name": "extract_expense_details",
169
+ "schema": {
170
+ "type": "object",
171
+ "properties": {
172
+ "total_amount": {"type": "number"},
173
+ "label": {"type": "string"},
174
+ "date": {"type": "string"},
175
+ "time": {"type": "string"},
176
+ "payment_type": {
177
+ "type": "string",
178
+ "enum": ["cash", "card", "upi", "unknown"]
179
+ },
180
+ "notes": {"type": "string"}
181
  },
182
+ "required": ["total_amount", "label"]
183
+ }
 
184
  }
 
 
 
 
 
185
 
186
+ prompt = f"""
187
+ Extract expense details from OCR text below:
188
 
189
  \"\"\"
190
  {full_text}
191
  \"\"\"
192
 
193
+ Rules:
194
+ - Do not guess missing values use "unknown"
195
+ - Notes format:
 
 
 
 
 
 
 
 
 
 
 
196
  "Spent <total_amount> on <label> on <date>."
 
 
 
 
 
 
 
 
 
197
  """
198
 
 
 
199
  response = client.chat.completions.create(
200
  model="gpt-4o-mini",
201
  response_format={"type": "json_schema", "json_schema": schema},
 
206
  temperature=0.1
207
  )
208
 
209
+ parsed = json.loads(response.choices[0].message.content)
210
+
211
+ parsed.setdefault("date", "unknown")
212
+ parsed.setdefault("time", "unknown")
213
+ parsed.setdefault("payment_type", "unknown")
214
+ parsed.setdefault("notes", "unknown")
215
+
216
+ # -------- CATEGORY API --------
217
  try:
218
+ cat_response = requests.post(
219
+ NOTES_CATEGORIZER_URL,
220
+ json={"notes": parsed["notes"]},
221
+ timeout=10
222
+ )
223
+
224
+ if cat_response.status_code == 200:
225
+ cat_data = cat_response.json()
226
+ parsed["category"] = cat_data.get("subcategory", "unknown")
227
+ parsed["category_title"] = cat_data.get("title")
228
+ else:
229
+ parsed["category"] = "unknown"
230
+ parsed["category_title"] = None
231
+
232
  except Exception:
233
+ parsed["category"] = "unknown"
234
+ parsed["category_title"] = None
 
 
 
235
 
236
+ response_time = time.time() - start_time
 
237
 
238
+ log_api_event(
239
+ status="success",
240
+ response_time=response_time,
241
+ user_id=user_id
242
+ )
 
 
243
 
244
+ return {
245
+ "status": "success",
246
+ "message": "Receipt processed and logged in DB",
247
+ "image_id": image_id,
248
+ "confidence": round(avg_confidence, 3),
249
+ "raw_text": full_text,
250
+ "parsed": parsed,
251
+ }
252
 
253
+ except Exception as e:
254
+ response_time = time.time() - start_time
255
+
256
+ log_api_event(
257
+ status="fail",
258
+ response_time=response_time,
259
+ user_id=user_id,
260
+ error_message=str(e)
261
  )
262
 
263
+ raise HTTPException(status_code=500, detail=str(e))
 
 
 
 
 
 
 
 
 
 
264
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  @app.get("/ping")
266
  def ping():
267
  return {"status": "alive"}
268
 
 
269
  if __name__ == "__main__":
270
  uvicorn.run("app:app", host="0.0.0.0", port=7860)
271
 
272
+
 
 
 
 
 
273
  # # app.py
274
  # import uvicorn
275
  # import numpy as np
 
297
 
298
  # client = OpenAI(api_key=OPENAI_API_KEY)
299
 
300
+
301
+ # CATEGORY_API_URL = os.getenv("CATEGORY_API_URL")
302
+ # NOTES_CATEGORIZER_URL = os.getenv("NOTES_CATEGORIZER_URL")
303
 
304
  # # S3 client
305
  # s3 = boto3.client(
 
333
  # ACL="private"
334
  # )
335
 
336
+ # # Also return a local path (if available) for debugging / local testing.
337
+ # # Developer note: we include a local container path at /mnt/data/image.png when applicable.
338
+ # return {"image_id": image_key, "message": "Uploaded successfully", "local_path": "/mnt/data/image.png"}
339
 
340
  # except Exception as e:
341
  # raise HTTPException(status_code=500, detail=str(e))
 
348
  # try:
349
  # obj = s3.get_object(Bucket=DO_BUCKET, Key=image_id)
350
  # raw_bytes = obj["Body"].read()
351
+ # except Exception:
352
+ # # Fallback: try to load from local path if exists (useful for local testing)
353
+ # local_path = "/mnt/data/image.png"
354
+ # if os.path.exists(local_path):
355
+ # with open(local_path, "rb") as f:
356
+ # raw_bytes = f.read()
357
+ # else:
358
+ # raise HTTPException(status_code=404, detail="Image not found")
359
 
360
  # img_array = np.frombuffer(raw_bytes, np.uint8)
361
  # img = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
 
378
  # "image_id": image_id,
379
  # "raw_text": full_text,
380
  # "confidence": round(avg_confidence, 3),
381
+ # "message": "Upload image with more clarity or enter manually.",
382
+ # "source_image_path": "/mnt/data/image.png"
383
  # }
384
 
385
  # # -------- JSON SCHEMA FOR GPT --------
 
450
  # temperature=0.1
451
  # )
452
 
453
+ # # The SDK may return the json directly in a field depending on version;
454
+ # # fall back to extracting message content.
455
+ # raw_content = None
456
+ # try:
457
+ # raw_content = response.choices[0].message.content
458
+ # parsed = json.loads(raw_content)
459
+ # except Exception:
460
+ # # try another path if SDK embeds the json directly
461
+ # try:
462
+ # parsed = response.choices[0].message.json # hypothetical
463
+ # except Exception:
464
+ # raise
465
 
466
  # except Exception as e:
467
  # raise HTTPException(status_code=500, detail=f"OpenAI Error: {str(e)}")
468
 
469
+ # # Ensure required keys exist and enforce strict defaults
470
+ # parsed.setdefault("total_amount", 0)
471
+ # parsed.setdefault("label", "unknown")
472
+ # parsed.setdefault("date", "unknown")
473
+ # parsed.setdefault("time", "unknown")
474
+ # parsed.setdefault("payment_type", "unknown")
475
+ # parsed.setdefault("notes", "unknown")
476
+
477
+ # # -------- CATEGORY API CALL (USING NOTES INSTEAD OF LABEL) --------
478
+ # # Use the notes text to derive a category/subcategory via the notes categorizer.
479
+ # notes_text = parsed.get("notes", "")
480
 
481
  # try:
482
  # cat_response = requests.post(
483
+ # NOTES_CATEGORIZER_URL,
484
+ # json={"notes": notes_text},
485
  # timeout=10
486
  # )
487
 
488
  # if cat_response.status_code == 200:
489
  # cat_data = cat_response.json()
490
+ # # category should be filled with the subcategory field from the notes API
491
+ # parsed["category"] = cat_data.get("subcategory", "unknown")
492
+ # # keep label unchanged
493
+ # parsed["label"] = parsed.get("label", "unknown")
494
+ # # also provide the top-level title for convenience
495
+ # parsed["category_title"] = cat_data.get("title", None)
496
  # else:
497
  # parsed["category"] = "unknown"
498
+ # parsed["category_title"] = None
499
 
500
  # except Exception:
501
  # parsed["category"] = "unknown"
502
+ # parsed["category_title"] = None
503
 
504
  # # -------- FINAL RESPONSE --------
505
  # return {
506
  # "image_id": image_id,
507
  # "raw_text": full_text,
508
  # "confidence": round(avg_confidence, 3),
509
+ # "parsed": parsed,
510
+ # # Developer/test helper: include local path (will be transformed if necessary)
511
+ # "source_image_path": "/mnt/data/image.png"
512
  # }
513
+
514
+ # @app.get("/ping")
515
+ # def ping():
516
+ # return {"status": "alive"}
517
 
518
 
519
  # if __name__ == "__main__":
520
+ # uvicorn.run("app:app", host="0.0.0.0", port=7860)