markobinario commited on
Commit
7250ce9
·
verified ·
1 Parent(s): f2998c3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +124 -315
app.py CHANGED
@@ -1,319 +1,128 @@
1
- import base64
2
- import io
3
- import json
4
- import os
5
- from typing import Dict, List, Tuple, Any, Optional
6
- import time
7
- import requests
8
- from PIL import Image
9
- import gradio as gr
10
- # =========================
11
- # Config
12
- # =========================
13
- DEFAULT_API_URL = os.environ.get("API_URL")
14
- TOKEN = os.environ.get("TOKEN")
15
- LOGO_IMAGE_PATH = './assets/logo.jpg'
16
- GOOGLE_FONTS_URL = "<link href='https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;700&display=swap' rel='stylesheet'>"
17
- LATEX_DELIMS = [
18
- {"left": "$$", "right": "$$", "display": True},
19
- {"left": "$", "right": "$", "display": False},
20
- {"left": "\\(", "right": "\\)", "display": False},
21
- {"left": "\\[", "right": "\\]", "display": True},
22
- ]
23
- AUTH_HEADER = {"Authorization": f"bearer {TOKEN}"}
24
- JSON_HEADERS = {**AUTH_HEADER, "Content-Type": "application/json"}
25
- # =========================
26
- # Base64 and Example Loading Logic
27
- # =========================
28
- def image_to_base64_data_url(filepath: str) -> str:
29
- """Reads a local image file and encodes it into a Base64 Data URL."""
30
- try:
31
- ext = os.path.splitext(filepath)[1].lower()
32
- mime_types = {'.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.gif': 'image/gif'}
33
- mime_type = mime_types.get(ext, 'image/jpeg')
34
- with open(filepath, "rb") as image_file:
35
- encoded_string = base64.b64encode(image_file.read()).decode("utf-8")
36
- return f"data:{mime_type};base64,{encoded_string}"
37
- except Exception as e:
38
- print(f"Error encoding image to Base64: {e}")
39
- return ""
40
-
41
-
42
- def _get_examples_from_dir(dir_path: str) -> List[List[str]]:
43
- supported_exts = {".png", ".jpg", ".jpeg", ".bmp", ".webp"}
44
- examples = []
45
- if not os.path.exists(dir_path): return []
46
- for filename in sorted(os.listdir(dir_path)):
47
- if os.path.splitext(filename)[1].lower() in supported_exts:
48
- examples.append([os.path.join(dir_path, filename)])
49
- return examples
50
-
51
- TARGETED_EXAMPLES_DIR = "examples/targeted"
52
- COMPLEX_EXAMPLES_DIR = "examples/complex"
53
- targeted_recognition_examples = _get_examples_from_dir(TARGETED_EXAMPLES_DIR)
54
- complex_document_examples = _get_examples_from_dir(COMPLEX_EXAMPLES_DIR)
55
-
56
- # =========================
57
- # UI Helpers
58
- # =========================
59
- def render_uploaded_image_div(file_path: str) -> str:
60
- data_url = image_to_base64_data_url(file_path)
61
- return f"""
62
- <div class="uploaded-image">
63
- <img src="{data_url}" alt="Uploaded image" style="width:100%;height:100%;object-fit:contain;"/>
64
- </div>
65
- """
66
-
67
- def update_preview_visibility(file_path: Optional[str]) -> Dict:
68
- if file_path:
69
- html_content = render_uploaded_image_div(file_path)
70
- return gr.update(value=html_content, visible=True)
71
- else:
72
- return gr.update(value="", visible=False)
73
-
74
- def _on_gallery_select(example_paths: List[str], evt: gr.SelectData):
75
- try:
76
- idx = evt.index
77
- return example_paths[idx]
78
- except Exception:
79
- return None
80
-
81
- # =========================
82
- # API Call Logic
83
- # =========================
84
- def _file_to_b64_image_only(file_path: str) -> Tuple[str, int]:
85
- if not file_path: raise ValueError("Please upload an image first.")
86
- ext = os.path.splitext(file_path)[1].lower()
87
- if ext not in {".png", ".jpg", ".jpeg", ".bmp", ".webp"}: raise ValueError("Only image files are supported.")
88
- with open(file_path, "rb") as f:
89
- return base64.b64encode(f.read()).decode("utf-8"), 1
90
-
91
- def _call_api(api_url: str, file_path: str, use_layout_detection: bool,
92
- prompt_label: Optional[str], use_chart_recognition: bool = False) -> Dict[str, Any]:
93
- b64, file_type = _file_to_b64_image_only(file_path)
94
- payload = {
95
- "file": b64,
96
- "useLayoutDetection": bool(use_layout_detection),
97
- "fileType": file_type,
98
- "layoutMergeBboxesMode": "union",
99
- }
100
- if not use_layout_detection:
101
- if not prompt_label:
102
- raise ValueError("Please select a recognition type.")
103
- payload["promptLabel"] = prompt_label.strip().lower()
104
- if use_layout_detection and use_chart_recognition:
105
- payload["useChartRecognition"] = True
106
-
107
- try:
108
- print(f"Sending API request to {api_url}...")
109
- start_time = time.time()
110
- resp = requests.post(api_url, json=payload, headers=JSON_HEADERS, timeout=600)
111
- end_time = time.time()
112
- duration = end_time - start_time
113
- print(f"Received API response in {duration:.2f} seconds.")
114
-
115
- resp.raise_for_status()
116
- data = resp.json()
117
- except requests.exceptions.RequestException as e:
118
- raise gr.Error(f"API request failed:{e}")
119
- except json.JSONDecodeError:
120
- raise gr.Error(f"Invalid JSON response from server:\n{getattr(resp, 'text', '')}")
121
-
122
- if data.get("errorCode", -1) != 0:
123
- raise gr.Error("API returned an error:")
124
- return data
125
-
126
-
127
- # =========================
128
- # API Response Processing
129
- # =========================
130
-
131
- # 【改动点】: 这个函数现在不再需要,因为我们不再将URL下载为PIL Image对象。
132
- # def url_to_pil_image(url: str) -> Optional[Image.Image]:
133
- # """Downloads an image from a URL and returns it as a PIL Image object for the Gradio Image component."""
134
- # if not url or not url.startswith(('http://', 'https://')):
135
- # print(f"Warning: Invalid URL provided for visualization image: {url}")
136
- # return None
137
- # try:
138
- # start_time = time.time()
139
- # response = requests.get(url, timeout=600)
140
- # end_time = time.time()
141
- # print(f"Fetched visualization image from {url} in {end_time - start_time:.2f} seconds.")
142
- #
143
- # response.raise_for_status()
144
- # image_bytes = response.content
145
- # pil_image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
146
- # return pil_image
147
- # except requests.exceptions.RequestException as e:
148
- # print(f"Error fetching visualization image from URL {url}: {e}")
149
- # return None
150
- # except Exception as e:
151
- # print(f"Error processing visualization image from URL {url}: {e}")
152
- # return None
153
-
154
- def _process_api_response_page(result: Dict[str, Any]) -> Tuple[str, str, str]:
155
- """
156
- Processes the API response.
157
- 1. Replaces markdown image placeholders with their direct URLs.
158
- 2. Constructs an HTML <img> tag string for the visualization image URL.
159
- """
160
- layout_results = (result or {}).get("layoutParsingResults", [])
161
- if not layout_results:
162
- return "No content was recognized.", "<p>No visualization available.</p>", ""
163
 
