Laramie2 commited on
Commit
82e8b96
·
verified ·
1 Parent(s): d4894c7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +208 -369
app.py CHANGED
@@ -6,96 +6,22 @@ import subprocess
6
  import sys
7
  from datetime import datetime
8
  from concurrent.futures import ThreadPoolExecutor, as_completed
9
- from typing import Iterable
10
- from gradio.themes import Soft
11
- from gradio.themes.utils import colors, fonts, sizes
12
 
13
- # Initialize environment paths
14
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
15
  PAPERS_DIR = os.path.join(BASE_DIR, "papers")
16
  CONFIG_PATH = os.path.join(BASE_DIR, "config.yaml")
17
  OUTPUT_DIR = os.path.join(BASE_DIR, "mineru_outputs")
18
- ZIP_OUTPUT_PATH = os.path.join(BASE_DIR, "mineru_results.zip")
19
 
20
  os.makedirs(PAPERS_DIR, exist_ok=True)
21
 
22
- # ==========================================
23
- # --- 🎨 Custom Purple Theme Definition ---
24
- # ==========================================
25
- colors.purple = colors.Color(
26
- name="purple",
27
- c50="#FAF5FF",
28
- c100="#F3E8FF",
29
- c200="#E9D5FF",
30
- c300="#DAB2FF",
31
- c400="#C084FC",
32
- c500="#A855F7",
33
- c600="#9333EA",
34
- c700="#7E22CE",
35
- c800="#6B21A8",
36
- c900="#581C87",
37
- c950="#3B0764",
38
- )
39
-
40
- class PurpleTheme(Soft):
41
- def __init__(
42
- self,
43
- *,
44
- primary_hue: colors.Color | str = colors.gray,
45
- secondary_hue: colors.Color | str = colors.purple,
46
- neutral_hue: colors.Color | str = colors.slate,
47
- text_size: sizes.Size | str = sizes.text_lg,
48
- font: fonts.Font | str | Iterable[fonts.Font | str] = (
49
- fonts.GoogleFont("Outfit"), "Arial", "sans-serif",
50
- ),
51
- font_mono: fonts.Font | str | Iterable[fonts.Font | str] = (
52
- fonts.GoogleFont("IBM Plex Mono"), "ui-monospace", "monospace",
53
- ),
54
- ):
55
- super().__init__(
56
- primary_hue=primary_hue,
57
- secondary_hue=secondary_hue,
58
- neutral_hue=neutral_hue,
59
- text_size=text_size,
60
- font=font,
61
- font_mono=font_mono,
62
- )
63
- super().set(
64
- background_fill_primary="*primary_50",
65
- background_fill_primary_dark="*primary_900",
66
- body_background_fill="linear-gradient(135deg, *primary_200, *primary_100)",
67
- body_background_fill_dark="linear-gradient(135deg, *primary_900, *primary_800)",
68
- button_primary_text_color="white",
69
- button_primary_text_color_hover="white",
70
- button_primary_background_fill="linear-gradient(90deg, *secondary_500, *secondary_600)",
71
- button_primary_background_fill_hover="linear-gradient(90deg, *secondary_600, *secondary_700)",
72
- button_primary_background_fill_dark="linear-gradient(90deg, *secondary_600, *secondary_700)",
73
- button_primary_background_fill_hover_dark="linear-gradient(90deg, *secondary_500, *secondary_600)",
74
- button_secondary_text_color="black",
75
- button_secondary_text_color_hover="white",
76
- button_secondary_background_fill="linear-gradient(90deg, *primary_300, *primary_300)",
77
- button_secondary_background_fill_hover="linear-gradient(90deg, *primary_400, *primary_400)",
78
- button_secondary_background_fill_dark="linear-gradient(90deg, *primary_500, *primary_600)",
79
- button_secondary_background_fill_hover_dark="linear-gradient(90deg, *primary_500, *primary_500)",
80
- slider_color="*secondary_500",
81
- slider_color_dark="*secondary_600",
82
- block_title_text_weight="600",
83
- block_border_width="3px",
84
- block_shadow="*shadow_drop_lg",
85
- button_primary_shadow="*shadow_drop_lg",
86
- button_large_padding="11px",
87
- color_accent_soft="*primary_100",
88
- block_label_background_fill="*primary_200",
89
- )
90
-
91
- purple_theme = PurpleTheme()
92
-
93
- # ==========================================
94
- # --- ⚙️ Backend Logic & Functions ---
95
- # ==========================================
96
  def get_debug_info():
 
97
  now = datetime.now().strftime("%H:%M:%S")
98
  files = os.listdir(PAPERS_DIR) if os.path.exists(PAPERS_DIR) else "Directory missing"
 
 
99
  output_detail = "Not generated"
100
  if os.path.exists(OUTPUT_DIR):
101
  all_output_items = []
@@ -103,64 +29,56 @@ def get_debug_info():
103
  for name in files_in_out:
104
  all_output_items.append(os.path.join(os.path.relpath(root, OUTPUT_DIR), name))
105
  output_detail = f"Found {len(all_output_items)} files: {all_output_items[:5]}..." if all_output_items else "Directory exists but is EMPTY"
106
- return f"[{now}] 📁 papers/ Content: {files}\n\n[{now}] 📂 mineru_outputs Status: {output_detail}"
 
107
 
108
  def save_pdf(file):
109
- if file is None: return "❌ Please select a PDF first", get_debug_info()
110
  try:
