Laramie2 commited on
Commit
37808b1
·
verified ·
1 Parent(s): 0b481cc

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +155 -138
app.py CHANGED
@@ -7,21 +7,21 @@ 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 = []
@@ -30,55 +30,54 @@ def get_debug_info():
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"
@@ -87,38 +86,37 @@ def run_mineru_parsing_and_dag_gen():
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,
@@ -130,31 +128,30 @@ def run_mineru_parsing_and_dag_gen():
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"]
@@ -165,78 +162,102 @@ def run_final_generation(task_type="all"):
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;
@@ -246,97 +267,93 @@ custom_css = """
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
 
 
7
  from datetime import datetime
8
  from concurrent.futures import ThreadPoolExecutor, as_completed
9
 
10
+ # Initialize environment paths
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") # Zip path
16
 
17
  os.makedirs(PAPERS_DIR, exist_ok=True)
18
 
19
  def get_debug_info():
20
+ """Read server file system status"""
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
+ # Recursively check output directory contents
25
  output_detail = "Not generated"
26
  if os.path.exists(OUTPUT_DIR):
27
  all_output_items = []
 
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/ Content: {files}\n\n[{now}] 📂 mineru_outputs Status: {output_detail}"
34
 
35
  def save_pdf(file):
36
+ if file is None: return "❌ Please select a PDF first", 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"✅ Saved: {os.path.basename(file.name)}", get_debug_info()
41
  except Exception as e:
42
+ return f"❌ Error: {str(e)}", get_debug_info()
43
 
44
  def save_api_settings(api_key, api_base_url=None):
45
+ if not api_key: return "❌ Key cannot be empty", 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
+ # Save API Key
53
  config.setdefault("api_keys", {})["gemini_api_key"] = api_key
54
 
55
+ # Save base URL if not empty
56
  if api_base_url:
57
  config["api_base_url"] = api_base_url
58
 
59
+ # Write to YAML
60
  with open(CONFIG_PATH, "w", encoding="utf-8") as f:
61
  yaml.dump(config, f, allow_unicode=True)
62
 
63
+ success_msg = "✅ Key saved"
 
64
  if api_base_url:
65
+ success_msg += ", Base URL updated"
66
 
67
  return success_msg, get_debug_info()
68
  except Exception as e:
69
+ return f"❌ Error: {str(e)}", get_debug_info()
70
 
71
 
72
  def run_mineru_parsing_and_dag_gen():
73
+ """Execute PDF parsing and DAG generation (supports real-time streaming)"""
74
  if not os.path.exists(PAPERS_DIR) or not any(f.endswith('.pdf') for f in os.listdir(PAPERS_DIR)):
75
+ yield "❌ No PDF file found", get_debug_info(), "No execution logs."
76
  return
77
 
78
  full_log = ""
79
  try:
80
+ # ================= Step 1: Mineru Parsing =================
81
  env = os.environ.copy()
82
  env["MINERU_FORMULA_ENABLE"] = "false"
83
  env["MINERU_TABLE_ENABLE"] = "false"
 
86
 
87
  command_mineru = ["mineru", "-p", PAPERS_DIR, "-o", OUTPUT_DIR]
88
 
89
+ full_log += "--- Mineru Executing ---\n"
90
+ yield "⏳ Executing Mineru parsing...", get_debug_info(), full_log
91
 
92
+ # 1. Use Popen for real-time streaming
93
  process_mineru = subprocess.Popen(
94
  command_mineru,
95
  env=env,
96
  stdout=subprocess.PIPE,
97
+ stderr=subprocess.STDOUT,
98
  text=True,
99
+ bufsize=1
100
  )
101
 
102
+ # 2. Read output line by line and yield to Gradio
103
  for line in iter(process_mineru.stdout.readline, ''):
104
  full_log += line
105
+ yield "⏳ Executing Mineru parsing...", get_debug_info(), full_log
106
 
107
  process_mineru.stdout.close()
108
  returncode_mineru = process_mineru.wait()
109
 
 
110
  if returncode_mineru != 0:
111
+ status = f"❌ Mineru parsing failed (Exit Code: {returncode_mineru})"
112
  yield status, get_debug_info(), full_log
113
  return
114
 
115
+ # ================= Step 2: DAG Generation =================
116
  command_dag = [sys.executable, "gen_dag.py"]
117
 
118
+ full_log += "\n--- DAG Gen Executing ---\n"
119
+ yield "⏳ Mineru parsing complete, executing DAG generation...", get_debug_info(), full_log
120
 
121
  process_dag = subprocess.Popen(
122
  command_dag,
 
128
 
129
  for line in iter(process_dag.stdout.readline, ''):
130
  full_log += line
131
+ yield "⏳ Executing DAG generation...", get_debug_info(), full_log
132
 
133
  process_dag.stdout.close()
134
  returncode_dag = process_dag.wait()
135
 
136
  if returncode_dag == 0:
137
+ status = "✅ PDF parsing & DAG generation fully completed"
138
  else:
139
+ status = f"❌ DAG generation failed (Exit Code: {returncode_dag})"
140
 
141
  yield status, get_debug_info(), full_log
142
 
143
  except Exception as e:
144
+ error_log = full_log + f"\n[Global Exception] Exception occurred:\n{str(e)}"
145
+ yield "❌ Execution Exception", get_debug_info(), error_log
146
 
147
  def run_final_generation(task_type="all"):
148
  """
