MohammadReza-Halakoo commited on
Commit
2ef1dc3
·
verified ·
1 Parent(s): 94b3206

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +199 -137
app.py CHANGED
@@ -1,70 +1,74 @@
1
- import tempfile
2
- import os
3
-
4
- # مسیر امن برای Hugging Face
5
- runtime_dir = os.path.join(tempfile.gettempdir(), ".streamlit")
6
- os.environ["STREAMLIT_RUNTIME_DIR"] = runtime_dir
7
- os.makedirs(runtime_dir, exist_ok=True)
8
 
9
- import argparse
10
  import io
 
11
  from typing import List
12
 
 
 
 
13
  import pypdfium2
 
14
  import streamlit as st
15
- # ✅ تغییر مهم
16
- from surya.ocr import batch_text_detection, run_ocr
 
 
 
 
 
 
17
  from surya.layout import batch_layout_detection
18
- from surya.model.detection.segformer import load_model, load_processor
 
 
 
 
 
 
19
  from surya.model.recognition.model import load_model as load_rec_model
20
  from surya.model.recognition.processor import load_processor as load_rec_processor
21
- from surya.model.ordering.processor import load_processor as load_order_processor
22
  from surya.model.ordering.model import load_model as load_order_model
 
23
  from surya.ordering import batch_ordering
 
 
24
  from surya.postprocessing.heatmap import draw_polys_on_image
25
  from surya.postprocessing.text import draw_text_on_image
26
- from PIL import Image
27
  from surya.languages import CODE_TO_LANGUAGE
28
  from surya.input.langs import replace_lang_with_code
29
  from surya.schema import OCRResult, TextDetectionResult, LayoutResult, OrderResult
30
- import pytesseract
31
- import cv2
32
- import numpy as np
33
 
34
- # -------------------
35
- # Args
36
- # -------------------
37
- parser = argparse.ArgumentParser(description="Run OCR on an image or PDF.")
38
- parser.add_argument("--math", action="store_true", help="Use math model for detection", default=False)
39
 
40
- try:
41
- args = parser.parse_args()
42
- except SystemExit as e:
43
- print(f"Error parsing arguments: {e}")
44
- os._exit(e.code)
45
-
46
- # -------------------
47
- # Helper Functions
48
- # -------------------
49
- def remove_border(image_path, output_path):
50
  image = cv2.imread(image_path)
 
 
51
  gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
52
  _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
53
  contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
54
- max_contour = max(contours, key=cv2.contourArea)
 
 
55
 
 
56
  epsilon = 0.02 * cv2.arcLength(max_contour, True)
57
  approx = cv2.approxPolyDP(max_contour, epsilon, True)
58
 
59
  if len(approx) == 4:
60
- pts = approx.reshape(4, 2)
61
  rect = np.zeros((4, 2), dtype="float32")
62
  s = pts.sum(axis=1)
63
- rect[0] = pts[np.argmin(s)]
64
- rect[2] = pts[np.argmax(s)]
65
  diff = np.diff(pts, axis=1)
66
- rect[1] = pts[np.argmin(diff)]
67
- rect[3] = pts[np.argmax(diff)]
68
  (tl, tr, br, bl) = rect
69
  widthA = np.linalg.norm(br - bl)
70
  widthB = np.linalg.norm(tr - tl)
@@ -73,8 +77,8 @@ def remove_border(image_path, output_path):
73
  heightB = np.linalg.norm(tl - bl)
74
  maxHeight = max(int(heightA), int(heightB))
75
  dst = np.array([[0, 0], [maxWidth - 1, 0],
76
- [maxWidth - 1, maxHeight - 1],
77
- [0, maxHeight - 1]], dtype="float32")
78
  M = cv2.getPerspectiveTransform(rect, dst)
79
  cropped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))
80
  cv2.imwrite(output_path, cropped)
@@ -84,82 +88,71 @@ def remove_border(image_path, output_path):
84
  return image
85
 
86
 