111
- # 先清空原有 PDF,确保只有最新的一份
112
- for f in os.listdir(PAPERS_DIR):
113
- file_to_del = os.path.join(PAPERS_DIR, f)
114
- if os.path.isfile(file_to_del):
115
- os.remove(file_to_del)
116
-
117
  file_path = os.path.join(PAPERS_DIR, os.path.basename(file.name))
118
  shutil.copy(file.name, file_path)
119
- return f"✅ Saved: {os.path.basename(file.name)}", get_debug_info()
120
  except Exception as e:
121
- return f"❌ Error: {str(e)}", get_debug_info()
122
-
123
- def clear_pdf():
124
- """Delete PDF from the backend directory when cleared in UI"""
125
- try:
126
- deleted_files = []
127
- for f in os.listdir(PAPERS_DIR):
128
- file_to_del = os.path.join(PAPERS_DIR, f)
129
- if os.path.isfile(file_to_del):
130
- os.remove(file_to_del)
131
- deleted_files.append(f)
132
- if deleted_files:
133
- return "🗑️ File deleted from workspace", get_debug_info()
134
- return "ℹ️ Workspace is already empty", get_debug_info()
135
- except Exception as e:
136
- return f"❌ Error deleting file: {str(e)}", get_debug_info()
137
 
138
  def save_api_settings(api_key, api_base_url=None):
139
- if not api_key: return "❌ Key cannot be empty", get_debug_info()
140
  try:
141
  config = {}
142
  if os.path.exists(CONFIG_PATH):
143
  with open(CONFIG_PATH, "r", encoding="utf-8") as f:
144
  config = yaml.safe_load(f) or {}
 
 
145
  config.setdefault("api_keys", {})["gemini_api_key"] = api_key
 
 
146
  if api_base_url:
147
  config["api_base_url"] = api_base_url
 
 
148
  with open(CONFIG_PATH, "w", encoding="utf-8") as f:
149
  yaml.dump(config, f, allow_unicode=True)
150
- success_msg = "✅ Key saved"
 
 
151
  if api_base_url:
152
- success_msg += ", Base URL updated"
 
153
  return success_msg, get_debug_info()
154
  except Exception as e:
155
- return f"❌ Error: {str(e)}", get_debug_info()
 
156
 
157
  def run_mineru_parsing_and_dag_gen():
 
158
  if not os.path.exists(PAPERS_DIR) or not any(f.endswith('.pdf') for f in os.listdir(PAPERS_DIR)):
159
- yield "❌ No PDF file found", get_debug_info(), "No execution logs."
160
  return
161
 
162
  full_log = ""
163
  try:
 
164
  env = os.environ.copy()
165
  env["MINERU_FORMULA_ENABLE"] = "false"
166
  env["MINERU_TABLE_ENABLE"] = "false"
@@ -168,336 +86,257 @@ def run_mineru_parsing_and_dag_gen():
168
  env["MINERU_VIRTUAL_VRAM_SIZE"] = "8"
169
 
170
  command_mineru = ["mineru", "-p", PAPERS_DIR, "-o", OUTPUT_DIR]
171
- full_log += "--- Mineru Executing ---\n"
172
- yield " Executing Mineru parsing...", get_debug_info(), full_log
 
173
 
 
174
  process_mineru = subprocess.Popen(
175
- command_mineru, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1
 
 
 
 
 
176
  )
 
 
177
  for line in iter(process_mineru.stdout.readline, ''):
178
  full_log += line
179
- yield "⏳ Executing Mineru parsing...", get_debug_info(), full_log
 
180
  process_mineru.stdout.close()
181
  returncode_mineru = process_mineru.wait()
182
 
 
183
  if returncode_mineru != 0:
184
- yield f"❌ Mineru parsing failed (Exit Code: {returncode_mineru})", get_debug_info(), full_log
 
185
  return
186
 
 
187
  command_dag = [sys.executable, "gen_dag.py"]
188
- full_log += "\n--- DAG Gen Executing ---\n"
189
- yield " Mineru parsing complete, executing DAG generation...", get_debug_info(), full_log
 
190
 
191
  process_dag = subprocess.Popen(
192
- command_dag, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1
 
 
 
 
193
  )
 
194
  for line in iter(process_dag.stdout.readline, ''):
195
  full_log += line
196
- yield "⏳ Executing DAG generation...", get_debug_info(), full_log
 
197
  process_dag.stdout.close()
198
  returncode_dag = process_dag.wait()
199
 
200
- status = "✅ PDF parsing & DAG generation fully completed" if returncode_dag == 0 else f"❌ DAG generation failed (Exit Code: {returncode_dag})"
 
 
 
 
201
  yield status, get_debug_info(), full_log
202
 
203
  except Exception as e:
204
- error_log = full_log + f"\n[Global Exception] Exception occurred:\n{str(e)}"
205
- yield "❌ Execution Exception", get_debug_info(), error_log
206
 
207
  def run_final_generation(task_type="all"):
 
 
 
 
208
  if not os.path.exists(OUTPUT_DIR):
209
- return "❌ Please run the parsing step first", get_debug_info(), "No output folder found.", None
210
 
 
211
  scripts_to_run = []
