Spaces:
Build error
Build error
| import gradio as gr | |
| import os | |
| from openai import OpenAI | |
| from get_machine_from_json import string_to_bsg | |
| import json | |
| # Read system prompt | |
| with open('user_system_prompt.txt', 'r', encoding='utf-8') as f: | |
| SYSTEM_PROMPT = f.read() | |
| # Read examples (if exists) | |
| EXAMPLES = [] | |
| try: | |
| with open('examples.json', 'r', encoding='utf-8') as f: | |
| examples_data = json.load(f) | |
| EXAMPLES = [[ex["description"]] for ex in examples_data.get("examples", [])] | |
| except: | |
| pass | |
| # Model configuration - Using HF Router with DeepSeek | |
| MODEL_NAME = "deepseek-ai/DeepSeek-V3.2-Exp" | |
| HF_ROUTER_BASE_URL = "https://router.huggingface.co/v1" | |
| # Initialize OpenAI-compatible client for HF Router | |
| def create_client(): | |
| """Create OpenAI-compatible client using HF Router""" | |
| # Get token from environment variable | |
| hf_token = os.environ.get("HF_TOKEN") | |
| if not hf_token: | |
| raise ValueError( | |
| "โ ๏ธ HF_TOKEN not set!\n\n" | |
| "Please add your Hugging Face token in Space Settings:\n\n" | |
| "1. Go to your Space Settings\n" | |
| "2. Find 'Repository secrets' section\n" | |
| "3. Click 'Add a secret'\n" | |
| "4. Name: HF_TOKEN\n" | |
| "5. Value: Your token (get from https://huggingface.co/settings/tokens)\n" | |
| "6. Save and restart Space\n\n" | |
| "Token should have READ permission and start with 'hf_'" | |
| ) | |
| return OpenAI( | |
| base_url=HF_ROUTER_BASE_URL, | |
| api_key=hf_token | |
| ) | |
| def generate_machine(user_prompt, temperature=0.7, max_tokens=4096): | |
| """ | |
| Generate machine design JSON using AI, then convert to XML | |
| Args: | |
| user_prompt: User's machine description | |
| temperature: Generation temperature | |
| max_tokens: Maximum tokens | |
| Returns: | |
| tuple: (ai_response, xml_string, status_message) | |
| """ | |
| if not user_prompt.strip(): | |
| return "", "", "โ Please enter a machine description!" | |
| try: | |
| client = create_client() | |
| # Build messages | |
| messages = [ | |
| {"role": "system", "content": SYSTEM_PROMPT}, | |
| {"role": "user", "content": user_prompt} | |
| ] | |
| # Call HF Router API | |
| response = "" | |
| try: | |
| stream = client.chat.completions.create( | |
| model=MODEL_NAME, | |
| messages=messages, | |
| temperature=temperature, | |
| max_tokens=max_tokens, | |
| stream=True, | |
| ) | |
| for chunk in stream: | |
| if chunk.choices and len(chunk.choices) > 0: | |
| if chunk.choices[0].delta and chunk.choices[0].delta.content: | |
| response += chunk.choices[0].delta.content | |
| except Exception as api_error: | |
| error_msg = str(api_error) | |
| if "401" in error_msg or "Unauthorized" in error_msg or "authentication" in error_msg.lower(): | |
| return "", "", ( | |
| "โ Authentication Failed!\n\n" | |
| "Please set your HF Token in Space Settings:\n\n" | |
| "1. Get token from https://huggingface.co/settings/tokens\n" | |
| "2. Go to Space Settings โ Repository secrets\n" | |
| "3. Add secret:\n" | |
| " - Name: HF_TOKEN\n" | |
| " - Value: your token (starts with 'hf_')\n" | |
| "4. Restart Space\n\n" | |
| "๐ก Token is FREE and only needs READ permission!\n\n" | |
| f"Error details: {error_msg}" | |
| ) | |
| elif "404" in error_msg: | |
| return "", "", ( | |
| "โ Model Not Found!\n\n" | |
| "The current model is not available on Inference API.\n" | |
| "Trying backup models automatically...\n\n" | |
| f"Error details: {error_msg}" | |
| ) | |
| elif "list index out of range" in error_msg: | |
| return "", "", ( | |
| "โ API Response Error!\n\n" | |
| "The model returned an unexpected response format.\n" | |
| "This might be due to:\n" | |
| "- Model is still loading\n" | |
| "- Temporary API issue\n" | |
| "- Rate limiting\n\n" | |
| "Please try again in a moment.\n\n" | |
| f"Error details: {error_msg}" | |
| ) | |
| else: | |
| return "", "", f"โ API Error: {error_msg}" | |
| # Try to convert to XML | |
| try: | |
| xml_string = string_to_bsg(response) | |
| status = "โ Generation successful! You can now download the .bsg file." | |
| return response, xml_string, status | |
| except Exception as e: | |
| return response, "", f"โ ๏ธ AI generation completed, but XML conversion failed: {str(e)}" | |
| except ValueError as e: | |
| return "", "", str(e) | |
| except IndexError as e: | |
| return "", "", ( | |
| "โ Response parsing error!\n\n" | |
| "The API response format was unexpected.\n" | |
| "Please try again. If this persists, the model may be temporarily unavailable.\n\n" | |
| f"Error: {str(e)}" | |
| ) | |
| except Exception as e: | |
| import traceback | |
| error_details = traceback.format_exc() | |
| return "", "", f"โ Generation failed: {str(e)}\n\nDetails:\n{error_details}" | |
| def convert_json_to_xml(json_input): | |
| """ | |
| Manually convert JSON to XML | |
| Args: | |
| json_input: JSON string or JSON data | |
| Returns: | |
| tuple: (xml_string, status_message) | |
| """ | |
| if not json_input.strip(): | |
| return "", "โ Please enter JSON data!" | |
| try: | |
| xml_string = string_to_bsg(json_input) | |
| return xml_string, "โ Conversion successful!" | |
| except Exception as e: | |
| return "", f"โ Conversion failed: {str(e)}" | |
| def save_xml_to_file(xml_content): | |
| """Save XML to .bsg file""" | |
| if not xml_content: | |
| return None | |
| output_path = "generated_machine.bsg" | |
| with open(output_path, 'w', encoding='utf-8') as f: | |
| f.write(xml_content) | |
| return output_path | |
| # Custom CSS matching index.html style | |
| custom_css = """ | |
| /* Global Styles */ | |
| .gradio-container { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif !important; | |
| } | |
| /* Remove default padding/margin for better control */ | |
| .contain { | |
| max-width: 1200px !important; | |
| margin: 0 auto !important; | |
| } | |
| /* Header styles matching index.html */ | |
| .header-box { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| padding: 30px; | |
| text-align: center; | |
| border-radius: 10px; | |
| margin-bottom: 20px; | |
| } | |
| .header-box h1 { | |
| font-size: 2.5em; | |
| margin-bottom: 10px; | |
| font-weight: 600; | |
| } | |
| .header-box p { | |
| font-size: 1.1em; | |
| opacity: 0.9; | |
| } | |
| .badge { | |
| display: inline-block; | |
| background: rgba(255, 255, 255, 0.2); | |
| padding: 5px 15px; | |
| border-radius: 20px; | |
| font-size: 0.9em; | |
| margin-top: 10px; | |
| } | |
| /* Info box styles */ | |
| .info-box { | |
| background: #e7f3ff; | |
| border-left: 4px solid #2196F3; | |
| padding: 15px; | |
| margin-bottom: 20px; | |
| border-radius: 4px; | |
| } | |
| .info-box h4 { | |
| margin-bottom: 10px; | |
| color: #1976D2; | |
| font-weight: 600; | |
| } | |
| .info-box ul { | |
| margin-left: 20px; | |
| line-height: 1.8; | |
| color: #333; | |
| } | |
| /* Input styles */ | |
| .gr-textbox, .gr-text-input { | |
| border: 2px solid #e0e0e0 !important; | |
| border-radius: 8px !important; | |
| font-size: 16px !important; | |
| } | |
| .gr-textbox:focus, .gr-text-input:focus { | |
| border-color: #667eea !important; | |
| } | |
| /* Button styles matching index.html */ | |
| .gr-button { | |
| padding: 15px 30px !important; | |
| font-size: 16px !important; | |
| font-weight: 600 !important; | |
| border: none !important; | |
| border-radius: 8px !important; | |
| cursor: pointer !important; | |
| transition: all 0.3s !important; | |
| text-transform: uppercase !important; | |
| letter-spacing: 0.5px !important; | |
| min-width: 150px !important; | |
| } | |
| .gr-button-primary { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; | |
| color: white !important; | |
| } | |
| .gr-button-primary:hover { | |
| transform: translateY(-2px) !important; | |
| box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4) !important; | |
| } | |
| .gr-button-secondary { | |
| background: #6c757d !important; | |
| color: white !important; | |
| } | |
| .gr-button-secondary:hover { | |
| background: #5a6268 !important; | |
| transform: translateY(-2px) !important; | |
| } | |
| /* Tab styles */ | |
| .tabs { | |
| border-bottom: 2px solid #e0e0e0 !important; | |
| margin-bottom: 20px !important; | |
| } | |
| button[role="tab"] { | |
| padding: 12px 24px !important; | |
| font-size: 16px !important; | |
| font-weight: 500 !important; | |
| color: #666 !important; | |
| border: none !important; | |
| background: transparent !important; | |
| cursor: pointer !important; | |
| transition: all 0.3s !important; | |
| border-bottom: 3px solid transparent !important; | |
| } | |
| button[role="tab"][aria-selected="true"] { | |
| color: #667eea !important; | |
| border-bottom-color: #667eea !important; | |
| } | |
| button[role="tab"]:hover { | |
| color: #667eea !important; | |
| } | |
| /* Footer styles */ | |
| .footer-box { | |
| text-align: center; | |
| margin-top: 30px; | |
| padding: 20px; | |
| background: #f8f9fa; | |
| border-radius: 10px; | |
| } | |
| .footer-box p { | |
| color: #666; | |
| margin: 5px 0; | |
| font-size: 0.95em; | |
| } | |
| .footer-box a { | |
| color: #667eea; | |
| text-decoration: none; | |
| } | |
| .footer-box a:hover { | |
| text-decoration: underline; | |
| } | |
| /* Accordion styles */ | |
| .gr-accordion { | |
| border: 2px solid #e0e0e0 !important; | |
| border-radius: 8px !important; | |
| margin-top: 15px !important; | |
| } | |
| /* Output text area styles */ | |
| .gr-text-input textarea { | |
| font-family: 'Consolas', 'Monaco', monospace !important; | |
| font-size: 14px !important; | |
| line-height: 1.6 !important; | |
| } | |
| /* Markdown output styles */ | |
| .gr-markdown { | |
| padding: 15px !important; | |
| background: #f8f9fa !important; | |
| border-radius: 8px !important; | |
| border: 1px solid #e0e0e0 !important; | |
| } | |
| /* Label styles */ | |
| label { | |
| font-weight: 500 !important; | |
| color: #555 !important; | |
| margin-bottom: 8px !important; | |
| } | |
| /* Examples section */ | |
| .gr-examples { | |
| margin-top: 15px !important; | |
| } | |
| /* Make sure content doesn't overflow */ | |
| .gr-row, .gr-column { | |
| width: 100% !important; | |
| } | |
| /* Adjust spacing */ | |
| .gr-box { | |
| padding: 20px !important; | |
| } | |
| /* File component styles */ | |
| .gr-file { | |
| margin-top: 15px !important; | |
| padding: 15px !important; | |
| border: 2px dashed #e0e0e0 !important; | |
| border-radius: 8px !important; | |
| } | |
| /* Mobile responsive styles */ | |
| @media (max-width: 768px) { | |
| .header-box h1 { | |
| font-size: 1.8em !important; | |
| } | |
| .header-box p { | |
| font-size: 0.95em !important; | |
| } | |
| .badge { | |
| font-size: 0.85em !important; | |
| padding: 4px 12px !important; | |
| } | |
| .info-box { | |
| padding: 12px !important; | |
| font-size: 0.9em !important; | |
| } | |
| .gr-button { | |
| padding: 12px 20px !important; | |
| font-size: 14px !important; | |
| } | |
| .footer-box { | |
| padding: 15px !important; | |
| font-size: 0.9em !important; | |
| } | |
| /* Mobile link buttons */ | |
| .header-box a { | |
| padding: 8px 16px !important; | |
| font-size: 0.85em !important; | |
| } | |
| } | |
| /* Dark mode support - ensures text is always readable */ | |
| @media (prefers-color-scheme: dark) { | |
| /* Keep header gradient but ensure text contrast */ | |
| .header-box { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; | |
| } | |
| .header-box h1, | |
| .header-box p, | |
| .header-box a, | |
| .badge { | |
| color: white !important; | |
| } | |
| /* Adjust info box for dark mode */ | |
| .info-box { | |
| background: #2d3748 !important; | |
| border-left-color: #4299e1 !important; | |
| color: #e2e8f0 !important; | |
| } | |
| .info-box h4 { | |
| color: #63b3ed !important; | |
| } | |
| .info-box ul { | |
| color: #e2e8f0 !important; | |
| } | |
| .info-box a { | |
| color: #90cdf4 !important; | |
| } | |
| /* Footer dark mode */ | |
| .footer-box { | |
| background: #2d3748 !important; | |
| color: #e2e8f0 !important; | |
| } | |
| .footer-box p { | |
| color: #e2e8f0 !important; | |
| } | |
| .footer-box a { | |
| color: #90cdf4 !important; | |
| } | |
| /* Input fields dark mode */ | |
| .gr-textbox, | |
| .gr-text-input { | |
| background: #2d3748 !important; | |
| color: #e2e8f0 !important; | |
| border-color: #4a5568 !important; | |
| } | |
| /* Labels dark mode */ | |
| label { | |
| color: #e2e8f0 !important; | |
| } | |
| /* Markdown output dark mode */ | |
| .gr-markdown { | |
| background: #2d3748 !important; | |
| color: #e2e8f0 !important; | |
| border-color: #4a5568 !important; | |
| } | |
| } | |
| /* Light mode explicit styles - ensures text is always readable */ | |
| @media (prefers-color-scheme: light) { | |
| .header-box h1, | |
| .header-box p, | |
| .header-box a, | |
| .badge { | |
| color: white !important; | |
| text-shadow: 0 1px 2px rgba(0,0,0,0.1); | |
| } | |
| .info-box { | |
| background: #e7f3ff !important; | |
| color: #1a202c !important; | |
| } | |
| .info-box h4 { | |
| color: #1976D2 !important; | |
| } | |
| .info-box ul { | |
| color: #333 !important; | |
| } | |
| .info-box a { | |
| color: #667eea !important; | |
| } | |
| .footer-box { | |
| background: #f8f9fa !important; | |
| color: #333 !important; | |
| } | |
| .footer-box a { | |
| color: #667eea !important; | |
| } | |
| } | |
| /* High contrast mode for accessibility */ | |
| @media (prefers-contrast: high) { | |
| .header-box h1, | |
| .header-box p, | |
| .badge { | |
| text-shadow: 0 2px 4px rgba(0,0,0,0.3); | |
| font-weight: 600 !important; | |
| } | |
| .info-box { | |
| border-left-width: 6px !important; | |
| } | |
| .gr-button { | |
| border: 2px solid currentColor !important; | |
| } | |
| } | |
| """ | |
| # Create Gradio interface | |
| with gr.Blocks(css=custom_css, theme=gr.themes.Soft(), title="๐ฎ BesiegeField Machine Generator") as demo: | |
| # Header | |
| gr.HTML(""" | |
| <div class="header-box"> | |
| <h1 style="color: white;">๐ฎ BesiegeField Machine Generator</h1> | |
| <p style="color: white; opacity: 0.9;">Generate your Besiege machine designs with AI</p> | |
| <span class="badge" style="font-weight: bold;">โจ single-agent only</span> | |
| <div style="margin-top: 20px; padding-top: 20px; border-top: 1px solid rgba(255,255,255,0.3);"> | |
| <p style="color: white; margin: 0 0 12px 0; font-size: 1em; text-align: center;">๐ <strong>Links</strong></p> | |
| <div style="display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; margin-top: 10px;"> | |
| <a href="https://besiegefield.github.io/" target="_blank" style="color: white; text-decoration: none; padding: 10px 20px; background: rgba(139, 126, 234, 0.5); border-radius: 20px; font-size: 0.95em; transition: all 0.3s; display: inline-flex; align-items: center; gap: 6px; font-weight: 500;" onmouseover="this.style.background='rgba(139, 126, 234, 0.7)'" onmouseout="this.style.background='rgba(139, 126, 234, 0.5)'"> | |
| <span style="font-size: 1.1em;">๐</span> Project Page | |
| </a> | |
| <a href="https://github.com/Godheritage/BesiegeField" target="_blank" style="color: white; text-decoration: none; padding: 10px 20px; background: rgba(139, 126, 234, 0.5); border-radius: 20px; font-size: 0.95em; transition: all 0.3s; display: inline-flex; align-items: center; gap: 6px; font-weight: 500;" onmouseover="this.style.background='rgba(139, 126, 234, 0.7)'" onmouseout="this.style.background='rgba(139, 126, 234, 0.5)'"> | |
| <svg style="width: 16px; height: 16px; flex-shrink: 0;" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path></svg> | |
| GitHub | |
| </a> | |
| <a href="https://arxiv.org/abs/2510.14980" target="_blank" style="color: white; text-decoration: none; padding: 10px 20px; background: rgba(139, 126, 234, 0.5); border-radius: 20px; font-size: 0.95em; transition: all 0.3s; display: inline-flex; align-items: center; gap: 6px; font-weight: 500;" onmouseover="this.style.background='rgba(139, 126, 234, 0.7)'" onmouseout="this.style.background='rgba(139, 126, 234, 0.5)'"> | |
| <span style="font-size: 1.1em;">๐</span> arXiv Paper | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| """) | |
| with gr.Tabs(): | |
| # Tab 1: AI Generation | |
| with gr.Tab("AI Generation"): | |
| with gr.Row(): | |
| with gr.Column(scale=5): | |
| gr.HTML(""" | |
| <div class="info-box"> | |
| <h4>๐ก How to Use</h4> | |
| <ul> | |
| <li>Describe the machine you want to create</li> | |
| <li>Click "Generate Machine"</li> | |
| <li>Wait for AI to generate (30-120 seconds)</li> | |
| <li>Download the generated .bsg file</li> | |
| <li>Put yout .bsg file into GameRoot/Besiege_Data/SavedMachines</li> | |
| <li>Open the game, open costume scene, load machine</li> | |
| <li>For detailed guidance, please refer to the ๐ Help Tutorial video</li> | |
| <li>You can check our <a href="https://github.com/Godheritage/BesiegeField" target="_blank" style="color: #667eea; text-decoration: underline; font-weight: 500;">GitHub repo</a> for more LLMs and agentic workflows</li> | |
| </ul> | |
| </div> | |
| """) | |
| with gr.Column(scale=1): | |
| help_video = gr.Video( | |
| label="๐ Help Tutorial", | |
| value="help.mp4" if os.path.exists("help.mp4") else None, | |
| autoplay=False, | |
| visible=os.path.exists("help.mp4") | |
| ) | |
| with gr.Row(): | |
| with gr.Column(): | |
| user_input = gr.Textbox( | |
| label="Describe Your Machine *", | |
| placeholder="e.g., Create a four-wheeled vehicle with powered wheels...", | |
| lines=5 | |
| ) | |
| # Add examples | |
| if EXAMPLES: | |
| gr.Examples( | |
| examples=EXAMPLES, | |
| inputs=user_input, | |
| label="๐ก Example Prompts" | |
| ) | |
| with gr.Accordion("โ๏ธ Advanced Settings", open=False): | |
| temperature = gr.Slider( | |
| minimum=0.1, | |
| maximum=1.5, | |
| value=0.7, | |
| step=0.1, | |
| label="Temperature", | |
| info="Higher values produce more creative but less stable results" | |
| ) | |
| max_tokens = gr.Slider( | |
| minimum=1024, | |
| maximum=8192, | |
| value=4096, | |
| step=512, | |
| label="Max Tokens", | |
| info="Maximum length of generation" | |
| ) | |
| generate_btn = gr.Button("๐ Generate Machine", variant="primary", size="lg") | |
| status_output = gr.Markdown(label="Status") | |
| with gr.Row(): | |
| with gr.Column(): | |
| ai_response = gr.Textbox( | |
| label="AI Response (JSON)", | |
| lines=10, | |
| max_lines=20, | |
| show_copy_button=True | |
| ) | |
| with gr.Column(): | |
| xml_output = gr.Textbox( | |
| label="XML Output", | |
| lines=10, | |
| max_lines=20, | |
| show_copy_button=True | |
| ) | |
| download_btn = gr.File(label="๐ฅ Download .bsg File") | |
| # Bind generate button | |
| def generate_and_save(user_prompt, temp, max_tok): | |
| ai_resp, xml_str, status = generate_machine(user_prompt, temp, max_tok) | |
| file_path = save_xml_to_file(xml_str) if xml_str else None | |
| return ai_resp, xml_str, status, file_path | |
| generate_btn.click( | |
| fn=generate_and_save, | |
| inputs=[user_input, temperature, max_tokens], | |
| outputs=[ai_response, xml_output, status_output, download_btn] | |
| ) | |
| # Tab 2: Manual Conversion | |
| with gr.Tab("Manual Conversion"): | |
| gr.HTML(""" | |
| <div class="info-box"> | |
| <h4>๐ก How to Use</h4> | |
| <ul> | |
| <li>Paste your JSON machine data</li> | |
| <li>Click "Convert to XML"</li> | |
| <li>Download the generated .bsg file</li> | |
| </ul> | |
| </div> | |
| """) | |
| json_input = gr.Textbox( | |
| label="JSON Input", | |
| placeholder='Paste your JSON data here...', | |
| lines=10, | |
| max_lines=20 | |
| ) | |
| convert_btn = gr.Button("๐ Convert to XML", variant="primary", size="lg") | |
| status_manual = gr.Markdown(label="Status") | |
| xml_output_manual = gr.Textbox( | |
| label="XML Output", | |
| lines=10, | |
| max_lines=20, | |
| show_copy_button=True | |
| ) | |
| download_manual_btn = gr.File(label="๐ฅ Download .bsg File") | |
| # Bind convert button | |
| def convert_and_save(json_str): | |
| xml_str, status = convert_json_to_xml(json_str) | |
| file_path = save_xml_to_file(xml_str) if xml_str else None | |
| return xml_str, status, file_path | |
| convert_btn.click( | |
| fn=convert_and_save, | |
| inputs=[json_input], | |
| outputs=[xml_output_manual, status_manual, download_manual_btn] | |
| ) | |
| # Footer | |
| gr.HTML(f""" | |
| <div class="footer-box"> | |
| <p> | |
| ๐ค Using <a href="https://huggingface.co/{MODEL_NAME}" target="_blank">{MODEL_NAME}</a> (HF Router) | |
| </p> | |
| <p> | |
| โ ๏ธ Note: Generated machines may require adjustments in-game | |
| </p> | |
| <p style="color: #999;"> | |
| ๐ก Free to use with HF Token | ๐ Get token: <a href="https://huggingface.co/settings/tokens" target="_blank">huggingface.co/settings/tokens</a> | |
| </p> | |
| <hr style="margin: 20px 0; border: none; border-top: 1px solid #e0e0e0;"> | |
| <p style="color: #666; font-size: 0.95em; margin-bottom: 10px;"> | |
| <strong>๐ Project Links:</strong> | |
| </p> | |
| <div style="display: flex; gap: 15px; justify-content: center; flex-wrap: wrap;"> | |
| <a href="https://besiegefield.github.io/" target="_blank" style="color: #667eea; text-decoration: none; font-weight: 500;"> | |
| ๐ Project Page | |
| </a> | |
| <span style="color: #ccc;">|</span> | |
| <a href="https://github.com/Godheritage/BesiegeField" target="_blank" style="color: #667eea; text-decoration: none; font-weight: 500;"> | |
| GitHub Repository | |
| </a> | |
| <span style="color: #ccc;">|</span> | |
| <a href="https://arxiv.org/abs/2510.14980" target="_blank" style="color: #667eea; text-decoration: none; font-weight: 500;"> | |
| ๐ arXiv: 2510.14980 | |
| </a> | |
| </div> | |
| </div> | |
| """) | |
| # Launch app | |
| if __name__ == "__main__": | |
| demo.launch() | |