164
- page0 = layout_results[0] or {}
165
-
166
- # Step 1: Process Markdown content (unchanged from previous optimization)
167
- md_data = page0.get("markdown") or {}
168
- md_text = md_data.get("text", "") or ""
169
- md_images_map = md_data.get("images", {})
170
- if md_images_map:
171
- for placeholder_path, image_url in md_images_map.items():
172
- md_text = md_text.replace(f'src="{placeholder_path}"', f'src="{image_url}"') \
173
- .replace(f']({placeholder_path})', f']({image_url})')
174
-
175
- # 【核心改动点】 Step 2: Process Visualization images by creating an HTML string
176
- output_html = "<p style='text-align:center; color:#888;'>No visualization image available.</p>"
177
- out_imgs = page0.get("outputImages") or {}
178
-
179
- # Get all image URLs and sort them
180
- sorted_urls = [img_url for _, img_url in sorted(out_imgs.items()) if img_url]
181
-
182
- # Logic to select the final visualization image URL
183
- output_image_url: Optional[str] = None
184
- if len(sorted_urls) >= 2:
185
- output_image_url = sorted_urls[1]
186
- elif sorted_urls:
187
- output_image_url = sorted_urls[0]
188
-
189
- # If a URL was found, create the <img> tag
190
- if output_image_url:
191
- print(f"Found visualization image URL: {output_image_url}")
192
- # The CSS will style this `img` tag because of the `#vis_image_doc img` selector
193
- output_html = f'<img src="{output_image_url}" alt="Detection Visualization">'
194
- else:
195
- print("Warning: No visualization image URL found in the API response.")
196
-
197
- return md_text or "(Empty result)", output_html, md_text
198
-
199
- # =========================
200
- # Handlers
201
- # =========================
202
- def handle_complex_doc(file_path: str, use_chart_recognition: bool) -> Tuple[str, str, str]:
203
- if not file_path: raise gr.Error("Please upload an image first.")
204
- data = _call_api(DEFAULT_API_URL, file_path, use_layout_detection=True, prompt_label=None, use_chart_recognition=use_chart_recognition)
205
- result = data.get("result", {})
206
- # Note the return types now align with the new function signature
207
- return _process_api_response_page(result)
208
-
209
- def handle_targeted_recognition(file_path: str, prompt_choice: str) -> Tuple[str, str]:
210
- if not file_path: raise gr.Error("Please upload an image first.")
211
- mapping = {"Text Recognition": "ocr", "Formula Recognition": "formula", "Table Recognition": "table", "Chart Recognition": "chart"}
212
- label = mapping.get(prompt_choice, "ocr")
213
- data = _call_api(DEFAULT_API_URL, file_path, use_layout_detection=False, prompt_label=label)
214
- result = data.get("result", {})
215
- md_preview, _, md_raw = _process_api_response_page(result)
216
- return md_preview, md_raw
217
-
218
- # =========================
219
- # CSS & UI
220
- # =========================
221
- custom_css = """
222
- /* 全局字体 */
223
- body, .gradio-container {
224
- font-family: "Noto Sans SC", "Microsoft YaHei", "PingFang SC", sans-serif;
225
- }
226
- /* ... (rest of the CSS is unchanged) ... */
227
- .app-header { text-align: center; max-width: 900px; margin: 0 auto 8px !important; }
228
- .gradio-container { padding: 4px 0 !important; }
229
- .gradio-container [data-testid="tabs"], .gradio-container .tabs { margin-top: 0 !important; }
230
- .gradio-container [data-testid="tabitem"], .gradio-container .tabitem { padding-top: 4px !important; }
231
- .quick-links { text-align: center; padding: 8px 0; border: 1px solid #e5e7eb; border-radius: 8px; margin: 8px auto; max-width: 900px; }
232
- .quick-links a { margin: 0 12px; font-size: 14px; font-weight: 600; color: #3b82f6; text-decoration: none; }
233
- .quick-links a:hover { text-decoration: underline; }
234
- .prompt-grid { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 6px; }
235
- .prompt-grid button { height: 40px !important; padding: 0 12px !important; border-radius: 8px !important; font-weight: 600 !important; font-size: 13px !important; letter-spacing: 0.2px; }
236
- #image_preview_vl, #image_preview_doc { height: 400px !important; overflow: auto; }
237
- #image_preview_vl img, #image_preview_doc img, #vis_image_doc img { width: 100% !important; height: auto !important; object-fit: contain !important; display: block; }
238
- #md_preview_vl, #md_preview_doc { max-height: 540px; min-height: 180px; overflow: auto; scrollbar-gutter: stable both-edges; }
239
- #md_preview_vl .prose, #md_preview_doc .prose { line-height: 1.7 !important; }
240
- #md_preview_vl .prose img, #md_preview_doc .prose img { display: block; margin: 0 auto; max-width: 100%; height: auto; }
241
- .notice { margin: 8px auto 0; max-width: 900px; padding: 10px 12px; border: 1px solid #e5e7eb; border-radius: 8px; background: #f8fafc; font-size: 14px; line-height: 1.6; }
242
- .notice strong { font-weight: 700; }
243
- .notice a { color: #3b82f6; text-decoration: none; }
244
- .notice a:hover { text-decoration: underline; }
245
- """
246
-
247
- with gr.Blocks(head=GOOGLE_FONTS_URL, css=custom_css, theme=gr.themes.Soft()) as demo:
248
- logo_data_url = image_to_base64_data_url(LOGO_IMAGE_PATH) if os.path.exists(LOGO_IMAGE_PATH) else ""
249
- gr.HTML(f"""<div class="app-header"><img src="{logo_data_url}" alt="App Logo" style="max-height:10%; width: auto; margin: 10px auto; display: block;"></div>""")
250
- gr.HTML("""<div class="notice"><strong>Heads up:</strong> The Hugging Face demo can be slow at times. For a faster experience, please try <a href="https://aistudio.baidu.com/application/detail/98365" target="_blank" rel="noopener noreferrer">Baidu AI Studio</a> or <a href="https://modelscope.cn/studios/PaddlePaddle/PaddleOCR-VL_Online_Demo/summary" target="_blank" rel="noopener noreferrer">ModelScope</a>.</div>""")
251
- gr.HTML("""<div class="quick-links"><a href="https://github.com/PaddlePaddle/PaddleOCR" target="_blank">GitHub</a> | <a href="https://ernie.baidu.com/blog/publication/PaddleOCR-VL_Technical_Report.pdf" target="_blank">Technical Report</a> | <a href="https://huggingface.co/PaddlePaddle/PaddleOCR-VL" target="_blank">Model</a></div>""")
252
-
253
- with gr.Tabs():
254
- with gr.Tab("Document Parsing"):
255
- with gr.Row():
256
- with gr.Column(scale=5):
257
- file_doc = gr.File(label="Upload Image", file_count="single", type="filepath", file_types=["image"])
258
- preview_doc_html = gr.HTML(value="", elem_id="image_preview_doc", visible=False)
259
- gr.Markdown("_( Use this mode for recognizing full-page documents with structured layouts, such as reports, papers, or magazines.)_")
260
- gr.Markdown("💡 *To recognize a single, pre-cropped element (e.g., a table or formula), switch to the 'Element-level Recognition' tab for better results.*")
261
- with gr.Row(variant="panel"):
262
- chart_parsing_switch = gr.Checkbox(label="Enable chart parsing", value=False, scale=1)
263
- btn_parse = gr.Button("Parse Document", variant="primary", scale=2)
264
- if complex_document_examples:
265
- complex_paths = [e[0] for e in complex_document_examples]
266
- complex_state = gr.State(complex_paths)
267
- gr.Markdown("**Document Examples (Click an image to load)**")
268
- gallery_complex = gr.Gallery(value=complex_paths, columns=4, height=400, preview=False, label=None, allow_preview=False)
269
- gallery_complex.select(fn=_on_gallery_select, inputs=[complex_state], outputs=[file_doc])
270
-
271
- with gr.Column(scale=7):
272
- with gr.Tabs():
273
- with gr.Tab("Markdown Preview"):
274
- md_preview_doc = gr.Markdown("Please upload an image and click 'Parse Document'.", latex_delimiters=LATEX_DELIMS, elem_id="md_preview_doc")
275
- with gr.Tab("Visualization"):
276
- # 【核心改动点】: 将 gr.Image 替换为 gr.HTML
277
- vis_image_doc = gr.HTML(label="Detection Visualization", elem_id="vis_image_doc")
278
- with gr.Tab("Markdown Source"):
279
- md_raw_doc = gr.Code(label="Markdown Source Code", language="markdown")
280
-
281
- file_doc.change(fn=update_preview_visibility, inputs=[file_doc], outputs=[preview_doc_html])
282
- btn_parse.click(fn=handle_complex_doc, inputs=[file_doc, chart_parsing_switch], outputs=[md_preview_doc, vis_image_doc, md_raw_doc])
283
-
284
- with gr.Tab("Element-level Recognition"):
285
- with gr.Row():
286
- with gr.Column(scale=5):
287
- file_vl = gr.File(label="Upload Image", file_count="single", type="filepath", file_types=["image"])
288
- preview_vl_html = gr.HTML(value="", elem_id="image_preview_vl", visible=False)
289
- gr.Markdown("_(Best for images with a **simple, single-column layout** (e.g., pure text), or for a **pre-cropped single element** like a table, formula, or chart.)_")
290
- gr.Markdown("Choose a recognition type:")
291
- with gr.Row(elem_classes=["prompt-grid"]):
292
- btn_ocr = gr.Button("Text Recognition", variant="secondary")
293
- btn_formula = gr.Button("Formula Recognition", "secondary")
294
- with gr.Row(elem_classes=["prompt-grid"]):
295
- btn_table = gr.Button("Table Recognition", variant="secondary")
296
- btn_chart = gr.Button("Chart Recognition", variant="secondary")
297
- if targeted_recognition_examples:
298
- targeted_paths = [e[0] for e in targeted_recognition_examples]
299
- targeted_state = gr.State(targeted_paths)
300
- gr.Markdown("**Element-level Recognition Examples (Click an image to load)**")
301
- gallery_targeted = gr.Gallery(value=targeted_paths, columns=4, height=400, preview=False, label=None, allow_preview=False)
302
- gallery_targeted.select(fn=_on_gallery_select, inputs=[targeted_state], outputs=[file_vl])
303
-
304
- with gr.Column(scale=7):
305
- with gr.Tabs():
306
- with gr.Tab("Recognition Result"):
307
- md_preview_vl = gr.Markdown("Please upload an image and click a recognition type.", latex_delimiters=LATEX_DELIMS, elem_id="md_preview_vl")
308
- with gr.Tab("Raw Output"):
309
- md_raw_vl = gr.Code(label="Raw Output", language="markdown")
310
 