212
- if task_type == "ppt": scripts_to_run = ["gen_ppt.py"]
213
- elif task_type == "poster": scripts_to_run = ["gen_poster.py"]
214
- elif task_type == "pr": scripts_to_run = ["gen_pr.py"]
215
- elif task_type == "all": scripts_to_run = ["gen_ppt.py", "gen_poster.py", "gen_pr.py"]
216
- else: return "❌ Unknown task type", get_debug_info(), "Invalid task_type.", None
217
-
218
- full_log = f"🚀 Preparing to start {len(scripts_to_run)} tasks...\n"
 
 
 
 
 
219
  success = True
220
 
 
221
  def execute_script(script):
222
  command = [sys.executable, script]
223
- return script, subprocess.run(command, capture_output=True, text=True, timeout=600)
 
 
 
 
 
 
224
 
225
  try:
 
226
  with ThreadPoolExecutor(max_workers=len(scripts_to_run)) as executor:
 
227
  future_to_script = {executor.submit(execute_script, s): s for s in scripts_to_run}
 
 
228
  for future in as_completed(future_to_script):
229
  script_name = future_to_script[future]
230
  try:
 
231
  _, result = future.result()
232
- full_log += f"\n================ ✅ Execution Complete: {script_name} ================\n"
 
233
  full_log += f"--- STDOUT ---\n{result.stdout}\n\n--- STDERR ---\n{result.stderr}\n"
 
 
234
  if result.returncode != 0:
235
  success = False
236
- full_log += f"❌ [Error] {script_name} returned non-zero exit code (Exit Code: {result.returncode})\n"
 
237
  except subprocess.TimeoutExpired as e:
238
  success = False
239
- full_log += f"\n================ ❌ Task Timeout: {script_name} ================\n{str(e)}\n"
240
  except Exception as e:
241
  success = False
242
- full_log += f"\n================ ❌ Task Exception: {script_name} ================\n{str(e)}\n"
243
 
244
- if not success: return f"❌ {task_type.upper()} contains failed tasks, please check logs", get_debug_info(), full_log, None
 
 
245
 
 
246
  zip_base_name = ZIP_OUTPUT_PATH.replace(".zip", "")
247
  shutil.make_archive(zip_base_name, 'zip', OUTPUT_DIR)
248
- return f"✅ {task_type.upper()} generated and zipped successfully", get_debug_info(), full_log, ZIP_OUTPUT_PATH
 
 
 
249
  except Exception as e:
250
- error_log = full_log + f"\n[Global Exception] Exception occurred:\n{str(e)}"
251
- return "❌ Global exception during final generation", get_debug_info(), error_log, None
252
 
253
  # ==========================================
254
- # --- 🚀 UI Configuration & Advanced CSS ---
255
  # ==========================================
