VietKien commited on
Commit
4687093
·
verified ·
1 Parent(s): 60d6bcc

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +94 -203
app.py CHANGED
@@ -6,305 +6,196 @@ import cv2
6
  import numpy as np
7
  from PIL import Image
8
 
9
- # Thêm dòng này lên đầu file hoặc ngay dưới các lệnh import để tắt check mạng
10
  os.environ["PADDLE_PDX_DISABLE_MODEL_SOURCE_CHECK"] = "True"
11
 
12
  try:
13
  from paddleocr import PaddleOCR
14
- # Sửa: Bỏ 'show_log', đổi 'use_angle_cls' thành 'use_textline_orientation'
15
- # Nếu vẫn báo lỗi tham số, chỉ cần dùng: PaddleOCR(lang='vi')
16
  ocr_engine = PaddleOCR(use_textline_orientation=True, lang='vi')
17
  HAS_OCR = True
18
  except ImportError:
19
  HAS_OCR = False
20
- print("⚠️ CHƯA CÀI PADDLEOCR: Vui lòng chạy 'pip install paddlepaddle paddleocr'")
21
  except Exception as e:
22
  HAS_OCR = False
23
- print(f"⚠️ Lỗi khởi tạo OCR: {e}")
24
 
25
- # --- 1. CẤU HÌNH NGÔN NGỮ (TỪ ĐIỂN) ---
26
  TRANS = {
27
  "vi": {
28
  "lang_btn": "🇬🇧 English",
29
- "app_title": "DOCUMENTS TOOLKIT PRO",
30
- # Sidebar items
31
  "menu_label": "🛠️ Chọn Chức năng",
32
  "menu_pdf": "📝 Chuyển PDF sang Word",
33
- "menu_ocr": "👁️ OCR (Trích xuất chữ từ ảnh)",
34
  "info_header": "ℹ️ Thông tin",
35
-
36
- # PDF Tool
37
  "pdf_title": "### 📄 CHUYỂN ĐỔI PDF SANG WORD",
38
- "pdf_input_label": "Tải file PDF (Chọn nhiều file)",
39
  "pdf_btn": "🚀 CHUYỂN ĐỔI NGAY",
40
-
41
- # OCR Tool
42
  "ocr_title": "### 👁️ TRÍCH XUẤT VĂN BẢN (OCR)",
43
- "ocr_input_label": "Tải hình ảnh (PNG, JPG)",
44
- "ocr_btn": "🔍 QUÉT VÀ TRÍCH XUẤT",
45
- "ocr_output_text": "Nội dung văn bản",
46
- "ocr_output_img": "Ảnh đã khoanh vùng chữ",
47
-
48
- # Messages
49
- "msg_warning": "⚠️ Vui lòng tải file lên!",
50
- "msg_processing": "Đang xử lý...",
51
  "msg_success": "✅ Hoàn tất!",
52
  "msg_error": "❌ Lỗi: ",
53
- "ocr_not_installed": "⚠️ Lỗi: Chưa cài thư viện PaddleOCR.",
54
-
55
- # Info Content
56
- "info_content": """
57
- **Phát triển bởi:** Chu Viết Kiên
58
- **Liên hệ:** kiencv.3107@gmail.com
59
- **Phiên bản:** 2.0 (OCR Update)
60
- """
61
  },
62
  "en": {
63
  "lang_btn": "🇻🇳 Tiếng Việt",
64
- "app_title": "DOCUMENTS TOOLKIT PRO",
65
- # Sidebar items
66
  "menu_label": "🛠️ Select Tool",
67
  "menu_pdf": "📝 PDF to Word Converter",
68
  "menu_ocr": "👁️ OCR (Image to Text)",
69
  "info_header": "ℹ️ Information",
70
-
71
- # PDF Tool
72
  "pdf_title": "### 📄 PDF TO WORD CONVERTER",
73
- "pdf_input_label": "Upload PDF Files",
74
  "pdf_btn": "🚀 CONVERT NOW",
75
-
76
- # OCR Tool
77
- "ocr_title": "### 👁️ OPTICAL CHARACTER RECOGNITION (OCR)",
78
- "ocr_input_label": "Upload Image (PNG, JPG)",
79
- "ocr_btn": "🔍 SCAN & EXTRACT",
80
  "ocr_output_text": "Extracted Text",
81
- "ocr_output_img": "Annotated Image",
82
-
83
- # Messages
84
- "msg_warning": "⚠️ Please upload a file!",
85
- "msg_processing": "Processing...",
86
  "msg_success": "✅ Done!",
87
  "msg_error": "❌ Error: ",
88
- "ocr_not_installed": "⚠️ Error: PaddleOCR library not installed.",
89
-
90
- # Info Content
91
- "info_content": """
92
- **Developer:** Chu Viet Kien
93
- **Contact:** kiencv.3107@gmail.com
94
- **Version:** 2.0 (OCR Update)
95
- """
96
  }
97
  }