149
+ Execute generation scripts and zip results
150
+ task_type supports: 'ppt', 'poster', 'pr', 'all'
151
  """
152
  if not os.path.exists(OUTPUT_DIR):
153
+ return "❌ Please run the parsing step first", get_debug_info(), "No output folder found.", None
154
 
 
155
  scripts_to_run = []
156
  if task_type == "ppt":
157
  scripts_to_run = ["gen_ppt.py"]
 
162
  elif task_type == "all":
163
  scripts_to_run = ["gen_ppt.py", "gen_poster.py", "gen_pr.py"]
164
  else:
165
+ return "❌ Unknown task type", get_debug_info(), "Invalid task_type.", None
166
 
167
+ full_log = f"🚀 Preparing to start {len(scripts_to_run)} tasks...\n"
168
  success = True
169
 
 
170
  def execute_script(script):
171
  command = [sys.executable, script]
172
  result = subprocess.run(
173
  command,
174
  capture_output=True,
175
  text=True,
176
+ timeout=600
177
  )
178
  return script, result
179
 
180
  try:
 
181
  with ThreadPoolExecutor(max_workers=len(scripts_to_run)) as executor:
 
182
  future_to_script = {executor.submit(execute_script, s): s for s in scripts_to_run}
183
 
 
184
  for future in as_completed(future_to_script):
185
  script_name = future_to_script[future]
186
  try:
 
187
  _, result = future.result()
188
 
189
+ full_log += f"\n================ ✅ Execution Complete: {script_name} ================\n"
190
  full_log += f"--- STDOUT ---\n{result.stdout}\n\n--- STDERR ---\n{result.stderr}\n"
191
 
 
192
  if result.returncode != 0:
193
  success = False
194
+ full_log += f"❌ [Error] {script_name} returned non-zero exit code (Exit Code: {result.returncode})\n"
195
 
196
  except subprocess.TimeoutExpired as e:
197
  success = False
198
+ full_log += f"\n================ ❌ Task Timeout: {script_name} ================\n{str(e)}\n"
199
  except Exception as e:
200
  success = False
201
+ full_log += f"\n================ ❌ Task Exception: {script_name} ================\n{str(e)}\n"
202
 
 
203
  if not success:
204
+ return f"❌ {task_type.upper()} contains failed tasks, please check logs", get_debug_info(), full_log, None
205
 
206
+ # Zip the mineru_outputs folder
207
  zip_base_name = ZIP_OUTPUT_PATH.replace(".zip", "")
208
  shutil.make_archive(zip_base_name, 'zip', OUTPUT_DIR)
209
 
210
+ success_msg = f"✅ {task_type.upper()} generated and zipped successfully"
211
  return success_msg, get_debug_info(), full_log, ZIP_OUTPUT_PATH
212
 
213
  except Exception as e:
214
+ error_log = full_log + f"\n[Global Exception] Exception occurred:\n{str(e)}"
215
+ return "❌ Global exception during final generation", get_debug_info(), error_log, None
216
 
217
  # ==========================================
218
+ # --- 🚀 UI Configuration (Left-Right Layout) ---
219
  # ==========================================
220
 
 
221
  custom_css = """
222
+ /* Prominent Title Animation from Reference */
223
+ #main-title {
224
+ text-align: center !important;
225
+ padding: 1rem 0 0.5rem 0;
226
+ }
227
+ #main-title h1 {
228
+ font-size: 2.6em !important;
229
+ font-weight: 800 !important;
230
+ background: linear-gradient(135deg, #4f46e5 0%, #818cf8 50%, #4338ca 100%);
231
+ background-size: 200% 200%;
232
+ -webkit-background-clip: text;
233
+ -webkit-text-fill-color: transparent;
234
+ background-clip: text;
235
+ animation: gradient-shift 4s ease infinite;
236
+ letter-spacing: -0.02em;
237
+ }
238
+ @keyframes gradient-shift {
239
+ 0%, 100% { background-position: 0% 50%; }
240
+ 50% { background-position: 100% 50%; }
241
+ }
242
+ #subtitle {
243
+ text-align: center !important;
244
+ margin-bottom: 1.5rem;
245
+ }
246
+ #subtitle p {
247
+ margin: 0 auto;
248
+ color: #666;
249
+ font-size: 1.1rem;
250
+ font-weight: 500;
251
+ }
252
+
253
+ /* Terminal Log Style */
254
  .log-box textarea {
255
  font-family: 'Courier New', Consolas, monospace !important;
256
  font-size: 13px !important;
257
  background-color: #1e1e1e !important;
258
  color: #4AF626 !important;
259
  }