256
- custom_css = """
257
- @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap');
258
-
259
- body, .gradio-container {
260
- background-color: #FAF5FF !important;
261
- background-image: linear-gradient(#E9D5FF 1px, transparent 1px), linear-gradient(90deg, #E9D5FF 1px, transparent 1px) !important;
262
- background-size: 40px 40px !important;
263
- font-family: 'Outfit', sans-serif !important;
264
- }
265
-
266
- .dark body, .dark .gradio-container {
267
- background-color: #1a1a1a !important;
268
- background-image: linear-gradient(rgba(168, 85, 247, .1) 1px, transparent 1px), linear-gradient(90deg, rgba(168, 85, 247, .1) 1px, transparent 1px) !important;
269
- }
270
-
271
- #col-container { margin: 0 auto; max-width: 1200px; padding: 20px; }
272
-
273
- #main-title { text-align: center !important; padding: 1.5rem 0 0.5rem 0; }
274
- #main-title h1 {
275
- font-size: 2.6em !important; font-weight: 800 !important;
276
- background: linear-gradient(135deg, #A855F7 0%, #C084FC 50%, #9333EA 100%);
277
- background-size: 200% 200%;
278
- -webkit-background-clip: text; -webkit-text-fill-color: transparent;
279
- animation: gradient-shift 4s ease infinite; letter-spacing: -0.02em;
280
- }
281
-
282
- @keyframes gradient-shift { 0%, 100% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } }
283
-
284
- #subtitle { text-align: center !important; margin-bottom: 2rem; }
285
- #subtitle p { margin: 0 auto; color: #666; font-size: 1.1rem; font-weight: 500; }
286
- .dark #subtitle p { color: #DAB2FF; }
287
-
288
- /* ======== 修改:模块卡片强化圆角和内外边距 ======== */
289
- .gradio-group {
290
- background: rgba(255, 255, 255, 0.9) !important;
291
- border: 2px solid #E9D5FF !important;
292
- border-radius: 24px !important; /* 更明显的圆角 */
293
- box-shadow: 0 4px 24px rgba(168, 85, 247, 0.08) !important;
294
- backdrop-filter: blur(10px);
295
- transition: all 0.3s ease;
296
- padding: 24px 24px !important; /* 边缘与内层内容的间距 */
297
- margin-bottom: 20px !important; /* 模块之间的垂直间距 */
298
- display: flex !important;
299
- flex-direction: column !important;
300
- gap: 16px !important; /* 内部组件之间的自然间距 */
301
- }
302
- .gradio-group:hover {
303
- box-shadow: 0 8px 32px rgba(168, 85, 247, 0.12) !important;
304
- border-color: #C084FC !important;
305
- }
306
- .dark .gradio-group {
307
- background: rgba(30, 30, 30, 0.9) !important;
308
- border-color: rgba(168, 85, 247, 0.3) !important;
309
- }
310
 
311
- /* 恢复 Gradio 内部组件圆角(Gradio默认会将 group 内的元素边缘变直) */
312
- .gradio-group > div,
313
- .gradio-group > .form {
314
- border-radius: 12px !important;
315
- }
316
-
317
- /* ======== 优化:文件上传框居中与背景清理 ======== */
318
- #pdf-upload-box {
319
- border: 2px dashed rgba(192, 132, 252, 0.6) !important;
320
- border-radius: 16px !important;
321
- background-color: rgba(250, 245, 255, 0.5) !important;
322
- transition: all 0.3s ease !important;
323
- min-height: 220px !important; /* 增加高度提供居中空间 */
324
- position: relative !important;
325
- margin-top: 10px !important;
326
- }
327
-
328
- #pdf-upload-box:hover {
329
- border-color: #A855F7 !important;
330
- background-color: rgba(243, 232, 255, 0.8) !important;
331
- box-shadow: 0 4px 15px rgba(168, 85, 247, 0.15) !important;
332
- }
333
-
334
- /* 隐藏原生的中/英文上传提示、图标及背景 */
335
- #pdf-upload-box .upload-container {
336
- background: transparent !important;
337
- }
338
- #pdf-upload-box .upload-container > span,
339
- #pdf-upload-box .upload-container > svg {
340
- display: none !important;
341
- }
342
-
343
- /* 使用伪元素在中央注入自定义文字和图标 */
344
- #pdf-upload-box .upload-container::before {
345
- content: "📤\\A Click here to select a PDF\\A or Drag & Drop the file here";
346
- white-space: pre-wrap;
347
- font-size: 1.2rem;
348
- line-height: 1.8;
349
- font-weight: 600;
350
- color: #9333EA;
351
- display: flex;
352
- align-items: center;
353
- justify-content: center;
354
- text-align: center;
355
- width: 100%;
356
- height: 100%;
357
- position: absolute;
358
- top: 0;
359
- left: 0;
360
- pointer-events: none; /* 防止遮挡拖拽事件 */
361
- }
362
-
363
- /* ======== 修改:减小按钮尺寸,保持字体一致 ======== */
364
- /* 主要操作按钮 (Start Mineru & Generate All) */
365
- .primary-action-btn {
366
- border-radius: 25px !important; /* 调小 */
367
- background: linear-gradient(135deg, #9333EA, #7E22CE) !important;
368
- color: white !important; font-weight: 700 !important; border: none !important;
369
- height: 50px !important; /* 调小:60 -> 50 */
370
- width: 80% !important;
371
- margin-left: auto !important; /* 新增:自动左边距 */
372
- margin-right: auto !important; /* 新增:自动右边距 */
373
- display: block !important; /* 新增:确保块级元素 */
374
- transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275), box-shadow 0.3s ease !important;
375
- box-shadow: 0 4px 15px rgba(126, 34, 206, 0.3) !important;
376
- cursor: pointer !important;
377
- font-size: 1.05rem !important; /* 字体保持一致:略调小以配合新尺寸 */
378
- margin-top: 10px !important;
379
- }
380
- .primary-action-btn:hover {
381
- transform: translateY(-5px) scale(1.02) !important;
382
- box-shadow: 0 10px 25px rgba(126, 34, 206, 0.5) !important;
383
- background: linear-gradient(135deg, #A855F7, #9333EA) !important;
384
- }
385
- .primary-action-btn:active { transform: translateY(2px) scale(0.98) !important; box-shadow: 0 2px 10px rgba(126, 34, 206, 0.2) !important; }
386
-
387
- /* 3个小按钮的样式 */
388
- .action-row { display: flex !important; gap: 13px !important; margin-bottom: 10px !important; margin-top: 10px !important;}
389
- .action-btn {
390
- border-radius: 24px !important; /* 调小 */
391
- background: linear-gradient(135deg, #A855F7, #9333EA) !important;
392
- color: white !important; font-weight: 600 !important; border: none !important;
393
- height: 48px !important; /* 调小:55 -> 48 */
394
- flex: 0.8 !important;
395
- transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275), box-shadow 0.3s ease !important;
396
- box-shadow: 0 4px 15px rgba(147, 51, 234, 0.2) !important;
397
- cursor: pointer !important; display: flex !important; align-items: center !important; justify-content: center !important;
398
- font-size: 1.05rem !important; /* 字体保持一致:显式设置 */
399
  }
400
- .action-btn:hover {
401
- transform: translateY(-5px) scale(1.03) !important;
402
- box-shadow: 0 10px 25px rgba(147, 51, 234, 0.4) !important;
403
- background: linear-gradient(135deg, #C084FC, #A855F7) !important;
 
 
404
  }
405
- .action-btn:active { transform: translateY(2px) scale(0.98) !important; box-shadow: 0 2px 10px rgba(147, 51, 234, 0.2) !important; }
406
-
407
- /* Terminal Log Style */
408
- .log-box textarea { font-family: 'IBM Plex Mono', monospace !important; font-size: 13px !important; background-color: #1e1e1e !important; color: #DAB2FF !important; border: 1px solid #C084FC !important; border-radius: 8px !important; }
409
- .status-text textarea { background-color: transparent !important; border: none !important; box-shadow: none !important; font-weight: 600 !important; color: #6B21A8 !important; }
410
- .dark .status-text textarea { color: #C084FC !important; }
411
- ::-webkit-scrollbar { width: 8px; height: 8px; }
412
- ::-webkit-scrollbar-track { background: rgba(168, 85, 247, 0.05); border-radius: 4px; }
413
- ::-webkit-scrollbar-thumb { background: linear-gradient(135deg, #A855F7, #C084FC); border-radius: 4px; }
414
- ::-webkit-scrollbar-thumb:hover { background: linear-gradient(135deg, #9333EA, #A855F7); }
415
  """
