Spaces:
Sleeping
Sleeping
| 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) |