260
+ /* Borderless Status Text */
261
  .status-text textarea {
262
  background-color: transparent !important;
263
  border: none !important;
 
267
  """
268
 
269
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="indigo"), css=custom_css) as demo:
270
+ gr.Markdown("# **PaperX / Mineru Parsing Platform**", elem_id="main-title")
271
+ gr.Markdown("One-click parsing of academic PDFs, DAG structuring, and multi-modal asset generation.", elem_id="subtitle")
272
 
273
+ with gr.Row():
274
+ # ================= LEFT COLUMN: SETTINGS & ACTIONS =================
275
+ with gr.Column(scale=1):
276
+
277
+ # 1. API Configuration
278
+ with gr.Accordion("⚙️ 1. Global API Configuration", open=False):
279
+ with gr.Row():
280
+ key_input = gr.Textbox(label="API Key", type="password", placeholder="sk-...", scale=1)
281
+ api_base_url_input = gr.Textbox(label="Base URL (Optional)", placeholder="https://api.example.com", scale=1)
282
+ key_btn = gr.Button("💾 Save API Configuration")
283
+
284
+ # 2. Document Parsing
285
+ with gr.Group():
286
+ gr.Markdown("### 📄 2. Document Parsing")
287
+ pdf_input = gr.File(label="Drag and drop or click to upload PDF", file_types=[".pdf"])
288
+
289
+ parse_btn = gr.Button("🚀 Start Mineru & DAG Extraction", variant="primary", size="lg")
290
+
291
+ parse_status = gr.Textbox(
292
+ show_label=False,
293
+ placeholder="Waiting for document upload...",
294
+ lines=1,
295
+ interactive=False,
296
+ elem_classes="status-text"
297
+ )
298
+
299
+ # 3. Asset Generation
300
+ with gr.Group():
301
+ gr.Markdown("### 🎯 3. Asset Generation")
302
+ gr.Markdown("Generate final formats based on DAG structure:")
303
+
304
+ with gr.Row():
305
+ gen_ppt_btn = gr.Button("📊 Generate PPT")
306
+ gen_poster_btn = gr.Button("🖼️ Generate Poster")
307
+ gen_pr_btn = gr.Button("📰 Generate PR Article")
308
+
309
+ gen_all_btn = gr.Button("✨ Generate All Assets (ALL)", variant="primary")
310
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
 
312
+ # ================= RIGHT COLUMN: OUTPUTS & LOGS =================
313
+ with gr.Column(scale=1):
314
+
315
+ # 4. Results & Downloads
316
+ with gr.Group():
317
+ gr.Markdown("### 📦 Generation Results & Download")
318
+ gen_status = gr.Textbox(
319
+ show_label=False,
320
+ placeholder="No generation task currently...",
321
+ lines=2,
322
+ interactive=False,
323
+ elem_classes="status-text"
324
+ )
325
+ download_file = gr.File(label="📥 Get Final Zip Archive", interactive=False, visible=False)
326
+
327
+ # 5. Debugging & Terminal
328
+ gr.Markdown("### 🛠️ Developer Monitoring (Debug Only)")
329
+ with gr.Tabs():
330
+ with gr.Tab("📜 Terminal Stream"):
331
+ cmd_logs = gr.Textbox(
332
+ label="Stdout / Stderr",
333
+ placeholder="Waiting for task to start...",
334
+ lines=18,
335
+ interactive=False,
336
+ elem_classes="log-box"
337
+ )
338
+
339
+ with gr.Tab("🔍 System Snapshot"):
340
+ refresh_btn = gr.Button("🔄 Refresh Directory Tree")
341
+ debug_view = gr.Textbox(label="Workspace Files", lines=17, interactive=False, value=get_debug_info())
342
+
343
+ # ================= LOGIC BINDINGS =================
344
 
345
  key_btn.click(fn=save_api_settings, inputs=[key_input, api_base_url_input], outputs=[parse_status, debug_view])
346
 
 
347
  pdf_input.upload(fn=save_pdf, inputs=pdf_input, outputs=[parse_status, debug_view])
348
+ pdf_input.clear(fn=lambda: ("ℹ️ File cleared", get_debug_info()), outputs=[parse_status, debug_view])
349
 
350
  parse_btn.click(
351
  fn=run_mineru_parsing_and_dag_gen,
352
  outputs=[parse_status, debug_view, cmd_logs]
353
  )
354
 
 
355
  def trigger_gen(task):
356
  status, debug, logs, file_path = run_final_generation(task)
 
357
  file_update = gr.update(value=file_path, visible=True) if file_path else gr.update(visible=False)
358
  return status, debug, logs, file_update
359