416
 
417
- with gr.Blocks(theme=purple_theme, css=custom_css) as demo:
418
- with gr.Column(elem_id="col-container"):
419
- gr.Markdown("# **PaperX / Mineru Parsing Platform**", elem_id="main-title")
420
- gr.Markdown("One-click parsing of academic PDFs, DAG structuring, and multi-modal asset generation.", elem_id="subtitle")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
422
  with gr.Row():
423
- # ================= LEFT COLUMN: SETTINGS & ACTIONS =================
424
- with gr.Column(scale=1):
425
-
426
- # 1. API Configuration
427
- with gr.Accordion("⚙️ 1. Global API Configuration", open=False, elem_classes="gradio-group"):
428
- with gr.Row():
429
- key_input = gr.Textbox(label="API Key", type="password", placeholder="sk-...", scale=1)
430
- api_base_url_input = gr.Textbox(label="Base URL (Optional)", placeholder="https://api.example.com", scale=1)
431
- key_btn = gr.Button("💾 Save API Configuration")
432
-
433
- # 2. Document Parsing
434
- with gr.Group(elem_classes="gradio-group"):
435
- gr.Markdown("### 📄 2. Document Parsing")
436
-
437
- # 简化了 Label,因为 CSS 已经接管了内部文字渲染
438
- pdf_input = gr.File(
439
- label="Upload Document",
440
- file_types=[".pdf"],
441
- elem_id="pdf-upload-box"
442
- )
443
-
444
- # 应用全新的 primary-action-btn 悬浮/圆角类
445
- parse_btn = gr.Button("🚀 Start Mineru & DAG Extraction", elem_classes="primary-action-btn")
446
-
447
- parse_status = gr.Textbox(
448
- show_label=False, placeholder="Waiting for document upload...", lines=1, interactive=False, elem_classes="status-text"
449
- )
450
-
451
- # 3. Asset Generation
452
- with gr.Group(elem_classes="gradio-group"):
453
- gr.Markdown("### 🎯 3. Asset Generation")
454
- gr.Markdown("Generate final formats based on DAG structure:")
455
-
456
- with gr.Row(elem_classes="action-row"):
457
- gen_ppt_btn = gr.Button("📊 Gen PPT", elem_classes="action-btn")
458
- gen_poster_btn = gr.Button("🖼️ Gen Poster", elem_classes="action-btn")
459
- gen_pr_btn = gr.Button("📰 Gen Article", elem_classes="action-btn")
460
-
461
- # 应用全新的 primary-action-btn 悬浮/圆角类
462
- gen_all_btn = gr.Button("✨ Generate All Assets (ALL)", elem_classes="primary-action-btn")
463
 
464
- # ================= RIGHT COLUMN: OUTPUTS & LOGS =================
465
- with gr.Column(scale=1):
466
-
467
- # 4. Results & Downloads
468
- with gr.Group(elem_classes="gradio-group"):
469
- gr.Markdown("### 📦 Generation Results & Download")
470
- gen_status = gr.Textbox(
471
- show_label=False, placeholder="No generation task currently...", lines=2, interactive=False, elem_classes="status-text"
472
- )
473
- download_file = gr.File(label="📥 Get Final Zip Archive", interactive=False, visible=False)
474
-
475
- # 5. Debugging & Terminal
476
- with gr.Group(elem_classes="gradio-group"):
477
- gr.Markdown("### 🛠️ Developer Monitoring (Debug Only)")
478
- with gr.Tabs():
479
- with gr.Tab("📜 Terminal Stream"):
480
- cmd_logs = gr.Textbox(
481
- label="Stdout / Stderr", placeholder="Waiting for task to start...", lines=14, interactive=False, elem_classes="log-box"
482
- )
483
-
484
- with gr.Tab("🔍 System Snapshot"):
485
- refresh_btn = gr.Button("🔄 Refresh Directory Tree")
486
- debug_view = gr.Textbox(
487
- label="Workspace Files", lines=13, interactive=False, value=get_debug_info(), elem_classes="log-box"
488
- )
489
-
490
- # ================= LOGIC BINDINGS =================
491
  key_btn.click(fn=save_api_settings, inputs=[key_input, api_base_url_input], outputs=[parse_status, debug_view])
492
 
 
493
  pdf_input.upload(fn=save_pdf, inputs=pdf_input, outputs=[parse_status, debug_view])
494
- # 增加清除逻辑,点击 "x" 后彻底删除后端文件
495
- pdf_input.clear(fn=clear_pdf, outputs=[parse_status, debug_view])
496
 
497
- parse_btn.click(fn=run_mineru_parsing_and_dag_gen, outputs=[parse_status, debug_view, cmd_logs])
 
 
 
498
 
 
499
  def trigger_gen(task):
500
  status, debug, logs, file_path = run_final_generation(task)
 
501
  file_update = gr.update(value=file_path, visible=True) if file_path else gr.update(visible=False)
502
  return status, debug, logs, file_update
503
 
 
6
  import sys
7
  from datetime import datetime
8
  from concurrent.futures import ThreadPoolExecutor, as_completed
 
 
 
9
 
