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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +381 -207
app.py CHANGED
@@ -6,22 +6,96 @@ import subprocess
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,56 +103,64 @@ def get_debug_info():
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,257 +168,349 @@ def run_mineru_parsing_and_dag_gen():
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
 
 
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
  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
  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: 16px 16px 16px 16px !important; /* 边缘与内层内容的间距 */
297
+ overflow: visible !important;
298
+ margin-bottom: 10px !important; /* 模块之间的垂直间距 */
299
+ display: flex !important;
300
+ flex-direction: column !important;
301
+ gap: 16px !important; /* 内部组件之间的自然间距 */
302
+ }
303
+ .gradio-group:hover {
304
+ box-shadow: 0 8px 32px rgba(168, 85, 247, 0.12) !important;
305
+ border-color: #C084FC !important;
306
+ }
307
+ .dark .gradio-group {
308
+ background: rgba(30, 30, 30, 0.9) !important;
309
+ border-color: rgba(168, 85, 247, 0.3) !important;
310
+ }
311
+
312
+ /* 恢复 Gradio 内部组���的圆角(Gradio默认会将 group 内的元素边缘变直) */
313
+ .gradio-group > div,
314
+ .gradio-group > .form {
315
+ border-radius: 12px !important;
316
+ }
317
+
318
+ /* ======== 优化:文件上传框居中与背景清理 ======== */
319
+ #pdf-upload-box {
320
+ border: 2px dashed rgba(192, 132, 252, 0.6) !important;
321
+ border-radius: 16px !important;
322
+ background-color: rgba(250, 245, 255, 0.5) !important;
323
+ transition: all 0.3s ease !important;
324
+ min-height: 220px !important; /* 增加高度提供居中空间 */
325
+ position: relative !important;
326
+ margin-top: 10px !important;
327
+ }
328
+
329
+ #pdf-upload-box:hover {
330
+ border-color: #A855F7 !important;
331
+ background-color: rgba(243, 232, 255, 0.8) !important;
332
+ box-shadow: 0 4px 15px rgba(168, 85, 247, 0.15) !important;
333
+ }
334
+
335
+ /* 隐藏原生的中/英文上传提示、图标及背景 */
336
+ #pdf-upload-box .upload-container {
337
+ background: transparent !important;
338
+ }
339
+ #pdf-upload-box .upload-container > span,
340
+ #pdf-upload-box .upload-container > svg {
341
+ display: none !important;
342
+ }
343
+
344
+ /* 使用伪元素在中央注入自定义文字和图标 */
345
+ #pdf-upload-box .upload-container::before {
346
+ content: "📤\\A Click here to select a PDF\\A or Drag & Drop the file here";
347
+ white-space: pre-wrap;
348
+ font-size: 1.2rem;
349
+ line-height: 1.8;
350
+ font-weight: 600;
351
+ color: #9333EA;
352
+ display: flex;
353
+ align-items: center;
354
+ justify-content: center;
355
+ text-align: center;
356
+ width: 100%;
357
+ height: 100%;
358
+ position: absolute;
359
+ top: 0;
360
+ left: 0;
361
+ pointer-events: none; /* 防止遮挡拖拽事件 */
362
+ }
363
+
364
+ /* ======== 修改:减小按钮尺寸,保持字体一致 ======== */
365
+ /* 主要操作按钮 (Start Mineru & Generate All) */
366
+ .primary-action-btn {
367
+ border-radius: 25px !important; /* 调小 */
368
+ background: linear-gradient(135deg, #9333EA, #7E22CE) !important;
369
+ color: white !important; font-weight: 700 !important; border: none !important;
370
+ height: 50px !important; /* 调小:60 -> 50 */
371
+ width: 80% !important;
372
+ margin-left: auto !important; /* 新增:自动左边距 */
373
+ margin-right: auto !important; /* 新增:自动右边距 */
374
+ margin-top: 10px !important;
375
+ margin-bottom: 10px !important;
376
+ display: block !important; /* 新增:确保块级元素 */
377
+ transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275), box-shadow 0.3s ease !important;
378
+ box-shadow: 0 4px 15px rgba(126, 34, 206, 0.3) !important;
379
+ cursor: pointer !important;
380
+ font-size: 1.15rem !important; /* 字体保持一致:略调小以配合新尺寸 */
381
+ }
382
+ .primary-action-btn:hover {
383
+ transform: translateY(-5px) scale(1.02) !important;
384
+ box-shadow: 0 10px 25px rgba(126, 34, 206, 0.5) !important;
385
+ background: linear-gradient(135deg, #A855F7, #9333EA) !important;
386
+ }
387
+ .primary-action-btn:active { transform: translateY(2px) scale(0.98) !important; box-shadow: 0 2px 10px rgba(126, 34, 206, 0.2) !important; }
388
+
389
+ /* 3个小按钮的样式 */
390
+ .action-row { display: flex !important; justify-content: center !important; gap: 10px !important; margin-bottom: 10px !important; margin-top: 10px !important;}
391
+ .action-btn {
392
+ border-radius: 24px !important; /* 调小 */
393
+ background: linear-gradient(135deg, #A855F7, #9333EA) !important;
394
+ color: white !important; font-weight: 600 !important; border: none !important;
395
+ height: 40px !important; /* 调小:55 -> 48 */
396
+ width: 120px !important;
397
+ flex: none !important;
398
+ /*文字垂直居中*/
399
+ display: flex !important;
400
+ align-items: center !important;
401
+ justify-content: center !important;
402
+ line-height: 1 !important;
403
+ padding: 0 !important;
404
+ margin-top: 10px !important;
405
+ margin-bottom: 10px !important;
406
 
407
+ transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275), box-shadow 0.3s ease !important;
408
+ box-shadow: 0 4px 15px rgba(147, 51, 234, 0.2) !important;
409
+ cursor: pointer !important; display: flex !important; align-items: center !important; justify-content: center !important;
410
+ font-size: 1.05rem !important; /* 字体保持一致:显式设置 */
411
+ }
412
+ .action-btn:hover {
413
+ transform: translateY(-5px) scale(1.03) !important;
414
+ box-shadow: 0 10px 25px rgba(147, 51, 234, 0.4) !important;
415
+ background: linear-gradient(135deg, #C084FC, #A855F7) !important;
416
+ }
417
+ .action-btn:active { transform: translateY(2px) scale(0.98) !important; box-shadow: 0 2px 10px rgba(147, 51, 234, 0.2) !important; }
418
+
419
+ /* Terminal Log Style */
420
+ .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; }
421
+ .status-text textarea { background-color: transparent !important; border: none !important; box-shadow: none !important; font-weight: 600 !important; color: #6B21A8 !important; }
422
+ .dark .status-text textarea { color: #C084FC !important; }
423
+ ::-webkit-scrollbar { width: 8px; height: 8px; }
424
+ ::-webkit-scrollbar-track { background: rgba(168, 85, 247, 0.05); border-radius: 4px; }
425
+ ::-webkit-scrollbar-thumb { background: linear-gradient(135deg, #A855F7, #C084FC); border-radius: 4px; }
426
+ ::-webkit-scrollbar-thumb:hover { background: linear-gradient(135deg, #9333EA, #A855F7); }
427
+ """
 
 
 
428
 
429
+ with gr.Blocks(theme=purple_theme, css=custom_css) as demo:
430
+ with gr.Column(elem_id="col-container"):
431
+ gr.Markdown("# **PaperX Platform**", elem_id="main-title")
432
+ gr.Markdown("One-click parsing of academic PDFs, DAG structuring, and multi-modal asset generation.", elem_id="subtitle")
433
 
 
434
  with gr.Row():
435
+ # ================= LEFT COLUMN: SETTINGS & ACTIONS =================
436
+ with gr.Column(scale=1):
437
+
438
+ # 1. API Configuration
439
+ with gr.Group(elem_classes="gradio-group"):
440
+ gr.Markdown("### ⚙️ 1. Global API Configuration")
441
+ with gr.Row():
442
+ key_input = gr.Textbox(label="API Key", type="password", placeholder="sk-...", scale=1)
443
+ api_base_url_input = gr.Textbox(label="Base URL (Optional)", placeholder="https://api.example.com", scale=1)
444
+ key_btn = gr.Button("💾 Save API Configuration")
445
+
446
+ # 2. Document Parsing
447
+ with gr.Group(elem_classes="gradio-group"):
448
+ gr.Markdown("### 📄 2. Document Parsing")
449
+
450
+ # 简化了 Label,因为 CSS 已经接管了内部文字渲染
451
+ pdf_input = gr.File(
452
+ label="Upload Document",
453
+ file_types=[".pdf"],
454
+ elem_id="pdf-upload-box"
455
+ )
456
+
457
+ # 应用全新的 primary-action-btn 悬浮/圆角类
458
+ parse_btn = gr.Button("🚀 Start Mineru & DAG Extraction", elem_classes="primary-action-btn")
459
+
460
+ parse_status = gr.Textbox(
461
+ show_label=False, placeholder="Waiting for document upload...", lines=1, interactive=False, elem_classes="status-text"
462
+ )
463
+
464
+ # 3. Asset Generation
465
+ with gr.Group(elem_classes="gradio-group"):
466
+ gr.Markdown("### 🎯 3. Asset Generation")
467
+ gr.Markdown("Generate final formats based on DAG structure:")
468
+
469
+ with gr.Row(elem_classes="action-row"):
470
+ gen_ppt_btn = gr.Button("📊 Gen PPT", elem_classes="action-btn")
471
+ gen_poster_btn = gr.Button("🖼️ Gen Poster", elem_classes="action-btn")
472
+ gen_pr_btn = gr.Button("📰 Gen Article", elem_classes="action-btn")
473
+
474
+ # 应用全新的 primary-action-btn 悬浮/圆角类
475
+ gen_all_btn = gr.Button("✨ Generate All Assets (ALL)", elem_classes="primary-action-btn")
476
 
477
+ # ================= RIGHT COLUMN: OUTPUTS & LOGS =================
478
+ with gr.Column(scale=1):
479
+
480
+ # 4. Results & Downloads
481
+ with gr.Group(elem_classes="gradio-group"):
482
+ gr.Markdown("### 📦 Generation Results & Download")
483
+ gen_status = gr.Textbox(
484
+ show_label=False, placeholder="No generation task currently...", lines=2, interactive=False, elem_classes="status-text"
485
+ )
486
+ download_file = gr.File(label="📥 Get Final Zip Archive", interactive=False, visible=False)
487
+
488
+ # 5. Debugging & Terminal
489
+ with gr.Group(elem_classes="gradio-group"):
490
+ gr.Markdown("### 🛠️ Developer Monitoring (Debug Only)")
491
+ with gr.Tabs():
492
+ with gr.Tab("📜 Terminal Stream"):
493
+ cmd_logs = gr.Textbox(
494
+ label="Stdout / Stderr", placeholder="Waiting for task to start...", lines=14, interactive=False, elem_classes="log-box"
495
+ )
496
+
497
+ with gr.Tab("🔍 System Snapshot"):
498
+ refresh_btn = gr.Button("🔄 Refresh Directory Tree")
499
+ debug_view = gr.Textbox(
500
+ label="Workspace Files", lines=13, interactive=False, value=get_debug_info(), elem_classes="log-box"
501
+ )
502
+
503
+ # ================= LOGIC BINDINGS =================
504
  key_btn.click(fn=save_api_settings, inputs=[key_input, api_base_url_input], outputs=[parse_status, debug_view])
505
 
 
506
  pdf_input.upload(fn=save_pdf, inputs=pdf_input, outputs=[parse_status, debug_view])
507
+ # 增加清除逻辑,点击 "x" 后彻底删除后端文件
508
+ pdf_input.clear(fn=clear_pdf, outputs=[parse_status, debug_view])
509
 
510
+ parse_btn.click(fn=run_mineru_parsing_and_dag_gen, outputs=[parse_status, debug_view, cmd_logs])
 
 
 
511
 
 
512
  def trigger_gen(task):
513
  status, debug, logs, file_path = run_final_generation(task)
 
514
  file_update = gr.update(value=file_path, visible=True) if file_path else gr.update(visible=False)
515
  return status, debug, logs, file_update
516