87
- def text_detection(img):
88
- pred = batch_text_detection([img], det_model, det_processor)[0]
89
- polygons = [p.polygon for p in pred.bboxes]
90
- det_img = draw_polys_on_image(polygons, img.copy())
91
- return det_img, pred
92
-
93
-
94
- def layout_detection(img):
95
- _, det_pred = text_detection(img)
96
- pred = batch_layout_detection([img], layout_model, layout_processor, [det_pred])[0]
97
- polygons = [p.polygon for p in pred.bboxes]
98
- labels = [p.label for p in pred.bboxes]
99
- layout_img = draw_polys_on_image(polygons, img.copy(), labels=labels, label_font_size=40)
100
- return layout_img, pred
101
-
102
-
103
- def order_detection(img):
104
- _, layout_pred = layout_detection(img)
105
- bboxes = [l.bbox for l in layout_pred.bboxes]
106
- pred = batch_ordering([img], [bboxes], order_model, order_processor)[0]
107
- polys = [l.polygon for l in pred.bboxes]
108
- positions = [str(l.position) for l in pred.bboxes]
109
- order_img = draw_polys_on_image(polys, img.copy(), labels=positions, label_font_size=40)
110
- return order_img, pred
111
-
112
-
113
- def ocr(img, langs: List[str]):
114
- replace_lang_with_code(langs)
115
- img_pred = run_ocr([img], [langs], det_model, det_processor, rec_model, rec_processor)[0]
116
- bboxes = [l.bbox for l in img_pred.text_lines]
117
- text = [l.text for l in img_pred.text_lines]
118
- rec_img = draw_text_on_image(bboxes, text, img.size, langs, has_math="_math" in langs)
119
- return rec_img, img_pred
120
-
121
-
122
- def open_pdf(pdf_file):
123
  stream = io.BytesIO(pdf_file.getvalue())
124
  return pypdfium2.PdfDocument(stream)
125
 
126
 
127
- @st.cache_data()
128
- def get_page_image(pdf_file, page_num, dpi=96):
129
  doc = open_pdf(pdf_file)
130
  renderer = doc.render(pypdfium2.PdfBitmap.to_pil, page_indices=[page_num - 1], scale=dpi / 72)
131
  png = list(renderer)[0]
132
  return png.convert("RGB")
133
 
134
 
135
- @st.cache_data()
136
- def page_count(pdf_file):
137
  doc = open_pdf(pdf_file)
138
  return len(doc)
139
 
140
- # -------------------
141
- # Streamlit UI Config
142
- # -------------------
143
- st.set_page_config(layout="wide")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  col2, col1 = st.columns([.5, .5])
145
 
146
- # -------------------
147
- # Load Models
148
- # -------------------
149
- @st.cache_resource()
150
  def load_det_cached():
151
- return load_model(checkpoint="vikp/surya_det2"), load_processor(checkpoint="vikp/surya_det2")
152
 
153
- @st.cache_resource()
154
  def load_rec_cached():
155
  return load_rec_model(checkpoint="MohammadReza-Halakoo/TrustOCR"), \
156
  load_rec_processor(checkpoint="MohammadReza-Halakoo/TrustOCR")
157
 
158
- @st.cache_resource()
159
  def load_layout_cached():
160
- return load_model(checkpoint="vikp/surya_layout2"), load_processor(checkpoint="vikp/surya_layout2")
161
 
162
- @st.cache_resource()
163
  def load_order_cached():
164
  return load_order_model(checkpoint="vikp/surya_order"), load_order_processor(checkpoint="vikp/surya_order")
165
 
@@ -169,20 +162,74 @@ rec_model, rec_processor = load_rec_cached()
169
  layout_model, layout_processor = load_layout_cached()
170
  order_model, order_processor = load_order_cached()
171
 
172
- # -------------------
173
- # UI
174
- # -------------------
175
- st.markdown("# TRUST OCR DEMO")
176
 
177
- in_file = st.sidebar.file_uploader("فایل PDF یا عکس :", type=["pdf", "png", "jpg", "jpeg", "gif", "webp"])
178
- languages = st.sidebar.multiselect("زبان‌ها", sorted(list(CODE_TO_LANGUAGE.values())), default=["Persian"], max_selections=4)
179
 