98
 
99
- # --- 2. HÀM XỬ LÝ LOGIC ---
100
-
101
- # 2.1 Logic PDF -> Word
102
  def convert_pdfs_to_word(pdf_files, lang_code, progress=gr.Progress()):
103
  T = TRANS[lang_code]
104
  if not pdf_files: return None, T["msg_warning"]
105
-
106
  if not isinstance(pdf_files, list): pdf_files = [pdf_files]
107
- converted_files = []
108
 
 
109
  try:
110
- for idx, pdf_file in enumerate(pdf_files):
111
- progress((idx / len(pdf_files)), desc=f"Processing {os.path.basename(pdf_file.name)}...")
112
- docx_name = os.path.splitext(os.path.basename(pdf_file.name))[0] + ".docx"
113
- cv = Converter(pdf_file.name)
114
- cv.convert(docx_name, start=0, end=None)
115
  cv.close()
116
- converted_files.append(docx_name)
117
 
118
- if len(converted_files) == 1:
119
- return converted_files[0], T["msg_success"]
120
  else:
121
- zip_name = "Converted_Docs.zip"
122
  with zipfile.ZipFile(zip_name, 'w') as zf:
123
- for f in converted_files: zf.write(f)
124
  return zip_name, T["msg_success"]
125
  except Exception as e:
126
- return None, f"{T['msg_error']} {str(e)}"
127
 
128
- # 2.2 Logic OCR
129
  def run_ocr_func(image, lang_code):
130
  T = TRANS[lang_code]
131
  if not HAS_OCR: return None, None, T["ocr_not_installed"]
132
  if image is None: return None, None, T["msg_warning"]
133
 
134
  try:
135
- # PaddleOCR nhận đường dẫn file hoặc numpy array
136
- # Gradio Image input mặc định trả về numpy array (RGB)
137
-
138
- # Chạy OCR
139
  result = ocr_engine.ocr(image, cls=True)
 
140
 
141
- # Xử lý kết quả
142
- txts = []
143
- boxes = []
144
- scores = []
145
-
146
- # Result cấu trúc: [ [ [box], (text, score) ], ... ]
147
- # Đôi khi result trả về list lồng nhau nếu có nhiều vùng
148
- if result and result[0]:
149
- for line in result[0]:
150
- boxes.append(line[0])
151
- txts.append(line[1][0])
152
- scores.append(line[1][1])
153
 
154
- # Tạo văn bản kết quả
155
  full_text = "\n".join(txts)
156
 
157
- # Vẽ box lên ảnh để hiển thị (Dùng OpenCV)
158
- # Convert RGB (Gradio) -> BGR (OpenCV)
159
  img_cv = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
160
  for box in boxes:
161
- box = np.array(box).astype(np.int32).reshape((-1, 1, 2))
162
- cv2.polylines(img_cv, [box], True, (0, 0, 255), 2)
163
-
164
- # Convert ngược lại BGR -> RGB để hiển thị trên Gradio
165
  final_img = cv2.cvtColor(img_cv, cv2.COLOR_BGR2RGB)
166
 
167
- # Lưu file text để tải về (tuỳ chọn)
168
- with open("ocr_result.txt", "w", encoding="utf-8") as f:
169
  f.write(full_text)
170
 
171
- return full_text, final_img, "ocr_result.txt"
172
-
173
  except Exception as e:
174
- return str(e), None, None
175
-
176
- # --- 3. UI HELPER FUNCTIONS ---
177
 
 
178
  def change_ui_language(lang):
