aaron1141 commited on
Commit
23eca1f
ยท
1 Parent(s): 87e4ed0

ux: default en, move examples section, clean up preview label and API table

Browse files
Files changed (1) hide show
  1. app.py +94 -115
app.py CHANGED
@@ -28,6 +28,10 @@ T = {
28
  sec_upload="๐Ÿ“„ ไธŠไผ  PDF",
29
  upload_label="PDF ๆ–‡ไปถ๏ผˆๅ•ๆ–‡ไปถ๏ผš้ข˜็ญ”ๆททๆŽ’๏ผ›ๅŒๆ–‡ไปถ๏ผš็ฌฌ1ไธช้ข˜็›ฎ๏ผŒ็ฌฌ2ไธช็ญ”ๆกˆ๏ผ‰",
30
  task_label="ไปปๅŠกๅ็งฐ",
 
 
 
 
31
  sec_llm="โš™๏ธ LLM ้…็ฝฎ",
32
  api_url_label="API Base URL",
33
  llm_key_label="LLM API Key๏ผˆDF_API_KEY๏ผ‰",
@@ -44,17 +48,7 @@ T = {
44
  status_label="่ฟ่กŒ็Šถๆ€",
45
  status_ph="็‚นๅ‡ปใ€Œๅผ€ๅง‹ๆๅ–ใ€ๅŽ่ฟ›ๅบฆๆ˜พ็คบๅœจ่ฟ™้‡Œ๏ผˆ่ฟ่กŒ้œ€ๆ•ฐๅˆ†้’Ÿ๏ผŒ่ฏท่€ๅฟƒ็ญ‰ๅพ…๏ผ‰โ€ฆ",
46
  output_label="ไธ‹่ฝฝ็ป“ๆžœ๏ผˆraw_vqa.jsonl๏ผ‰",
47
- preview_label="็ป“ๆžœ้ข„่งˆ๏ผˆๆœ€ๅคšๅฑ•็คบ 3 ๆก๏ผ‰",
48
- sec_examples="๐Ÿ“‹ ๅ†…็ฝฎ็คบไพ‹ PDF๏ผˆ็‚นๅ‡ปๅŠ ่ฝฝๅนถ้ข„่งˆ๏ผ‰",
49
- pdf_preview_label="PDF ้ข„่งˆ",
50
- ex1_label="็คบไพ‹ 1๏ผšๅ•ๆ–‡ไปถ้ข˜็ญ”ๆททๆŽ’",
51
- ex2_label="็คบไพ‹ 2๏ผšๅŒๆ–‡ไปถ๏ผˆ้ข˜็›ฎ + ็ญ”ๆกˆ๏ผ‰",
52
- key_note=(
53
- "**ไธคไธช API Key ็š„ๅŒบๅˆซ๏ผš**\n\n"
54
- "| Key | ็”จ้€” | ็”ณ่ฏทๅœฐๅ€ |\n|-----|------|----------|\n"
55
- "| LLM API Key | ่ฐƒ็”จ GPT/Gemini ็ญ‰ๅคงๆจกๅž‹ๆๅ– QA | ๅฏนๅบ” LLM ๆœๅŠกๅ•† |\n"
56
- "| **MinerU API Key** | ่งฃๆž PDF ็‰ˆ้ข๏ผˆๅฎŒๅ…จ็‹ฌ็ซ‹๏ผ‰ | [mineru.net/apiManage/token](https://mineru.net/apiManage/token) |"
57
- ),
58
  ),
59
  "en": dict(
60
  lang_btn="ไธญๆ–‡",
@@ -68,6 +62,10 @@ T = {
68
  sec_upload="๐Ÿ“„ Upload PDF",
69
  upload_label="PDF File(s) โ€” single: Q&A interleaved; two files: 1st questions, 2nd answers",
70
  task_label="Task Name",
 
 
 
 
71
  sec_llm="โš™๏ธ LLM Configuration",
72
  api_url_label="API Base URL",
73
  llm_key_label="LLM API Key (DF_API_KEY)",
@@ -84,20 +82,12 @@ T = {
84
  status_label="Status",
85
  status_ph="Click 'Start Extraction' to begin (may take several minutes)โ€ฆ",
86
  output_label="Download Result (raw_vqa.jsonl)",
87
- preview_label="Result Preview (up to 3 items)",
88
- sec_examples="๐Ÿ“‹ Example PDFs (click to load & preview)",
89
- pdf_preview_label="PDF Preview",
90
- ex1_label="Example 1: Single file (Q&A mixed)",
91
- ex2_label="Example 2: Two files (questions + answers)",
92
- key_note=(
93
- "**API Key Reference:**\n\n"
94
- "| Key | Purpose | Where to Get |\n|-----|---------|---------------|\n"
95
- "| LLM API Key | Call GPT/Gemini etc. for QA extraction | Your LLM provider |\n"
96
- "| **MinerU API Key** | PDF layout parsing (completely independent) | [mineru.net/apiManage/token](https://mineru.net/apiManage/token) |"
97
- ),
98
  ),
99
  }
100
 
 
 
101
  EXAMPLES = [
102
  ("examples/VQA/questionextract_test.pdf",),
103
  ("examples/VQA/math_question.pdf", "examples/VQA/math_answer.pdf"),
@@ -107,21 +97,19 @@ EXAMPLES = [
107
  # โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
108
 
109
  def _pdf_to_iframe(path: str) -> str:
110
- """Encode a local PDF as base64 and return an iframe HTML string."""
111
  try:
112
  with open(os.path.join(_REPO_ROOT, path), "rb") as f:
113
  b64 = base64.b64encode(f.read()).decode()
114
  return (
115
  f'<iframe src="data:application/pdf;base64,{b64}" '
116
  f'width="100%" height="520px" '
117
- f'style="border:1px solid #d1d5db;border-radius:10px;"></iframe>'
118
  )
119
  except Exception as e:
120
- return f"<p style='color:red'>ๆ— ๆณ•้ข„่งˆ PDF๏ผš{e}</p>"
121
 
122
 
123
- def _render_preview(jsonl_path: str, lang: str = "zh") -> str:
124
- """Render up to 3 JSONL QA items as styled HTML cards."""
125
  if not jsonl_path or not os.path.exists(jsonl_path):
126
  return ""
127
  items = []
@@ -139,7 +127,7 @@ def _render_preview(jsonl_path: str, lang: str = "zh") -> str:
139
 
140
  if not items:
141
  label_empty = "๏ผˆๆ—  QA ๆ•ฐๆฎ๏ผ‰" if lang == "zh" else "(No QA data)"
142
- return f"<p style='color:#666'>{label_empty}</p>"
143
 
144
  label_q = "้ข˜็›ฎ" if lang == "zh" else "Question"
145
  label_a = "็ญ”ๆกˆ" if lang == "zh" else "Answer"
@@ -153,18 +141,16 @@ def _render_preview(jsonl_path: str, lang: str = "zh") -> str:
153
  name = item.get("name", "")
154
  sol_short = (sol[:300] + "โ€ฆ") if len(sol) > 300 else sol
155
  sol_block = (
156
- f'<div style="margin-top:8px">'
157
  f'<span style="font-weight:600;color:#374151">{label_s}:</span>'
158
  f'<div style="margin-top:4px;white-space:pre-wrap;font-size:13px;color:#4b5563">{sol_short}</div>'
159
  f'</div>'
160
  ) if sol and sol != a else ""
161
 
162
  cards.append(f"""
163
- <div style="border:1px solid #e5e7eb;border-radius:12px;padding:18px;margin-bottom:14px;
164
  background:#ffffff;box-shadow:0 1px 4px rgba(0,0,0,.06);">
165
- <div style="font-size:11px;color:#9ca3af;margin-bottom:10px">
166
- #{i+1} &nbsp;ยท&nbsp; {name}
167
- </div>
168
  <div style="margin-bottom:10px">
169
  <span style="font-weight:600;color:#111827">{label_q}:</span>
170
  <div style="margin-top:4px;white-space:pre-wrap;font-size:14px">{q}</div>
@@ -181,26 +167,25 @@ def _render_preview(jsonl_path: str, lang: str = "zh") -> str:
181
  with open(jsonl_path, encoding="utf-8") as f:
182
  total = sum(1 for l in f if l.strip())
183
  if total > 3:
184
- more = f"๏ผˆๅ…ฑ {total} ๆก๏ผŒไป…ๅฑ•็คบๅ‰ 3 ๆก๏ผ‰" if lang == "zh" else f"({total} items total, showing first 3)"
185
  total_hint = f'<div style="font-size:12px;color:#6b7280;margin-bottom:10px">{more}</div>'
186
  except Exception:
187
  pass
188
 
 
189
  return (
190
- '<div style="max-height:560px;overflow-y:auto;padding-right:4px">'
191
- + total_hint
192
- + "".join(cards)
193
  + "</div>"
194
  )
195
 
196
 
197
- # โ”€โ”€ Backend (generator so stop button works) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
198
 
199
  def run_vqa_extraction(
200
  pdf_files, task_name, api_url, llm_api_key, mineru_api_key, model_name, max_workers, lang,
201
  ):
202
- t = T[lang]
203
-
204
  if pdf_files is None or (isinstance(pdf_files, list) and len(pdf_files) == 0):
205
  yield None, "โŒ ่ฏทๅ…ˆไธŠไผ  PDF ๆ–‡ไปถใ€‚" if lang == "zh" else "โŒ Please upload a PDF file first.", ""
206
  return
@@ -213,7 +198,7 @@ def run_vqa_extraction(
213
  msg = (
214
  "โŒ ่ฏทๅกซๅ†™ MinerU API Key๏ผˆ็‹ฌ็ซ‹ไบŽ LLM Key๏ผŒๅŽป https://mineru.net/apiManage/token ็”ณ่ฏท๏ผ‰ใ€‚"
215
  if lang == "zh" else
216
- "โŒ Please enter your MinerU API Key (independent from LLM key, get it at https://mineru.net/apiManage/token)."
217
  )
218
  yield None, msg, ""; return
219
 
@@ -229,8 +214,7 @@ def run_vqa_extraction(
229
  try:
230
  os.chdir(workspace)
231
 
232
- step1 = "โณ [1/4] ๆ•ด็† PDF ๆ–‡ไปถโ€ฆ" if lang == "zh" else "โณ [1/4] Preparing PDF filesโ€ฆ"
233
- yield None, step1, ""
234
 
235
  if not isinstance(pdf_files, list):
236
  pdf_files = [pdf_files]
@@ -249,12 +233,11 @@ def run_vqa_extraction(
249
  }
250
  fout.write(json.dumps(entry, ensure_ascii=False) + "\n")
251
 
252
- step2 = "โณ [2/4] ๅŠ ่ฝฝ Pipeline ๆจกๅ—โ€ฆ" if lang == "zh" else "โณ [2/4] Loading pipeline moduleโ€ฆ"
253
- yield None, step2, ""
254
  try:
255
  from pipelines.vqa_extract_optimized_pipeline import PDF_VQA_extract_optimized_pipeline
256
  except Exception:
257
- err = f"โŒ ๅฏผๅ…ฅ Pipeline ๅคฑ่ดฅ๏ผš\n{traceback.format_exc()}"
258
  yield None, err, ""; return
259
 
260
  try:
@@ -275,8 +258,11 @@ def run_vqa_extraction(
275
  yield None, f"โŒ {msg}", ""
276
  return
277
 
278
- step3 = "โณ [3/4] MinerU ่งฃๆž PDF + LLM ๆๅ– QA๏ผˆๅฏ่ƒฝ้œ€่ฆๆ•ฐๅˆ†้’Ÿ๏ผ‰โ€ฆ" if lang == "zh" else "โณ [3/4] MinerU parsing + LLM QA extraction (may take several minutes)โ€ฆ"
279
- yield None, step3, ""
 
 
 
280
 
281
  try:
282
  pipeline.forward()
@@ -284,22 +270,21 @@ def run_vqa_extraction(
284
  msg = str(e)
285
  if "no api found" in msg.lower() or "Apply upload urls failed" in msg:
286
  err = (
287
- "โŒ MinerU API Key ๆ— ๆ•ˆๆˆ–ๅทฒ่ฟ‡ๆœŸใ€‚่ฏทๅˆฐ https://mineru.net/apiManage/token ้‡ๆ–ฐ็”ณ่ฏทใ€‚\n\nๅŽŸๅง‹้”™่ฏฏ๏ผš" + msg
288
- if lang == "zh" else
289
- "โŒ MinerU API Key is invalid or expired. Please get a new one at https://mineru.net/apiManage/token.\n\nRaw error: " + msg
290
  )
291
  elif "Cannot connect to LLM server" in msg:
292
- err = ("โŒ ๆ— ๆณ•่ฟžๆŽฅ LLM API๏ผŒ่ฏทๆฃ€ๆŸฅ Base URLใ€‚\n\n" if lang == "zh" else "โŒ Cannot connect to LLM API. Check Base URL.\n\n") + msg
293
  else:
294
  err = f"โŒ {msg}"
295
  yield None, err, ""; return
296
 
297
- step4 = "โณ [4/4] ๆ•ด็†่พ“ๅ‡บ็ป“ๆžœโ€ฆ" if lang == "zh" else "โณ [4/4] Collecting outputโ€ฆ"
298
- yield None, step4, ""
299
 
300
  step_files = [f for f in os.listdir(cache_dir) if re.match(r"vqa_step\d+\.jsonl", f)]
301
  if not step_files:
302
- msg = "โŒ Pipeline ๅฎŒๆˆไฝ†ๆœชๆ‰พๅˆฐ่พ“ๅ‡บๆ–‡ไปถใ€‚" if lang == "zh" else "โŒ Pipeline finished but no output file found."
303
  yield None, msg, ""; return
304
 
305
  max_step = max(int(re.findall(r"vqa_step(\d+)\.jsonl", f)[0]) for f in step_files)
@@ -319,12 +304,11 @@ def run_vqa_extraction(
319
  f_out.write(json.dumps(out, ensure_ascii=False) + "\n")
320
  count += 1
321
 
322
- done = f"โœ… ๅฎŒๆˆ๏ผๅ…ฑๆๅ– {count} ๆก QA ๅฏนใ€‚" if lang == "zh" else f"โœ… Done! Extracted {count} QA pairs."
323
- preview_html = _render_preview(result_file, lang)
324
- yield result_file, done, preview_html
325
 
326
  except Exception:
327
- yield None, f"โŒ ๆœช็Ÿฅ้”™่ฏฏ๏ผš\n{traceback.format_exc()}", ""
328
  finally:
329
  os.chdir(original_cwd)
330
 
@@ -336,90 +320,95 @@ CSS = """
336
  .example-btn { margin: 4px 0 !important; }
337
  """
338
 
 
 
339
  with gr.Blocks(
340
  title="FlipVQA-Miner",
341
  theme=gr.themes.Soft(primary_hue="blue", neutral_hue="slate"),
342
  css=CSS,
343
  ) as demo:
344
 
345
- lang_state = gr.State("zh")
346
 
347
  # โ”€โ”€ Header โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
348
  with gr.Row(elem_id="title-row"):
349
  with gr.Column(scale=5):
350
  gr.Markdown("# FlipVQA-Miner: Multimodal Knowledge Extraction")
351
  with gr.Column(scale=0, min_width=110):
352
- lang_btn = gr.Button(T["zh"]["lang_btn"], elem_id="lang-btn", size="sm")
353
 
354
- subtitle_md = gr.Markdown(T["zh"]["subtitle"])
355
- desc_md = gr.Markdown(T["zh"]["desc"])
356
 
357
  # โ”€โ”€ Main layout โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
358
  with gr.Row():
359
  # โ”€โ”€ Left column: inputs โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
360
  with gr.Column(scale=1):
361
- sec_upload_md = gr.Markdown(f"### {T['zh']['sec_upload']}")
 
 
362
  pdf_files = gr.File(
363
- label=T["zh"]["upload_label"],
364
  file_types=[".pdf"],
365
  file_count="multiple",
366
  )
367
- task_name = gr.Textbox(label=T["zh"]["task_label"], value="task1")
 
 
 
 
 
 
 
 
 
 
368
 
369
- sec_llm_md = gr.Markdown(f"### {T['zh']['sec_llm']}")
 
370
  api_url = gr.Textbox(
371
- label=T["zh"]["api_url_label"],
372
  value="https://generativelanguage.googleapis.com/v1beta/openai/",
373
  )
374
  llm_api_key = gr.Textbox(
375
- label=T["zh"]["llm_key_label"],
376
  type="password",
377
- placeholder=T["zh"]["llm_key_ph"],
378
  )
379
  model_name = gr.Textbox(
380
- label=T["zh"]["model_label"],
381
  value="gemini-2.5-pro",
382
- placeholder=T["zh"]["model_ph"],
383
  )
384
 
385
- sec_mineru_md = gr.Markdown(f"### {T['zh']['sec_mineru']}")
 
386
  mineru_api_key = gr.Textbox(
387
- label=T["zh"]["mineru_key_label"],
388
  type="password",
389
  placeholder="sk2-...",
390
- info=T["zh"]["mineru_key_info"],
391
  )
392
- max_workers = gr.Slider(label=T["zh"]["workers_label"], minimum=1, maximum=30, value=5, step=1)
393
-
394
- with gr.Row():
395
- run_btn = gr.Button(T["zh"]["run_btn"], variant="primary", scale=4)
396
- stop_btn = gr.Button(T["zh"]["stop_btn"], variant="stop", scale=1)
397
 
398
- # โ”€โ”€ Example PDFs (below upload) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
399
- sec_examples_md = gr.Markdown(f"### {T['zh']['sec_examples']}")
400
  with gr.Row():
401
- ex1_btn = gr.Button(T["zh"]["ex1_label"], elem_classes="example-btn", scale=1)
402
- ex2_btn = gr.Button(T["zh"]["ex2_label"], elem_classes="example-btn", scale=1)
403
-
404
- pdf_preview_md = gr.Markdown(f"#### {T['zh']['pdf_preview_label']}")
405
- pdf_preview = gr.HTML(value="<p style='color:#9ca3af;font-size:13px'>็‚นๅ‡ปไธŠๆ–น็คบไพ‹ๆŒ‰้’ฎ้ข„่งˆ PDF</p>")
406
 
407
  # โ”€โ”€ Right column: outputs โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
408
  with gr.Column(scale=1):
409
- sec_output_md = gr.Markdown(f"### {T['zh']['sec_output']}")
410
  status_box = gr.Textbox(
411
- label=T["zh"]["status_label"],
412
  interactive=False,
413
  lines=6,
414
- placeholder=T["zh"]["status_ph"],
415
  )
416
- output_file = gr.File(label=T["zh"]["output_label"], interactive=False)
417
 
418
- preview_md = gr.Markdown(f"#### {T['zh']['preview_label']}")
419
  preview_box = gr.HTML(value="")
420
 
421
- key_note_md = gr.Markdown(T["zh"]["key_note"])
422
-
423
  # โ”€โ”€ Event: Run โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
424
  run_event = run_btn.click(
425
  fn=run_vqa_extraction,
@@ -432,26 +421,17 @@ with gr.Blocks(
432
 
433
  # โ”€โ”€ Event: Example buttons โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
434
  def load_example(ex_index, lang):
435
- paths = [os.path.join(_REPO_ROOT, p) for p in EXAMPLES[ex_index]]
436
- # build combined preview for all PDFs in example
437
  iframes = "".join(_pdf_to_iframe(p) for p in EXAMPLES[ex_index])
438
- label = T[lang]["ex1_label"] if ex_index == 0 else T[lang]["ex2_label"]
439
  html = (
440
- f'<div style="font-size:12px;color:#6b7280;margin-bottom:6px">{label}</div>'
441
  + iframes
442
  )
443
  return paths, html
444
 
445
- ex1_btn.click(
446
- fn=lambda lang: load_example(0, lang),
447
- inputs=[lang_state],
448
- outputs=[pdf_files, pdf_preview],
449
- )
450
- ex2_btn.click(
451
- fn=lambda lang: load_example(1, lang),
452
- inputs=[lang_state],
453
- outputs=[pdf_files, pdf_preview],
454
- )
455
 
456
  # โ”€โ”€ Event: Language toggle โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
457
  def toggle_lang(current_lang):
@@ -465,6 +445,10 @@ with gr.Blocks(
465
  gr.update(value=f"### {t['sec_upload']}"),
466
  gr.update(label=t["upload_label"]),
467
  gr.update(label=t["task_label"]),
 
 
 
 
468
  gr.update(value=f"### {t['sec_llm']}"),
469
  gr.update(label=t["api_url_label"]),
470
  gr.update(label=t["llm_key_label"], placeholder=t["llm_key_ph"]),
@@ -477,12 +461,7 @@ with gr.Blocks(
477
  gr.update(value=f"### {t['sec_output']}"),
478
  gr.update(label=t["status_label"], placeholder=t["status_ph"]),
479
  gr.update(label=t["output_label"]),
480
- gr.update(value=f"### {t['sec_examples']}"),
481
- gr.update(value=t["ex1_label"]),
482
- gr.update(value=t["ex2_label"]),
483
- gr.update(value=f"#### {t['pdf_preview_label']}"),
484
  gr.update(value=f"#### {t['preview_label']}"),
485
- gr.update(value=t["key_note"]),
486
  )
487
 
488
  lang_btn.click(
@@ -492,12 +471,12 @@ with gr.Blocks(
492
  lang_state, lang_btn,
493
  subtitle_md, desc_md,
494
  sec_upload_md, pdf_files, task_name,
 
495
  sec_llm_md, api_url, llm_api_key, model_name,
496
  sec_mineru_md, mineru_api_key, max_workers,
497
  run_btn, stop_btn,
498
  sec_output_md, status_box, output_file,
499
- sec_examples_md, ex1_btn, ex2_btn, pdf_preview_md,
500
- preview_md, key_note_md,
501
  ],
502
  )
503
 
 
28
  sec_upload="๐Ÿ“„ ไธŠไผ  PDF",
29
  upload_label="PDF ๆ–‡ไปถ๏ผˆๅ•ๆ–‡ไปถ๏ผš้ข˜็ญ”ๆททๆŽ’๏ผ›ๅŒๆ–‡ไปถ๏ผš็ฌฌ1ไธช้ข˜็›ฎ๏ผŒ็ฌฌ2ไธช็ญ”ๆกˆ๏ผ‰",
30
  task_label="ไปปๅŠกๅ็งฐ",
31
+ sec_examples="๐Ÿ“‹ ๅ†…็ฝฎ็คบไพ‹ PDF๏ผˆ็‚นๅ‡ปๅŠ ่ฝฝๅนถ้ข„่งˆ๏ผ‰",
32
+ ex1_label="็คบไพ‹ 1๏ผšๅ•ๆ–‡ไปถ้ข˜็ญ”ๆททๆŽ’",
33
+ ex2_label="็คบไพ‹ 2๏ผšๅŒๆ–‡ไปถ๏ผˆ้ข˜็›ฎ + ็ญ”ๆกˆ๏ผ‰",
34
+ pdf_preview_label="PDF ้ข„่งˆ",
35
  sec_llm="โš™๏ธ LLM ้…็ฝฎ",
36
  api_url_label="API Base URL",
37
  llm_key_label="LLM API Key๏ผˆDF_API_KEY๏ผ‰",
 
48
  status_label="่ฟ่กŒ็Šถๆ€",
49
  status_ph="็‚นๅ‡ปใ€Œๅผ€ๅง‹ๆๅ–ใ€ๅŽ่ฟ›ๅบฆๆ˜พ็คบๅœจ่ฟ™้‡Œ๏ผˆ่ฟ่กŒ้œ€ๆ•ฐๅˆ†้’Ÿ๏ผŒ่ฏท่€ๅฟƒ็ญ‰ๅพ…๏ผ‰โ€ฆ",
50
  output_label="ไธ‹่ฝฝ็ป“ๆžœ๏ผˆraw_vqa.jsonl๏ผ‰",
51
+ preview_label="็ป“ๆžœ้ข„่งˆ",
 
 
 
 
 
 
 
 
 
 
52
  ),
53
  "en": dict(
54
  lang_btn="ไธญๆ–‡",
 
62
  sec_upload="๐Ÿ“„ Upload PDF",
63
  upload_label="PDF File(s) โ€” single: Q&A interleaved; two files: 1st questions, 2nd answers",
64
  task_label="Task Name",
65
+ sec_examples="๐Ÿ“‹ Example PDFs (click to load & preview)",
66
+ ex1_label="Example 1: Single file (Q&A mixed)",
67
+ ex2_label="Example 2: Two files (questions + answers)",
68
+ pdf_preview_label="PDF Preview",
69
  sec_llm="โš™๏ธ LLM Configuration",
70
  api_url_label="API Base URL",
71
  llm_key_label="LLM API Key (DF_API_KEY)",
 
82
  status_label="Status",
83
  status_ph="Click 'Start Extraction' to begin (may take several minutes)โ€ฆ",
84
  output_label="Download Result (raw_vqa.jsonl)",
85
+ preview_label="Result Preview",
 
 
 
 
 
 
 
 
 
 
86
  ),
87
  }
88
 
89
+ _DEFAULT_LANG = "en"
90
+
91
  EXAMPLES = [
92
  ("examples/VQA/questionextract_test.pdf",),
93
  ("examples/VQA/math_question.pdf", "examples/VQA/math_answer.pdf"),
 
97
  # โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
98
 
99
  def _pdf_to_iframe(path: str) -> str:
 
100
  try:
101
  with open(os.path.join(_REPO_ROOT, path), "rb") as f:
102
  b64 = base64.b64encode(f.read()).decode()
103
  return (
104
  f'<iframe src="data:application/pdf;base64,{b64}" '
105
  f'width="100%" height="520px" '
106
+ f'style="border:1px solid #d1d5db;border-radius:10px;display:block;margin-bottom:8px;"></iframe>'
107
  )
108
  except Exception as e:
109
+ return f"<p style='color:red'>Cannot preview PDF: {e}</p>"
110
 
111
 
112
+ def _render_preview(jsonl_path: str, lang: str = "en") -> str:
 
113
  if not jsonl_path or not os.path.exists(jsonl_path):
114
  return ""
115
  items = []
 
127
 
128
  if not items:
129
  label_empty = "๏ผˆๆ—  QA ๆ•ฐๆฎ๏ผ‰" if lang == "zh" else "(No QA data)"
130
+ return f'<div style="padding:16px;color:#666">{label_empty}</div>'
131
 
132
  label_q = "้ข˜็›ฎ" if lang == "zh" else "Question"
133
  label_a = "็ญ”ๆกˆ" if lang == "zh" else "Answer"
 
141
  name = item.get("name", "")
142
  sol_short = (sol[:300] + "โ€ฆ") if len(sol) > 300 else sol
143
  sol_block = (
144
+ f'<div style="margin-top:10px">'
145
  f'<span style="font-weight:600;color:#374151">{label_s}:</span>'
146
  f'<div style="margin-top:4px;white-space:pre-wrap;font-size:13px;color:#4b5563">{sol_short}</div>'
147
  f'</div>'
148
  ) if sol and sol != a else ""
149
 
150
  cards.append(f"""
151
+ <div style="border:1px solid #e5e7eb;border-radius:12px;padding:18px;margin-bottom:12px;
152
  background:#ffffff;box-shadow:0 1px 4px rgba(0,0,0,.06);">
153
+ <div style="font-size:11px;color:#9ca3af;margin-bottom:10px">#{i+1} &nbsp;ยท&nbsp; {name}</div>
 
 
154
  <div style="margin-bottom:10px">
155
  <span style="font-weight:600;color:#111827">{label_q}:</span>
156
  <div style="margin-top:4px;white-space:pre-wrap;font-size:14px">{q}</div>
 
167
  with open(jsonl_path, encoding="utf-8") as f:
168
  total = sum(1 for l in f if l.strip())
169
  if total > 3:
170
+ more = f"๏ผˆๅ…ฑ {total} ๆก๏ผŒไป…ๅฑ•็คบๅ‰ 3 ๆก๏ผ‰" if lang == "zh" else f"{total} items total โ€” showing first 3"
171
  total_hint = f'<div style="font-size:12px;color:#6b7280;margin-bottom:10px">{more}</div>'
172
  except Exception:
173
  pass
174
 
175
+ inner = total_hint + "".join(cards)
176
  return (
177
+ '<div style="background:#f9fafb;border:1px solid #e5e7eb;border-radius:12px;'
178
+ 'padding:16px;max-height:580px;overflow-y:auto;">'
179
+ + inner
180
  + "</div>"
181
  )
182
 
183
 
184
+ # โ”€โ”€ Backend (generator โ†’ stop button works) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
185
 
186
  def run_vqa_extraction(
187
  pdf_files, task_name, api_url, llm_api_key, mineru_api_key, model_name, max_workers, lang,
188
  ):
 
 
189
  if pdf_files is None or (isinstance(pdf_files, list) and len(pdf_files) == 0):
190
  yield None, "โŒ ่ฏทๅ…ˆไธŠไผ  PDF ๆ–‡ไปถใ€‚" if lang == "zh" else "โŒ Please upload a PDF file first.", ""
191
  return
 
198
  msg = (
199
  "โŒ ่ฏทๅกซๅ†™ MinerU API Key๏ผˆ็‹ฌ็ซ‹ไบŽ LLM Key๏ผŒๅŽป https://mineru.net/apiManage/token ็”ณ่ฏท๏ผ‰ใ€‚"
200
  if lang == "zh" else
201
+ "โŒ Please enter your MinerU API Key (get it at https://mineru.net/apiManage/token)."
202
  )
203
  yield None, msg, ""; return
204
 
 
214
  try:
215
  os.chdir(workspace)
216
 
217
+ yield None, "โณ [1/4] Preparing PDF filesโ€ฆ" if lang == "en" else "โณ [1/4] ๆ•ด็† PDF ๆ–‡ไปถโ€ฆ", ""
 
218
 
219
  if not isinstance(pdf_files, list):
220
  pdf_files = [pdf_files]
 
233
  }
234
  fout.write(json.dumps(entry, ensure_ascii=False) + "\n")
235
 
236
+ yield None, "โณ [2/4] Loading pipeline moduleโ€ฆ" if lang == "en" else "โณ [2/4] ๅŠ ่ฝฝ Pipeline ๆจกๅ—โ€ฆ", ""
 
237
  try:
238
  from pipelines.vqa_extract_optimized_pipeline import PDF_VQA_extract_optimized_pipeline
239
  except Exception:
240
+ err = f"โŒ Failed to import pipeline:\n{traceback.format_exc()}"
241
  yield None, err, ""; return
242
 
243
  try:
 
258
  yield None, f"โŒ {msg}", ""
259
  return
260
 
261
+ yield None, (
262
+ "โณ [3/4] MinerU parsing + LLM QA extraction (may take several minutes)โ€ฆ"
263
+ if lang == "en" else
264
+ "โณ [3/4] MinerU ่งฃๆž PDF + LLM ๆๅ– QA๏ผˆๅฏ่ƒฝ้œ€่ฆๆ•ฐๅˆ†้’Ÿ๏ผ‰โ€ฆ"
265
+ ), ""
266
 
267
  try:
268
  pipeline.forward()
 
270
  msg = str(e)
271
  if "no api found" in msg.lower() or "Apply upload urls failed" in msg:
272
  err = (
273
+ "โŒ MinerU API Key invalid or expired. Get a new one at https://mineru.net/apiManage/token\n\n" + msg
274
+ if lang == "en" else
275
+ "โŒ MinerU API Key ๆ— ๆ•ˆๆˆ–ๅทฒ่ฟ‡ๆœŸใ€‚่ฏทๅˆฐ https://mineru.net/apiManage/token ้‡ๆ–ฐ็”ณ่ฏทใ€‚\n\n" + msg
276
  )
277
  elif "Cannot connect to LLM server" in msg:
278
+ err = ("โŒ Cannot connect to LLM API. Check Base URL.\n\n" if lang == "en" else "โŒ ๆ— ๆณ•่ฟžๆŽฅ LLM API๏ผŒ่ฏทๆฃ€ๆŸฅ Base URLใ€‚\n\n") + msg
279
  else:
280
  err = f"โŒ {msg}"
281
  yield None, err, ""; return
282
 
283
+ yield None, "โณ [4/4] Collecting outputโ€ฆ" if lang == "en" else "โณ [4/4] ๆ•ด็†่พ“ๅ‡บ็ป“ๆžœโ€ฆ", ""
 
284
 
285
  step_files = [f for f in os.listdir(cache_dir) if re.match(r"vqa_step\d+\.jsonl", f)]
286
  if not step_files:
287
+ msg = "โŒ Pipeline finished but no output file found." if lang == "en" else "โŒ Pipeline ๅฎŒๆˆไฝ†ๆœชๆ‰พๅˆฐ่พ“ๅ‡บๆ–‡ไปถใ€‚"
288
  yield None, msg, ""; return
289
 
290
  max_step = max(int(re.findall(r"vqa_step(\d+)\.jsonl", f)[0]) for f in step_files)
 
304
  f_out.write(json.dumps(out, ensure_ascii=False) + "\n")
305
  count += 1
306
 
307
+ done = f"โœ… Done! Extracted {count} QA pairs." if lang == "en" else f"โœ… ๅฎŒๆˆ๏ผๅ…ฑๆๅ– {count} ๆก QA ๅฏนใ€‚"
308
+ yield result_file, done, _render_preview(result_file, lang)
 
309
 
310
  except Exception:
311
+ yield None, f"โŒ Unexpected error:\n{traceback.format_exc()}", ""
312
  finally:
313
  os.chdir(original_cwd)
314
 
 
320
  .example-btn { margin: 4px 0 !important; }
321
  """
322
 
323
+ _L = _DEFAULT_LANG # shorthand for initial render
324
+
325
  with gr.Blocks(
326
  title="FlipVQA-Miner",
327
  theme=gr.themes.Soft(primary_hue="blue", neutral_hue="slate"),
328
  css=CSS,
329
  ) as demo:
330
 
331
+ lang_state = gr.State(_DEFAULT_LANG)
332
 
333
  # โ”€โ”€ Header โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
334
  with gr.Row(elem_id="title-row"):
335
  with gr.Column(scale=5):
336
  gr.Markdown("# FlipVQA-Miner: Multimodal Knowledge Extraction")
337
  with gr.Column(scale=0, min_width=110):
338
+ lang_btn = gr.Button(T[_L]["lang_btn"], elem_id="lang-btn", size="sm")
339
 
340
+ subtitle_md = gr.Markdown(T[_L]["subtitle"])
341
+ desc_md = gr.Markdown(T[_L]["desc"])
342
 
343
  # โ”€โ”€ Main layout โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
344
  with gr.Row():
345
  # โ”€โ”€ Left column: inputs โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
346
  with gr.Column(scale=1):
347
+
348
+ # 1. Upload PDF
349
+ sec_upload_md = gr.Markdown(f"### {T[_L]['sec_upload']}")
350
  pdf_files = gr.File(
351
+ label=T[_L]["upload_label"],
352
  file_types=[".pdf"],
353
  file_count="multiple",
354
  )
355
+ task_name = gr.Textbox(label=T[_L]["task_label"], value="task1")
356
+
357
+ # 2. Example PDFs (between upload and LLM config)
358
+ sec_examples_md = gr.Markdown(f"### {T[_L]['sec_examples']}")
359
+ with gr.Row():
360
+ ex1_btn = gr.Button(T[_L]["ex1_label"], elem_classes="example-btn", scale=1)
361
+ ex2_btn = gr.Button(T[_L]["ex2_label"], elem_classes="example-btn", scale=1)
362
+ pdf_preview_md = gr.Markdown(f"#### {T[_L]['pdf_preview_label']}")
363
+ pdf_preview = gr.HTML(
364
+ value="<p style='color:#9ca3af;font-size:13px;padding:8px 0'>Click an example button to preview a PDF here.</p>"
365
+ )
366
 
367
+ # 3. LLM config
368
+ sec_llm_md = gr.Markdown(f"### {T[_L]['sec_llm']}")
369
  api_url = gr.Textbox(
370
+ label=T[_L]["api_url_label"],
371
  value="https://generativelanguage.googleapis.com/v1beta/openai/",
372
  )
373
  llm_api_key = gr.Textbox(
374
+ label=T[_L]["llm_key_label"],
375
  type="password",
376
+ placeholder=T[_L]["llm_key_ph"],
377
  )
378
  model_name = gr.Textbox(
379
+ label=T[_L]["model_label"],
380
  value="gemini-2.5-pro",
381
+ placeholder=T[_L]["model_ph"],
382
  )
383
 
384
+ # 4. MinerU config
385
+ sec_mineru_md = gr.Markdown(f"### {T[_L]['sec_mineru']}")
386
  mineru_api_key = gr.Textbox(
387
+ label=T[_L]["mineru_key_label"],
388
  type="password",
389
  placeholder="sk2-...",
390
+ info=T[_L]["mineru_key_info"],
391
  )
392
+ max_workers = gr.Slider(label=T[_L]["workers_label"], minimum=1, maximum=30, value=5, step=1)
 
 
 
 
393
 
 
 
394
  with gr.Row():
395
+ run_btn = gr.Button(T[_L]["run_btn"], variant="primary", scale=4)
396
+ stop_btn = gr.Button(T[_L]["stop_btn"], variant="stop", scale=1)
 
 
 
397
 
398
  # โ”€โ”€ Right column: outputs โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
399
  with gr.Column(scale=1):
400
+ sec_output_md = gr.Markdown(f"### {T[_L]['sec_output']}")
401
  status_box = gr.Textbox(
402
+ label=T[_L]["status_label"],
403
  interactive=False,
404
  lines=6,
405
+ placeholder=T[_L]["status_ph"],
406
  )
407
+ output_file = gr.File(label=T[_L]["output_label"], interactive=False)
408
 
409
+ preview_md = gr.Markdown(f"#### {T[_L]['preview_label']}")
410
  preview_box = gr.HTML(value="")
411
 
 
 
412
  # โ”€โ”€ Event: Run โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
413
  run_event = run_btn.click(
414
  fn=run_vqa_extraction,
 
421
 
422
  # โ”€โ”€ Event: Example buttons โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
423
  def load_example(ex_index, lang):
424
+ paths = [os.path.join(_REPO_ROOT, p) for p in EXAMPLES[ex_index]]
425
+ label = T[lang]["ex1_label"] if ex_index == 0 else T[lang]["ex2_label"]
426
  iframes = "".join(_pdf_to_iframe(p) for p in EXAMPLES[ex_index])
 
427
  html = (
428
+ f'<div style="font-size:12px;color:#6b7280;margin-bottom:8px">{label}</div>'
429
  + iframes
430
  )
431
  return paths, html
432
 
433
+ ex1_btn.click(fn=lambda lang: load_example(0, lang), inputs=[lang_state], outputs=[pdf_files, pdf_preview])
434
+ ex2_btn.click(fn=lambda lang: load_example(1, lang), inputs=[lang_state], outputs=[pdf_files, pdf_preview])
 
 
 
 
 
 
 
 
435
 
436
  # โ”€โ”€ Event: Language toggle โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
437
  def toggle_lang(current_lang):
 
445
  gr.update(value=f"### {t['sec_upload']}"),
446
  gr.update(label=t["upload_label"]),
447
  gr.update(label=t["task_label"]),
448
+ gr.update(value=f"### {t['sec_examples']}"),
449
+ gr.update(value=t["ex1_label"]),
450
+ gr.update(value=t["ex2_label"]),
451
+ gr.update(value=f"#### {t['pdf_preview_label']}"),
452
  gr.update(value=f"### {t['sec_llm']}"),
453
  gr.update(label=t["api_url_label"]),
454
  gr.update(label=t["llm_key_label"], placeholder=t["llm_key_ph"]),
 
461
  gr.update(value=f"### {t['sec_output']}"),
462
  gr.update(label=t["status_label"], placeholder=t["status_ph"]),
463
  gr.update(label=t["output_label"]),
 
 
 
 
464
  gr.update(value=f"#### {t['preview_label']}"),
 
465
  )
466
 
467
  lang_btn.click(
 
471
  lang_state, lang_btn,
472
  subtitle_md, desc_md,
473
  sec_upload_md, pdf_files, task_name,
474
+ sec_examples_md, ex1_btn, ex2_btn, pdf_preview_md,
475
  sec_llm_md, api_url, llm_api_key, model_name,
476
  sec_mineru_md, mineru_api_key, max_workers,
477
  run_btn, stop_btn,
478
  sec_output_md, status_box, output_file,
479
+ preview_md,
 
480
  ],
481
  )
482