10
+ # 初始化环境路径
11
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
12
  PAPERS_DIR = os.path.join(BASE_DIR, "papers")
13
  CONFIG_PATH = os.path.join(BASE_DIR, "config.yaml")
14
  OUTPUT_DIR = os.path.join(BASE_DIR, "mineru_outputs")
15
+ ZIP_OUTPUT_PATH = os.path.join(BASE_DIR, "mineru_results.zip") # 压缩包路径
16
 
17
  os.makedirs(PAPERS_DIR, exist_ok=True)
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  def get_debug_info():
20
+ """读取服务器文件系统状态"""
21
  now = datetime.now().strftime("%H:%M:%S")
22
  files = os.listdir(PAPERS_DIR) if os.path.exists(PAPERS_DIR) else "Directory missing"
23
+
24
+ # 递归检查输出目录下的内容
25
  output_detail = "Not generated"
26
  if os.path.exists(OUTPUT_DIR):
27
  all_output_items = []
 
29
  for name in files_in_out:
30
  all_output_items.append(os.path.join(os.path.relpath(root, OUTPUT_DIR), name))
31
  output_detail = f"Found {len(all_output_items)} files: {all_output_items[:5]}..." if all_output_items else "Directory exists but is EMPTY"
32
+
33
+ return f"[{now}] 📁 papers/ 内容: {files}\n\n[{now}] 📂 mineru_outputs 状态: {output_detail}"
34
 
35
  def save_pdf(file):
36
+ if file is None: return "❌ 请先选择 PDF", get_debug_info()
37
  try:
 
 
 
 
 
 
38
  file_path = os.path.join(PAPERS_DIR, os.path.basename(file.name))
39
  shutil.copy(file.name, file_path)
40
+ return f"✅ 已保存: {os.path.basename(file.name)}", get_debug_info()
41
  except Exception as e:
42
+ return f"❌ 出错: {str(e)}", get_debug_info()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
  def save_api_settings(api_key, api_base_url=None):
45
+ if not api_key: return "❌ Key 不能为空", get_debug_info()
46
  try:
47
  config = {}
48
  if os.path.exists(CONFIG_PATH):
49
  with open(CONFIG_PATH, "r", encoding="utf-8") as f:
50
  config = yaml.safe_load(f) or {}
51
+
52
+ # 保存 API Key
53
  config.setdefault("api_keys", {})["gemini_api_key"] = api_key
54
+
55
+ # 如果 api_base_url 不为空(不是 None 且不是空字符串),则保存该值
56
  if api_base_url:
57
  config["api_base_url"] = api_base_url
58
+
59
+ # 写入 YAML 文件
60
  with open(CONFIG_PATH, "w", encoding="utf-8") as f:
61
  yaml.dump(config, f, allow_unicode=True)
62
+
63
+ # 动态生成成功提示语
64
+ success_msg = "✅ Key 已保存"
65
  if api_base_url:
66
+ success_msg += "Base URL 已更新"
67
+
68
  return success_msg, get_debug_info()
69
  except Exception as e:
70
+ return f"❌ 出错: {str(e)}", get_debug_info()
71
+
72
 
73
  def run_mineru_parsing_and_dag_gen():
74
+ """执行 PDF 解析并捕获完整日志,随后执行DAG生成流程(支持实时流式输出)"""
75
  if not os.path.exists(PAPERS_DIR) or not any(f.endswith('.pdf') for f in os.listdir(PAPERS_DIR)):
76
+ yield "❌ 未发现 PDF 文件", get_debug_info(), "No execution logs."
77
  return
78
 
79
  full_log = ""
80
  try:
81
+ # ================= 第一步:执行 Mineru 解析 =================
82
  env = os.environ.copy()
83
  env["MINERU_FORMULA_ENABLE"] = "false"
84
  env["MINERU_TABLE_ENABLE"] = "false"
 
86
  env["MINERU_VIRTUAL_VRAM_SIZE"] = "8"
87
 
88
  command_mineru = ["mineru", "-p", PAPERS_DIR, "-o", OUTPUT_DIR]
89
+
90
+ full_log += "--- Mineru 执行中 ---\n"
91
+ yield "⏳ 正在执行 Mineru 解析...", get_debug_info(), full_log
92
 
93
+ # 1. 使用 Popen 替代 run,开启实时流
94
  process_mineru = subprocess.Popen(
95
+ command_mineru,
96
+ env=env,
97
+ stdout=subprocess.PIPE,
98
+ stderr=subprocess.STDOUT, # 将 stderr 错误流合并到 stdout 一起输出
99
+ text=True,
100
+ bufsize=1 # 开启行缓冲
101
  )
102
+
103
+ # 2. 逐行读取输出并实时 yield 给 Gradio 界面
104
  for line in iter(process_mineru.stdout.readline, ''):
105
  full_log += line
106
+ yield "⏳ 正在执行 Mineru 解析...", get_debug_info(), full_log
107
+
108
  process_mineru.stdout.close()
109
  returncode_mineru = process_mineru.wait()
110
 
111
+ # 如果解析失败,直接 yield 返回
112
  if returncode_mineru != 0:
113
+ status = f"❌ Mineru 解析失败 (Exit Code: {returncode_mineru})"
114
+ yield status, get_debug_info(), full_log
115
  return
116
 
117
+ # ================= 第二步:执行 DAG 生成 =================
118
  command_dag = [sys.executable, "gen_dag.py"]