179
- new_lang = "en" if lang == "vi" else "vi"
180
- T = TRANS[new_lang]
181
-
182
  return (
183
- new_lang, # Update State
184
- T["lang_btn"],
185
- gr.update(label=T["menu_label"], choices=[T["menu_pdf"], T["menu_ocr"]], value=T["menu_pdf"]), # Radio
186
- T["info_header"], # Accordion Label
187
- T["info_content"], # Info Text
188
-
189
- # PDF UI
190
- T["pdf_title"],
191
- gr.update(label=T["pdf_input_label"]),
192
- T["pdf_btn"],
193
-
194
- # OCR UI
195
- T["ocr_title"],
196
- gr.update(label=T["ocr_input_label"]),
197
- T["ocr_btn"],
198
- gr.update(label=T["ocr_output_text"]),
199
- gr.update(label=T["ocr_output_img"])
200
  )
201
 
202
- def toggle_tool(menu_choice, lang_code):
203
- # Hàm này quyết định ẩn hiện Group nào dựa trên Radio Button
204
- T = TRANS[lang_code]
205
- if menu_choice == T["menu_pdf"] or menu_choice == TRANS["en"]["menu_pdf"] or menu_choice == TRANS["vi"]["menu_pdf"]:
206
- return gr.update(visible=True), gr.update(visible=False)
207
- else:
208
- return gr.update(visible=False), gr.update(visible=True)
209
-
210
- # --- 4. GIAO DIỆN CHÍNH ---
211
 
 
212
  custom_css = """
213
- .gradio-container {background-color: #f9f9f9}
214
- #btn_convert {background: linear-gradient(90deg, #4b6cb7, #182848); color: white;}
215
- #btn_ocr {background: linear-gradient(90deg, #d53369, #daae51); color: white;}
216
- footer {visibility: hidden}
217
  """
218
 
219
- with gr.Blocks(theme=gr.themes.Soft(), css=custom_css, title="Toolkit Pro") as demo:
 
220
 
221
- lang_state = gr.State(value="vi")
222
 
223
- # --- SIDEBAR (THANH BÊN) ---
224
  with gr.Sidebar():
225
- gr.Markdown("## 🛠️ CONTROL PANEL")
226
-
227
- # 1. Nút đổi ngôn ngữ
228
  btn_lang = gr.Button(TRANS["vi"]["lang_btn"])
229
-
230
- # 2. Menu chọn công cụ
231
  radio_menu = gr.Radio(
232
  choices=[TRANS["vi"]["menu_pdf"], TRANS["vi"]["menu_ocr"]],
233
  value=TRANS["vi"]["menu_pdf"],
234
- label=TRANS["vi"]["menu_label"],
235
- interactive=True
236
  )
237
-
238
- gr.HTML("<hr>")
239
-
240
- # 3. Thông tin tác giả
241
  with gr.Accordion(TRANS["vi"]["info_header"], open=True) as info_acc:
242
  info_md = gr.Markdown(TRANS["vi"]["info_content"])
243
 
244
- # --- MAIN CONTENT AREA ---
245
-
246
- # === GROUP 1: PDF TO WORD ===
247
  with gr.Group(visible=True) as group_pdf:
248
  header_pdf = gr.Markdown(TRANS["vi"]["pdf_title"])
249
  with gr.Row():
250
- with gr.Column():
251
- input_pdf = gr.File(label=TRANS["vi"]["pdf_input_label"], file_types=[".pdf"], file_count="multiple", height=200)
252
- with gr.Column():
253
- output_docx = gr.File(label="Download Result", height=100)
254
- status_pdf = gr.Textbox(label="Status", interactive=False)
255
-
256
- btn_run_pdf = gr.Button(TRANS["vi"]["pdf_btn"], elem_id="btn_convert")
257
 
258
- # === GROUP 2: OCR ===
259
  with gr.Group(visible=False) as group_ocr:
260
  header_ocr = gr.Markdown(TRANS["vi"]["ocr_title"])
261
  with gr.Row():
