Files changed (4) hide show
  1. README.md +6 -52
  2. app.py +219 -593
  3. languages.json +95 -0
  4. requirements.txt +2 -4
README.md CHANGED
@@ -1,58 +1,12 @@
1
  ---
2
- title: Surya OCR Studio
3
- emoji: 📄
4
- colorFrom: green
5
- colorTo: blue
6
  sdk: gradio
7
- sdk_version: 6.11.0
8
  app_file: app.py
9
  pinned: false
10
  ---
11
 
12
- # Surya OCR Studio
13
-
14
- Complete document OCR toolkit supporting 90+ languages with ZeroGPU.
15
-
16
- ## Features
17
-
18
- | Feature | Description |
19
- |---------|-------------|
20
- | **OCR** | Text recognition in 90+ languages |
21
- | **Text Detection** | Line-level text detection |
22
- | **Layout Analysis** | Identify tables, figures, headers, captions, etc. |
23
- | **Table Recognition** | Extract table structure to Markdown |
24
- | **LaTeX OCR** | Convert equation images to LaTeX |
25
-
26
- ## Usage
27
-
28
- ### OCR
29
- 1. Upload an image (JPG, PNG, etc.)
30
- 2. Specify language codes (e.g., "en" or "en, pt, es")
31
- 3. Click "Run OCR"
32
-
33
- ### Table Recognition
34
- 1. Upload an image of a table
35
- 2. Click "Recognize Table"
36
- 3. Get Markdown output
37
-
38
- ### LaTeX OCR
39
- 1. Upload a cropped equation image
40
- 2. Click "Extract LaTeX"
41
- 3. Copy the LaTeX code
42
-
43
- ## Supported Languages
44
-
45
- English (en), Portuguese (pt), Spanish (es), French (fr), German (de), Italian (it), Dutch (nl), Russian (ru), Chinese (zh), Japanese (ja), Korean (ko), Arabic (ar), Hindi (hi), and 80+ more.
46
-
47
- ## Model
48
-
49
- - [Surya OCR](https://github.com/datalab-to/surya) by datalab-to
50
-
51
- ## License
52
-
53
- - Code: GPL-3.0
54
- - Model weights: Modified AI Pubs Open Rail-M license (free for research, personal use, and startups under $2M funding/revenue)
55
-
56
- ## Space by
57
-
58
- [@artificialguybr](https://twitter.com/artificialguybr)
 
1
  ---
2
+ title: Surya OCR
3
+ emoji: 👀
4
+ colorFrom: purple
5
+ colorTo: green
6
  sdk: gradio
7
+ sdk_version: 4.41.0
8
  app_file: app.py
9
  pinned: false
10
  ---
11
 
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py CHANGED
@@ -1,623 +1,249 @@
1
- """
2
- Surya OCR Studio - Complete Implementation
3
- Features: OCR, Text Detection, Layout Analysis, Table Recognition, LaTeX OCR
4
- """
5
-
6
- # Import spaces FIRST before any CUDA-related packages
7
- import spaces
8
  import gradio as gr
9
  import logging
10
  import os
11
  import json
12
- from PIL import Image, ImageDraw, ImageFont
13
- from typing import List, Optional
14
  import torch
15
-
16
- # Configure logging
17
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
18
- logger = logging.getLogger(__name__)
19
-
20
- # Performance optimizations for ZeroGPU
21
- os.environ["RECOGNITION_BATCH_SIZE"] = "64"
22
- os.environ["DETECTOR_BATCH_SIZE"] = "8"
23
- os.environ["LAYOUT_BATCH_SIZE"] = "8"
24
- os.environ["TABLE_REC_BATCH_SIZE"] = "16"
25
-
26
- # Surya imports
27
- from surya.foundation import FoundationPredictor
28
- from surya.recognition import RecognitionPredictor
29
- from surya.detection import DetectionPredictor
30
- from surya.layout import LayoutPredictor
31
- from surya.table_rec import TableRecPredictor
32
- from surya.texify import TexifyPredictor
33
  from surya.settings import settings
 
 
34
 
35
- logger.info("Loading Surya models...")
36
-
37
- # Initialize predictors (lazy loading for faster startup)
38
- _foundation_predictor = None
39
- _detection_predictor = None
40
- _recognition_predictor = None
41
- _layout_predictor = None
42
- _table_rec_predictor = None
43
- _texify_predictor = None
44
-
45
- def get_foundation_predictor():
46
- global _foundation_predictor
47
- if _foundation_predictor is None:
48
- _foundation_predictor = FoundationPredictor()
49
- return _foundation_predictor
50
-
51
- def get_detection_predictor():
52
- global _detection_predictor
53
- if _detection_predictor is None:
54
- _detection_predictor = DetectionPredictor()
55
- return _detection_predictor
56
-
57
- def get_recognition_predictor():
58
- global _recognition_predictor
59
- if _recognition_predictor is None:
60
- _recognition_predictor = RecognitionPredictor(get_foundation_predictor())
61
- return _recognition_predictor
62
-
63
- def get_layout_predictor():
64
- global _layout_predictor
65
- if _layout_predictor is None:
66
- _layout_predictor = LayoutPredictor(
67
- FoundationPredictor(checkpoint=settings.LAYOUT_MODEL_CHECKPOINT)
68
- )
69
- return _layout_predictor
70
-
71
- def get_table_rec_predictor():
72
- global _table_rec_predictor
73
- if _table_rec_predictor is None:
74
- _table_rec_predictor = TableRecPredictor()
75
- return _table_rec_predictor
76
-
77
- def get_texify_predictor():
78
- global _texify_predictor
79
- if _texify_predictor is None:
80
- _texify_predictor = TexifyPredictor()
81
- return _texify_predictor
82
-
83
- logger.info("Models will be loaded on first use.")
84
-
85
- # Layout labels and colors
86
- LAYOUT_LABELS = {
87
- 'Text': '#10B981', # Green
88
- 'Title': '#EF4444', # Red
89
- 'Section-header': '#F59E0B', # Amber
90
- 'Table': '#3B82F6', # Blue
91
- 'Figure': '#8B5CF6', # Purple
92
- 'Picture': '#8B5CF6', # Purple
93
- 'Caption': '#EC4899', # Pink
94
- 'Page-header': '#6366F1', # Indigo
95
- 'Page-footer': '#6366F1', # Indigo
96
- 'Footnote': '#84CC16', # Lime
97
- 'Formula': '#F97316', # Orange
98
- 'List-item': '#14B8A6', # Teal
99
- 'Form': '#A855F7', # Fuchsia
100
- 'Handwriting': '#64748B', # Slate
101
- 'Table-of-contents': '#0EA5E9', # Sky
102
- }
103
-
104
- # Supported languages
105
- LANGUAGES = {
106
- "en": "English", "pt": "Portuguese", "es": "Spanish", "fr": "French",
107
- "de": "German", "it": "Italian", "nl": "Dutch", "ru": "Russian",
108
- "zh": "Chinese", "ja": "Japanese", "ko": "Korean", "ar": "Arabic",
109
- "hi": "Hindi", "bn": "Bengali", "tr": "Turkish", "vi": "Vietnamese",
110
- "th": "Thai", "id": "Indonesian", "pl": "Polish", "uk": "Ukrainian",
111
- "cs": "Czech", "sv": "Swedish", "da": "Danish", "no": "Norwegian",
112
- "fi": "Finnish", "el": "Greek", "he": "Hebrew", "hu": "Hungarian",
113
- "ro": "Romanian", "sk": "Slovak", "bg": "Bulgarian", "hr": "Croatian",
114
- "sl": "Slovenian", "et": "Estonian", "lv": "Latvian", "lt": "Lithuanian",
115
- "fa": "Persian", "ur": "Urdu", "ta": "Tamil", "te": "Telugu",
116
- "ml": "Malayalam", "kn": "Kannada", "gu": "Gujarati", "mr": "Marathi",
117
- "pa": "Punjabi", "ne": "Nepali", "si": "Sinhala", "my": "Burmese",
118
- "km": "Khmer", "lo": "Lao", "ka": "Georgian", "hy": "Armenian",
119
- }
120
-
121
-
122
- def prepare_image(image) -> Image.Image:
123
- """Prepare image for processing"""
124
- if isinstance(image, str):
125
- image = Image.open(image)
126
- elif hasattr(image, 'name'):
127
- image = Image.open(image.name)
128
-
129
- if image.mode != 'RGB':
130
- image = image.convert('RGB')
131
- return image
132
-
133
-
134
- def draw_text_lines(image, text_lines, color=(0, 255, 0)):
135
- """Draw text line bounding boxes"""
136
- draw = ImageDraw.Draw(image)
137
- for line in text_lines:
138
- if hasattr(line, 'bbox'):
139
- bbox = line.bbox
140
- if len(bbox) == 4:
141
- draw.rectangle(bbox, outline=color, width=2)
142
- return image
143
-
144
-
145
- def draw_layout_boxes(image, bboxes):
146
- """Draw layout boxes with labels and colors"""
147
- draw = ImageDraw.Draw(image)
148
-
149
- for bbox in bboxes:
150
- label = getattr(bbox, 'label', 'Text')
151
- color = LAYOUT_LABELS.get(label, '#FFFFFF')
152
-
153
- # Convert hex to RGB
154
- rgb = tuple(int(color.lstrip('#')[i:i+2], 16) for i in (0, 2, 4))
155
-
156
- box = bbox.bbox if hasattr(bbox, 'bbox') else bbox
157
- if len(box) == 4:
158
- draw.rectangle(box, outline=rgb, width=2)
159
-
160
- # Draw label
161
- label_text = label.replace('-', ' ').title()
162
- draw.text((box[0], box[1] - 12), label_text, fill=rgb)
163
-
164
- return image
165
 
 
 
 
166
 
167
- def draw_table_cells(image, predictions):
168
- """Draw table cells with row/column info"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  draw = ImageDraw.Draw(image)
170
-
171
- # Draw rows in blue
172
- for row in predictions.rows:
173
- draw.rectangle(row.bbox, outline=(0, 0, 255), width=2)
174
-
175
- # Draw columns in green
176
- for col in predictions.cols:
177
- draw.rectangle(col.bbox, outline=(0, 255, 0), width=2)
178
-
179
- # Draw cells in red
180
- for cell in predictions.cells:
181
- draw.rectangle(cell.bbox, outline=(255, 0, 0), width=1)
182
- # Draw cell text if available
183
- if hasattr(cell, 'text') and cell.text:
184
- draw.text((cell.bbox[0], cell.bbox[1]), cell.text[:10], fill=(100, 100, 100))
185
-
186
  return image
187
 
188
-
189
- @spaces.GPU(duration=120)
190
- def process_ocr(image, languages: str, disable_math: bool = False):
191
- """Run OCR on image"""
192
- logger.info(f"Running OCR with languages: {languages}")
193
  try:
194
- image = prepare_image(image)
195
-
196
- # Parse languages
197
- langs = [l.strip() for l in languages.split(',') if l.strip()]
198
- if not langs:
199
- langs = ['en']
200
-
201
- # Get predictors
202
- det_pred = get_detection_predictor()
203
- rec_pred = get_recognition_predictor()
204
-
205
- # Run OCR
206
- predictions = rec_pred(
207
- [image],
208
- det_predictor=det_pred,
209
- langs=[langs],
210
- disable_math=disable_math
211
- )
212
-
213
- if not predictions or len(predictions) == 0:
214
- return "", {}, None
215
-
216
- pred = predictions[0]
217
-
218
- # Extract text
219
- text_lines = pred.text_lines if hasattr(pred, 'text_lines') else []
220
- full_text = "\n".join([line.text for line in text_lines if hasattr(line, 'text')])
221
-
222
- # Build JSON result
223
- result = {
224
- "text": full_text,
225
- "languages": langs,
226
- "num_lines": len(text_lines),
227
- "lines": [
228
- {
229
- "text": line.text,
230
- "confidence": round(line.confidence, 3) if hasattr(line, 'confidence') else 1.0,
231
- "bbox": list(line.bbox) if hasattr(line, 'bbox') else []
232
- }
233
- for line in text_lines
234
- ]
235
- }
236
-
237
- # Draw bounding boxes
238
- img_with_boxes = draw_text_lines(image.copy(), text_lines)
239
-
240
- return full_text, result, img_with_boxes
241
-
242
- except Exception as e:
243
- logger.error(f"OCR Error: {e}", exc_info=True)
244
- return f"Error: {str(e)}", {"error": str(e)}, None
245
-
246
-
247
- @spaces.GPU(duration=60)
248
- def process_detection(image):
249
- """Run text detection on image"""
250
- logger.info("Running text detection")
251
- try:
252
- image = prepare_image(image)
253
-
254
- det_pred = get_detection_predictor()
255
- predictions = det_pred([image])
256
-
257
- if not predictions or len(predictions) == 0:
258
- return {}, None
259
-
260
- pred = predictions[0]
261
-
262
- # Build result
263
- result = {
264
- "num_lines": len(pred.bboxes) if hasattr(pred, 'bboxes') else 0,
265
- "image_size": list(image.size),
266
- "bboxes": [
267
- {
268
- "bbox": list(bbox.bbox) if hasattr(bbox, 'bbox') else list(bbox),
269
- "confidence": round(bbox.confidence, 3) if hasattr(bbox, 'confidence') else 1.0
270
- }
271
- for bbox in (pred.bboxes if hasattr(pred, 'bboxes') else [])
272
- ]
273
- }
274
 
275
- # Draw boxes
276
- img_with_boxes = image.copy()
277
- draw = ImageDraw.Draw(img_with_boxes)
278
- for bbox in (pred.bboxes if hasattr(pred, 'bboxes') else []):
279
- box = bbox.bbox if hasattr(bbox, 'bbox') else bbox
280
- draw.rectangle(box, outline=(0, 255, 0), width=2)
281
 
282
- return result, img_with_boxes
 
283
 
 
 
284
  except Exception as e:
285
- logger.error(f"Detection Error: {e}", exc_info=True)
286
- return {"error": str(e)}, None
287
 
288
-
289
- @spaces.GPU(duration=60)
290
- def process_layout(image):
291
- """Run layout analysis on image"""
292
- logger.info("Running layout analysis")
293
  try:
294
- image = prepare_image(image)
295
-
296
- layout_pred = get_layout_predictor()
297
- predictions = layout_pred([image])
298
-
299
- if not predictions or len(predictions) == 0:
300
- return {}, None
301
-
302
- pred = predictions[0]
303
-
304
- # Count by label
305
- label_counts = {}
306
- for bbox in (pred.bboxes if hasattr(pred, 'bboxes') else []):
307
- label = getattr(bbox, 'label', 'Unknown')
308
- label_counts[label] = label_counts.get(label, 0) + 1
309
-
310
- # Build result
311
- result = {
312
- "num_elements": len(pred.bboxes) if hasattr(pred, 'bboxes') else 0,
313
- "label_counts": label_counts,
314
- "elements": [
315
- {
316
- "label": getattr(bbox, 'label', 'Unknown'),
317
- "confidence": round(getattr(bbox, 'confidence', 1.0), 3),
318
- "position": getattr(bbox, 'position', -1),
319
- "bbox": list(bbox.bbox) if hasattr(bbox, 'bbox') else []
320
- }
321
- for bbox in (pred.bboxes if hasattr(pred, 'bboxes') else [])
322
- ]
323
- }
324
-
325
- # Draw layout boxes
326
- img_with_boxes = draw_layout_boxes(image.copy(), pred.bboxes if hasattr(pred, 'bboxes') else [])
327
-
328
- return result, img_with_boxes
329
-
330
  except Exception as e:
331
- logger.error(f"Layout Error: {e}", exc_info=True)
332
- return {"error": str(e)}, None
333
-
334
 
335
- @spaces.GPU(duration=90)
336
- def process_table(image):
337
- """Run table recognition on image"""
338
- logger.info("Running table recognition")
339
  try:
340
- image = prepare_image(image)
341
-
342
- table_pred = get_table_rec_predictor()
343
- predictions = table_pred([image])
344
-
345
- if not predictions or len(predictions) == 0:
346
- return {}, None, ""
347
-
348
- pred = predictions[0]
349
-
350
- # Build markdown table
351
- md_table = ""
352
- if hasattr(pred, 'cells') and pred.cells:
353
- # Find max row and col
354
- max_row = max(c.row_id for c in pred.cells) if pred.cells else 0
355
- max_col = max(c.col_id for c in pred.cells) if pred.cells else 0
356
-
357
- # Create table data
358
- table_data = [["" for _ in range(max_col + 1)] for _ in range(max_row + 1)]
359
-
360
- for cell in pred.cells:
361
- text = getattr(cell, 'text', '')
362
- table_data[cell.row_id][cell.col_id] = text
363
-
364
- # Build markdown
365
- md_lines = []
366
- for i, row in enumerate(table_data):
367
- md_lines.append("| " + " | ".join(row) + " |")
368
- if i == 0:
369
- md_lines.append("| " + " | ".join(["---"] * len(row)) + " |")
370
-
371
- md_table = "\n".join(md_lines)
372
-
373
- # Build result
374
- result = {
375
- "num_rows": len(pred.rows) if hasattr(pred, 'rows') else 0,
376
- "num_cols": len(pred.cols) if hasattr(pred, 'cols') else 0,
377
- "num_cells": len(pred.cells) if hasattr(pred, 'cells') else 0,
378
- "rows": [
379
- {"row_id": r.row_id, "is_header": getattr(r, 'is_header', False)}
380
- for r in (pred.rows if hasattr(pred, 'rows') else [])
381
- ],
382
- "cols": [
383
- {"col_id": c.col_id, "is_header": getattr(c, 'is_header', False)}
384
- for c in (pred.cols if hasattr(pred, 'cols') else [])
385
- ]
386
- }
387
-
388
- # Draw table cells
389
- img_with_boxes = draw_table_cells(image.copy(), pred)
390
-
391
- return result, img_with_boxes, md_table
392
-
393
  except Exception as e:
394
- logger.error(f"Table Recognition Error: {e}", exc_info=True)
395
- return {"error": str(e)}, None, ""
396
-
397
 
398
- @spaces.GPU(duration=60)
399
- def process_latex(image):
400
- """Run LaTeX OCR on image (equation)"""
401
- logger.info("Running LaTeX OCR")
402
  try:
403
- image = prepare_image(image)
404
-
405
- texify_pred = get_texify_predictor()
406
- predictions = texify_pred([image])
407
-
408
- if not predictions or len(predictions) == 0:
409
- return "", {}
410
-
411
- pred = predictions[0]
412
- latex = pred.text if hasattr(pred, 'text') else str(pred)
413
-
414
- result = {
415
- "latex": latex,
416
- "markdown": f"$$\n{latex}\n$$"
417
- }
418
-
419
- return latex, result
420
-
421
  except Exception as e:
422
- logger.error(f"LaTeX OCR Error: {e}", exc_info=True)
423
- return f"Error: {str(e)}", {"error": str(e)}
424
-
425
-
426
- # ============== GRADIO UI ==============
427
-
428
- CSS = """
429
- @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=Fira+Code:wght@400;500&display=swap');
430
-
431
- :root {
432
- --bg: #0a0f1a;
433
- --surf: #0f1629;
434
- --card: #151d32;
435
- --border: #1e2a45;
436
- --border2: #2a3a5a;
437
- --green: #10b981;
438
- --blue: #3b82f6;
439
- --text: #e2e8f0;
440
- --muted: #64748b;
441
- }
442
-
443
- body, .gradio-container {
444
- background: var(--bg) !important;
445
- font-family: 'Outfit', sans-serif !important;
446
- color: var(--text) !important;
447
- }
448
-
449
- .gradio-container::before {
450
- content: '';
451
- position: fixed; inset: 0; pointer-events: none; z-index: 0;
452
- background: radial-gradient(ellipse 70% 50% at 50% -10%, rgba(16,185,129,0.08) 0%, transparent 65%);
453
- }
454
-
455
- .app-hero { padding: 40px 0 20px; text-align: center; }
456
- .app-hero h1 {
457
- font-size: 2.8rem; font-weight: 800; letter-spacing: -0.04em;
458
- background: linear-gradient(135deg, #10b981, #06b6d4, #3b82f6);
459
- -webkit-background-clip: text; -webkit-text-fill-color: transparent;
460
- margin-bottom: 8px;
461
- }
462
- .app-hero .tagline { color: var(--muted); font-size: 1rem; }
463
- .pills { display: flex; justify-content: center; gap: 8px; margin-top: 16px; flex-wrap: wrap; }
464
- .pill {
465
- background: var(--card); border: 1px solid var(--border2); border-radius: 100px;
466
- padding: 5px 14px; font-size: 0.75rem; color: var(--muted); font-family: 'Fira Code', monospace;
467
- }
468
- .pill.green { color: var(--green); border-color: rgba(16,185,129,0.3); }
469
-
470
- .tabs button { font-family: 'Outfit', sans-serif !important; font-weight: 500 !important; }
471
-
472
- button.primary-btn {
473
- background: linear-gradient(135deg, #10b981, #06b6d4) !important;
474
- border: none !important; color: #000 !important; font-weight: 600 !important;
475
- padding: 12px 24px !important;
476
- }
477
-
478
- .output-image img { border-radius: 8px; }
479
-
480
- footer { display: none !important; }
481
- """
482
 
483
- # Language string for the dropdown
484
- LANGUAGE_OPTIONS = [f"{code} - {name}" for code, name in sorted(LANGUAGES.items(), key=lambda x: x[1])]
485
-
486
- with gr.Blocks(theme=gr.themes.Base(), css=CSS, title="Surya OCR Studio") as app:
487
-
488
- gr.HTML("""
489
- <div class="app-hero">
490
- <h1>📄 Surya OCR Studio</h1>
491
- <p class="tagline">Document OCR · Layout Analysis · Table Recognition · 90+ Languages</p>
492
- <div class="pills">
493
- <span class="pill green">ZeroGPU ⚡</span>
494
- <span class="pill">90+ Languages</span>
495
- <span class="pill">Layout Detection</span>
496
- <span class="pill">Table Recognition</span>
497
- <span class="pill">LaTeX OCR</span>
498
- </div>
499
- </div>
500
- """)
501
 
502
- with gr.Tabs() as tabs:
503
-
504
- # ============ OCR TAB ============
505
- with gr.TabItem("📝 OCR"):
506
- gr.Markdown("### Optical Character Recognition\nExtract text from images in 90+ languages.")
507
-
508
- with gr.Row():
509
- with gr.Column(scale=2):
510
- ocr_input = gr.Image(label="📄 Upload Image", type="pil", height=400)
511
-
512
- with gr.Row():
513
- ocr_langs = gr.Textbox(
514
- label="Languages (comma-separated codes)",
515
- value="en",
516
- placeholder="en, pt, es, de, fr...",
517
- info="Use language codes: en=English, pt=Portuguese, es=Spanish..."
518
- )
519
- ocr_disable_math = gr.Checkbox(label="Disable math detection", value=False)
520
-
521
- ocr_btn = gr.Button("🚀 Run OCR", variant="primary", elem_classes=["primary-btn"])
522
-
523
- with gr.Column(scale=3):
524
- ocr_text = gr.Textbox(label="📝 Extracted Text", lines=12, show_copy_button=True)
525
- ocr_json = gr.JSON(label="📊 Detailed Results")
526
-
527
- ocr_image = gr.Image(label="🖼️ Detected Text Lines", elem_classes=["output-image"])
528
-
529
- ocr_btn.click(
530
- process_ocr,
531
- [ocr_input, ocr_langs, ocr_disable_math],
532
- [ocr_text, ocr_json, ocr_image]
533
- )
534
-
535
- # ============ TEXT DETECTION TAB ============
536
- with gr.TabItem("🔍 Text Detection"):
537
- gr.Markdown("### Text Line Detection\nDetect text lines in documents without OCR.")
538
-
539
- with gr.Row():
540
- det_input = gr.Image(label="📄 Upload Image", type="pil", height=400)
541
- with gr.Column():
542
- det_json = gr.JSON(label="📊 Detection Results")
543
- det_btn = gr.Button("🔍 Detect Text Lines", variant="primary", elem_classes=["primary-btn"])
544
-
545
- det_image = gr.Image(label="🖼️ Detected Lines", elem_classes=["output-image"])
546
-
547
- det_btn.click(process_detection, [det_input], [det_json, det_image])
548
-
549
- # ============ LAYOUT ANALYSIS TAB ============
550
- with gr.TabItem("📊 Layout Analysis"):
551
- gr.Markdown("### Document Layout Analysis\nIdentify document structure: titles, tables, figures, etc.")
552
-
553
- with gr.Row():
554
- layout_input = gr.Image(label="📄 Upload Image", type="pil", height=400)
555
- with gr.Column():
556
- layout_json = gr.JSON(label="📊 Layout Results")
557
- layout_btn = gr.Button("📊 Analyze Layout", variant="primary", elem_classes=["primary-btn"])
558
-
559
- layout_image = gr.Image(label="🖼️ Layout Elements", elem_classes=["output-image"])
560
-
561
- # Legend
562
- gr.Markdown("""
563
- **Legend:**
564
- 🟢 Text | 🔴 Title | 🟡 Section Header | 🔵 Table | 🟣 Figure/Picture | 🩷 Caption | 🔷 Header/Footer
565
- """)
566
-
567
- layout_btn.click(process_layout, [layout_input], [layout_json, layout_image])
568
-
569
- # ============ TABLE RECOGNITION TAB ============
570
- with gr.TabItem("📋 Table Recognition"):
571
- gr.Markdown("### Table Recognition\nExtract table structure and convert to Markdown.")
572
-
573
- with gr.Row():
574
- table_input = gr.Image(label="📄 Upload Table Image", type="pil", height=400)
575
- with gr.Column():
576
- table_json = gr.JSON(label="📊 Table Structure")
577
- table_btn = gr.Button("📋 Recognize Table", variant="primary", elem_classes=["primary-btn"])
578
-
579
- table_image = gr.Image(label="🖼️ Table Cells", elem_classes=["output-image"])
580
- table_md = gr.Textbox(label="📝 Markdown Output", lines=10, show_copy_button=True)
581
-
582
- table_btn.click(process_table, [table_input], [table_json, table_image, table_md])
583
-
584
- # ============ LATEX OCR TAB ============
585
- with gr.TabItem("🔢 LaTeX OCR"):
586
- gr.Markdown("### LaTeX Equation OCR\nConvert equation images to LaTeX code.\n\n**Tip:** Crop the image to just the equation for best results.")
587
-
588
- with gr.Row():
589
- latex_input = gr.Image(label="📄 Upload Equation Image", type="pil", height=300)
590
- with gr.Column():
591
- latex_code = gr.Textbox(label="🔢 LaTeX Code", lines=5, show_copy_button=True)
592
- latex_json = gr.JSON(label="📊 Results")
593
- latex_btn = gr.Button("🔢 Extract LaTeX", variant="primary", elem_classes=["primary-btn"])
594
-
595
- latex_btn.click(process_latex, [latex_input], [latex_code, latex_json])
596
-
597
- # ============ FOOTER ============
598
- gr.Markdown("""
599
- ---
600
-
601
- ### ℹ️ About Surya OCR
602
-
603
- | Feature | Description |
604
- |---------|-------------|
605
- | **OCR** | Text recognition in 90+ languages |
606
- | **Detection** | Line-level text detection |
607
- | **Layout** | Identify tables, figures, headers, etc. |
608
- | **Tables** | Extract table structure to Markdown |
609
- | **LaTeX** | Convert equations to LaTeX |
610
-
611
- **Performance Tips:**
612
- - Use higher resolution images for better accuracy
613
- - For blurry text, try preprocessing (binarization, deskewing)
614
- - Specify correct language codes for best OCR results
615
-
616
- **Model:** [Surya OCR](https://github.com/datalab-to/surya) by datalab-to
617
- **Space by:** [@artificialguybr](https://twitter.com/artificialguybr)
618
- """)
619
-
620
 
621
  if __name__ == "__main__":
622
- app.queue(max_size=20)
623
- app.launch()
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
  import logging
3
  import os
4
  import json
5
+ from PIL import Image, ImageDraw
 
6
  import torch
7
+ from surya.ocr import run_ocr
8
+ from surya.detection import batch_text_detection
9
+ from surya.layout import batch_layout_detection
10
+ from surya.ordering import batch_ordering
11
+ from surya.model.detection.model import load_model as load_det_model, load_processor as load_det_processor
12
+ from surya.model.recognition.model import load_model as load_rec_model
13
+ from surya.model.recognition.processor import load_processor as load_rec_processor
 
 
 
 
 
 
 
 
 
 
 
14
  from surya.settings import settings
15
+ from surya.model.ordering.processor import load_processor as load_order_processor
16
+ from surya.model.ordering.model import load_model as load_order_model
17
 
18
+ # Configuração do TorchDynamo
19
+ torch._dynamo.config.capture_scalar_outputs = True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
+ # Configuração de logging
22
+ logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
23
+ logger = logging.getLogger(__name__)
24
 
25
+ # Configuração de variáveis de ambiente
26
+ logger.info("Configurando variáveis de ambiente para otimização de performance")
27
+ os.environ["RECOGNITION_BATCH_SIZE"] = "512"
28
+ os.environ["DETECTOR_BATCH_SIZE"] = "36"
29
+ os.environ["ORDER_BATCH_SIZE"] = "32"
30
+ os.environ["RECOGNITION_STATIC_CACHE"] = "true"
31
+
32
+ # Carregamento de modelos
33
+ logger.info("Iniciando carregamento dos modelos...")
34
+
35
+ try:
36
+ logger.debug("Carregando modelo e processador de detecção...")
37
+ det_processor, det_model = load_det_processor(), load_det_model()
38
+ logger.debug("Modelo e processador de detecção carregados com sucesso")
39
+ except Exception as e:
40
+ logger.error(f"Erro ao carregar modelo de detecção: {e}")
41
+ raise
42
+
43
+ try:
44
+ logger.debug("Carregando modelo e processador de reconhecimento...")
45
+ rec_model, rec_processor = load_rec_model(), load_rec_processor()
46
+ logger.debug("Modelo e processador de reconhecimento carregados com sucesso")
47
+ except Exception as e:
48
+ logger.error(f"Erro ao carregar modelo de reconhecimento: {e}")
49
+ raise
50
+
51
+ try:
52
+ logger.debug("Carregando modelo e processador de layout...")
53
+ layout_model = load_det_model(checkpoint=settings.LAYOUT_MODEL_CHECKPOINT)
54
+ layout_processor = load_det_processor(checkpoint=settings.LAYOUT_MODEL_CHECKPOINT)
55
+ logger.debug("Modelo e processador de layout carregados com sucesso")
56
+ except Exception as e:
57
+ logger.error(f"Erro ao carregar modelo de layout: {e}")
58
+ raise
59
+
60
+ try:
61
+ logger.debug("Carregando modelo e processador de ordenação...")
62
+ order_model = load_order_model()
63
+ order_processor = load_order_processor()
64
+ logger.debug("Modelo e processador de ordenação carregados com sucesso")
65
+ except Exception as e:
66
+ logger.error(f"Erro ao carregar modelo de ordenação: {e}")
67
+ raise
68
+
69
+ logger.info("Todos os modelos foram carregados com sucesso")
70
+
71
+ # Compilação do modelo de reconhecimento
72
+ logger.info("Iniciando compilação do modelo de reconhecimento...")
73
+ try:
74
+ rec_model.decoder.model = torch.compile(rec_model.decoder.model)
75
+ logger.info("Compilação do modelo de reconhecimento concluída com sucesso")
76
+ except Exception as e:
77
+ logger.error(f"Erro durante a compilação do modelo de reconhecimento: {e}")
78
+ logger.warning("Continuando sem compilação do modelo")
79
+
80
+ class CustomJSONEncoder(json.JSONEncoder):
81
+ def default(self, obj):
82
+ if isinstance(obj, Image.Image):
83
+ return "Image object (not serializable)"
84
+ if hasattr(obj, '__dict__'):
85
+ return {k: self.default(v) for k, v in obj.__dict__.items()}
86
+ return str(obj)
87
+
88
+ def serialize_result(result):
89
+ return json.dumps(result, cls=CustomJSONEncoder, indent=2)
90
+
91
+ def draw_boxes(image, predictions, color=(255, 0, 0)):
92
  draw = ImageDraw.Draw(image)
93
+ if isinstance(predictions, list):
94
+ for pred in predictions:
95
+ if hasattr(pred, 'bboxes'):
96
+ for bbox in pred.bboxes:
97
+ draw.rectangle(bbox, outline=color, width=2)
98
+ elif hasattr(pred, 'bbox'):
99
+ draw.rectangle(pred.bbox, outline=color, width=2)
100
+ elif hasattr(pred, 'polygon'):
101
+ draw.polygon(pred.polygon, outline=color, width=2)
102
+ elif hasattr(predictions, 'bboxes'):
103
+ for bbox in predictions.bboxes:
104
+ draw.rectangle(bbox, outline=color, width=2)
 
 
 
 
105
  return image
106
 
107
+ def ocr_workflow(image, langs):
108
+ logger.info(f"Iniciando workflow OCR com idiomas: {langs}")
 
 
 
109
  try:
110
+ image = Image.open(image.name)
111
+ logger.debug(f"Imagem carregada: {image.size}")
112
+ predictions = run_ocr([image], [langs.split(',')], det_model, det_processor, rec_model, rec_processor)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
 
114
+ # Draw bounding boxes on the image
115
+ image_with_boxes = draw_boxes(image.copy(), predictions[0].text_lines)
 
 
 
 
116
 
117
+ # Format the OCR results
118
+ formatted_text = "\n".join([line.text for line in predictions[0].text_lines])
119
 
120
+ logger.info("Workflow OCR concluído com sucesso")
121
+ return serialize_result(predictions), image_with_boxes, formatted_text
122
  except Exception as e:
123
+ logger.error(f"Erro durante o workflow OCR: {e}")
124
+ return serialize_result({"error": str(e)}), None, ""
125
 
126
+ def text_detection_workflow(image):
127
+ logger.info("Iniciando workflow de detecção de texto")
 
 
 
128
  try:
129
+ image = Image.open(image.name)
130
+ logger.debug(f"Imagem carregada: {image.size}")
131
+ predictions = batch_text_detection([image], det_model, det_processor)
132
+
133
+ # Draw bounding boxes on the image
134
+ image_with_boxes = draw_boxes(image.copy(), predictions)
135
+
136
+ # Convert predictions to a serializable format
137
+ serializable_predictions = []
138
+ for pred in predictions:
139
+ serializable_pred = {
140
+ 'bboxes': [bbox.tolist() if hasattr(bbox, 'tolist') else bbox for bbox in pred.bboxes],
141
+ 'polygons': [poly.tolist() if hasattr(poly, 'tolist') else poly for poly in pred.polygons],
142
+ 'confidences': pred.confidences,
143
+ 'vertical_lines': [line.tolist() if hasattr(line, 'tolist') else line for line in pred.vertical_lines],
144
+ 'image_bbox': pred.image_bbox.tolist() if hasattr(pred.image_bbox, 'tolist') else pred.image_bbox
145
+ }
146
+ serializable_predictions.append(serializable_pred)
147
+
148
+ logger.info("Workflow de detecção de texto concluído com sucesso")
149
+ return serialize_result(serializable_predictions), image_with_boxes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  except Exception as e:
151
+ logger.error(f"Erro durante o workflow de detecção de texto: {e}")
152
+ return serialize_result({"error": str(e)}), None
 
153
 
154
+ def layout_analysis_workflow(image):
155
+ logger.info("Iniciando workflow de análise de layout")
 
 
156
  try:
157
+ image = Image.open(image.name)
158
+ logger.debug(f"Imagem carregada: {image.size}")
159
+ line_predictions = batch_text_detection([image], det_model, det_processor)
160
+ logger.debug(f"Detecção de linhas concluída. Número de linhas detectadas: {len(line_predictions[0].bboxes)}")
161
+ layout_predictions = batch_layout_detection([image], layout_model, layout_processor, line_predictions)
162
+
163
+ # Draw bounding boxes on the image
164
+ image_with_boxes = draw_boxes(image.copy(), layout_predictions[0], color=(0, 255, 0))
165
+
166
+ # Convert predictions to a serializable format
167
+ serializable_predictions = []
168
+ for pred in layout_predictions:
169
+ serializable_pred = {
170
+ 'bboxes': [
171
+ {
172
+ 'bbox': bbox.bbox.tolist() if hasattr(bbox.bbox, 'tolist') else bbox.bbox,
173
+ 'polygon': bbox.polygon.tolist() if hasattr(bbox.polygon, 'tolist') else bbox.polygon,
174
+ 'confidence': bbox.confidence,
175
+ 'label': bbox.label
176
+ } for bbox in pred.bboxes
177
+ ],
178
+ 'image_bbox': pred.image_bbox.tolist() if hasattr(pred.image_bbox, 'tolist') else pred.image_bbox
179
+ }
180
+ serializable_predictions.append(serializable_pred)
181
+
182
+ logger.info("Workflow de análise de layout concluído com sucesso")
183
+ return serialize_result(serializable_predictions), image_with_boxes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  except Exception as e:
185
+ logger.error(f"Erro durante o workflow de análise de layout: {e}")
186
+ return serialize_result({"error": str(e)}), None
 
187
 
188
+ def reading_order_workflow(image):
189
+ logger.info("Iniciando workflow de ordem de leitura")
 
 
190
  try:
191
+ image = Image.open(image.name)
192
+ logger.debug(f"Imagem carregada: {image.size}")
193
+ line_predictions = batch_text_detection([image], det_model, det_processor)
194
+ logger.debug(f"Detecção de linhas concluída. Número de linhas detectadas: {len(line_predictions[0].bboxes)}")
195
+ layout_predictions = batch_layout_detection([image], layout_model, layout_processor, line_predictions)
196
+ logger.debug(f"Análise de layout concluída. Número de elementos de layout: {len(layout_predictions[0].bboxes)}")
197
+ bboxes = [pred.bbox for pred in layout_predictions[0].bboxes]
198
+ order_predictions = batch_ordering([image], [bboxes], order_model, order_processor)
199
+
200
+ # Draw bounding boxes on the image
201
+ image_with_boxes = image.copy()
202
+ draw = ImageDraw.Draw(image_with_boxes)
203
+ for i, bbox in enumerate(order_predictions[0].bboxes):
204
+ draw.rectangle(bbox.bbox, outline=(0, 0, 255), width=2)
205
+ draw.text((bbox.bbox[0], bbox.bbox[1]), str(bbox.position), fill=(255, 0, 0))
206
+
207
+ logger.info("Workflow de ordem de leitura concluído com sucesso")
208
+ return serialize_result(order_predictions), image_with_boxes
209
  except Exception as e:
210
+ logger.error(f"Erro durante o workflow de ordem de leitura: {e}")
211
+ return serialize_result({"error": str(e)}), None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
 
213
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
214
+ gr.Markdown("# Análise de Documentos com Surya")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
 
216
+ with gr.Tab("OCR"):
217
+ gr.Markdown("## Reconhecimento Óptico de Caracteres")
218
+ with gr.Row():
219
+ ocr_input = gr.File(label="Carregar Imagem ou PDF")
220
+ ocr_langs = gr.Textbox(label="Idiomas (separados por vírgula)", value="en")
221
+ ocr_button = gr.Button("Executar OCR")
222
+ ocr_output = gr.JSON(label="Resultados OCR")
223
+ ocr_image = gr.Image(label="Imagem com Bounding Boxes")
224
+ ocr_text = gr.Textbox(label="Texto Extraído", lines=10)
225
+ ocr_button.click(ocr_workflow, inputs=[ocr_input, ocr_langs], outputs=[ocr_output, ocr_image, ocr_text])
226
+
227
+ with gr.Tab("Detecção de Texto"):
228
+ gr.Markdown("## Detecção de Linhas de Texto")
229
+ det_input = gr.File(label="Carregar Imagem ou PDF")
230
+ det_button = gr.Button("Executar Detecção de Texto")
231
+ det_output = gr.JSON(label="Resultados da Detecção de Texto")
232
+ det_image = gr.Image(label="Imagem com Bounding Boxes")
233
+ det_button.click(text_detection_workflow, inputs=det_input, outputs=[det_output, det_image])
234
+
235
+ with gr.Tab("Análise de Layout"):
236
+ gr.Markdown("## Análise de Layout e Ordem de Leitura")
237
+ layout_input = gr.File(label="Carregar Imagem ou PDF")
238
+ layout_button = gr.Button("Executar Análise de Layout")
239
+ order_button = gr.Button("Determinar Ordem de Leitura")
240
+ layout_output = gr.JSON(label="Resultados da Análise de Layout")
241
+ layout_image = gr.Image(label="Imagem com Layout")
242
+ order_output = gr.JSON(label="Resultados da Ordem de Leitura")
243
+ order_image = gr.Image(label="Imagem com Ordem de Leitura")
244
+ layout_button.click(layout_analysis_workflow, inputs=layout_input, outputs=[layout_output, layout_image])
245
+ order_button.click(reading_order_workflow, inputs=layout_input, outputs=[order_output, order_image])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
 
247
  if __name__ == "__main__":
248
+ logger.info("Iniciando aplicativo Gradio...")
249
+ demo.launch()
languages.json ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "Afrikaans": "af",
3
+ "Amharic": "am",
4
+ "Arabic": "ar",
5
+ "Assamese": "as",
6
+ "Azerbaijani": "az",
7
+ "Belarusian": "be",
8
+ "Bulgarian": "bg",
9
+ "Bengali": "bn",
10
+ "Breton": "br",
11
+ "Bosnian": "bs",
12
+ "Catalan": "ca",
13
+ "Czech": "cs",
14
+ "Welsh": "cy",
15
+ "Danish": "da",
16
+ "German": "de",
17
+ "Greek": "el",
18
+ "English": "en",
19
+ "Esperanto": "eo",
20
+ "Spanish": "es",
21
+ "Estonian": "et",
22
+ "Basque": "eu",
23
+ "Persian": "fa",
24
+ "Finnish": "fi",
25
+ "French": "fr",
26
+ "Western Frisian": "fy",
27
+ "Irish": "ga",
28
+ "Scottish Gaelic": "gd",
29
+ "Galician": "gl",
30
+ "Gujarati": "gu",
31
+ "Hausa": "ha",
32
+ "Hebrew": "he",
33
+ "Hindi": "hi",
34
+ "Croatian": "hr",
35
+ "Hungarian": "hu",
36
+ "Armenian": "hy",
37
+ "Indonesian": "id",
38
+ "Icelandic": "is",
39
+ "Italian": "it",
40
+ "Japanese": "ja",
41
+ "Javanese": "jv",
42
+ "Georgian": "ka",
43
+ "Kazakh": "kk",
44
+ "Khmer": "km",
45
+ "Kannada": "kn",
46
+ "Korean": "ko",
47
+ "Kurdish": "ku",
48
+ "Kyrgyz": "ky",
49
+ "Latin": "la",
50
+ "Lao": "lo",
51
+ "Lithuanian": "lt",
52
+ "Latvian": "lv",
53
+ "Malagasy": "mg",
54
+ "Macedonian": "mk",
55
+ "Malayalam": "ml",
56
+ "Mongolian": "mn",
57
+ "Marathi": "mr",
58
+ "Malay": "ms",
59
+ "Burmese": "my",
60
+ "Nepali": "ne",
61
+ "Dutch": "nl",
62
+ "Norwegian": "no",
63
+ "Oromo": "om",
64
+ "Oriya": "or",
65
+ "Punjabi": "pa",
66
+ "Polish": "pl",
67
+ "Pashto": "ps",
68
+ "Portuguese": "pt",
69
+ "Romanian": "ro",
70
+ "Russian": "ru",
71
+ "Sanskrit": "sa",
72
+ "Sindhi": "sd",
73
+ "Sinhala": "si",
74
+ "Slovak": "sk",
75
+ "Slovenian": "sl",
76
+ "Somali": "so",
77
+ "Albanian": "sq",
78
+ "Serbian": "sr",
79
+ "Sundanese": "su",
80
+ "Swedish": "sv",
81
+ "Swahili": "sw",
82
+ "Tamil": "ta",
83
+ "Telugu": "te",
84
+ "Thai": "th",
85
+ "Tagalog": "tl",
86
+ "Turkish": "tr",
87
+ "Uyghur": "ug",
88
+ "Ukrainian": "uk",
89
+ "Urdu": "ur",
90
+ "Uzbek": "uz",
91
+ "Vietnamese": "vi",
92
+ "Xhosa": "xh",
93
+ "Yiddish": "yi",
94
+ "Chinese": "zh"
95
+ }
requirements.txt CHANGED
@@ -1,5 +1,3 @@
 
1
  surya-ocr
2
- transformers>=4.56.1
3
- torch>=2.7.0
4
- pillow
5
- spaces
 
1
+ torch
2
  surya-ocr
3
+ pillow