Godheritage's picture
Update app.py
f381aba verified
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()