262
- # Cột trái: Input
263
- with gr.Column(scale=1):
264
- input_img = gr.Image(label=TRANS["vi"]["ocr_input_label"], type="numpy", height=300)
265
- btn_run_ocr = gr.Button(TRANS["vi"]["ocr_btn"], elem_id="btn_ocr")
266
-
267
- # Cột phải: Output
268
- with gr.Column(scale=1):
269
- output_text = gr.Textbox(label=TRANS["vi"]["ocr_output_text"], lines=10, show_copy_button=True)
270
- output_txt_file = gr.File(label="Download .txt")
271
-
272
- # Hiển thị ảnh kết quả bên dưới
273
- output_img_viz = gr.Image(label=TRANS["vi"]["ocr_output_img"], interactive=False)
274
-
275
- # --- EVENT HANDLERS ---
276
-
277
- # 1. Logic chạy PDF
278
- btn_run_pdf.click(
279
- fn=convert_pdfs_to_word,
280
- inputs=[input_pdf, lang_state],
281
- outputs=[output_docx, status_pdf]
282
- )
283
-
284
- # 2. Logic chạy OCR
285
- btn_run_ocr.click(
286
- fn=run_ocr_func,
287
- inputs=[input_img, lang_state],
288
- outputs=[output_text, output_img_viz, output_txt_file]
289
- )
290
 
291
- # 3. Logic chuyển đổi Tab (Ẩn hiện Group)
292
- radio_menu.change(
293
- fn=toggle_tool,
294
- inputs=[radio_menu, lang_state],
295
- outputs=[group_pdf, group_ocr]
296
- )
297
 
298
- # 4. Logic đổi ngôn ngữ (Cập nhật toàn bộ Label)
299
- btn_lang.click(
300
- fn=change_ui_language,
301
- inputs=[lang_state],
302
- outputs=[
303
- lang_state, btn_lang, radio_menu, info_acc, info_md, # Sidebar updates
304
- header_pdf, input_pdf, btn_run_pdf, # PDF updates
305
- header_ocr, input_img, btn_run_ocr, output_text, output_img_viz # OCR updates
306
- ]
307
- )
308
 
309
  if __name__ == "__main__":
310
- demo.launch()
 
 
6
  import numpy as np
7
  from PIL import Image
8
 
9
+ # --- CẤU HÌNH FIX LỖI PADDLE ---
10
  os.environ["PADDLE_PDX_DISABLE_MODEL_SOURCE_CHECK"] = "True"
11
 
12
  try:
13
  from paddleocr import PaddleOCR
14
+ # Khởi tạo OCR
 
15
  ocr_engine = PaddleOCR(use_textline_orientation=True, lang='vi')
16
  HAS_OCR = True
17
  except ImportError:
18
  HAS_OCR = False
19
+ print("⚠️ CHƯA CÀI PADDLEOCR")
20
  except Exception as e:
21
  HAS_OCR = False
22
+ print(f"⚠️ Lỗi OCR init: {e}")
23
 
24
+ # --- 1. TỪ ĐIỂN NGÔN NGỮ ---
25
  TRANS = {
26
  "vi": {
27
  "lang_btn": "🇬🇧 English",
 
 
28
  "menu_label": "🛠️ Chọn Chức năng",
29
  "menu_pdf": "📝 Chuyển PDF sang Word",
30
+ "menu_ocr": "👁️ OCR (Trích xuất chữ)",
31
  "info_header": "ℹ️ Thông tin",
 
 
32
  "pdf_title": "### 📄 CHUYỂN ĐỔI PDF SANG WORD",
33
+ "pdf_input_label": "Tải file PDF",
34
  "pdf_btn": "🚀 CHUYỂN ĐỔI NGAY",
 
 
35
  "ocr_title": "### 👁️ TRÍCH XUẤT VĂN BẢN (OCR)",
36
+ "ocr_input_label": "Tải hình ảnh",
37
+ "ocr_btn": "🔍 QUÉT NGAY",
38
+ "ocr_output_text": "Kết quả văn bản",
39
+ "ocr_output_img": "Ảnh đã xử ",
40
+ "msg_warning": "⚠️ Vui lòng tải file!",
 
 
 
41
  "msg_success": "✅ Hoàn tất!",
42
  "msg_error": "❌ Lỗi: ",
43
+ "ocr_not_installed": "⚠️ Lỗi: Chưa cài thư viện OCR.",
44
+ "info_content": "**Dev:** Chu Viết Kiên | **Ver:** 2.1 (Stable)"
 
 
 
 
 
 
45
  },
46
  "en": {
47
  "lang_btn": "🇻🇳 Tiếng Việt",
 
 
48
  "menu_label": "🛠️ Select Tool",
49
  "menu_pdf": "📝 PDF to Word Converter",
50
  "menu_ocr": "👁️ OCR (Image to Text)",
51
  "info_header": "ℹ️ Information",
 
 
52
  "pdf_title": "### 📄 PDF TO WORD CONVERTER",
53
+ "pdf_input_label": "Upload PDF",
54
  "pdf_btn": "🚀 CONVERT NOW",
55
+ "ocr_title": "### 👁️ OPTICAL CHARACTER RECOGNITION",
56
+ "ocr_input_label": "Upload Image",
57
+ "ocr_btn": "🔍 SCAN NOW",
 
 
58
  "ocr_output_text": "Extracted Text",
59
+ "ocr_output_img": "Processed Image",
60
+ "msg_warning": "⚠️ Please upload file!",
 
 
 
61
  "msg_success": "✅ Done!",
62
  "msg_error": "❌ Error: ",
63
+ "ocr_not_installed": "⚠️ Error: OCR lib missing.",
64
+ "info_content": "**Dev:** Chu Viet Kien | **Ver:** 2.1 (Stable)"
 
 
 
 
 
 
65
  }
66
  }