180
- if in_file is None:
181
- st.stop()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
 
183
- filetype = in_file.type
184
  if "pdf" in filetype:
185
- page_number = st.sidebar.number_input(f"صفحه:", min_value=1, value=1, max_value=page_count(in_file))
 
 
 
 
 
186
  pil_image = get_page_image(in_file, page_number)
187
  else:
188
  bytes_data = in_file.getvalue()
@@ -191,40 +238,55 @@ else:
191
  file_path = os.path.join(temp_dir, in_file.name)
192
  with open(file_path, "wb") as f:
193
  f.write(bytes_data)
194
- out_file = file_path.split('.')[0] + "-1.JPG"
195
- remove_border(file_path, out_file)
196
- pil_image = Image.open(out_file).convert("RGB")
197
-
198
- text_det = st.sidebar.button("تشخیص متن")
199
- text_rec = st.sidebar.button("تبدیل به متن")
200
- layout_det = st.sidebar.button("آنالیز صفحه")
201
- order_det = st.sidebar.button("ترتیب خوانش")
202
-
203
- if text_det:
204
- osd = pytesseract.image_to_osd(pil_image, output_type='dict')
205
- im_fixed = pil_image.copy().rotate(osd['orientation'])
206
- det_img, pred = text_detection(im_fixed)
207
- with col1:
208
- st.image(det_img, caption="تشخیص متن", use_column_width=True)
209
-
210
- if layout_det:
211
- layout_img, pred = layout_detection(pil_image)
212
- with col1:
213
- st.image(layout_img, caption="آنالیز صفحه", use_column_width=True)
214
-
215
- if text_rec:
216
- rec_img, pred = ocr(pil_image, languages)
217
- with col1:
218
- text_tab, json_tab = st.tabs(["متن صفحه", "JSON"])
219
- with text_tab:
220
- st.text("\n".join([p.text for p in pred.text_lines]))
221
- with json_tab:
222
- st.json(pred.model_dump(), expanded=True)
223
-
224
- if order_det:
225
- order_img, pred = order_detection(pil_image)
226
- with col1:
227
- st.image(order_img, caption="ترتیب خوانش", use_column_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
 
229
  with col2:
230
- st.image(pil_image, caption="تصویر ورودی", use_column_width=True)
 
1
+ # app.py — TRUST OCR DEMO (Streamlit) for surya-ocr==0.4.14
 
 
 
 
 
 
2
 
3
+ import os
4
  import io
5
+ import tempfile
6
  from typing import List
7
 
8
+ import numpy as np
9
+ import cv2
10
+ from PIL import Image
11
  import pypdfium2
12
+ import pytesseract
13
  import streamlit as st
14
+
15
+ # ===== Safe runtime dir for Streamlit/HF cache (esp. in containers) =====
16
+ runtime_dir = os.path.join(tempfile.gettempdir(), ".streamlit")
17
+ os.environ["STREAMLIT_RUNTIME_DIR"] = runtime_dir
18
+ os.makedirs(runtime_dir, exist_ok=True)
19
+
20
+ # ===== Surya imports (v0.4.x) =====
21
+ from surya.detection import batch_text_detection
22
  from surya.layout import batch_layout_detection
23
+
24
+ # Detection model loaders: prefer segformer; fallback to model (older path)
25
+ try:
26
+ from surya.model.detection.segformer import load_model as load_det_model, load_processor as load_det_processor
27
+ except ImportError:
28
+ from surya.model.detection.model import load_model as load_det_model, load_processor as load_det_processor
29
+
30
  from surya.model.recognition.model import load_model as load_rec_model
31
  from surya.model.recognition.processor import load_processor as load_rec_processor
32
+
33
  from surya.model.ordering.model import load_model as load_order_model
34
+ from surya.model.ordering.processor import load_processor as load_order_processor
35
  from surya.ordering import batch_ordering
36
+
37
+ from surya.ocr import run_ocr
38
  from surya.postprocessing.heatmap import draw_polys_on_image
39
  from surya.postprocessing.text import draw_text_on_image
 
40
  from surya.languages import CODE_TO_LANGUAGE
41
  from surya.input.langs import replace_lang_with_code
42
  from surya.schema import OCRResult, TextDetectionResult, LayoutResult, OrderResult
 
 
 
43
 
 
 
 
 
 
44
 
45
+ # ===================== Helper Functions =====================
46
+
47
+ def remove_border(image_path: str, output_path: str) -> np.ndarray:
48
+ """Remove outer border & deskew (perspective) if a rectangular contour is found."""
 
 
 
 
 
 
49
  image = cv2.imread(image_path)
50
+ if image is None:
51
+ raise ValueError(f"Cannot read image: {image_path}")
52
  gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
53
  _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
54
  contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
55
+ if not contours:
56
+ cv2.imwrite(output_path, image)
57
+ return image
58
 
59
+ max_contour = max(contours, key=cv2.contourArea)
60
  epsilon = 0.02 * cv2.arcLength(max_contour, True)
61
  approx = cv2.approxPolyDP(max_contour, epsilon, True)
62
 
63
  if len(approx) == 4:
64
+ pts = approx.reshape(4, 2).astype("float32")
65
  rect = np.zeros((4, 2), dtype="float32")
66
  s = pts.sum(axis=1)
67
+ rect[0] = pts[np.argmin(s)] # tl
68
+ rect[2] = pts[np.argmax(s)] # br
69
  diff = np.diff(pts, axis=1)
70
+ rect[1] = pts[np.argmin(diff)] # tr
71
+ rect[3] = pts[np.argmax(diff)] # bl
72
  (tl, tr, br, bl) = rect
73
  widthA = np.linalg.norm(br - bl)
74
  widthB = np.linalg.norm(tr - tl)
 
77
  heightB = np.linalg.norm(tl - bl)
78
  maxHeight = max(int(heightA), int(heightB))
79
  dst = np.array([[0, 0], [maxWidth - 1, 0],
80
+ [maxWidth - 1, maxHeight - 1],
81
+ [0, maxHeight - 1]], dtype="float32")
82
  M = cv2.getPerspectiveTransform(rect, dst)
83
  cropped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))