311
- file_vl.change(fn=update_preview_visibility, inputs=[file_vl], outputs=[preview_vl_html])
312
- btn_ocr.click(fn=handle_targeted_recognition, inputs=[file_vl, gr.State("Text Recognition")], outputs=[md_preview_vl, md_raw_vl])
313
- btn_formula.click(fn=handle_targeted_recognition, inputs=[file_vl, gr.State("Formula Recognition")], outputs=[md_preview_vl, md_raw_vl])
314
- btn_table.click(fn=handle_targeted_recognition, inputs=[file_vl, gr.State("Table Recognition")], outputs=[md_preview_vl, md_raw_vl])
315
- btn_chart.click(fn=handle_targeted_recognition, inputs=[file_vl, gr.State("Chart Recognition")], outputs=[md_preview_vl, md_raw_vl])
316
 
317
  if __name__ == "__main__":
318
- port = int(os.getenv("PORT", "7860"))
319
- demo.queue(max_size=6).launch(server_name="0.0.0.0", server_port=port,share=False)
 
1
+ *** Begin Patch
2
+ *** Update File: app.py
3
+ @@
4
+ - if 'pdf' in request.files and request.files['pdf'].filename:
5
+ - pdf = request.files['pdf']
6
+ - pdf_path = os.path.join(temp_dir, pdf.filename)
7
+ - pdf.save(pdf_path)
8
+ - print(f"📄 PDF saved to {pdf_path}")
9
+ -
10
+ - try:
11
+ - print("🚀 Sending PDF to /handle_complex_doc...")
12
+ - result = ocr_client.predict(
13
+ - file_path=handle_file(pdf_path),
14
+ - use_chart_recognition=False,
15
+ - api_name="/handle_complex_doc"
16
+ - )
17
+ - print("✅ OCR completed for PDF")
18
+ - print(f"OCR raw result: {result}")
19
+ -
20
+ - if isinstance(result, (list, tuple)) and len(result) >= 1:
21
+ - extracted_text = str(result[0])
22
+ - elif isinstance(result, str):
23
+ - extracted_text = result
24
+ - else:
25
+ - extracted_text = f"Unexpected result format: {type(result)}"
26
+ -
27
+ - return jsonify({"extracted_text": extracted_text.strip()})
28
+ - except Exception as e:
29
+ - return jsonify({"error": f"Error processing PDF: {e}"}), 500
30
+ + if 'pdf' in request.files and request.files['pdf'].filename:
31
+ + pdf = request.files['pdf']
32
+ + pdf_path = os.path.join(temp_dir, pdf.filename)
33
+ + pdf.save(pdf_path)
34
+ + print(f"📄 PDF saved to {pdf_path}")
35
+ +
36
+ + # Try local text extraction via PyMuPDF first
37
+ + try:
38
+ + doc = fitz.open(pdf_path)
39
+ + collected_text = []
40
+ + for page_index, page in enumerate(doc):
41
+ + text = page.get_text("text") or ""
42
+ + collected_text.append(f"--- Page {page_index + 1} ---\n{text.strip()}\n")
43
+ + local_text = "\n".join(collected_text).strip()
44
+ + if local_text:
45
+ + print("✅ Extracted text locally via PyMuPDF")
46
+ + return jsonify({"extracted_text": local_text})
47
+ + except Exception as e:
48
+ + print(f"⚠️ Local PyMuPDF extraction failed: {e}")
49
+ +
50
+ + # Fallback to remote endpoint for complex layout parsing
51
+ + try:
52
+ + print("🚀 Sending PDF to /handle_complex_doc...")
53
+ + result = ocr_client.predict(
54
+ + file_path=handle_file(pdf_path),
55
+ + use_chart_recognition=False,
56
+ + api_name="/handle_complex_doc"
57
+ + )
58
+ + print("✅ OCR completed for PDF")
59
+ + print(f"OCR raw result: {result}")
60
+ +
61
+ + if isinstance(result, (list, tuple)) and len(result) >= 1:
62
+ + extracted_text = str(result[0])
63
+ + elif isinstance(result, str):
64
+ + extracted_text = result
65
+ + else:
66
+ + extracted_text = f"Unexpected result format: {type(result)}"
67
+ +
68
+ + return jsonify({"extracted_text": extracted_text.strip()})
69
+ + except Exception as e:
70
+ + return jsonify({"error": f"Error processing PDF: {e}"}), 500
71
+ *** End Patched_text = f"Unexpected result format: {type(result)}"
72
+
73
+ return jsonify({"extracted_text": extracted_text.strip()})
74
+ except Exception as e:
75
+ return jsonify({"error": f"Error processing image: {e}"}), 500
76
+
77
+ # Handle PDF uploads
78
+ if 'pdf' in request.files and request.files['pdf'].filename:
79
+ pdf = request.files['pdf']
80
+ pdf_path = os.path.join(temp_dir, pdf.filename)
81
+ pdf.save(pdf_path)
82
+ print(f"📄 PDF saved to {pdf_path}")
83
+
84
+ # Try local text extraction via PyMuPDF first
85
+ try:
86
+ doc = fitz.open(pdf_path)
87
+ collected_text = []
88
+ for page_index, page in enumerate(doc):
89
+ text = page.get_text("text") or ""
90
+ collected_text.append(f"--- Page {page_index + 1} ---\n{text.strip()}\n")
91
+ local_text = "\n".join(collected_text).strip()
92
+ if local_text:
93
+ print("✅ Extracted text locally via PyMuPDF")
94
+ return jsonify({"extracted_text": local_text})
95
+ except Exception as e:
96
+ print(f"⚠️ Local PyMuPDF extraction failed: {e}")
97
+
98
+ # Fallback to remote endpoint for complex layout parsing
99
+ try:
100
+ print("🚀 Sending PDF to /handle_complex_doc...")
101
+ result = ocr_client.predict(
102
+ file_path=handle_file(pdf_path),
103
+ use_chart_recognition=False,
104
+ api_name="/handle_complex_doc"
105
+ )
106
+ print("✅ OCR completed for PDF")
107
+ print(f"OCR raw result: {result}")
108
+
109
+ if isinstance(result, (list, tuple)) and len(result) >= 1:
110
+ extracted_text = str(result[0])
111
+ elif isinstance(result, str):
112
+ extracted_text = result
113
+ else:
114
+ extracted_text = f"Unexpected result format: {type(result)}"
115
+
116
+ return jsonify({"extracted_text": extracted_text.strip()})
117
+ except Exception as e:
118
+ return jsonify({"error": f"Error processing PDF: {e}"}), 500
119
+
120
+ return jsonify({"error": "No file uploaded. Please upload an image or a PDF."}), 400
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
+ except Exception as e:
123
+ print(f"❌ Fatal error in /extract: {e}")
124
+ return jsonify({"error": f"Fatal error: {str(e)}"}), 500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
 
 
 
 
 
126
 
127
  if __name__ == "__main__":
128
+ app.run(debug=True)