67
 
68
+ # --- 2. HÀM LOGIC ---
 
 
69
  def convert_pdfs_to_word(pdf_files, lang_code, progress=gr.Progress()):
70
  T = TRANS[lang_code]
71
  if not pdf_files: return None, T["msg_warning"]
 
72
  if not isinstance(pdf_files, list): pdf_files = [pdf_files]
 
73
 
74
+ converted = []
75
  try:
76
+ for idx, pdf in enumerate(pdf_files):
77
+ docx = os.path.splitext(os.path.basename(pdf.name))[0] + ".docx"
78
+ cv = Converter(pdf.name)
79
+ cv.convert(docx)
 
80
  cv.close()
81
+ converted.append(docx)
82
 
83
+ if len(converted) == 1:
84
+ return converted[0], T["msg_success"]
85
  else:
86
+ zip_name = "Result.zip"
87
  with zipfile.ZipFile(zip_name, 'w') as zf:
88
+ for f in converted: zf.write(f)
89
  return zip_name, T["msg_success"]
90
  except Exception as e:
91
+ return None, f"{T['msg_error']} {e}"
92
 
 
93
  def run_ocr_func(image, lang_code):
94
  T = TRANS[lang_code]
95
  if not HAS_OCR: return None, None, T["ocr_not_installed"]
96
  if image is None: return None, None, T["msg_warning"]
97
 
98
  try:
99
+ # OCR
 
 
 
100
  result = ocr_engine.ocr(image, cls=True)
101
+ txts, boxes = [], []
102
 
103
+ # Xử lý format output của Paddle
104
+ if result:
105
+ data = result[0] if isinstance(result[0], list) else result
106
+ if data:
107
+ for line in data:
108
+ boxes.append(line[0])
109
+ txts.append(line[1][0])
 
 
 
 
 
110
 
 
111
  full_text = "\n".join(txts)
112
 
113
+ # Vẽ box
 
114
  img_cv = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
115
  for box in boxes:
116
+ pts = np.array(box, np.int32).reshape((-1, 1, 2))
117
+ cv2.polylines(img_cv, [pts], True, (0, 0, 255), 2)
 
 
118
  final_img = cv2.cvtColor(img_cv, cv2.COLOR_BGR2RGB)
119
 
120
+ with open("ocr_text.txt", "w", encoding="utf-8") as f:
 
121
  f.write(full_text)
122
 
123
+ return full_text, final_img, "ocr_text.txt"
 
124
  except Exception as e:
125
+ return f"Err: {e}", None, None
 
 
126
 
127
+ # --- 3. UI HELPERS ---
128
  def change_ui_language(lang):
129
+ new = "en" if lang == "vi" else "vi"
130
+ T = TRANS[new]
 
131
  return (
132
+ new, T["lang_btn"],
133
+ gr.update(label=T["menu_label"], choices=[T["menu_pdf"], T["menu_ocr"]], value=T["menu_pdf"]),
134
+ T["info_header"], T["info_content"],
135
+ T["pdf_title"], gr.update(label=T["pdf_input_label"]), T["pdf_btn"],
136
+ T["ocr_title"], gr.update(label=T["ocr_input_label"]), T["ocr_btn"],
137
+ gr.update(label=T["ocr_output_text"]), gr.update(label=T["ocr_output_img"])
 
 
 
 
 
 
 
 
 
 
 
138
  )