84
  cv2.imwrite(output_path, cropped)
 
88
  return image
89
 
90
 
91
+ def open_pdf(pdf_file) -> pypdfium2.PdfDocument:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  stream = io.BytesIO(pdf_file.getvalue())
93
  return pypdfium2.PdfDocument(stream)
94
 
95
 
96
+ @st.cache_data(show_spinner=False)
97
+ def get_page_image(pdf_file, page_num: int, dpi: int = 96) -> Image.Image:
98
  doc = open_pdf(pdf_file)
99
  renderer = doc.render(pypdfium2.PdfBitmap.to_pil, page_indices=[page_num - 1], scale=dpi / 72)
100
  png = list(renderer)[0]
101
  return png.convert("RGB")
102
 
103
 
104
+ @st.cache_data(show_spinner=False)
105
+ def page_count(pdf_file) -> int:
106
  doc = open_pdf(pdf_file)
107
  return len(doc)
108
 
109
+
110
+ # ===================== Streamlit UI =====================
111
+
112
+ st.set_page_config(page_title="TRUST OCR DEMO", layout="wide")
113
+ st.markdown("# TRUST OCR DEMO")
114
+
115
+ # Sidebar controls
116
+ in_file = st.sidebar.file_uploader("فایل PDF یا عکس :", type=["pdf", "png", "jpg", "jpeg", "gif", "webp"])
117
+ languages = st.sidebar.multiselect(
118
+ "زبان‌ها (Languages)",
119
+ sorted(list(CODE_TO_LANGUAGE.values())),
120
+ default=["Persian"],
121
+ max_selections=4
122
+ )
123
+ auto_rotate = st.sidebar.toggle("چرخش خودکار (Tesseract OSD)", value=True)
124
+ auto_border = st.sidebar.toggle("حذف قاب/کادر تصویر ورودی", value=True)
125
+
126
+ text_det_btn = st.sidebar.button("تشخیص متن (Detection)")
127
+ layout_det_btn = st.sidebar.button("آنالیز صفحه (Layout)")
128
+ order_det_btn = st.sidebar.button("ترتیب خوانش (Reading Order)")
129
+ text_rec_btn = st.sidebar.button("تبدیل به متن (Recognition)")
130
+
131
+ if in_file is None:
132
+ st.info("یک فایل PDF/عکس از سایدبار انتخاب کنید. | Please upload a file to begin.")
133
+ st.stop()
134
+
135
+ filetype = in_file.type
136
+
137
+ # Two-column layout (left: outputs / right: input image)
138
  col2, col1 = st.columns([.5, .5])