119
+
120
+ full_log += "\n--- DAG Gen 执行中 ---\n"
121
+ yield "⏳ Mineru 解析完成,正在执行 DAG 生成...", get_debug_info(), full_log
122
 
123
  process_dag = subprocess.Popen(
124
+ command_dag,
125
+ stdout=subprocess.PIPE,
126
+ stderr=subprocess.STDOUT,
127
+ text=True,
128
+ bufsize=1
129
  )
130
+
131
  for line in iter(process_dag.stdout.readline, ''):
132
  full_log += line
133
+ yield "⏳ 正在执行 DAG 生成...", get_debug_info(), full_log
134
+
135
  process_dag.stdout.close()
136
  returncode_dag = process_dag.wait()
137
 
138
+ if returncode_dag == 0:
139
+ status = "✅ PDF解析与DAG生成全部完成"
140
+ else:
141
+ status = f"❌ DAG生成失败 (Exit Code: {returncode_dag})"
142
+
143
  yield status, get_debug_info(), full_log
144
 
145
  except Exception as e:
146
+ error_log = full_log + f"\n[全局异常] Exception occurred:\n{str(e)}"
147
+ yield "❌ 运行异常", get_debug_info(), error_log
148
 
149
  def run_final_generation(task_type="all"):
150
+ """
151
+ 执行对应的生成脚本并压缩结果(支持并行执行)
152
+ task_type 支持: 'ppt', 'poster', 'pr', 'all'
153
+ """
154
  if not os.path.exists(OUTPUT_DIR):
155
+ return "❌ 请先执行第二步解析", get_debug_info(), "No output folder found.", None
156
 
157
+ # 根据传入的 task_type 决定要运行哪些脚本
158
  scripts_to_run = []
159
+ if task_type == "ppt":
160
+ scripts_to_run = ["gen_ppt.py"]
161
+ elif task_type == "poster":
162
+ scripts_to_run = ["gen_poster.py"]
163
+ elif task_type == "pr":
164
+ scripts_to_run = ["gen_pr.py"]
165
+ elif task_type == "all":
166
+ scripts_to_run = ["gen_ppt.py", "gen_poster.py", "gen_pr.py"]
167
+ else:
168
+ return "❌ 未知任务类型", get_debug_info(), "Invalid task_type.", None
169
+
170
+ full_log = f"🚀 准备启动 {len(scripts_to_run)} 个任务...\n"
171
  success = True
172
 
173
+ # 定义单个脚本的执行包装器
174
  def execute_script(script):
175
  command = [sys.executable, script]
176
+ result = subprocess.run(
177
+ command,
178
+ capture_output=True,
179
+ text=True,
180
+ timeout=600 # 每个脚本独立的超时时间
181
+ )
182
+ return script, result
183
 
184
  try:
185
+ # 使用 ThreadPoolExecutor 并行执行脚本
186
  with ThreadPoolExecutor(max_workers=len(scripts_to_run)) as executor:
187
+ # 提交所有任务
188
  future_to_script = {executor.submit(execute_script, s): s for s in scripts_to_run}
189
+
190
+ # as_completed 会在某个任务完成时立刻生成结果
191
  for future in as_completed(future_to_script):
192
  script_name = future_to_script[future]
193
  try:
194
+ # 获取该任务的执行结果
195
  _, result = future.result()
196
+
197
+ full_log += f"\n================ ✅ 执行完成: {script_name} ================\n"
198
  full_log += f"--- STDOUT ---\n{result.stdout}\n\n--- STDERR ---\n{result.stderr}\n"
199
+
200
+ # 检查此任务是否失败
201
  if result.returncode != 0:
202
  success = False
203
+ full_log += f"❌ [错误] {script_name} 返回非零退出码 (Exit Code: {result.returncode})\n"
204
+
205
  except subprocess.TimeoutExpired as e:
206
  success = False
207
+ full_log += f"\n================ ❌ 任务超时: {script_name} ================\n{str(e)}\n"
208
  except Exception as e:
209
  success = False
210
+ full_log += f"\n================ ❌ 任务异常: {script_name} ================\n{str(e)}\n"
211
 
212
+ # 如果有任何一个脚本执行失败,直接返回,不打包压缩
213
+ if not success:
214
+ return f"❌ {task_type.upper()} 包含失败任务,请检查日志", get_debug_info(), full_log, None
215
 
216
+ # 所有脚本都运行成功后,压缩 mineru_outputs 文件夹
217
  zip_base_name = ZIP_OUTPUT_PATH.replace(".zip", "")
218
  shutil.make_archive(zip_base_name, 'zip', OUTPUT_DIR)
219
+
220
+ success_msg = f"✅ {task_type.upper()} 生成并压缩完成"
221
+ return success_msg, get_debug_info(), full_log, ZIP_OUTPUT_PATH
222
+
223
  except Exception as e:
224
+ error_log = full_log + f"\n[全局异常] Exception occurred:\n{str(e)}"
225
+ return "❌ 最终生成发生全局异常", get_debug_info(), error_log, None
226
 
227
  # ==========================================
228
+ # --- 🚀 全新美化的 UI (Hugging Face 优化版) ---
229
  # ==========================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
 