139
 
140
+ def toggle_tool(menu, lang):
141
+ is_pdf = (menu == TRANS["vi"]["menu_pdf"] or menu == TRANS["en"]["menu_pdf"])
142
+ return (gr.update(visible=True), gr.update(visible=False)) if is_pdf else (gr.update(visible=False), gr.update(visible=True))
 
 
 
 
 
 
143
 
144
+ # --- 4. GIAO DIỆN ---
145
  custom_css = """
146
+ #convert_btn, #ocr_btn {color: white; background: linear-gradient(90deg, #2b5876, #4e4376);}
 
 
 
147
  """
148
 
149
+ # Chuyển theme ra khỏi constructor Blocks để tránh warning
150
+ with gr.Blocks(title="Toolkit Pro") as demo:
151
 
152
+ lang_state = gr.State("vi")
153
 
 
154
  with gr.Sidebar():
155
+ gr.Markdown("## 🛠️ MENU")
 
 
156
  btn_lang = gr.Button(TRANS["vi"]["lang_btn"])
 
 
157
  radio_menu = gr.Radio(
158
  choices=[TRANS["vi"]["menu_pdf"], TRANS["vi"]["menu_ocr"]],
159
  value=TRANS["vi"]["menu_pdf"],
160
+ label=TRANS["vi"]["menu_label"]
 
161
  )
 
 
 
 
162
  with gr.Accordion(TRANS["vi"]["info_header"], open=True) as info_acc:
163
  info_md = gr.Markdown(TRANS["vi"]["info_content"])
164
 
165
+ # PDF GROUP
 
 
166
  with gr.Group(visible=True) as group_pdf:
167
  header_pdf = gr.Markdown(TRANS["vi"]["pdf_title"])
168
  with gr.Row():
169
+ in_pdf = gr.File(label=TRANS["vi"]["pdf_input_label"], file_types=[".pdf"], file_count="multiple")
170
+ out_word = gr.File(label="Result")
171
+ btn_pdf = gr.Button(TRANS["vi"]["pdf_btn"], elem_id="convert_btn")
172
+ st_pdf = gr.Textbox(label="Status", interactive=False)
 
 
 
173
 
174
+ # OCR GROUP
175
  with gr.Group(visible=False) as group_ocr:
176
  header_ocr = gr.Markdown(TRANS["vi"]["ocr_title"])
177
  with gr.Row():
178
+ with gr.Column():
179
+ in_img = gr.Image(label=TRANS["vi"]["ocr_input_label"], type="numpy", height=300)
180
+ btn_ocr = gr.Button(TRANS["vi"]["ocr_btn"], elem_id="ocr_btn")
181
+ with gr.Column():
182
+ # ĐÃ SỬA: Bỏ show_copy_button=True để tránh lỗi crash
183
+ out_txt = gr.Textbox(label=TRANS["vi"]["ocr_output_text"], lines=10)
184
+ out_file = gr.File(label="Download .txt")
185
+ out_img_viz = gr.Image(label=TRANS["vi"]["ocr_output_img"], interactive=False)
186
+
187
+ # EVENTS
188
+ btn_pdf.click(convert_pdfs_to_word, [in_pdf, lang_state], [out_word, st_pdf])
189
+ btn_ocr.click(run_ocr_func, [in_img, lang_state], [out_txt, out_img_viz, out_file])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
 
191
+ radio_menu.change(toggle_tool, [radio_menu, lang_state], [group_pdf, group_ocr])
 
 
 
 
 
192
 
193
+ btn_lang.click(change_ui_language, [lang_state], [
194
+ lang_state, btn_lang, radio_menu, info_acc, info_md,
195
+ header_pdf, in_pdf, btn_pdf,
196
+ header_ocr, in_img, btn_ocr, out_txt, out_img_viz
197
+ ])
 
 
 
 
 
198
 
199
  if __name__ == "__main__":
200
+ # Di chuyển theme và css xuống đây để đúng chuẩn mới
201
+ demo.launch(theme=gr.themes.Soft(), css=custom_css)