139
 
140
+ # ===================== Load Models (cached) =====================
141
+
142
+ @st.cache_resource(show_spinner=True)
 
143
  def load_det_cached():
144
+ return load_det_model(checkpoint="vikp/surya_det2"), load_det_processor(checkpoint="vikp/surya_det2")
145
 
146
+ @st.cache_resource(show_spinner=True)
147
  def load_rec_cached():
148
  return load_rec_model(checkpoint="MohammadReza-Halakoo/TrustOCR"), \
149
  load_rec_processor(checkpoint="MohammadReza-Halakoo/TrustOCR")
150
 
151
+ @st.cache_resource(show_spinner=True)
152
  def load_layout_cached():
153
+ return load_det_model(checkpoint="vikp/surya_layout2"), load_det_processor(checkpoint="vikp/surya_layout2")
154
 
155
+ @st.cache_resource(show_spinner=True)
156
  def load_order_cached():
157
  return load_order_model(checkpoint="vikp/surya_order"), load_order_processor(checkpoint="vikp/surya_order")
158
 
 
162
  layout_model, layout_processor = load_layout_cached()
163
  order_model, order_processor = load_order_cached()
164
 
 
 
 
 
165
 
166
+ # ===================== High-level Ops =====================
 
167
 
168
+ def _apply_auto_rotate(pil_img: Image.Image) -> Image.Image:
169
+ """Auto-rotate using Tesseract OSD if enabled."""
170
+ if not auto_rotate:
171
+ return pil_img
172
+ try:
173
+ osd = pytesseract.image_to_osd(pil_img, output_type=pytesseract.Output.DICT)
174
+ angle = int(osd.get("rotate", 0)) # 0/90/180/270
175
+ if angle and angle % 360 != 0:
176
+ # Tesseract returns counter-clockwise; PIL rotates counter-clockwise with positive values
177
+ return pil_img.rotate(-angle, expand=True)
178
+ return pil_img
179
+ except Exception as e:
180
+ st.warning(f"OSD rotation failed, continuing without rotation. Error: {e}")
181
+ return pil_img
182
+
183
+
184
+ def text_detection(pil_img: Image.Image):
185
+ """Text block detection via Surya detection pipeline."""
186
+ pred: TextDetectionResult = batch_text_detection([pil_img], det_model, det_processor)[0]
187
+ polygons = [p.polygon for p in pred.bboxes]
188
+ det_img = draw_polys_on_image(polygons, pil_img.copy())
189
+ return det_img, pred
190
+
191
+
192
+ def layout_detection(pil_img: Image.Image):
193
+ """Page layout analysis (requires detection result)."""
194
+ _, det_pred = text_detection(pil_img)
195
+ pred: LayoutResult = batch_layout_detection([pil_img], layout_model, layout_processor, [det_pred])[0]
196
+ polygons = [p.polygon for p in pred.bboxes]
197
+ labels = [p.label for p in pred.bboxes]
198
+ layout_img = draw_polys_on_image(polygons, pil_img.copy(), labels=labels, label_font_size=40)
199
+ return layout_img, pred
200
+
201
+
202
+ def order_detection(pil_img: Image.Image):
203
+ """Reading order estimation (requires layout result)."""
204
+ _, layout_pred = layout_detection(pil_img)
205
+ bboxes = [l.bbox for l in layout_pred.bboxes]
206
+ pred: OrderResult = batch_ordering([pil_img], [bboxes], order_model, order_processor)[0]
207
+ polys = [l.polygon for l in pred.bboxes]
208
+ positions = [str(l.position) for l in pred.bboxes]
209
+ order_img = draw_polys_on_image(polys, pil_img.copy(), labels=positions, label_font_size=40)
210
+ return order_img, pred
211
+
212
+
213
+ def ocr_page(pil_img: Image.Image, langs: List[str]):
214
+ """Full-page OCR using Surya run_ocr."""
215
+ # User selects languages by names; convert to codes
216
+ replace_lang_with_code(langs) # in-place
217
+ img_pred: OCRResult = run_ocr([pil_img], [langs], det_model, det_processor, rec_model, rec_processor)[0]
218
+ bboxes = [l.bbox for l in img_pred.text_lines]
219
+ text = [l.text for l in img_pred.text_lines]
220
+ rec_img = draw_text_on_image(bboxes, text, pil_img.size, langs, has_math="_math" in langs)
221
+ return rec_img, img_pred
222
+
223
+
224
+ # ===================== Input Handling =====================
225
 
 
226
  if "pdf" in filetype:
