Spaces:
Running
Running
| import gradio as gr | |
| import os | |
| import shutil | |
| import subprocess | |
| import sys | |
| import queue | |
| import threading | |
| import uuid | |
| from datetime import datetime | |
| from concurrent.futures import ThreadPoolExecutor, as_completed | |
| from typing import Iterable | |
| from gradio.themes import Soft | |
| from gradio.themes.utils import colors, fonts, sizes | |
| import threading | |
| import subprocess | |
| # ========================================== | |
| # --- 🌐 异步安装 Playwright 浏览器 --- | |
| # ========================================== | |
| def setup_playwright(): | |
| """在后台静默安装 Playwright,防止阻塞 Gradio 启动导致 HF 500 超时""" | |
| try: | |
| import playwright | |
| print("⏳ [System] Downloading Playwright Chromium in background...") | |
| # 增加 --with-deps 尝试安装系统级依赖 (虽然在非 root 容器可能失效,但有备无患) | |
| subprocess.run(["playwright", "install", "chromium"], check=True) | |
| print("✅ [System] Playwright browsers ready.") | |
| except Exception as e: | |
| print(f"❌ [System] Playwright setup failed: {e}") | |
| # 这一步非常关键:启动一个后台守护线程去下载,主进程直接往下走! | |
| threading.Thread(target=setup_playwright, daemon=True).start() | |
| # ========================================== | |
| # --- 📁 全局目录配置 (修改为 Session 基础目录) --- | |
| # ========================================== | |
| BASE_DIR = os.path.dirname(os.path.abspath(__file__)) | |
| SESSIONS_BASE_DIR = os.path.join(BASE_DIR, "user_sessions") | |
| os.makedirs(SESSIONS_BASE_DIR, exist_ok=True) | |
| def get_user_dirs(session_id): | |
| """根据 Session ID 生成用户专属的隔离目录""" | |
| user_base = os.path.join(SESSIONS_BASE_DIR, session_id) | |
| papers_dir = os.path.join(user_base, "papers") | |
| output_dir = os.path.join(user_base, "mineru_outputs") | |
| zip_path = os.path.join(user_base, "mineru_results.zip") | |
| os.makedirs(papers_dir, exist_ok=True) | |
| os.makedirs(output_dir, exist_ok=True) | |
| return papers_dir, output_dir, zip_path | |
| import time | |
| # ========================================== | |
| # --- 🧹 垃圾回收 (后台清理过期 Session) --- | |
| # ========================================== | |
| # 设定 Session 过期时间(例如:2 小时 = 7200 秒) | |
| SESSION_MAX_AGE_SECONDS = 2 * 60 * 60 | |
| # 设定清理器执行间隔(例如:每 30 分钟扫描一次 = 1800 秒) | |
| CLEANUP_INTERVAL_SECONDS = 30 * 60 | |
| def cleanup_expired_sessions(): | |
| """后台运行的垃圾回收任务""" | |
| while True: | |
| try: | |
| if os.path.exists(SESSIONS_BASE_DIR): | |
| current_time = time.time() | |
| for session_folder in os.listdir(SESSIONS_BASE_DIR): | |
| folder_path = os.path.join(SESSIONS_BASE_DIR, session_folder) | |
| # 确保只处理目录 | |
| if os.path.isdir(folder_path): | |
| # 获取文件夹的最后修改时间 | |
| folder_mtime = os.path.getmtime(folder_path) | |
| # 判断是否超过了最大存活时间 | |
| if (current_time - folder_mtime) > SESSION_MAX_AGE_SECONDS: | |
| try: | |
| shutil.rmtree(folder_path) | |
| print(f"🧹 [Garbage Collector] Deleted expired session: {session_folder}") | |
| except Exception as e: | |
| print(f"⚠️ [Garbage Collector] Failed to delete {session_folder}: {e}") | |
| except Exception as e: | |
| print(f"⚠️ [Garbage Collector] Error during cleanup scan: {e}") | |
| # 休眠到下一次扫描时间 | |
| time.sleep(CLEANUP_INTERVAL_SECONDS) | |
| def start_garbage_collector(): | |
| """启动后台守护线程""" | |
| gc_thread = threading.Thread(target=cleanup_expired_sessions, daemon=True) | |
| gc_thread.start() | |
| print("🚀 [Garbage Collector] Background cleanup service started.") | |
| # ========================================== | |
| # --- 🎨 Custom Purple Theme Definition --- | |
| # ========================================== | |
| colors.purple = colors.Color( | |
| name="purple", c50="#FAF5FF", c100="#F3E8FF", c200="#E9D5FF", | |
| c300="#DAB2FF", c400="#C084FC", c500="#A855F7", c600="#9333EA", | |
| c700="#7E22CE", c800="#6B21A8", c900="#581C87", c950="#3B0764", | |
| ) | |
| class PurpleTheme(Soft): | |
| def __init__(self, **kwargs): | |
| super().__init__( | |
| primary_hue=colors.gray, secondary_hue=colors.purple, neutral_hue=colors.slate, | |
| font=(fonts.GoogleFont("Outfit"), "Arial", "sans-serif"), | |
| font_mono=(fonts.GoogleFont("IBM Plex Mono"), "ui-monospace", "monospace"), | |
| ) | |
| super().set( | |
| background_fill_primary="*primary_50", | |
| background_fill_primary_dark="*primary_900", | |
| body_background_fill="linear-gradient(135deg, *primary_200, *primary_100)", | |
| body_background_fill_dark="linear-gradient(135deg, *primary_900, *primary_800)", | |
| button_primary_text_color="white", | |
| button_primary_background_fill="linear-gradient(90deg, *secondary_500, *secondary_600)", | |
| button_primary_background_fill_hover="linear-gradient(90deg, *secondary_600, *secondary_700)", | |
| button_secondary_text_color="black", | |
| button_secondary_background_fill="linear-gradient(90deg, *primary_300, *primary_300)", | |
| slider_color="*secondary_500", | |
| block_border_width="3px", | |
| block_shadow="*shadow_drop_lg", | |
| button_primary_shadow="*shadow_drop_lg", | |
| ) | |
| purple_theme = PurpleTheme() | |
| # ========================================== | |
| # --- 🚀 HTML Progress Bar Components --- | |
| # ========================================== | |
| def empty_progress_html(text="Waiting for action..."): | |
| return f""" | |
| <div class="custom-progress-container" style="background-color: transparent; border: 2px dashed rgba(168, 85, 247, 0.4);"> | |
| <div class="custom-progress-text" style="color: #A855F7;">{text}</div> | |
| </div> | |
| """ | |
| def create_progress_html(percent, text, status="active"): | |
| """ | |
| status: "active" (紫色滚动条纹), "success" (绿色), "error" (红色) | |
| """ | |
| return f""" | |
| <div class="custom-progress-container"> | |
| <div class="custom-progress-bar {status}" style="width: {percent}%;"></div> | |
| <div class="custom-progress-text">{text} ({percent}%)</div> | |
| </div> | |
| """ | |
| # ========================================== | |
| # --- ⚙️ Backend Logic & Functions --- | |
| # ========================================== | |
| def get_tree_html(dir_path): | |
| if not os.path.exists(dir_path): | |
| return "<div style='margin-left: 15px; color: #888;'><i>Directory missing</i></div>" | |
| def build_html(current_path): | |
| html = "" | |
| try: items = sorted(os.listdir(current_path)) | |
| except Exception: return "" | |
| if not items: return "" | |
| for item in items: | |
| item_path = os.path.join(current_path, item) | |
| if os.path.isdir(item_path): | |
| html += f'<details style="margin-left: 15px; cursor: pointer; margin-top: 4px;"><summary style="outline: none; color: #C084FC;">📁 <b>{item}</b></summary>' | |
| inner_html = build_html(item_path) | |
| html += inner_html if inner_html else "<div style='margin-left: 20px; color: #888; font-size: 12px;'><i>Empty</i></div>" | |
| html += '</details>' | |
| else: | |
| html += f'<div style="margin-left: 18px; padding-left: 12px; border-left: 1px dotted #A855F7; margin-top: 4px; color: #DAB2FF;">📄 {item}</div>' | |
| return html | |
| content = build_html(dir_path) | |
| return content if content else "<div style='margin-left: 15px; color: #888;'><i>Empty directory</i></div>" | |
| def get_debug_info(session_id): | |
| papers_dir, output_dir, _ = get_user_dirs(session_id) | |
| papers_tree = get_tree_html(papers_dir) | |
| output_tree = get_tree_html(output_dir) | |
| html = f""" | |
| <div style="font-family: 'IBM Plex Mono', monospace; font-size: 13px; background-color: #1e1e1e; border: 1px solid #C084FC; border-radius: 8px; padding: 16px; max-height: 400px; overflow-y: auto;"> | |
| <div style="color: #888; margin-bottom: 8px;">Session ID: {session_id[:8]}...</div> | |
| <details open style="margin-bottom: 12px; cursor: pointer;"> | |
| <summary style="outline: none; font-size: 15px; color: #A855F7;">📁 <b>papers/</b></summary> | |
| {papers_tree} | |
| </details> | |
| <details open style="cursor: pointer;"> | |
| <summary style="outline: none; font-size: 15px; color: #A855F7;">📂 <b>mineru_outputs/</b></summary> | |
| {output_tree} | |
| </details> | |
| </div> | |
| """ | |
| return html | |
| def save_api_settings(api_key, api_base_url, session_id): | |
| if not api_key: | |
| return "❌ Key cannot be empty", get_debug_info(session_id), False, "", "" | |
| success_msg = "✅ Key saved securely in session memory" | |
| if api_base_url: success_msg += ", Base URL updated" | |
| return success_msg, get_debug_info(session_id), True, api_key, api_base_url | |
| def save_pdf(file, session_id): | |
| if file is None: | |
| return gr.update(visible=False), get_debug_info(session_id), False | |
| try: | |
| papers_dir, _, _ = get_user_dirs(session_id) | |
| for f in os.listdir(papers_dir): | |
| file_to_del = os.path.join(papers_dir, f) | |
| if os.path.isfile(file_to_del): os.remove(file_to_del) | |
| file_path = os.path.join(papers_dir, os.path.basename(file.name)) | |
| shutil.copy(file.name, file_path) | |
| return gr.update(value=create_progress_html(100, f"✅ PDF Uploaded: {os.path.basename(file.name)}", "success"), visible=True), get_debug_info(session_id), True | |
| except Exception as e: | |
| return gr.update(value=create_progress_html(0, f"❌ Error: {str(e)}", "error"), visible=True), get_debug_info(session_id), False | |
| def clear_pdf(session_id): | |
| try: | |
| user_base = os.path.join(SESSIONS_BASE_DIR, session_id) | |
| if os.path.exists(user_base): | |
| shutil.rmtree(user_base) | |
| disable_btn = gr.update(interactive=False) | |
| return gr.update(visible=False), gr.update(visible=False), get_debug_info(session_id), False, disable_btn, disable_btn, disable_btn, disable_btn | |
| except Exception as e: | |
| return gr.update(value=create_progress_html(0, f"❌ Clear Error: {str(e)}", "error"), visible=True), gr.update(), get_debug_info(session_id), False, gr.update(), gr.update(), gr.update(), gr.update() | |
| def build_user_env(api_key, api_base_url, papers_dir, output_dir): | |
| env = os.environ.copy() | |
| env["MINERU_FORMULA_ENABLE"] = "false" | |
| env["MINERU_TABLE_ENABLE"] = "false" | |
| env["MINERU_DEVICE_MODE"] = "cpu" | |
| env["MINERU_VIRTUAL_VRAM_SIZE"] = "8" | |
| if api_key: env["GEMINI_API_KEY"] = api_key | |
| if api_base_url: env["GEMINI_API_BASE_URL"] = api_base_url | |
| env["USER_PAPERS_DIR"] = papers_dir | |
| env["USER_OUTPUT_DIR"] = output_dir | |
| return env | |
| def run_mineru_parsing_and_dag_gen(session_id, api_key, api_base_url, progress=gr.Progress()): | |
| no_change = gr.update() | |
| disable_btn = gr.update(interactive=False) | |
| papers_dir, output_dir, _ = get_user_dirs(session_id) | |
| if not os.path.exists(papers_dir) or not any(f.endswith('.pdf') for f in os.listdir(papers_dir)): | |
| yield gr.update(value=create_progress_html(0, "❌ No PDF file found", "error"), visible=True), get_debug_info(session_id), "No execution logs.", no_change, no_change, no_change, no_change | |
| return | |
| full_log = "" | |
| try: | |
| env = build_user_env(api_key, api_base_url, papers_dir, output_dir) | |
| command_mineru = ["mineru", "-p", papers_dir, "-o", output_dir] | |
| full_log += f"--- Mineru Executing (Session: {session_id[:8]}) ---\n" | |
| # 10% | |
| progress(0.1, desc="启动 Mineru 解析...") | |
| yield gr.update(value=create_progress_html(10, "⏳ Starting Mineru parsing...", "active"), visible=True), get_debug_info(session_id), full_log, no_change, no_change, no_change, no_change | |
| process_mineru = subprocess.Popen( | |
| command_mineru, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1 | |
| ) | |
| # 30% | |
| progress(0.3, desc="Mineru 正在解析 PDF...") | |
| for line in iter(process_mineru.stdout.readline, ''): | |
| full_log += line | |
| # ========================================== | |
| # 🔔 双端输出 1:Mineru 阶段 | |
| # ========================================== | |
| print(f"[Mineru | {session_id[:6]}] {line}", end="", flush=True) | |
| yield gr.update(value=create_progress_html(30, "⏳ Mineru parsing PDF...", "active"), visible=True), get_debug_info(session_id), full_log, no_change, no_change, no_change, no_change | |
| process_mineru.stdout.close() | |
| returncode_mineru = process_mineru.wait() | |
| if returncode_mineru != 0: | |
| progress(1.0, desc="Mineru 解析失败") | |
| yield gr.update(value=create_progress_html(30, f"❌ Mineru failed (Code {returncode_mineru})", "error"), visible=True), get_debug_info(session_id), full_log, disable_btn, disable_btn, disable_btn, disable_btn | |
| return | |
| command_dag = [sys.executable, "gen_dag.py"] | |
| full_log += "\n--- DAG Gen Executing ---\n" | |
| # 60% | |
| progress(0.6, desc="执行 DAG 生成...") | |
| yield gr.update(value=create_progress_html(60, "⏳ Executing DAG generation...", "active"), visible=True), get_debug_info(session_id), full_log, no_change, no_change, no_change, no_change | |
| process_dag = subprocess.Popen( | |
| command_dag, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1 | |
| ) | |
| # 80% | |
| progress(0.8, desc="构建图结构中...") | |
| for line in iter(process_dag.stdout.readline, ''): | |
| full_log += line | |
| # ========================================== | |
| # 🔔 双端输出 2:DAG 生成阶段 | |
| # ========================================== | |
| print(f"[DAG | {session_id[:6]}] {line}", end="", flush=True) | |
| yield gr.update(value=create_progress_html(80, "⏳ Building DAG...", "active"), visible=True), get_debug_info(session_id), full_log, no_change, no_change, no_change, no_change | |
| process_dag.stdout.close() | |
| returncode_dag = process_dag.wait() | |
| if returncode_dag == 0: | |
| progress(1.0, desc="解析与构建完成!") | |
| enable_btn = gr.update(interactive=True) | |
| yield gr.update(value=create_progress_html(100, "✅ Fully completed", "success"), visible=True), get_debug_info(session_id), full_log, enable_btn, enable_btn, enable_btn, enable_btn | |
| else: | |
| progress(1.0, desc="DAG 生成失败") | |
| yield gr.update(value=create_progress_html(80, "❌ DAG generation failed", "error"), visible=True), get_debug_info(session_id), full_log, disable_btn, disable_btn, disable_btn, disable_btn | |
| except Exception as e: | |
| progress(1.0, desc="发生异常") | |
| error_log = full_log + f"\n[Global Exception]:\n{str(e)}" | |
| print(f"[Exception | {session_id[:6]}] {str(e)}", flush=True) | |
| yield gr.update(value=create_progress_html(0, "❌ Execution Exception", "error"), visible=True), get_debug_info(session_id), error_log, disable_btn, disable_btn, disable_btn, disable_btn | |
| def run_final_generation(task_type, session_id, api_key, api_base_url, progress=gr.Progress()): | |
| papers_dir, output_dir, zip_path = get_user_dirs(session_id) | |
| if not os.path.exists(output_dir): | |
| yield gr.update(value=create_progress_html(0, "❌ Please run parsing first", "error"), visible=True), get_debug_info(session_id), "No output folder found.", gr.update(visible=False) | |
| return | |
| scripts_to_run = [] | |
| if task_type == "ppt": scripts_to_run = ["gen_ppt.py"] | |
| elif task_type == "poster": scripts_to_run = ["gen_poster.py"] | |
| elif task_type == "pr": scripts_to_run = ["gen_pr.py"] | |
| elif task_type == "all": scripts_to_run = ["gen_ppt.py", "gen_poster.py", "gen_pr.py"] | |
| full_log = f"🚀 Starting {len(scripts_to_run)} tasks for session {session_id[:8]}...\n" | |
| print(f"[GEN Start | {session_id[:6]}] Starting {task_type.upper()}", flush=True) | |
| progress(0.1, desc=f"启动 {task_type.upper()} 生成任务...") | |
| yield gr.update(value=create_progress_html(10, f"⏳ Starting {task_type.upper()}...", "active"), visible=True), get_debug_info(session_id), full_log, gr.update(visible=False) | |
| q = queue.Queue() | |
| processes = [] | |
| env = build_user_env(api_key, api_base_url, papers_dir, output_dir) | |
| def enqueue_output(out, script_name): | |
| for line in iter(out.readline, ''): | |
| q.put((script_name, line)) | |
| out.close() | |
| try: | |
| for script in scripts_to_run: | |
| p = subprocess.Popen( | |
| [sys.executable, script], env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1 | |
| ) | |
| processes.append((script, p)) | |
| t = threading.Thread(target=enqueue_output, args=(p.stdout, script)) | |
| t.daemon = True | |
| t.start() | |
| active_processes = len(processes) | |
| progress(0.5, desc=f"正在并行生成 {task_type.upper()}...") | |
| while active_processes > 0 or not q.empty(): | |
| try: | |
| script_name, line = q.get(timeout=0.1) | |
| full_log += f"[{script_name}] {line}" | |
| # ========================================== | |
| # 🔔 双端输出 3:生成阶段 | |
| # ========================================== | |
| print(f"[{script_name.upper()} | {session_id[:6]}] {line}", end="", flush=True) | |
| yield gr.update(value=create_progress_html(50, f"⏳ Generating {task_type.upper()}...", "active"), visible=True), get_debug_info(session_id), full_log, gr.update(visible=False) | |
| except queue.Empty: | |
| active_processes = sum(1 for _, p in processes if p.poll() is None) | |
| success = all(p.returncode == 0 for _, p in processes) | |
| if not success: | |
| progress(1.0, desc="生成失败") | |
| yield gr.update(value=create_progress_html(50, "❌ Tasks failed", "error"), visible=True), get_debug_info(session_id), full_log, gr.update(visible=False) | |
| return | |
| full_log += "\n📦 Zipping output directory...\n" | |
| progress(0.9, desc="打包压缩结果...") | |
| yield gr.update(value=create_progress_html(90, "⏳ Zipping outputs...", "active"), visible=True), get_debug_info(session_id), full_log, gr.update(visible=False) | |
| zip_base_name = zip_path.replace(".zip", "") | |
| shutil.make_archive(zip_base_name, 'zip', output_dir) | |
| full_log += "✅ All tasks completed successfully.\n" | |
| progress(1.0, desc="全部完成!") | |
| yield gr.update(value=create_progress_html(100, f"✅ {task_type.upper()} Generated", "success"), visible=True), get_debug_info(session_id), full_log, gr.update(value=zip_path, visible=True) | |
| except Exception as e: | |
| progress(1.0, desc="发生全局异常") | |
| error_log = full_log + f"\n[Global Exception]:\n{str(e)}" | |
| print(f"[Exception | {session_id[:6]}] {str(e)}", flush=True) | |
| yield gr.update(value=create_progress_html(0, "❌ Global exception", "error"), visible=True), get_debug_info(session_id), error_log, gr.update(visible=False) | |
| # ========================================== | |
| # --- 🚀 UI Configuration & Advanced CSS --- | |
| # ========================================== | |
| custom_css = """ | |
| @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap'); | |
| body, .gradio-container { | |
| background-color: #FAF5FF !important; | |
| background-image: linear-gradient(#E9D5FF 1px, transparent 1px), linear-gradient(90deg, #E9D5FF 1px, transparent 1px) !important; | |
| background-size: 40px 40px !important; | |
| font-family: 'Outfit', sans-serif !important; | |
| } | |
| .dark body, .dark .gradio-container { | |
| background-color: #1a1a1a !important; | |
| background-image: linear-gradient(rgba(168, 85, 247, .1) 1px, transparent 1px), linear-gradient(90deg, rgba(168, 85, 247, .1) 1px, transparent 1px) !important; | |
| } | |
| #col-container { margin: 0 auto; max-width: 90%; padding: 20px; } | |
| #main-title { text-align: center !important; padding: 1.5rem 0 0.5rem 0; } | |
| #main-title h1 { | |
| font-size: 2.6em !important; font-weight: 800 !important; | |
| background: linear-gradient(135deg, #A855F7 0%, #C084FC 50%, #9333EA 100%); | |
| background-size: 200% 200%; | |
| -webkit-background-clip: text; -webkit-text-fill-color: transparent; | |
| animation: gradient-shift 4s ease infinite; letter-spacing: -0.02em; | |
| } | |
| @keyframes gradient-shift { 0%, 100% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } } | |
| #subtitle { text-align: center !important; margin-bottom: 2rem; } | |
| #subtitle p { margin: 0 auto; color: #666; font-size: 1.1rem; font-weight: 500; } | |
| .dark #subtitle p { color: #DAB2FF; } | |
| .gradio-group { | |
| background: rgba(255, 255, 255, 0.9) !important; | |
| border: 2px solid #E9D5FF !important; | |
| border-radius: 24px !important; | |
| box-shadow: 0 4px 24px rgba(168, 85, 247, 0.08) !important; | |
| backdrop-filter: blur(10px); | |
| transition: all 0.3s ease; | |
| padding: 16px 16px 16px 16px !important; | |
| overflow: visible !important; | |
| margin-bottom: 10px !important; | |
| display: flex !important; | |
| flex-direction: column !important; | |
| gap: 16px !important; | |
| } | |
| .gradio-group:hover { | |
| box-shadow: 0 8px 32px rgba(168, 85, 247, 0.12) !important; | |
| border-color: #C084FC !important; | |
| } | |
| .dark .gradio-group { | |
| background: rgba(30, 30, 30, 0.9) !important; | |
| border-color: rgba(168, 85, 247, 0.3) !important; | |
| } | |
| /* ================= 进度条自定义 CSS ================= */ | |
| @keyframes progress-bar-stripes { | |
| from { background-position: 1rem 0; } | |
| to { background-position: 0 0; } | |
| } | |
| .custom-progress-container { | |
| width: 100%; | |
| background-color: rgba(233, 213, 255, 0.3); | |
| border-radius: 12px; | |
| overflow: hidden; | |
| position: relative; | |
| height: 40px; | |
| border: 1px solid rgba(168, 85, 247, 0.3); | |
| box-shadow: inset 0 2px 4px rgba(0,0,0,0.05); | |
| } | |
| .custom-progress-bar { | |
| height: 100%; | |
| border-radius: 12px; | |
| transition: width 0.4s ease; | |
| } | |
| .custom-progress-bar.active { | |
| background-color: #A855F7; | |
| background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); | |
| background-size: 1rem 1rem; | |
| animation: progress-bar-stripes 1s linear infinite; | |
| box-shadow: 0 0 10px rgba(168, 85, 247, 0.5); | |
| } | |
| .custom-progress-bar.success { background-image: none; background-color: #10B981; box-shadow: 0 0 10px rgba(16, 185, 129, 0.5);} | |
| .custom-progress-bar.error { background-image: none; background-color: #EF4444; box-shadow: 0 0 10px rgba(239, 68, 68, 0.5);} | |
| .custom-progress-text { | |
| position: absolute; top: 0; left: 0; width: 100%; height: 100%; | |
| display: flex; align-items: center; justify-content: center; | |
| font-weight: 600; color: #581C87; font-size: 14px; | |
| text-shadow: 0 0 4px rgba(255,255,255,0.8); | |
| } | |
| .dark .custom-progress-text { color: #E9D5FF; text-shadow: 0 0 4px rgba(0,0,0,0.8); } | |
| #pdf-upload-box { | |
| border: 2px dashed rgba(192, 132, 252, 0.6) !important; | |
| border-radius: 16px !important; | |
| background-color: rgba(250, 245, 255, 0.5) !important; | |
| transition: all 0.3s ease !important; | |
| min-height: 220px !important; | |
| position: relative !important; | |
| margin-top: 10px !important; | |
| } | |
| #pdf-upload-box:hover { | |
| border-color: #A855F7 !important; | |
| background-color: rgba(243, 232, 255, 0.8) !important; | |
| box-shadow: 0 4px 15px rgba(168, 85, 247, 0.15) !important; | |
| } | |
| #pdf-upload-box .upload-container { background: transparent !important; } | |
| #pdf-upload-box .upload-container > span, | |
| #pdf-upload-box .upload-container > svg { display: none !important; } | |
| #pdf-upload-box .upload-container::before { | |
| content: "📤\\A Click here to select a PDF\\A or Drag & Drop the file here"; | |
| white-space: pre-wrap; | |
| font-size: 1.2rem; | |
| line-height: 1.8; | |
| font-weight: 600; | |
| color: #9333EA; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| text-align: center; | |
| width: 100%; | |
| height: 100%; | |
| position: absolute; | |
| top: 0; left: 0; | |
| pointer-events: none; | |
| } | |
| .primary-action-btn { | |
| border-radius: 25px !important; | |
| background: linear-gradient(135deg, #9333EA, #7E22CE) !important; | |
| color: white !important; font-weight: 700 !important; border: none !important; | |
| height: 50px !important; | |
| width: 80% !important; | |
| margin-left: auto !important; | |
| margin-right: auto !important; | |
| margin-top: 10px !important; | |
| margin-bottom: 10px !important; | |
| display: block !important; | |
| transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275), box-shadow 0.3s ease !important; | |
| box-shadow: 0 4px 15px rgba(126, 34, 206, 0.3) !important; | |
| cursor: pointer !important; | |
| font-size: 1.15rem !important; | |
| } | |
| .primary-action-btn:hover { | |
| transform: translateY(-5px) scale(1.02) !important; | |
| box-shadow: 0 10px 25px rgba(126, 34, 206, 0.5) !important; | |
| background: linear-gradient(135deg, #A855F7, #9333EA) !important; | |
| } | |
| .primary-action-btn:active { transform: translateY(2px) scale(0.98) !important; box-shadow: 0 2px 10px rgba(126, 34, 206, 0.2) !important; } | |
| .action-row { display: flex !important; justify-content: center !important; gap: 10px !important; margin-bottom: 10px !important; margin-top: 10px !important;} | |
| .action-btn { | |
| border-radius: 24px !important; | |
| background: linear-gradient(135deg, #A855F7, #9333EA) !important; | |
| color: white !important; font-weight: 600 !important; border: none !important; | |
| height: 40px !important; | |
| width: 120px !important; | |
| flex: none !important; | |
| display: flex !important; | |
| align-items: center !important; | |
| justify-content: center !important; | |
| line-height: 1 !important; | |
| padding: 0 !important; | |
| margin-top: 10px !important; | |
| margin-bottom: 10px !important; | |
| transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275), box-shadow 0.3s ease !important; | |
| box-shadow: 0 4px 15px rgba(147, 51, 234, 0.2) !important; | |
| cursor: pointer !important; | |
| font-size: 1.05rem !important; | |
| } | |
| .action-btn:hover { | |
| transform: translateY(-5px) scale(1.03) !important; | |
| box-shadow: 0 10px 25px rgba(147, 51, 234, 0.4) !important; | |
| background: linear-gradient(135deg, #C084FC, #A855F7) !important; | |
| } | |
| .action-btn:active { transform: translateY(2px) scale(0.98) !important; box-shadow: 0 2px 10px rgba(147, 51, 234, 0.2) !important; } | |
| .primary-action-btn:disabled, .action-btn:disabled { | |
| background: #e5e7eb !important; | |
| color: #9ca3af !important; | |
| box-shadow: none !important; | |
| transform: none !important; | |
| cursor: not-allowed !important; | |
| border: 1px solid #d1d5db !important; | |
| } | |
| .dark .primary-action-btn:disabled, .dark .action-btn:disabled { | |
| background: #374151 !important; | |
| color: #6b7280 !important; | |
| border: 1px solid #4b5563 !important; | |
| } | |
| .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; } | |
| .status-text textarea { background-color: transparent !important; border: none !important; box-shadow: none !important; font-weight: 600 !important; color: #6B21A8 !important; } | |
| .dark .status-text textarea { color: #C084FC !important; } | |
| ::-webkit-scrollbar { width: 8px; height: 8px; } | |
| ::-webkit-scrollbar-track { background: rgba(168, 85, 247, 0.05); border-radius: 4px; } | |
| ::-webkit-scrollbar-thumb { background: linear-gradient(135deg, #A855F7, #C084FC); border-radius: 4px; } | |
| ::-webkit-scrollbar-thumb:hover { background: linear-gradient(135deg, #9333EA, #A855F7); } | |
| details > summary { transition: color 0.2s ease; } | |
| details > summary:hover { color: #E9D5FF !important; } | |
| """ | |
| with gr.Blocks(theme=purple_theme, css=custom_css) as demo: | |
| session_id_state = gr.State("") | |
| user_api_key_state = gr.State("") | |
| user_api_base_state = gr.State("") | |
| api_saved_state = gr.State(False) | |
| pdf_ready_state = gr.State(False) | |
| with gr.Column(elem_id="col-container"): | |
| gr.Markdown("# **PaperX Platform**", elem_id="main-title") | |
| gr.Markdown("One-click parsing of academic PDFs, DAG structuring, and multi-modal asset generation.", elem_id="subtitle") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| # 1. API Configuration | |
| with gr.Group(elem_classes="gradio-group"): | |
| gr.Markdown("### ⚙️ 1. Global API Configuration") | |
| with gr.Row(): | |
| key_input = gr.Textbox(label="Gemini API Key", type="password", placeholder="sk-...", scale=1) | |
| api_base_url_input = gr.Textbox(label="Base URL (Optional)", placeholder="https://api.example.com", scale=1) | |
| key_btn = gr.Button("💾 Save API Configuration") | |
| api_status = gr.Textbox(show_label=False, interactive=False, elem_classes="status-text") | |
| # 2. Document Parsing | |
| with gr.Group(elem_classes="gradio-group"): | |
| gr.Markdown("### 📄 2. Document Parsing") | |
| pdf_input = gr.File(label="Upload Document", file_types=[".pdf"], elem_id="pdf-upload-box") | |
| parse_btn = gr.Button("🚀 Start Mineru & DAG Extraction", elem_classes="primary-action-btn", interactive=False) | |
| # 默认隐藏进度条 | |
| parse_progress = gr.HTML(visible=False) | |
| # 3. Asset Generation | |
| with gr.Group(elem_classes="gradio-group"): | |
| gr.Markdown("### 🎯 3. Asset Generation") | |
| with gr.Row(elem_classes="action-row"): | |
| gen_ppt_btn = gr.Button("📊 Gen PPT", elem_classes="action-btn", interactive=False) | |
| gen_poster_btn = gr.Button("🖼️ Gen Poster", elem_classes="action-btn", interactive=False) | |
| gen_pr_btn = gr.Button("📰 Gen PR", elem_classes="action-btn", interactive=False) | |
| gen_all_btn = gr.Button("✨ Generate All Assets (ALL)", elem_classes="primary-action-btn", interactive=False) | |
| # 默认隐藏进度条 | |
| gen_progress = gr.HTML(visible=False) | |
| with gr.Column(scale=1): | |
| # 4. Results & Downloads | |
| with gr.Group(elem_classes="gradio-group"): | |
| gr.Markdown("### 📦 Generation Results & Download") | |
| download_placeholder = gr.HTML( | |
| ''' | |
| <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 160px; border: 2px dashed rgba(192, 132, 252, 0.6); border-radius: 16px; background-color: rgba(250, 245, 255, 0.5); color: #9333EA; text-align: center; transition: all 0.3s ease;"> | |
| <span style="font-size: 32px; margin-bottom: 8px;">📦</span> | |
| <span style="font-weight: 600; font-size: 16px; margin-bottom: 4px;">Awaiting Generation</span> | |
| <span style="font-size: 13px; color: #A855F7; opacity: 0.8;">Generated assets will appear here as a downloadable ZIP archive.</span> | |
| </div> | |
| ''' | |
| ) | |
| download_file = gr.File(label="📥 Get Final Zip Archive", interactive=False, visible=False) | |
| # 5. Debugging | |
| with gr.Group(elem_classes="gradio-group"): | |
| gr.Markdown("### 🛠️ Developer Monitoring (Debug Only)") | |
| with gr.Tabs(): | |
| with gr.Tab("📜 Terminal Stream"): | |
| cmd_logs = gr.Textbox(show_label=False, lines=14, interactive=False, elem_classes="log-box") | |
| with gr.Tab("🔍 System Snapshot"): | |
| refresh_btn = gr.Button("🔄 Refresh Directory Tree") | |
| debug_view = gr.HTML() | |
| # ================= LOGIC BINDINGS ================= | |
| def init_app_for_user(): | |
| new_session_id = str(uuid.uuid4()) | |
| debug_html = get_debug_info(new_session_id) | |
| return new_session_id, debug_html | |
| demo.load(fn=init_app_for_user, inputs=None, outputs=[session_id_state, debug_view]) | |
| key_btn.click( | |
| fn=save_api_settings, | |
| inputs=[key_input, api_base_url_input, session_id_state], | |
| outputs=[api_status, debug_view, api_saved_state, user_api_key_state, user_api_base_state] | |
| ) | |
| pdf_input.upload( | |
| fn=save_pdf, | |
| inputs=[pdf_input, session_id_state], | |
| outputs=[parse_progress, debug_view, pdf_ready_state] | |
| ) | |
| pdf_input.clear( | |
| fn=clear_pdf, | |
| inputs=[session_id_state], | |
| outputs=[parse_progress, gen_progress, debug_view, pdf_ready_state, gen_ppt_btn, gen_poster_btn, gen_pr_btn, gen_all_btn] | |
| ) | |
| def check_parse_btn_ready(api_ready, pdf_ready): | |
| return gr.update(interactive=(api_ready and pdf_ready)) | |
| api_saved_state.change(fn=check_parse_btn_ready, inputs=[api_saved_state, pdf_ready_state], outputs=parse_btn) | |
| pdf_ready_state.change(fn=check_parse_btn_ready, inputs=[api_saved_state, pdf_ready_state], outputs=parse_btn) | |
| parse_btn.click( | |
| fn=run_mineru_parsing_and_dag_gen, | |
| inputs=[session_id_state, user_api_key_state, user_api_base_state], | |
| outputs=[parse_progress, debug_view, cmd_logs, gen_ppt_btn, gen_poster_btn, gen_pr_btn, gen_all_btn] | |
| ) | |
| def trigger_gen_ppt(sid, ak, ab, progress=gr.Progress()): yield from run_final_generation("ppt", sid, ak, ab, progress) | |
| def trigger_gen_poster(sid, ak, ab, progress=gr.Progress()): yield from run_final_generation("poster", sid, ak, ab, progress) | |
| def trigger_gen_pr(sid, ak, ab, progress=gr.Progress()): yield from run_final_generation("pr", sid, ak, ab, progress) | |
| def trigger_gen_all(sid, ak, ab, progress=gr.Progress()): yield from run_final_generation("all", sid, ak, ab, progress) | |
| gen_ppt_btn.click(fn=trigger_gen_ppt, inputs=[session_id_state, user_api_key_state, user_api_base_state], outputs=[gen_progress, debug_view, cmd_logs, download_file]) | |
| gen_poster_btn.click(fn=trigger_gen_poster, inputs=[session_id_state, user_api_key_state, user_api_base_state], outputs=[gen_progress, debug_view, cmd_logs, download_file]) | |
| gen_pr_btn.click(fn=trigger_gen_pr, inputs=[session_id_state, user_api_key_state, user_api_base_state], outputs=[gen_progress, debug_view, cmd_logs, download_file]) | |
| gen_all_btn.click(fn=trigger_gen_all, inputs=[session_id_state, user_api_key_state, user_api_base_state], outputs=[gen_progress, debug_view, cmd_logs, download_file]) | |
| refresh_btn.click(fn=get_debug_info, inputs=[session_id_state], outputs=debug_view) | |
| def toggle_empty_placeholder(file_val): | |
| return gr.update(visible=(file_val is None)) | |
| download_file.change( | |
| fn=toggle_empty_placeholder, | |
| inputs=[download_file], | |
| outputs=[download_placeholder] | |
| ) | |
| if __name__ == "__main__": | |
| start_garbage_collector() | |
| # 并发放宽至 5 | |
| # demo.queue(default_concurrency_limit=5).launch() | |
| demo.queue(default_concurrency_limit=5).launch(server_name="0.0.0.0", server_port=7860) |