|
|
|
|
|
import gradio as gr |
|
|
import os |
|
|
import json |
|
|
from pathlib import Path |
|
|
import base64 |
|
|
import re |
|
|
from threading import Thread |
|
|
from http.server import HTTPServer, SimpleHTTPRequestHandler |
|
|
import socket |
|
|
from dotenv import load_dotenv |
|
|
from ProjectPageAgent.parse_paper import parse_paper_for_project_page, save_parsed_content |
|
|
from ProjectPageAgent.html_finder import HtmlFinder |
|
|
from ProjectPageAgent.content_planner import ProjectPageContentPlanner |
|
|
from ProjectPageAgent.html_generator import ProjectPageHTMLGenerator, to_url |
|
|
from utils.wei_utils import get_agent_config |
|
|
import os |
|
|
import subprocess |
|
|
|
|
|
from ProjectPageAgent.content_planner import filter_references |
|
|
from utils.src.utils import run_sync_screenshots |
|
|
from ProjectPageAgent.main_pipline import matching, copy_static_files |
|
|
|
|
|
load_dotenv() |
|
|
|
|
|
subprocess.run(["playwright", "install", "chromium"], check=True) |
|
|
|
|
|
|
|
|
def get_agent_config_with_keys(model_type, openai_api_key="", gemini_api_key="", |
|
|
qwen_api_key="", zhipuai_api_key="", openrouter_api_key=""): |
|
|
""" |
|
|
Get agent configuration with user-provided API keys. |
|
|
Falls back to environment variables if user keys are not provided. |
|
|
Note: This function sets environment variables but does NOT restore them. |
|
|
The environment variables will remain set for the duration of the application. |
|
|
""" |
|
|
|
|
|
api_keys = { |
|
|
'OPENAI_API_KEY': openai_api_key, |
|
|
'GEMINI_API_KEY': gemini_api_key, |
|
|
'QWEN_API_KEY': qwen_api_key, |
|
|
'ZHIPUAI_API_KEY': zhipuai_api_key, |
|
|
'OPENROUTER_API_KEY': openrouter_api_key |
|
|
} |
|
|
|
|
|
|
|
|
for key, value in api_keys.items(): |
|
|
if value and value.strip(): |
|
|
os.environ[key] = value |
|
|
|
|
|
|
|
|
config = get_agent_config(model_type) |
|
|
return config |
|
|
|
|
|
def validate_api_keys(model_name_t, model_name_v, openai_api_key, gemini_api_key, |
|
|
qwen_api_key, zhipuai_api_key, openrouter_api_key): |
|
|
""" |
|
|
Validate that required API keys are provided for the selected models. |
|
|
""" |
|
|
errors = [] |
|
|
|
|
|
|
|
|
if model_name_t in ['4o', '4o-mini', 'gpt-4.1', 'gpt-4.1-mini', 'o1', 'o3', 'o3-mini']: |
|
|
if not openai_api_key or not openai_api_key.strip(): |
|
|
errors.append("OpenAI API key is required for GPT models") |
|
|
elif model_name_t in ['gemini', 'gemini-2.5-pro', 'gemini-2.5-flash']: |
|
|
if not gemini_api_key or not gemini_api_key.strip(): |
|
|
errors.append("Gemini API key is required for Gemini models") |
|
|
elif model_name_t in ['qwen', 'qwen-plus', 'qwen-max', 'qwen-long']: |
|
|
if not qwen_api_key or not qwen_api_key.strip(): |
|
|
errors.append("Qwen API key is required for Qwen models") |
|
|
elif model_name_t.startswith('openrouter_'): |
|
|
if not openrouter_api_key or not openrouter_api_key.strip(): |
|
|
errors.append("OpenRouter API key is required for OpenRouter models") |
|
|
|
|
|
|
|
|
if model_name_v in ['4o', '4o-mini']: |
|
|
if not openai_api_key or not openai_api_key.strip(): |
|
|
errors.append("OpenAI API key is required for GPT vision models") |
|
|
elif model_name_v in ['gemini', 'gemini-2.5-pro', 'gemini-2.5-flash']: |
|
|
if not gemini_api_key or not gemini_api_key.strip(): |
|
|
errors.append("Gemini API key is required for Gemini vision models") |
|
|
elif model_name_v in ['qwen-vl-max', 'qwen-2.5-vl-72b']: |
|
|
if not qwen_api_key or not qwen_api_key.strip(): |
|
|
errors.append("Qwen API key is required for Qwen vision models") |
|
|
elif model_name_v.startswith('openrouter_'): |
|
|
if not openrouter_api_key or not openrouter_api_key.strip(): |
|
|
errors.append("OpenRouter API key is required for OpenRouter vision models") |
|
|
|
|
|
return errors |
|
|
|
|
|
|
|
|
current_html_dir = None |
|
|
preview_server = None |
|
|
preview_port = None |
|
|
template_preview_servers = [] |
|
|
|
|
|
class CustomHTTPRequestHandler(SimpleHTTPRequestHandler): |
|
|
def __init__(self, *args, **kwargs): |
|
|
super().__init__(*args, directory=current_html_dir, **kwargs) |
|
|
|
|
|
def log_message(self, format, *args): |
|
|
pass |
|
|
|
|
|
def find_free_port(start_port=8000, max_attempts=100): |
|
|
for port in range(start_port, start_port + max_attempts): |
|
|
try: |
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: |
|
|
s.bind(('', port)) |
|
|
return port |
|
|
except OSError: |
|
|
continue |
|
|
raise RuntimeError(f"Could not find available port") |
|
|
|
|
|
def start_preview_server(html_dir): |
|
|
global current_html_dir, preview_server, preview_port |
|
|
stop_preview_server() |
|
|
current_html_dir = html_dir |
|
|
preview_port = find_free_port() |
|
|
preview_server = HTTPServer(('0.0.0.0', preview_port), CustomHTTPRequestHandler) |
|
|
server_thread = Thread(target=preview_server.serve_forever, daemon=True) |
|
|
server_thread.start() |
|
|
return preview_port |
|
|
|
|
|
def stop_preview_server(): |
|
|
global preview_server, preview_port |
|
|
if preview_server: |
|
|
preview_server.shutdown() |
|
|
preview_server = None |
|
|
preview_port = None |
|
|
|
|
|
def start_ephemeral_server_for_dir(html_dir): |
|
|
port = find_free_port() |
|
|
class _TempHandler(SimpleHTTPRequestHandler): |
|
|
def __init__(self, *args, **kwargs): |
|
|
super().__init__(*args, directory=html_dir, **kwargs) |
|
|
def log_message(self, format, *args): |
|
|
pass |
|
|
srv = HTTPServer(('0.0.0.0', port), _TempHandler) |
|
|
t = Thread(target=srv.serve_forever, daemon=True) |
|
|
t.start() |
|
|
template_preview_servers.append((srv, port)) |
|
|
return port |
|
|
|
|
|
def stop_all_template_preview_servers(): |
|
|
global template_preview_servers |
|
|
for srv, _ in template_preview_servers: |
|
|
try: |
|
|
srv.shutdown() |
|
|
except Exception: |
|
|
pass |
|
|
template_preview_servers = [] |
|
|
|
|
|
class GenerationArgs: |
|
|
def __init__(self, paper_path, model_name_t, model_name_v, template_root, |
|
|
template_dir, template_file, output_dir, style_preference, tmp_dir, |
|
|
full_content_check_times, background_color, has_navigation, |
|
|
has_hero_section, title_color, page_density, image_layout, |
|
|
html_check_times, resume, human_input): |
|
|
self.paper_path = paper_path |
|
|
self.model_name_t = model_name_t |
|
|
self.model_name_v = model_name_v |
|
|
self.template_root = template_root |
|
|
self.template_dir = template_dir |
|
|
self.template_file = template_file |
|
|
self.output_dir = output_dir |
|
|
self.style_preference = style_preference |
|
|
self.tmp_dir = tmp_dir |
|
|
self.full_content_check_times = full_content_check_times |
|
|
self.background_color = background_color |
|
|
self.has_navigation = has_navigation |
|
|
self.has_hero_section = has_hero_section |
|
|
self.title_color = title_color |
|
|
self.page_density = page_density |
|
|
self.image_layout = image_layout |
|
|
self.html_check_times = html_check_times |
|
|
self.resume = resume |
|
|
self.human_input = human_input |
|
|
self.paper_name = None |
|
|
|
|
|
|
|
|
|
|
|
def format_section_to_markdown(section_data): |
|
|
""" |
|
|
Convert Section JSON to beautifully formatted Markdown |
|
|
|
|
|
Args: |
|
|
section_data: Section JSON data |
|
|
|
|
|
Returns: |
|
|
str: Formatted Markdown string |
|
|
""" |
|
|
if not section_data: |
|
|
return "No data available" |
|
|
|
|
|
md_lines = [] |
|
|
|
|
|
|
|
|
md_lines.append("# π Paper Page Structure Preview\n") |
|
|
|
|
|
|
|
|
if "title" in section_data: |
|
|
md_lines.append(f"## π Title\n**{section_data['title']}**\n") |
|
|
|
|
|
if "authors" in section_data: |
|
|
md_lines.append(f"## π₯ Authors\n{section_data['authors']}\n") |
|
|
|
|
|
if "affiliation" in section_data: |
|
|
md_lines.append(f"## ποΈ Affiliation\n{section_data['affiliation']}\n") |
|
|
|
|
|
|
|
|
md_lines.append("## π Page Sections\n") |
|
|
|
|
|
section_count = 0 |
|
|
for key, value in section_data.items(): |
|
|
if key in ["title", "authors", "affiliation"]: |
|
|
continue |
|
|
|
|
|
section_count += 1 |
|
|
|
|
|
|
|
|
section_title = key.replace("_", " ").title() |
|
|
md_lines.append(f"### {section_count}. {section_title}\n") |
|
|
|
|
|
|
|
|
if isinstance(value, dict): |
|
|
|
|
|
for sub_key, sub_value in value.items(): |
|
|
sub_title = sub_key.replace("_", " ").title() |
|
|
md_lines.append(f"**{sub_title}**: {sub_value}\n") |
|
|
elif isinstance(value, list): |
|
|
|
|
|
for item in value: |
|
|
if isinstance(item, str): |
|
|
md_lines.append(f"- {item}\n") |
|
|
elif isinstance(item, dict): |
|
|
for k, v in item.items(): |
|
|
md_lines.append(f"- **{k}**: {v}\n") |
|
|
else: |
|
|
|
|
|
md_lines.append(f"{value}\n") |
|
|
|
|
|
md_lines.append("") |
|
|
|
|
|
|
|
|
md_lines.append("---\n") |
|
|
md_lines.append(f"**π Total {section_count} sections**\n") |
|
|
|
|
|
return "\n".join(md_lines) |
|
|
|
|
|
|
|
|
def format_full_content_to_markdown(content_data, figures=None): |
|
|
""" |
|
|
Convert Full Content JSON to beautifully formatted Markdown |
|
|
|
|
|
Args: |
|
|
content_data: Full Content JSON data |
|
|
figures: Images and tables data (optional) |
|
|
|
|
|
Returns: |
|
|
str: Formatted Markdown string |
|
|
""" |
|
|
if not content_data: |
|
|
return "No data available" |
|
|
|
|
|
md_lines = [] |
|
|
|
|
|
|
|
|
md_lines.append("# π Full Content Preview\n") |
|
|
|
|
|
|
|
|
if "title" in content_data: |
|
|
md_lines.append(f"# {content_data['title']}\n") |
|
|
|
|
|
if "authors" in content_data: |
|
|
md_lines.append(f"**Authors**: {content_data['authors']}\n") |
|
|
|
|
|
if "affiliation" in content_data: |
|
|
md_lines.append(f"**Affiliation**: {content_data['affiliation']}\n") |
|
|
|
|
|
md_lines.append("---\n") |
|
|
|
|
|
|
|
|
section_count = 0 |
|
|
image_count = 0 |
|
|
table_count = 0 |
|
|
|
|
|
for key, value in content_data.items(): |
|
|
if key in ["title", "authors", "affiliation"]: |
|
|
continue |
|
|
|
|
|
section_count += 1 |
|
|
|
|
|
|
|
|
section_title = key.replace("_", " ").title() |
|
|
md_lines.append(f"## {section_count}. {section_title}\n") |
|
|
|
|
|
|
|
|
if isinstance(value, dict): |
|
|
|
|
|
for sub_key, sub_value in value.items(): |
|
|
if sub_key.lower() in ['content', 'description', 'text']: |
|
|
|
|
|
md_lines.append(f"{sub_value}\n") |
|
|
elif sub_key.lower() in ['image', 'figure', 'img']: |
|
|
|
|
|
image_count += 1 |
|
|
if isinstance(sub_value, dict): |
|
|
caption = sub_value.get('caption', f'Figure {image_count}') |
|
|
path = sub_value.get('path', '') |
|
|
md_lines.append(f"\n**πΌοΈ {caption}**\n") |
|
|
if path: |
|
|
md_lines.append(f"*Image path: `{path}`*\n") |
|
|
else: |
|
|
md_lines.append(f"\n**πΌοΈ Figure {image_count}**: {sub_value}\n") |
|
|
elif sub_key.lower() in ['table']: |
|
|
|
|
|
table_count += 1 |
|
|
md_lines.append(f"\n**π Table {table_count}**\n") |
|
|
if isinstance(sub_value, dict): |
|
|
caption = sub_value.get('caption', f'Table {table_count}') |
|
|
md_lines.append(f"*{caption}*\n") |
|
|
else: |
|
|
md_lines.append(f"{sub_value}\n") |
|
|
elif sub_key.lower() in ['code']: |
|
|
|
|
|
md_lines.append(f"\n```\n{sub_value}\n```\n") |
|
|
else: |
|
|
|
|
|
sub_title = sub_key.replace("_", " ").title() |
|
|
md_lines.append(f"\n### {sub_title}\n") |
|
|
md_lines.append(f"{sub_value}\n") |
|
|
|
|
|
elif isinstance(value, list): |
|
|
|
|
|
for idx, item in enumerate(value): |
|
|
if isinstance(item, dict): |
|
|
|
|
|
if 'title' in item or 'name' in item: |
|
|
item_title = item.get('title', item.get('name', f'Item {idx+1}')) |
|
|
md_lines.append(f"\n### {item_title}\n") |
|
|
|
|
|
for k, v in item.items(): |
|
|
if k not in ['title', 'name']: |
|
|
if k.lower() in ['content', 'description', 'text']: |
|
|
md_lines.append(f"{v}\n") |
|
|
elif k.lower() in ['image', 'figure']: |
|
|
image_count += 1 |
|
|
md_lines.append(f"\n**πΌοΈ Figure {image_count}**: {v}\n") |
|
|
elif k.lower() == 'table': |
|
|
table_count += 1 |
|
|
md_lines.append(f"\n**π Table {table_count}**: {v}\n") |
|
|
else: |
|
|
k_title = k.replace("_", " ").title() |
|
|
md_lines.append(f"**{k_title}**: {v}\n") |
|
|
else: |
|
|
|
|
|
md_lines.append(f"- {item}\n") |
|
|
|
|
|
else: |
|
|
|
|
|
md_lines.append(f"{value}\n") |
|
|
|
|
|
md_lines.append("") |
|
|
|
|
|
|
|
|
md_lines.append("\n---\n") |
|
|
stats = [] |
|
|
stats.append(f"π **Statistics**") |
|
|
stats.append(f"- Sections: {section_count}") |
|
|
if image_count > 0: |
|
|
stats.append(f"- Images: {image_count}") |
|
|
if table_count > 0: |
|
|
stats.append(f"- Tables: {table_count}") |
|
|
|
|
|
|
|
|
if figures: |
|
|
if 'images' in figures and figures['images']: |
|
|
stats.append(f"- Available images: {len(figures['images'])}") |
|
|
if 'tables' in figures and figures['tables']: |
|
|
stats.append(f"- Available tables: {len(figures['tables'])}") |
|
|
|
|
|
md_lines.append("\n".join(stats)) |
|
|
md_lines.append("\n") |
|
|
|
|
|
return "\n".join(md_lines) |
|
|
|
|
|
|
|
|
|
|
|
class GenerationState: |
|
|
def __init__(self): |
|
|
self.reset() |
|
|
|
|
|
def reset(self): |
|
|
self.args = None |
|
|
self.paper_content = None |
|
|
self.figures = None |
|
|
self.generated_section = None |
|
|
self.text_page_content = None |
|
|
self.generated_content = None |
|
|
self.html_content = None |
|
|
self.html_file_path = None |
|
|
self.html_dir = None |
|
|
self.planner = None |
|
|
self.html_generator = None |
|
|
self.agent_config_t = None |
|
|
self.total_input_tokens_t = 0 |
|
|
self.total_output_tokens_t = 0 |
|
|
self.current_stage = "init" |
|
|
self.preview_url = None |
|
|
|
|
|
state = GenerationState() |
|
|
|
|
|
def create_project_zip(project_dir, output_dir, paper_name): |
|
|
""" |
|
|
Create project archive |
|
|
|
|
|
Args: |
|
|
project_dir: Project directory path |
|
|
output_dir: Output directory |
|
|
paper_name: Paper name |
|
|
|
|
|
Returns: |
|
|
str: Archive path, None if failed |
|
|
""" |
|
|
import zipfile |
|
|
|
|
|
zip_filename = f"{paper_name}_project_page.zip" |
|
|
zip_path = os.path.join(output_dir, zip_filename) |
|
|
|
|
|
print(f"Creating project archive: {zip_path}") |
|
|
|
|
|
try: |
|
|
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf: |
|
|
|
|
|
for root, dirs, files in os.walk(project_dir): |
|
|
for file in files: |
|
|
file_path = os.path.join(root, file) |
|
|
|
|
|
arcname = os.path.relpath(file_path, output_dir) |
|
|
zipf.write(file_path, arcname) |
|
|
|
|
|
print(f"Archive created successfully: {zip_path}") |
|
|
|
|
|
|
|
|
zip_size = os.path.getsize(zip_path) |
|
|
zip_size_mb = zip_size / (1024 * 1024) |
|
|
print(f"Archive size: {zip_size_mb:.2f} MB") |
|
|
|
|
|
return zip_path |
|
|
|
|
|
except Exception as e: |
|
|
print(f"Archive creation failed: {e}") |
|
|
return None |
|
|
|
|
|
def start_generation(pdf_file, model_name_t, model_name_v, template_root, |
|
|
template_dir, template_file, output_dir, style_preference, |
|
|
tmp_dir, full_content_check_times, background_color, |
|
|
has_navigation, has_hero_section, title_color, page_density, |
|
|
image_layout, html_check_times, resume, human_input, |
|
|
template_choice_value, openai_api_key, gemini_api_key, |
|
|
qwen_api_key, zhipuai_api_key, openrouter_api_key): |
|
|
"""Start generation process""" |
|
|
if pdf_file is None: |
|
|
return "β Please upload a PDF file", gr.update(visible=False), "", "", gr.update(), gr.update(), "" |
|
|
|
|
|
|
|
|
validation_errors = validate_api_keys( |
|
|
model_name_t, model_name_v, openai_api_key, gemini_api_key, |
|
|
qwen_api_key, zhipuai_api_key, openrouter_api_key |
|
|
) |
|
|
|
|
|
if validation_errors: |
|
|
error_msg = "β API Key Validation Failed:\n" + "\n".join(f"β’ {error}" for error in validation_errors) |
|
|
return error_msg, gr.update(visible=False), "", "", gr.update(), gr.update(), "" |
|
|
|
|
|
state.reset() |
|
|
|
|
|
|
|
|
if not (template_dir and str(template_dir).strip()): |
|
|
if not template_choice_value: |
|
|
stop_all_template_preview_servers() |
|
|
template_requirement = { |
|
|
"background_color": background_color, |
|
|
"has_hero_section": has_hero_section, |
|
|
"Page density": page_density, |
|
|
"image_layout": image_layout, |
|
|
"has_navigation": has_navigation, |
|
|
"title_color": title_color |
|
|
} |
|
|
try: |
|
|
matched = matching(template_requirement) |
|
|
except Exception as e: |
|
|
return f"β Template recommendation failed: {e}", gr.update(visible=False), "", "", gr.update(choices=[], value=None), gr.update(visible=False, value=""), "" |
|
|
|
|
|
html_finder_ = HtmlFinder() |
|
|
with open('templates/template_link.json','r') as f: |
|
|
template_link = json.load(f) |
|
|
previews = [] |
|
|
for name in matched: |
|
|
t_dir = os.path.join(template_root, name) |
|
|
try: |
|
|
html_path = html_finder_.find_html(t_dir) |
|
|
if not os.path.exists(html_path): |
|
|
continue |
|
|
html_dir = os.path.dirname(os.path.abspath(html_path)) |
|
|
filename = os.path.basename(html_path) |
|
|
port = start_ephemeral_server_for_dir(html_dir) |
|
|
url = template_link[name] |
|
|
previews.append((name, html_path, url)) |
|
|
except Exception: |
|
|
continue |
|
|
|
|
|
if not previews: |
|
|
return "β No previewable templates found", gr.update(visible=False), "", "", gr.update(choices=[], value=None), gr.update(visible=False, value=""), "" |
|
|
|
|
|
md_lines = ["### π Please select a template to preview before clicking **Start Generation**", ""] |
|
|
for name, _, url in previews: |
|
|
md_lines.append(f"- **{name}** β [{url}]({url})") |
|
|
md = "\n".join(md_lines) |
|
|
|
|
|
return "Recommended 3 templates, please select one to continue", gr.update(visible=False), "", "", gr.update(choices=[n for n, _, _ in previews], value=None), gr.update(visible=True, value=md), "" |
|
|
|
|
|
template_dir = os.path.join(template_root, template_choice_value) |
|
|
|
|
|
|
|
|
args = GenerationArgs( |
|
|
paper_path=pdf_file.name, |
|
|
model_name_t=model_name_t, |
|
|
model_name_v=model_name_v, |
|
|
template_root=template_root, |
|
|
template_dir=template_dir, |
|
|
template_file=template_file, |
|
|
output_dir=output_dir, |
|
|
style_preference=style_preference, |
|
|
tmp_dir=tmp_dir, |
|
|
full_content_check_times=full_content_check_times, |
|
|
background_color=background_color, |
|
|
has_navigation=has_navigation, |
|
|
has_hero_section=has_hero_section, |
|
|
title_color=title_color, |
|
|
page_density=page_density, |
|
|
image_layout=image_layout, |
|
|
html_check_times=html_check_times, |
|
|
resume=resume, |
|
|
human_input=human_input |
|
|
) |
|
|
|
|
|
if not args.template_dir: |
|
|
return "β Please select a template", gr.update(visible=False), "", "", gr.update(), gr.update(), "" |
|
|
|
|
|
if not args.template_file: |
|
|
html_finder_ = HtmlFinder() |
|
|
args.template_file = html_finder_.find_html(args.template_dir) |
|
|
|
|
|
paper_name = args.paper_path.split('/')[-1].replace('.pdf', '') if '/' in args.paper_path else args.paper_path.replace('.pdf', '') |
|
|
args.paper_name = paper_name |
|
|
|
|
|
os.makedirs(args.tmp_dir, exist_ok=True) |
|
|
|
|
|
try: |
|
|
|
|
|
agent_config_t = get_agent_config_with_keys( |
|
|
args.model_name_t, openai_api_key, gemini_api_key, |
|
|
qwen_api_key, zhipuai_api_key, openrouter_api_key |
|
|
) |
|
|
state.agent_config_t = agent_config_t |
|
|
state.args = args |
|
|
|
|
|
|
|
|
print("="*50) |
|
|
print("STEP 1: Parsing Research Paper") |
|
|
print("="*50) |
|
|
|
|
|
raw_content_path = f'project_contents/{args.paper_name}_raw_content.json' |
|
|
if not os.path.exists(raw_content_path): |
|
|
agent_config_v = get_agent_config_with_keys( |
|
|
args.model_name_v, openai_api_key, gemini_api_key, |
|
|
qwen_api_key, zhipuai_api_key, openrouter_api_key |
|
|
) |
|
|
input_token, output_token, raw_result, images, tables = parse_paper_for_project_page(args, agent_config_t) |
|
|
state.total_input_tokens_t += input_token |
|
|
state.total_output_tokens_t += output_token |
|
|
raw_content_path, _ = save_parsed_content(args, raw_result, images, tables, input_token, output_token) |
|
|
|
|
|
with open(raw_content_path, 'r') as f: |
|
|
paper_content = json.load(f) |
|
|
|
|
|
images = paper_content.get('images', []) |
|
|
tables = paper_content.get('tables', []) |
|
|
figures = {'images': images, 'tables': tables} |
|
|
paper_content = paper_content.get('markdown_content', "") |
|
|
|
|
|
state.paper_content = paper_content |
|
|
state.figures = figures |
|
|
|
|
|
|
|
|
print("="*50) |
|
|
print("STEP 2: Filtering Content") |
|
|
print("="*50) |
|
|
|
|
|
planner = ProjectPageContentPlanner(agent_config_t, args) |
|
|
state.planner = planner |
|
|
|
|
|
paper_content, figures, input_token, output_token = planner.filter_raw_content(paper_content, figures) |
|
|
state.total_input_tokens_t += input_token |
|
|
state.total_output_tokens_t += output_token |
|
|
state.paper_content = paper_content |
|
|
state.figures = figures |
|
|
|
|
|
|
|
|
print("="*50) |
|
|
print("STEP 3: Generating Sections") |
|
|
print("="*50) |
|
|
|
|
|
state.current_stage = "section" |
|
|
|
|
|
generated_section, input_token, output_token = generate_section_initial() |
|
|
state.total_input_tokens_t += input_token |
|
|
state.total_output_tokens_t += output_token |
|
|
|
|
|
|
|
|
section_display_md = format_section_to_markdown(generated_section) |
|
|
section_display_json = json.dumps(generated_section, indent=2, ensure_ascii=False) |
|
|
|
|
|
return ( |
|
|
f"β
Section generation completed, please review and provide feedback\n\nTokens: {input_token} β {output_token}", |
|
|
gr.update(visible=True), |
|
|
section_display_md, |
|
|
section_display_json, |
|
|
gr.update(), |
|
|
gr.update(visible=False, value=""), |
|
|
"" |
|
|
) |
|
|
|
|
|
except Exception as e: |
|
|
import traceback |
|
|
error_msg = f"β Generation failed: {str(e)}\n{traceback.format_exc()}" |
|
|
return error_msg, gr.update(visible=False), "", "", gr.update(), gr.update(), "" |
|
|
|
|
|
def generate_section_initial(): |
|
|
"""Generate initial Section""" |
|
|
import yaml |
|
|
from jinja2 import Environment, StrictUndefined |
|
|
from utils.wei_utils import account_token |
|
|
from utils.src.utils import get_json_from_response |
|
|
|
|
|
with open('utils/prompt_templates/page_templates/section_generation.yaml', 'r') as f: |
|
|
planner_config = yaml.safe_load(f) |
|
|
|
|
|
jinja_env = Environment(undefined=StrictUndefined) |
|
|
template = jinja_env.from_string(planner_config["template"]) |
|
|
|
|
|
jinja_args = { |
|
|
'paper_content': state.paper_content, |
|
|
'json_format_example': json.dumps(state.paper_content, indent=2) |
|
|
} |
|
|
|
|
|
prompt = template.render(**jinja_args) |
|
|
|
|
|
state.planner.planner_agent.reset() |
|
|
response = state.planner.planner_agent.step(prompt) |
|
|
input_token, output_token = account_token(response) |
|
|
generated_section = get_json_from_response(response.msgs[0].content) |
|
|
|
|
|
def create_dynamic_page_dict(sections): |
|
|
poster_dict = { |
|
|
"title": "Title of the paper", |
|
|
"authors": "Authors of the paper", |
|
|
"affiliation": "Affiliation of the authors", |
|
|
} |
|
|
poster_dict.update(sections) |
|
|
return poster_dict |
|
|
|
|
|
generated_section = create_dynamic_page_dict(generated_section) |
|
|
state.generated_section = generated_section |
|
|
|
|
|
generated_path = f'project_contents/{state.args.paper_name}_generated_section.json' |
|
|
with open(generated_path, 'w') as f: |
|
|
json.dump(generated_section, f, indent=4) |
|
|
|
|
|
return generated_section, input_token, output_token |
|
|
|
|
|
def submit_section_feedback(feedback_text): |
|
|
"""Submit Section feedback""" |
|
|
if not feedback_text or feedback_text.strip().lower() == 'yes': |
|
|
|
|
|
result = proceed_to_text_content() |
|
|
status, fc_section_visible, fc_display_visible, fc_display_md, fc_display_json, fc_feedback_visible = result |
|
|
return ( |
|
|
status, |
|
|
"", |
|
|
"", |
|
|
"", |
|
|
gr.update(visible=False), |
|
|
fc_section_visible, |
|
|
fc_display_visible, |
|
|
fc_display_md, |
|
|
fc_display_json, |
|
|
fc_feedback_visible |
|
|
) |
|
|
|
|
|
|
|
|
from camel.messages import BaseMessage |
|
|
from utils.wei_utils import account_token |
|
|
from utils.src.utils import get_json_from_response |
|
|
|
|
|
message = BaseMessage.make_assistant_message( |
|
|
role_name='User', |
|
|
content=f'human feedback: {feedback_text}\n\nPlease make modifications based on this feedback. Output format as specified above.' |
|
|
) |
|
|
response = state.planner.planner_agent.step(message) |
|
|
input_token, output_token = account_token(response) |
|
|
state.total_input_tokens_t += input_token |
|
|
state.total_output_tokens_t += output_token |
|
|
|
|
|
generated_section = get_json_from_response(response.msgs[0].content) |
|
|
state.generated_section = generated_section |
|
|
|
|
|
generated_path = f'project_contents/{state.args.paper_name}_generated_section.json' |
|
|
with open(generated_path, 'w') as f: |
|
|
json.dump(generated_section, f, indent=4) |
|
|
|
|
|
|
|
|
section_display_md = format_section_to_markdown(generated_section) |
|
|
section_display_json = json.dumps(generated_section, indent=2, ensure_ascii=False) |
|
|
|
|
|
return ( |
|
|
f"β
Section updated, please continue reviewing\n\nTokens: {input_token} β {output_token}", |
|
|
section_display_md, |
|
|
section_display_json, |
|
|
"", |
|
|
gr.update(visible=True), |
|
|
gr.update(visible=False), |
|
|
gr.update(visible=False), |
|
|
"", |
|
|
"", |
|
|
gr.update(visible=False) |
|
|
) |
|
|
|
|
|
def proceed_to_text_content(): |
|
|
"""Enter Text Content generation stage""" |
|
|
print("="*50) |
|
|
print("STEP 4: Generating Text Content") |
|
|
print("="*50) |
|
|
|
|
|
text_page_content, input_token, output_token = state.planner.text_content_generation( |
|
|
state.paper_content, state.figures, state.generated_section |
|
|
) |
|
|
state.total_input_tokens_t += input_token |
|
|
state.total_output_tokens_t += output_token |
|
|
state.text_page_content = text_page_content |
|
|
|
|
|
|
|
|
return proceed_to_full_content() |
|
|
|
|
|
def proceed_to_full_content(): |
|
|
"""Enter Full Content generation stage""" |
|
|
print("="*50) |
|
|
print("STEP 5: Generating Full Content") |
|
|
print("="*50) |
|
|
|
|
|
state.current_stage = "full_content" |
|
|
|
|
|
generated_content, input_token, output_token = generate_full_content_initial() |
|
|
state.total_input_tokens_t += input_token |
|
|
state.total_output_tokens_t += output_token |
|
|
|
|
|
|
|
|
content_display_md = format_full_content_to_markdown(generated_content, state.figures) |
|
|
content_display_json = json.dumps(generated_content, indent=2, ensure_ascii=False) |
|
|
|
|
|
return ( |
|
|
f"β
Full Content generation completed, please review and provide feedback\n\nTokens: {input_token} β {output_token}", |
|
|
gr.update(visible=True), |
|
|
gr.update(visible=True), |
|
|
content_display_md, |
|
|
content_display_json, |
|
|
gr.update(visible=True) |
|
|
) |
|
|
|
|
|
def generate_full_content_initial(): |
|
|
"""Generate initial Full Content""" |
|
|
import yaml |
|
|
from jinja2 import Environment, StrictUndefined |
|
|
from utils.wei_utils import account_token |
|
|
from utils.src.utils import get_json_from_response |
|
|
|
|
|
with open('utils/prompt_templates/page_templates/full_content_generation.yaml', 'r') as f: |
|
|
planner_config = yaml.safe_load(f) |
|
|
|
|
|
jinja_env = Environment(undefined=StrictUndefined) |
|
|
template = jinja_env.from_string(planner_config["template"]) |
|
|
|
|
|
jinja_args = { |
|
|
'paper_content': state.paper_content, |
|
|
'figures': json.dumps(state.figures, indent=2), |
|
|
'project_page_content': json.dumps(state.text_page_content, indent=2) |
|
|
} |
|
|
|
|
|
prompt = template.render(**jinja_args) |
|
|
|
|
|
state.planner.planner_agent.reset() |
|
|
response = state.planner.planner_agent.step(prompt) |
|
|
input_token, output_token = account_token(response) |
|
|
generated_content = get_json_from_response(response.msgs[0].content) |
|
|
|
|
|
state.generated_content = generated_content |
|
|
|
|
|
first_path = f'project_contents/{state.args.paper_name}_generated_full_content.v0.json' |
|
|
with open(first_path, 'w', encoding='utf-8') as f: |
|
|
json.dump(generated_content, f, ensure_ascii=False, indent=2) |
|
|
|
|
|
return generated_content, input_token, output_token |
|
|
|
|
|
def submit_full_content_feedback(feedback_text): |
|
|
"""Submit Full Content feedback""" |
|
|
if not feedback_text or feedback_text.strip().lower() == 'yes': |
|
|
|
|
|
result = proceed_to_html_generation() |
|
|
status, html_feedback_visible, preview_info, preview_url, open_btn_visible = result |
|
|
return ( |
|
|
status, |
|
|
"", |
|
|
"", |
|
|
"", |
|
|
gr.update(visible=False), |
|
|
html_feedback_visible, |
|
|
preview_info, |
|
|
preview_url, |
|
|
open_btn_visible |
|
|
) |
|
|
|
|
|
|
|
|
from camel.messages import BaseMessage |
|
|
from utils.wei_utils import account_token |
|
|
from utils.src.utils import get_json_from_response |
|
|
|
|
|
message = BaseMessage.make_assistant_message( |
|
|
role_name='User', |
|
|
content=f'human feedback: {feedback_text}\n\nPlease make modifications based on this feedback. Output format as specified above.' |
|
|
) |
|
|
response = state.planner.planner_agent.step(message) |
|
|
input_token, output_token = account_token(response) |
|
|
state.total_input_tokens_t += input_token |
|
|
state.total_output_tokens_t += output_token |
|
|
|
|
|
generated_content = get_json_from_response(response.msgs[0].content) |
|
|
state.generated_content = generated_content |
|
|
|
|
|
final_path = f'project_contents/{state.args.paper_name}_generated_full_content.json' |
|
|
with open(final_path, 'w', encoding='utf-8') as f: |
|
|
json.dump(generated_content, f, ensure_ascii=False, indent=2) |
|
|
|
|
|
|
|
|
content_display_md = format_full_content_to_markdown(generated_content, state.figures) |
|
|
content_display_json = json.dumps(generated_content, indent=2, ensure_ascii=False) |
|
|
|
|
|
return ( |
|
|
f"β
Full Content updated, please continue reviewing\n\nTokens: {input_token} β {output_token}", |
|
|
content_display_md, |
|
|
content_display_json, |
|
|
"", |
|
|
gr.update(visible=True), |
|
|
gr.update(visible=False), |
|
|
"", |
|
|
"", |
|
|
gr.update(visible=False) |
|
|
) |
|
|
|
|
|
def proceed_to_html_generation(): |
|
|
"""Enter HTML generation stage""" |
|
|
print("="*50) |
|
|
print("STEP 6: Generating HTML") |
|
|
print("="*50) |
|
|
|
|
|
state.current_stage = "html" |
|
|
|
|
|
|
|
|
static_dir = copy_static_files( |
|
|
state.args.template_file, |
|
|
state.args.template_dir, |
|
|
state.args.output_dir, |
|
|
state.args.paper_name |
|
|
) |
|
|
|
|
|
|
|
|
html_relative_path = os.path.relpath(state.args.template_file, state.args.template_dir) |
|
|
html_dir = '/'.join(html_relative_path.strip().split('/')[:-1]) |
|
|
state.html_dir = html_dir |
|
|
|
|
|
html_generator = ProjectPageHTMLGenerator(state.agent_config_t, state.args) |
|
|
state.html_generator = html_generator |
|
|
|
|
|
with open(state.args.template_file, 'r', encoding='utf-8') as file: |
|
|
html_template = file.read() |
|
|
|
|
|
|
|
|
assets_dir = html_generator.create_assets_directory(state.args, html_dir, state.args.output_dir) |
|
|
|
|
|
|
|
|
html_content, input_token, output_token = html_generator.generate_complete_html( |
|
|
state.args, state.generated_content, html_dir, html_template |
|
|
) |
|
|
state.total_input_tokens_t += input_token |
|
|
state.total_output_tokens_t += output_token |
|
|
|
|
|
|
|
|
html_dir_path = os.path.join(state.args.output_dir, state.args.paper_name, html_dir) |
|
|
os.makedirs(html_dir_path, exist_ok=True) |
|
|
|
|
|
html_file_path_no_modify = os.path.join(html_dir_path, 'index_no_modify_table.html') |
|
|
with open(html_file_path_no_modify, 'w', encoding='utf-8') as file: |
|
|
file.write(html_content) |
|
|
|
|
|
|
|
|
screenshot_path_no_modify = os.path.join(html_dir_path, 'page_final_no_modify_table.png') |
|
|
run_sync_screenshots(to_url(html_file_path_no_modify), screenshot_path_no_modify) |
|
|
|
|
|
|
|
|
html_content, input_token, output_token = html_generator.modify_html_table(html_content, html_dir) |
|
|
state.total_input_tokens_t += input_token |
|
|
state.total_output_tokens_t += output_token |
|
|
|
|
|
state.html_content = html_content |
|
|
|
|
|
|
|
|
html_file_path = os.path.join(html_dir_path, 'index.html') |
|
|
with open(html_file_path, 'w', encoding='utf-8') as file: |
|
|
file.write(html_content) |
|
|
|
|
|
state.html_file_path = html_file_path |
|
|
|
|
|
|
|
|
run_sync_screenshots( |
|
|
to_url(html_file_path), |
|
|
os.path.join(html_dir_path, 'page_final.png') |
|
|
) |
|
|
|
|
|
|
|
|
html_full_dir = os.path.dirname(os.path.abspath(html_file_path)) |
|
|
port = start_preview_server(html_full_dir) |
|
|
preview_url = f"http://localhost:{port}/index.html" |
|
|
state.preview_url = preview_url |
|
|
|
|
|
|
|
|
preview_info = f""" |
|
|
### π HTML Generation Completed |
|
|
|
|
|
**Preview URL**: {preview_url} |
|
|
|
|
|
**Instructions**: |
|
|
1. Click the **"π Open Preview in New Tab"** button below to view the generated webpage |
|
|
2. Carefully review the page in the new tab |
|
|
3. If satisfied, enter **'yes'** in the feedback box and submit |
|
|
4. If modifications are needed, provide detailed feedback and submit |
|
|
|
|
|
**Token Usage**: {input_token} β {output_token} |
|
|
""" |
|
|
|
|
|
return ( |
|
|
f"β
HTML generation completed\n\nTokens: {input_token} β {output_token}", |
|
|
gr.update(visible=True), |
|
|
preview_info, |
|
|
preview_url, |
|
|
gr.update(visible=True) |
|
|
) |
|
|
|
|
|
def submit_html_feedback(feedback_text): |
|
|
"""Submit HTML feedback""" |
|
|
if not feedback_text or feedback_text.strip().lower() == 'yes': |
|
|
|
|
|
result = finalize_generation() |
|
|
status, html_file = result |
|
|
return ( |
|
|
status, |
|
|
"", |
|
|
"", |
|
|
gr.update(visible=False), |
|
|
gr.update(visible=False), |
|
|
html_file |
|
|
) |
|
|
|
|
|
|
|
|
html_content, input_token, output_token = state.html_generator.modify_html_from_human_feedback( |
|
|
state.html_content, feedback_text |
|
|
) |
|
|
state.total_input_tokens_t += input_token |
|
|
state.total_output_tokens_t += output_token |
|
|
state.html_content = html_content |
|
|
|
|
|
|
|
|
html_dir_path = os.path.dirname(state.html_file_path) |
|
|
|
|
|
|
|
|
import time |
|
|
timestamp = int(time.time()) |
|
|
html_file_feedback = os.path.join(html_dir_path, f'index_feedback_{timestamp}.html') |
|
|
with open(html_file_feedback, 'w', encoding='utf-8') as file: |
|
|
file.write(html_content) |
|
|
|
|
|
|
|
|
with open(state.html_file_path, 'w', encoding='utf-8') as file: |
|
|
file.write(html_content) |
|
|
|
|
|
|
|
|
screenshot_path = os.path.join(html_dir_path, 'page_final.png') |
|
|
try: |
|
|
run_sync_screenshots(to_url(state.html_file_path), screenshot_path) |
|
|
except Exception as e: |
|
|
print(f"Screenshot generation failed: {e}") |
|
|
|
|
|
|
|
|
preview_info = f""" |
|
|
### π HTML Updated |
|
|
|
|
|
**Preview URL**: {state.preview_url} |
|
|
|
|
|
**Instructions**: |
|
|
1. Click the **"π Open Preview in New Tab"** button below to view the updated webpage |
|
|
2. **Refresh the browser** to see the latest version |
|
|
3. If satisfied, enter **'yes'** in the feedback box and submit |
|
|
4. If further modifications are needed, continue providing feedback |
|
|
|
|
|
**Token Usage**: {input_token} β {output_token} |
|
|
""" |
|
|
|
|
|
return ( |
|
|
f"β
HTML updated, please refresh the preview page\n\nTokens: {input_token} β {output_token}", |
|
|
preview_info, |
|
|
"", |
|
|
gr.update(visible=True), |
|
|
gr.update(visible=True), |
|
|
None |
|
|
) |
|
|
|
|
|
def finalize_generation(): |
|
|
"""Complete generation and save final results""" |
|
|
import time |
|
|
|
|
|
|
|
|
html_dir_path = os.path.dirname(state.html_file_path) |
|
|
|
|
|
|
|
|
final_html_path = os.path.join(html_dir_path, 'index_final.html') |
|
|
with open(final_html_path, 'w', encoding='utf-8') as file: |
|
|
file.write(state.html_content) |
|
|
|
|
|
|
|
|
with open(state.html_file_path, 'w', encoding='utf-8') as file: |
|
|
file.write(state.html_content) |
|
|
|
|
|
|
|
|
metadata = state.html_generator.generate_metadata(state.generated_content, state.args) |
|
|
metadata_path = state.html_generator.save_metadata(metadata, state.args, state.args.output_dir) |
|
|
|
|
|
|
|
|
readme_path = os.path.join(state.args.output_dir, state.args.paper_name, 'README.md') |
|
|
readme_content = f"""# {state.args.paper_name} - Project Page |
|
|
|
|
|
## π Project Information |
|
|
|
|
|
- **Paper Name**: {state.args.paper_name} |
|
|
- **Generation Time**: {time.strftime('%Y-%m-%d %H:%M:%S')} |
|
|
- **Text Model**: {state.args.model_name_t} |
|
|
- **Vision Model**: {state.args.model_name_v} |
|
|
|
|
|
## π Usage |
|
|
|
|
|
1. Extract this archive to any directory |
|
|
2. Open `index.html` to view the project page |
|
|
3. All resources (CSS, images, etc.) are included |
|
|
|
|
|
## π File Structure |
|
|
|
|
|
- `index.html` - Main page file |
|
|
- `index_final.html` - Final confirmed version |
|
|
- `assets/` - Image and table resources |
|
|
- `css/` or `styles/` - Style files |
|
|
- `js/` or `scripts/` - JavaScript files |
|
|
- `metadata.json` - Page metadata |
|
|
- `generation_log.json` - Generation log |
|
|
|
|
|
## π‘ Tips |
|
|
|
|
|
- Recommended browsers: Chrome, Firefox, Safari, Edge |
|
|
- For web deployment, simply upload the entire folder |
|
|
- Feel free to modify HTML and CSS for customization |
|
|
|
|
|
--- |
|
|
Generated by Paper2ProjectPage |
|
|
""" |
|
|
|
|
|
with open(readme_path, 'w', encoding='utf-8') as f: |
|
|
f.write(readme_content) |
|
|
|
|
|
|
|
|
log_data = { |
|
|
'paper_name': state.args.paper_name, |
|
|
'paper_path': state.args.paper_path, |
|
|
'models': { |
|
|
'text_model': state.args.model_name_t, |
|
|
'vision_model': state.args.model_name_v |
|
|
}, |
|
|
'token_usage': { |
|
|
'text_input_tokens': state.total_input_tokens_t, |
|
|
'text_output_tokens': state.total_output_tokens_t |
|
|
}, |
|
|
'output_files': { |
|
|
'html_file': state.html_file_path, |
|
|
'final_html_file': final_html_path, |
|
|
'metadata_file': metadata_path, |
|
|
'readme_file': readme_path |
|
|
}, |
|
|
'timestamp': time.strftime('%Y-%m-%d %H:%M:%S') |
|
|
} |
|
|
|
|
|
log_path = f"{state.args.output_dir}/{state.args.paper_name}/generation_log.json" |
|
|
with open(log_path, 'w') as f: |
|
|
json.dump(log_data, f, indent=4, ensure_ascii=False) |
|
|
|
|
|
|
|
|
project_dir = os.path.join(state.args.output_dir, state.args.paper_name) |
|
|
zip_path = create_project_zip(project_dir, state.args.output_dir, state.args.paper_name) |
|
|
|
|
|
if zip_path and os.path.exists(zip_path): |
|
|
|
|
|
zip_size = os.path.getsize(zip_path) |
|
|
zip_size_mb = zip_size / (1024 * 1024) |
|
|
zip_filename = os.path.basename(zip_path) |
|
|
|
|
|
success_msg = f""" |
|
|
β
Project page generation completed! |
|
|
|
|
|
π Output directory: {state.args.output_dir}/{state.args.paper_name} |
|
|
π HTML file: {state.html_file_path} |
|
|
π Final version: {final_html_path} |
|
|
π Metadata: {metadata_path} |
|
|
π README: {readme_path} |
|
|
π Log file: {log_path} |
|
|
π¦ Archive: {zip_filename} ({zip_size_mb:.2f} MB) |
|
|
π’ Total token usage: {state.total_input_tokens_t} β {state.total_output_tokens_t} |
|
|
|
|
|
π All feedback completed, page successfully generated! |
|
|
Click the button below to download the complete project archive (including HTML, CSS, images, README, and all resources). |
|
|
""" |
|
|
|
|
|
return ( |
|
|
success_msg, |
|
|
zip_path |
|
|
) |
|
|
|
|
|
else: |
|
|
error_msg = f""" |
|
|
β οΈ Project page generated, but archive creation failed! |
|
|
|
|
|
π Output directory: {state.args.output_dir}/{state.args.paper_name} |
|
|
π HTML file: {state.html_file_path} |
|
|
π Metadata: {metadata_path} |
|
|
|
|
|
You can manually retrieve all files from the output directory {project_dir}. |
|
|
""" |
|
|
return ( |
|
|
error_msg, |
|
|
state.html_file_path |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_css = """ |
|
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap'); |
|
|
|
|
|
* { |
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif !important; |
|
|
} |
|
|
|
|
|
code, pre, .code { |
|
|
font-family: 'JetBrains Mono', 'Courier New', Consolas, Monaco, monospace !important; |
|
|
} |
|
|
|
|
|
h1, h2, h3, h4, h5, h6 { |
|
|
font-weight: 600 !important; |
|
|
letter-spacing: -0.02em !important; |
|
|
} |
|
|
|
|
|
.markdown-text { |
|
|
line-height: 1.7 !important; |
|
|
font-size: 15px !important; |
|
|
} |
|
|
|
|
|
.gr-button { |
|
|
font-weight: 500 !important; |
|
|
letter-spacing: 0.01em !important; |
|
|
} |
|
|
|
|
|
.gr-input, .gr-textarea { |
|
|
font-size: 14px !important; |
|
|
line-height: 1.6 !important; |
|
|
} |
|
|
|
|
|
.gr-box { |
|
|
border-radius: 8px !important; |
|
|
} |
|
|
|
|
|
/* Better spacing for English content */ |
|
|
.gr-markdown p { |
|
|
margin-bottom: 0.8em !important; |
|
|
} |
|
|
|
|
|
.gr-markdown ul, .gr-markdown ol { |
|
|
margin-left: 1.2em !important; |
|
|
} |
|
|
|
|
|
.gr-markdown li { |
|
|
margin-bottom: 0.4em !important; |
|
|
} |
|
|
""" |
|
|
|
|
|
with gr.Blocks(title="Paper2ProjectPage Generator", theme=gr.themes.Soft(), css=custom_css) as demo: |
|
|
|
|
|
gr.Markdown(""" |
|
|
# π AutoPage Generator with Interactive Feedback |
|
|
|
|
|
Upload your research paper PDF and generate beautiful project pages through multi-round interactive feedback |
|
|
""") |
|
|
|
|
|
with gr.Row(): |
|
|
with gr.Column(scale=1): |
|
|
|
|
|
pdf_input = gr.File( |
|
|
label="π Upload PDF Paper", |
|
|
file_types=[".pdf"], |
|
|
type="filepath" |
|
|
) |
|
|
|
|
|
gr.Markdown("### π API Keys Configuration") |
|
|
gr.Markdown(""" |
|
|
**β οΈ Security Notice**: Your API keys are only stored in memory during the session and are never saved to disk. |
|
|
|
|
|
**π How to get API keys:** |
|
|
- **OpenAI**: Get your API key from [OpenAI Platform](https://platform.openai.com/api-keys) |
|
|
- **Gemini**: Get your API key from [Google AI Studio](https://aistudio.google.com/app/apikey) |
|
|
- **Qwen**: Get your API key from [DashScope](https://dashscope.console.aliyun.com/apiKey) |
|
|
- **ZhipuAI**: Get your API key from [ZhipuAI Console](https://open.bigmodel.cn/usercenter/apikeys) |
|
|
- **OpenRouter**: Get your API key from [OpenRouter](https://openrouter.ai/keys) |
|
|
|
|
|
**π For HuggingFace Spaces**: You can also set these as environment variables in your Space settings. |
|
|
""") |
|
|
|
|
|
with gr.Row(): |
|
|
openai_api_key = gr.Textbox( |
|
|
label="OpenAI API Key", |
|
|
value=os.getenv("OPENAI_API_KEY", ""), |
|
|
type="password", |
|
|
placeholder="sk-...", |
|
|
info="Required for GPT models" |
|
|
) |
|
|
gemini_api_key = gr.Textbox( |
|
|
label="Gemini API Key", |
|
|
value=os.getenv("GEMINI_API_KEY", ""), |
|
|
type="password", |
|
|
placeholder="AI...", |
|
|
info="Required for Gemini models" |
|
|
) |
|
|
|
|
|
with gr.Row(): |
|
|
qwen_api_key = gr.Textbox( |
|
|
label="Qwen API Key", |
|
|
value=os.getenv("QWEN_API_KEY", ""), |
|
|
type="password", |
|
|
placeholder="sk-...", |
|
|
info="Required for Qwen models" |
|
|
) |
|
|
zhipuai_api_key = gr.Textbox( |
|
|
label="ZhipuAI API Key", |
|
|
value=os.getenv("ZHIPUAI_API_KEY", ""), |
|
|
type="password", |
|
|
placeholder="...", |
|
|
info="Required for GLM models" |
|
|
) |
|
|
|
|
|
openrouter_api_key = gr.Textbox( |
|
|
label="OpenRouter API Key", |
|
|
value=os.getenv("OPENROUTER_API_KEY", ""), |
|
|
type="password", |
|
|
placeholder="sk-or-...", |
|
|
info="Required for OpenRouter models" |
|
|
) |
|
|
|
|
|
gr.Markdown("### π€ Model Configuration") |
|
|
|
|
|
|
|
|
text_model_options = [ |
|
|
("GPT-4o", "4o"), |
|
|
("GPT-4o Mini", "4o-mini"), |
|
|
("GPT-4.1", "gpt-4.1"), |
|
|
("GPT-4.1 Mini", "gpt-4.1-mini"), |
|
|
("O1", "o1"), |
|
|
("O3", "o3"), |
|
|
("O3 Mini", "o3-mini"), |
|
|
("Gemini 2.5 Pro", "gemini"), |
|
|
("Gemini 2.5 Pro (Alt)", "gemini-2.5-pro"), |
|
|
("Gemini 2.5 Flash", "gemini-2.5-flash"), |
|
|
("Qwen", "qwen"), |
|
|
("Qwen Plus", "qwen-plus"), |
|
|
("Qwen Max", "qwen-max"), |
|
|
("Qwen Long", "qwen-long"), |
|
|
("OpenRouter Qwen Plus", "openrouter_qwen-plus"), |
|
|
("OpenRouter GPT-4o Mini", "openrouter_gpt-4o-mini"), |
|
|
("OpenRouter Gemini 2.5 Flash", "openrouter_gemini-2.5-flash"), |
|
|
("OpenRouter O3", "openrouter_openai/o3"), |
|
|
("OpenRouter Claude Sonnet 4.5", "openrouter_claude-sonnet-4.5"), |
|
|
] |
|
|
|
|
|
|
|
|
vision_model_options = [ |
|
|
("GPT-4o", "4o"), |
|
|
("GPT-4o Mini", "4o-mini"), |
|
|
("Gemini 2.5 Pro", "gemini"), |
|
|
("Gemini 2.5 Pro (Alt)", "gemini-2.5-pro"), |
|
|
("Gemini 2.5 Flash", "gemini-2.5-flash"), |
|
|
("Qwen VL Max", "qwen-vl-max"), |
|
|
("Qwen 2.5 VL 72B", "qwen-2.5-vl-72b"), |
|
|
("OpenRouter Qwen VL 72B", "openrouter_qwen_vl_72b"), |
|
|
("OpenRouter Qwen VL 7B", "openrouter_qwen_vl_7b"), |
|
|
("OpenRouter Qwen VL Max", "openrouter_qwen-vl-max"), |
|
|
("OpenRouter Gemini 2.5 Flash", "openrouter_gemini-2.5-flash"), |
|
|
] |
|
|
|
|
|
with gr.Row(): |
|
|
model_name_t = gr.Dropdown( |
|
|
label="Text Model", |
|
|
choices=text_model_options, |
|
|
value="gemini", |
|
|
info="Select model for text processing" |
|
|
) |
|
|
model_name_v = gr.Dropdown( |
|
|
label="Vision Model", |
|
|
choices=vision_model_options, |
|
|
value="gemini", |
|
|
info="Select model for vision processing" |
|
|
) |
|
|
|
|
|
gr.Markdown("### π Path Configuration") |
|
|
template_root = gr.Textbox( |
|
|
label="Template Root", |
|
|
value="templates", |
|
|
info="Root directory for templates" |
|
|
) |
|
|
template_dir = gr.Textbox( |
|
|
label="Template Directory", |
|
|
value="", |
|
|
info="Selected template directory (optional)" |
|
|
) |
|
|
template_file = gr.Textbox( |
|
|
label="Template File", |
|
|
value="", |
|
|
info="Specific template file path (optional)" |
|
|
) |
|
|
template_choice = gr.Radio( |
|
|
label="Recommended Templates", |
|
|
choices=[], |
|
|
value=None, |
|
|
info="Select from recommended templates", |
|
|
visible=True |
|
|
) |
|
|
output_dir = gr.Textbox( |
|
|
label="Output Directory", |
|
|
value="generated_project_pages", |
|
|
info="Directory for output files" |
|
|
) |
|
|
style_preference = gr.Textbox( |
|
|
label="Style Preference JSON", |
|
|
value="", |
|
|
info="Style preference JSON file path (optional)" |
|
|
) |
|
|
tmp_dir = gr.Textbox( |
|
|
label="Temporary Directory", |
|
|
value="tmp", |
|
|
info="Directory for temporary files" |
|
|
) |
|
|
|
|
|
template_preview_links = gr.Markdown( |
|
|
label="Template Preview Links", |
|
|
value="", |
|
|
visible=False |
|
|
) |
|
|
|
|
|
|
|
|
resume = gr.Radio( |
|
|
label="Resume From Step", |
|
|
choices=['parse_pdf', 'generate_content','full_content_check', 'generate_html', 'html_check','modify_table','html_feedback'], |
|
|
value='parse_pdf', |
|
|
visible=False |
|
|
) |
|
|
|
|
|
human_input = gr.Radio( |
|
|
label="Enable Human Feedback", |
|
|
choices=[0, 1], |
|
|
value=1, |
|
|
visible=False |
|
|
) |
|
|
|
|
|
with gr.Column(scale=1): |
|
|
gr.Markdown("### π¨ Style Configuration") |
|
|
|
|
|
background_color = gr.Radio( |
|
|
label="Background Color", |
|
|
choices=["light", "dark"], |
|
|
value="light", |
|
|
info="Background color theme" |
|
|
) |
|
|
|
|
|
has_navigation = gr.Radio( |
|
|
label="Has Navigation", |
|
|
choices=["yes", "no"], |
|
|
value="yes", |
|
|
info="Include navigation bar" |
|
|
) |
|
|
|
|
|
has_hero_section = gr.Radio( |
|
|
label="Has Hero Section", |
|
|
choices=["yes", "no"], |
|
|
value="yes", |
|
|
info="Include hero/header section" |
|
|
) |
|
|
|
|
|
title_color = gr.Radio( |
|
|
label="Title Color", |
|
|
choices=["pure", "colorful"], |
|
|
value="pure", |
|
|
info="Title color style" |
|
|
) |
|
|
|
|
|
page_density = gr.Radio( |
|
|
label="Page Density", |
|
|
choices=["spacious", "compact"], |
|
|
value="spacious", |
|
|
info="Page spacing density" |
|
|
) |
|
|
|
|
|
image_layout = gr.Radio( |
|
|
label="Image Layout", |
|
|
choices=["rotation", "parallelism"], |
|
|
value="parallelism", |
|
|
info="Image layout style" |
|
|
) |
|
|
|
|
|
gr.Markdown("### βοΈ Advanced Options") |
|
|
|
|
|
full_content_check_times = gr.Number( |
|
|
label="Full Content Check Times", |
|
|
value=1, |
|
|
precision=0, |
|
|
info="Number of full content validation checks" |
|
|
) |
|
|
|
|
|
html_check_times = gr.Number( |
|
|
label="HTML Check Times", |
|
|
value=1, |
|
|
precision=0, |
|
|
info="Number of HTML validation checks" |
|
|
) |
|
|
|
|
|
|
|
|
start_btn = gr.Button("π Start Generation", variant="primary", size="lg") |
|
|
|
|
|
|
|
|
status_output = gr.Textbox( |
|
|
label="π Generation Status", |
|
|
lines=5, |
|
|
interactive=False |
|
|
) |
|
|
|
|
|
|
|
|
with gr.Group(visible=False) as feedback_section: |
|
|
gr.Markdown("### π Section Generation Results") |
|
|
gr.Markdown("Please review the generated section structure. If satisfied, enter **'yes'**, otherwise provide modification feedback:") |
|
|
|
|
|
with gr.Tabs(): |
|
|
with gr.Tab("π Preview (Markdown)"): |
|
|
section_display_md = gr.Markdown( |
|
|
label="Section Preview", |
|
|
value="" |
|
|
) |
|
|
with gr.Tab("π Raw Data (JSON)"): |
|
|
section_display_json = gr.Code( |
|
|
label="Section JSON", |
|
|
language="json", |
|
|
value="", |
|
|
lines=15 |
|
|
) |
|
|
|
|
|
section_feedback_input = gr.TextArea( |
|
|
label="Your Feedback", |
|
|
placeholder="Enter 'yes' to continue, or provide modification feedback...", |
|
|
lines=3 |
|
|
) |
|
|
section_submit_btn = gr.Button("Submit Feedback", variant="primary") |
|
|
|
|
|
|
|
|
with gr.Group(visible=False) as feedback_full_content: |
|
|
gr.Markdown("### π Full Content Generation Results") |
|
|
gr.Markdown("Please review the generated full content. If satisfied, enter **'yes'**, otherwise provide modification feedback:") |
|
|
|
|
|
with gr.Tabs(): |
|
|
with gr.Tab("π Preview (Markdown)"): |
|
|
full_content_display_md = gr.Markdown( |
|
|
label="Full Content Preview", |
|
|
value="" |
|
|
) |
|
|
with gr.Tab("π Raw Data (JSON)"): |
|
|
full_content_display_json = gr.Code( |
|
|
label="Full Content JSON", |
|
|
language="json", |
|
|
value="", |
|
|
lines=15 |
|
|
) |
|
|
|
|
|
full_content_feedback_input = gr.TextArea( |
|
|
label="Your Feedback", |
|
|
placeholder="Enter 'yes' to continue, or provide modification feedback...", |
|
|
lines=3 |
|
|
) |
|
|
full_content_submit_btn = gr.Button("Submit Feedback", variant="primary") |
|
|
|
|
|
|
|
|
with gr.Group(visible=False) as feedback_html: |
|
|
gr.Markdown("### π HTML Generation Results") |
|
|
|
|
|
|
|
|
preview_info_display = gr.Markdown( |
|
|
value="", |
|
|
label="Preview Information" |
|
|
) |
|
|
|
|
|
|
|
|
preview_url_state = gr.Textbox(visible=False) |
|
|
|
|
|
|
|
|
open_preview_btn = gr.Button( |
|
|
"π Open Preview in New Tab", |
|
|
variant="secondary", |
|
|
size="lg", |
|
|
visible=False |
|
|
) |
|
|
|
|
|
gr.Markdown("---") |
|
|
|
|
|
|
|
|
html_feedback_input = gr.TextArea( |
|
|
label="Your Feedback", |
|
|
placeholder="Enter 'yes' to finalize, or provide modification feedback...", |
|
|
lines=3 |
|
|
) |
|
|
html_submit_btn = gr.Button("Submit Feedback", variant="primary") |
|
|
|
|
|
|
|
|
html_file_output = gr.File( |
|
|
label="π₯ Download Project Archive", |
|
|
interactive=False |
|
|
) |
|
|
|
|
|
gr.Markdown(""" |
|
|
--- |
|
|
### π‘ User Guide |
|
|
|
|
|
1. **Upload PDF**: Select your research paper PDF file |
|
|
2. **Configure Parameters**: Adjust model, path, and style settings as needed |
|
|
3. **Start Generation**: Click the "Start Generation" button |
|
|
4. **Three-Stage Feedback**: |
|
|
- π **Section Feedback**: Review the generated page structure (Markdown preview + JSON data), provide feedback or enter 'yes' to continue |
|
|
- π **Full Content Feedback**: Review the generated complete content (Markdown preview + JSON data), provide feedback or enter 'yes' to continue |
|
|
- π **HTML Feedback**: View the generated webpage in a new tab, provide feedback or enter 'yes' to finalize |
|
|
5. **Download Results**: Download the complete project archive after completion |
|
|
|
|
|
β οΈ **Tips**: |
|
|
- Each stage supports multiple rounds of feedback until you're satisfied |
|
|
- Section and Full Content stages offer **Markdown preview** and **JSON raw data** viewing options |
|
|
- Markdown preview is more visually appealing, JSON data shows complete structure |
|
|
- HTML stage requires clicking "Open Preview in New Tab" to view the full page in browser |
|
|
- Enter 'yes' to indicate satisfaction and proceed to the next stage |
|
|
- The final ZIP download includes the complete project folder with all resources |
|
|
""") |
|
|
|
|
|
|
|
|
start_btn.click( |
|
|
fn=start_generation, |
|
|
inputs=[ |
|
|
pdf_input, model_name_t, model_name_v, template_root, |
|
|
template_dir, template_file, output_dir, style_preference, |
|
|
tmp_dir, full_content_check_times, background_color, |
|
|
has_navigation, has_hero_section, title_color, page_density, |
|
|
image_layout, html_check_times, resume, human_input, |
|
|
template_choice, openai_api_key, gemini_api_key, |
|
|
qwen_api_key, zhipuai_api_key, openrouter_api_key |
|
|
], |
|
|
outputs=[ |
|
|
status_output, |
|
|
feedback_section, |
|
|
section_display_md, |
|
|
section_display_json, |
|
|
template_choice, |
|
|
template_preview_links, |
|
|
section_feedback_input |
|
|
] |
|
|
) |
|
|
|
|
|
section_submit_btn.click( |
|
|
fn=submit_section_feedback, |
|
|
inputs=[section_feedback_input], |
|
|
outputs=[ |
|
|
status_output, |
|
|
section_display_md, |
|
|
section_display_json, |
|
|
section_feedback_input, |
|
|
feedback_section, |
|
|
feedback_full_content, |
|
|
full_content_display_md, |
|
|
full_content_display_md, |
|
|
full_content_display_json, |
|
|
full_content_feedback_input |
|
|
] |
|
|
) |
|
|
|
|
|
full_content_submit_btn.click( |
|
|
fn=submit_full_content_feedback, |
|
|
inputs=[full_content_feedback_input], |
|
|
outputs=[ |
|
|
status_output, |
|
|
full_content_display_md, |
|
|
full_content_display_json, |
|
|
full_content_feedback_input, |
|
|
feedback_full_content, |
|
|
feedback_html, |
|
|
preview_info_display, |
|
|
preview_url_state, |
|
|
open_preview_btn |
|
|
] |
|
|
) |
|
|
|
|
|
html_submit_btn.click( |
|
|
fn=submit_html_feedback, |
|
|
inputs=[html_feedback_input], |
|
|
outputs=[ |
|
|
status_output, |
|
|
preview_info_display, |
|
|
html_feedback_input, |
|
|
feedback_html, |
|
|
open_preview_btn, |
|
|
html_file_output |
|
|
] |
|
|
) |
|
|
|
|
|
|
|
|
open_preview_btn.click( |
|
|
fn=None, |
|
|
inputs=[preview_url_state], |
|
|
outputs=None, |
|
|
js="(url) => window.open(url, '_blank')" |
|
|
) |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
demo.launch( |
|
|
server_name="0.0.0.0", |
|
|
server_port=7860, |
|
|
share=False, |
|
|
show_error=True |
|
|
) |
|
|
|