231
+ # 自定义 CSS:让终端日志看起来像真正Terminal
232
+ custom_css = """
233
+ .log-box textarea {
234
+ font-family: 'Courier New', Consolas, monospace !important;
235
+ font-size: 13px !important;
236
+ background-color: #1e1e1e !important;
237
+ color: #4AF626 !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  }
239
+ /* 弱化状态框的边框感,使其更像文字标签 */
240
+ .status-text textarea {
241
+ background-color: transparent !important;
242
+ border: none !important;
243
+ box-shadow: none !important;
244
+ font-weight: bold;
245
  }
 
 
 
 
 
 
 
 
 
 
246
  """
247
 
248
+ with gr.Blocks(theme=gr.themes.Soft(primary_hue="indigo"), css=custom_css) as demo:
249
+ gr.Markdown("# 📑 PaperX / Mineru 智能解析平台")
250
+ gr.Markdown("将学术 PDF 一键解析、DAG 结构化并生成多模态产物。")
251
+
252
+ # 1. 隐藏式全局配置
253
+ with gr.Accordion("⚙️ 1. 全局 API 配置", open=False):
254
+ with gr.Row():
255
+ key_input = gr.Textbox(label="API Key", type="password", placeholder="sk-...", scale=1)
256
+ api_base_url_input = gr.Textbox(label="Base URL (可选)", placeholder="https://api.example.com", scale=1)
257
+ key_btn = gr.Button("💾 保存 API 配置")
258
+
259
+ # 2. 核心上传与解析
260
+ with gr.Group():
261
+ gr.Markdown("### 📄 2. 文档解析")
262
+ # 拖拽自动上传
263
+ pdf_input = gr.File(label="拖拽或点击上传 PDF", file_types=[".pdf"])
264
+
265
+ # 解析按钮独占一行,成为视觉焦点
266
+ parse_btn = gr.Button("🚀 开始执行 Mineru & DAG 抽取", variant="primary", size="lg")
267
 
268
+ # 弱化状态框视觉
269
+ parse_status = gr.Textbox(
270
+ show_label=False,
271
+ placeholder="等待上传文档...",
272
+ lines=1,
273
+ interactive=False,
274
+ elem_classes="status-text"
275
+ )
276
+
277
+ # 3. 最终产物生成
278
+ with gr.Group():
279
+ gr.Markdown("### 🎯 3. 产物生成")
280
+ gr.Markdown("基于 DAG 结构生成最终所需格式:")
281
+
282
+ # 层次化布局:上面三个单项,下面一个全部
283
  with gr.Row():
284
+ gen_ppt_btn = gr.Button("📊 单独生成 PPT")
285
+ gen_poster_btn = gr.Button("🖼️ 单独生成 Poster")
286
+ gen_pr_btn = gr.Button("📰 单独生成 PR 文章")
287
+
288
+ gen_all_btn = gr.Button(" 一键生成全部 (ALL)", variant="primary")
289
+
290
+ # 4. 生成结果与下载 (从 Tabs 中独立出来,紧跟工作流)
291
+ with gr.Group():
292
+ gr.Markdown("### 📦 4. 生成结果 & 下载")
293
+ gen_status = gr.Textbox(
294
+ show_label=False,
295
+ placeholder="当前暂无生成任务...",
296
+ lines=2,
297
+ interactive=False,
298
+ elem_classes="status-text"
299
+ )
300
+ # 默认隐藏下载框,生成成功后再动态展示
301
+ download_file = gr.File(label="📥 获取最终压缩包", interactive=False, visible=False)
302
+
303
+ gr.HTML("<hr style='margin-top: 30px; margin-bottom: 30px;' />") # 添加一条视觉分割线
304
+
305
+ # 5. 开发者监控区 (置于最底部,方便后续精简 UI 时直接删除这部分)
306
+ gr.Markdown("### 🛠️ 开发者后台监控 (仅供调试)")
307
+ with gr.Tabs():
308
+ # Tab 1: 实时终端
309
+ with gr.Tab("📜 终端流 (Terminal)"):
310
+ cmd_logs = gr.Textbox(
311
+ label="Stdout / Stderr",
312
+ placeholder="等待任务开始...",
313
+ lines=15,
314
+ interactive=False,
315
+ elem_classes="log-box"
316
+ )
317
+
318
+ # Tab 2: 文件系统快照
319
+ with gr.Tab("🔍 系统快照 (Debug)"):
320
+ refresh_btn = gr.Button("🔄 刷新目录树")
321
+ debug_view = gr.Textbox(label="Workspace Files", lines=15, interactive=False, value=get_debug_info())
 
 
322
 
323
+ # ================= 逻辑绑定 =================
324
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
  key_btn.click(fn=save_api_settings, inputs=[key_input, api_base_url_input], outputs=[parse_status, debug_view])
326
 
327
+ # 绑定上传和清除事件,实现自动化
328
  pdf_input.upload(fn=save_pdf, inputs=pdf_input, outputs=[parse_status, debug_view])
329
+ pdf_input.clear(fn=lambda: ("ℹ️ 已清空文件", get_debug_info()), outputs=[parse_status, debug_view])
 
330
 
331
+ parse_btn.click(
332
+ fn=run_mineru_parsing_and_dag_gen,
333
+ outputs=[parse_status, debug_view, cmd_logs]
334
+ )
335
 
336
+ # 动态控制下载组件的显示与隐藏
337
  def trigger_gen(task):
338
  status, debug, logs, file_path = run_final_generation(task)
339
+ # 如果 file_path 存在,显示下载组件并赋值;否则保持隐藏
340
  file_update = gr.update(value=file_path, visible=True) if file_path else gr.update(visible=False)
341
  return status, debug, logs, file_update
342