227
+ try:
228
+ pg_cnt = page_count(in_file)
229
+ except Exception as e:
230
+ st.error(f"خواندن PDF ناموفق بود: {e}")
231
+ st.stop()
232
+ page_number = st.sidebar.number_input("صفحه:", min_value=1, value=1, max_value=pg_cnt)
233
  pil_image = get_page_image(in_file, page_number)
234
  else:
235
  bytes_data = in_file.getvalue()
 
238
  file_path = os.path.join(temp_dir, in_file.name)
239
  with open(file_path, "wb") as f:
240
  f.write(bytes_data)
241
+ out_file = os.path.splitext(file_path)[0] + "-1.JPG"
242
+ try:
243
+ if auto_border:
244
+ _ = remove_border(file_path, out_file)
245
+ pil_image = Image.open(out_file).convert("RGB")
246
+ else:
247
+ pil_image = Image.open(file_path).convert("RGB")
248
+ except Exception as e:
249
+ st.warning(f"حذف قاب/بازخوانی تصویر با خطا مواجه شد؛ تصویر اصلی استفاده می‌شود. Error: {e}")
250
+ pil_image = Image.open(file_path).convert("RGB")
251
+
252
+ # Auto-rotate if enabled
253
+ pil_image = _apply_auto_rotate(pil_image)
254
+
255
+ # ===================== Buttons Logic =====================
256
+
257
+ with col1:
258
+ if text_det_btn:
259
+ try:
260
+ det_img, det_pred = text_detection(pil_image)
261
+ st.image(det_img, caption="تشخیص متن (Detection)", use_column_width=True)
262
+ except Exception as e:
263
+ st.error(f"خطا در تشخیص متن: {e}")
264
+
265
+ if layout_det_btn:
266
+ try:
267
+ layout_img, layout_pred = layout_detection(pil_image)
268
+ st.image(layout_img, caption="آنالیز صفحه (Layout)", use_column_width=True)
269
+ except Exception as e:
270
+ st.error(f"خطا در آنالیز صفحه: {e}")
271
+
272
+ if order_det_btn:
273
+ try:
274
+ order_img, order_pred = order_detection(pil_image)
275
+ st.image(order_img, caption="ترتیب خوانش (Reading Order)", use_column_width=True)
276
+ except Exception as e:
277
+ st.error(f"خطا در ترتیب خوانش: {e}")
278
+
279
+ if text_rec_btn:
280
+ try:
281
+ lang_names = list(languages) if languages else ["Persian"]
282
+ rec_img, ocr_pred = ocr_page(pil_image, lang_names)
283
+ text_tab, json_tab = st.tabs(["متن صفحه | Page Text", "JSON"])
284
+ with text_tab:
285
+ st.text("\n".join([p.text for p in ocr_pred.text_lines]))
286
+ with json_tab:
287
+ st.json(ocr_pred.model_dump(), expanded=False)
288
+ except Exception as e:
289
+ st.error(f"خطا در بازشناسی متن (Recognition): {e}")
290
 
291
  with col2:
292
+ st.image(pil_image, caption="تصویر ورودی | Input Preview", use_column_width=True)