Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """ | |
| Kariana UMCP - Unified Gradio Dashboard for Unreal Engine MCP | |
| ============================================================= | |
| HuggingFace Spaces Entry Point | |
| MCP 1st Birthday Hackathon Submission | |
| Track: MCP in Action | Category: Multimodal + Productivity | |
| """ | |
| import gradio as gr | |
| import json | |
| import socket | |
| import logging | |
| from typing import Dict, List, Optional, Any | |
| from datetime import datetime | |
| from dataclasses import dataclass | |
| import os | |
| import re | |
| # Configure logging | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| # ============================================================================= | |
| # CONNECTION & DISCOVERY | |
| # ============================================================================= | |
| class UnrealInstance: | |
| """Represents a discovered Unreal Engine instance""" | |
| host: str | |
| port: int | |
| instance_id: Optional[str] = None | |
| project_name: Optional[str] = None | |
| version: Optional[str] = None | |
| authenticated: bool = False | |
| # Global state | |
| _connected_instance: Optional[UnrealInstance] = None | |
| _session_token: Optional[str] = None | |
| def parse_connection_string(conn_str: str) -> tuple: | |
| """Parse connection string like 'localhost:9877' or 'tcp://0.tcp.ngrok.io:12345'""" | |
| conn_str = conn_str.strip() | |
| # Remove protocol prefix if present | |
| if conn_str.startswith("tcp://"): | |
| conn_str = conn_str[6:] | |
| elif conn_str.startswith("http://"): | |
| conn_str = conn_str[7:] | |
| elif conn_str.startswith("https://"): | |
| conn_str = conn_str[8:] | |
| # Default values | |
| host = "localhost" | |
| port = 9877 | |
| # Parse host:port | |
| if ":" in conn_str: | |
| parts = conn_str.rsplit(":", 1) | |
| host = parts[0] | |
| try: | |
| port = int(parts[1]) | |
| except ValueError: | |
| pass | |
| elif conn_str: | |
| host = conn_str | |
| return host, port | |
| def send_socket_command(host: str, port: int, command: dict, timeout: float = 10.0) -> Optional[dict]: | |
| """Send command to KarianaUMCP socket server""" | |
| try: | |
| sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |
| sock.settimeout(timeout) | |
| sock.connect((host, port)) | |
| message = json.dumps(command) + "\n" | |
| sock.send(message.encode('utf-8')) | |
| response = sock.recv(16384).decode('utf-8') | |
| sock.close() | |
| return json.loads(response.strip()) | |
| except socket.timeout: | |
| logger.warning(f"Timeout connecting to {host}:{port}") | |
| return {"success": False, "error": f"Connection timeout to {host}:{port}"} | |
| except ConnectionRefusedError: | |
| logger.warning(f"Connection refused at {host}:{port}") | |
| return {"success": False, "error": f"Connection refused at {host}:{port}. Is Unreal running?"} | |
| except socket.gaierror as e: | |
| logger.warning(f"DNS resolution failed for {host}: {e}") | |
| return {"success": False, "error": f"Cannot resolve host: {host}"} | |
| except Exception as e: | |
| logger.error(f"Socket error: {e}") | |
| return {"success": False, "error": str(e)} | |
| def test_connection(host: str, port: int) -> tuple: | |
| """Test connection to a KarianaUMCP instance""" | |
| response = send_socket_command(host, port, {"type": "get_server_info"}, timeout=5.0) | |
| if response and (response.get("success") or response.get("server") == "KarianaUMCP"): | |
| return True, response | |
| return False, response | |
| def validate_pin(pin: str, host: str, port: int) -> tuple: | |
| """Validate PIN against the instance""" | |
| global _connected_instance, _session_token | |
| if not pin or len(pin) != 4 or not pin.isdigit(): | |
| return False, "Invalid PIN format (must be 4 digits)" | |
| response = send_socket_command(host, port, {"type": "validate_pin", "pin": pin}) | |
| if response and response.get("success"): | |
| _connected_instance = UnrealInstance( | |
| host=host, | |
| port=port, | |
| instance_id=response.get("instance_id"), | |
| project_name=response.get("project"), | |
| authenticated=True | |
| ) | |
| _session_token = response.get("session_token") | |
| return True, f"Connected to {response.get('project', 'Unreal')} at {host}:{port}" | |
| error = response.get("error", "Invalid PIN") if response else "Connection failed" | |
| return False, error | |
| def get_connection_status() -> tuple: | |
| """Get current connection status""" | |
| if _connected_instance and _connected_instance.authenticated: | |
| return True, _connected_instance | |
| return False, None | |
| def send_command(command_type: str, **params) -> Optional[dict]: | |
| """Send command to connected instance""" | |
| if not _connected_instance: | |
| return {"success": False, "error": "Not connected"} | |
| command = {"type": command_type, **params} | |
| return send_socket_command(_connected_instance.host, _connected_instance.port, command) | |
| # ============================================================================= | |
| # TAB: SETUP | |
| # ============================================================================= | |
| def create_setup_tab(): | |
| """Create Setup & Connection tab""" | |
| # Plugin version and download URLs | |
| PLUGIN_VERSION = "1.0.1" | |
| # Dropbox download links (GitHub repo is private) | |
| DOWNLOAD_URLS = { | |
| "windows": "https://www.dropbox.com/scl/fi/y4zfyfkadmik2sma5jkwp/KarianaUMCP-v1.0.1-windows.zip?rlkey=u0wk39o1av7mnflcnhfu7cij1&st=nvuj5kaj&dl=0", | |
| "macos": "https://www.dropbox.com/scl/fi/gydzrcjujtf14c743s07m/KarianaUMCP-v1.0.1-macos.zip?rlkey=239u0ltp17s665nzg08pkatzs&st=5hs5wokz&dl=0", | |
| "linux": "https://www.dropbox.com/scl/fi/za196pymwr8l0wgamdact/KarianaUMCP-v1.0.1-linux.zip?rlkey=eq37irxfif6hlahkxtdygjiqo&st=j5q2p9yh&dl=0" | |
| } | |
| def get_download_url(platform: str) -> str: | |
| return DOWNLOAD_URLS.get(platform, "#") | |
| with gr.Column(): | |
| # ===== STEP 1: PLUGIN DOWNLOAD ===== | |
| gr.Markdown("## Step 1: Install the Plugin") | |
| gr.Markdown("*Download and install the KarianaUMCP plugin into your Unreal project*") | |
| with gr.Accordion("Download Plugin", open=True): | |
| gr.Markdown(f""" | |
| ### Download for Your Platform | |
| | Platform | Download Link | | |
| |----------|---------------| | |
| | **Windows** | [KarianaUMCP-v{PLUGIN_VERSION}-windows.zip]({get_download_url('windows')}) | | |
| | **macOS** | [KarianaUMCP-v{PLUGIN_VERSION}-macos.zip]({get_download_url('macos')}) | | |
| | **Linux** | [KarianaUMCP-v{PLUGIN_VERSION}-linux.zip]({get_download_url('linux')}) | | |
| ### Installation Steps | |
| 1. **Download** the ZIP file for your platform | |
| 2. **Extract** the contents to: `YourProject/Plugins/KarianaUMCP/` | |
| 3. **Open** your Unreal project | |
| 4. **Enable** Python Script Plugin (Edit > Plugins > Python) | |
| 5. **Restart** Unreal Editor | |
| 6. **Look for the PIN** in the Output Log (Window > Output Log) | |
| The PIN will appear as a prominent banner like: | |
| ``` | |
| ============================================================ | |
| KARIANA UMCP - CONNECTION PIN: 1234 | |
| PORT: 9877 | |
| ============================================================ | |
| ``` | |
| """) | |
| gr.Markdown("---") | |
| # ===== STEP 2: CONNECTION ===== | |
| gr.Markdown("## Step 2: Connect to Unreal Engine") | |
| with gr.Accordion("Connection Options", open=True): | |
| gr.Markdown(""" | |
| ### How to Connect | |
| **Option A: Local Network (Same Computer/LAN)** | |
| - If Unreal is on the same network, use your computer's IP address | |
| - Find your IP: `ipconfig` (Windows) or `ifconfig` (Mac/Linux) | |
| - Example: `192.168.1.100:9877` | |
| **Option B: Remote Access via ngrok (Recommended)** | |
| 1. Install ngrok: https://ngrok.com/download | |
| 2. In terminal, run: `ngrok tcp 9877` | |
| 3. Copy the forwarding URL (e.g., `0.tcp.ngrok.io:12345`) | |
| 4. Paste it below | |
| """) | |
| with gr.Group(): | |
| gr.Markdown("### Enter Connection Details") | |
| with gr.Row(): | |
| connection_url = gr.Textbox( | |
| label="Unreal Engine Address", | |
| placeholder="e.g., 192.168.1.100:9877 or 0.tcp.ngrok.io:12345", | |
| info="Enter your computer's IP:port or ngrok URL", | |
| scale=3 | |
| ) | |
| test_btn = gr.Button("Test Connection", variant="secondary", scale=1) | |
| connection_test_result = gr.Markdown("") | |
| with gr.Row(): | |
| pin_input = gr.Textbox( | |
| label="Connection PIN", | |
| placeholder="Enter 4-digit PIN from Unreal", | |
| max_lines=1, | |
| scale=2 | |
| ) | |
| connect_btn = gr.Button("Connect", variant="primary", size="lg", scale=1) | |
| connection_result = gr.Markdown("") | |
| gr.Markdown("---") | |
| # ===== CONNECTION STATUS ===== | |
| gr.Markdown("### Connection Status") | |
| with gr.Row(): | |
| status_indicator = gr.Markdown("**Status:** Not connected") | |
| refresh_status_btn = gr.Button("Refresh", size="sm") | |
| server_info_display = gr.JSON(label="Server Info", value={}) | |
| # Event handlers | |
| def handle_test_connection(url): | |
| if not url or not url.strip(): | |
| return "**Please enter a connection URL**" | |
| # Detect localhost and warn user - this won't work from cloud! | |
| url_lower = url.lower().strip() | |
| if "localhost" in url_lower or "127.0.0.1" in url_lower: | |
| return """**'localhost' won't work from this dashboard!** | |
| This dashboard runs on HuggingFace servers, not your computer. | |
| 'localhost' refers to the HuggingFace server, not your Mac/PC. | |
| **You need ngrok to expose your local Unreal port:** | |
| 1. Open Terminal and run: `ngrok tcp 9877` | |
| 2. Copy the URL (e.g., `0.tcp.ngrok.io:12345`) | |
| 3. Paste that ngrok URL here instead of localhost | |
| Your Unreal PIN stays the same - just change the URL!""" | |
| host, port = parse_connection_string(url) | |
| success, response = test_connection(host, port) | |
| if success: | |
| project = response.get("project", "Unknown") | |
| version = response.get("version", "1.0.0") | |
| return f"**Connection successful!** Found KarianaUMCP server.\n\nProject: {project} | Version: {version}\n\n*Now enter your PIN and click Connect*" | |
| else: | |
| error = response.get("error", "Unknown error") if isinstance(response, dict) else str(response) | |
| return f"**Connection failed:** {error}\n\n*Make sure Unreal is running with the plugin enabled*" | |
| def handle_connect(url, pin): | |
| if not url or not url.strip(): | |
| return "**Please enter a connection URL first**" | |
| if not pin or not pin.strip(): | |
| return "**Please enter the PIN from Unreal's Output Log**" | |
| host, port = parse_connection_string(url) | |
| success, message = validate_pin(pin.strip(), host, port) | |
| if success: | |
| return f"**Connected!** {message}" | |
| return f"**Connection failed:** {message}" | |
| def handle_refresh_status(): | |
| connected, instance = get_connection_status() | |
| if connected: | |
| info = send_command("get_server_info") or {} | |
| project = instance.project_name or "Unknown" | |
| return f"**Status:** Connected to {project} ({instance.host}:{instance.port})", info | |
| return "**Status:** Not connected", {} | |
| test_btn.click(handle_test_connection, inputs=[connection_url], outputs=[connection_test_result]) | |
| connect_btn.click(handle_connect, inputs=[connection_url, pin_input], outputs=[connection_result]) | |
| refresh_status_btn.click(handle_refresh_status, outputs=[status_indicator, server_info_display]) | |
| return {"pin_input": pin_input, "connection_url": connection_url} | |
| # ============================================================================= | |
| # TAB: MONITORING | |
| # ============================================================================= | |
| def create_monitoring_tab(): | |
| """Create Monitoring tab""" | |
| with gr.Column(): | |
| gr.Markdown("## Real-Time Monitoring") | |
| with gr.Row(): | |
| status_display = gr.Markdown("**Status:** Not connected") | |
| refresh_btn = gr.Button("Refresh", size="sm") | |
| with gr.Row(): | |
| with gr.Column(): | |
| ping_btn = gr.Button("Ping", size="sm") | |
| ping_result = gr.Textbox(label="Ping Result", interactive=False) | |
| with gr.Column(): | |
| server_info = gr.JSON(label="Server Info", value={}) | |
| gr.Markdown("---") | |
| gr.Markdown("### Unreal Engine Logs") | |
| with gr.Row(): | |
| log_limit = gr.Slider(minimum=10, maximum=200, value=50, step=10, label="Max Lines") | |
| fetch_logs_btn = gr.Button("Fetch Logs", size="sm") | |
| logs_display = gr.TextArea(label="Log Output", lines=12, interactive=False) | |
| # Event handlers | |
| def handle_ping(): | |
| start = datetime.now() | |
| response = send_command("ping") | |
| elapsed = (datetime.now() - start).total_seconds() * 1000 | |
| if response and response.get("success"): | |
| return f"Pong! Response time: {elapsed:.1f}ms" | |
| error = response.get("error", "Unknown") if response else "Not connected" | |
| return f"Ping failed: {error}" | |
| def handle_refresh(): | |
| connected, instance = get_connection_status() | |
| if connected: | |
| info = send_command("get_server_info") or {} | |
| return f"**Status:** Connected to {instance.project_name} (Port {instance.port})", info | |
| return "**Status:** Not connected", {} | |
| def handle_fetch_logs(limit): | |
| response = send_command("get_ue_logs", limit=int(limit)) | |
| if response and response.get("success"): | |
| logs = response.get("logs", []) | |
| return "\n".join(logs) if logs else "No logs available" | |
| error = response.get("error", "Unknown") if response else "Not connected" | |
| return f"Failed to fetch logs: {error}" | |
| ping_btn.click(handle_ping, outputs=[ping_result]) | |
| refresh_btn.click(handle_refresh, outputs=[status_display, server_info]) | |
| fetch_logs_btn.click(handle_fetch_logs, inputs=[log_limit], outputs=[logs_display]) | |
| return {} | |
| # ============================================================================= | |
| # TAB: SCENE CONTROL | |
| # ============================================================================= | |
| def create_scene_tab(): | |
| """Create Scene Control tab""" | |
| with gr.Column(): | |
| gr.Markdown("## Scene Control") | |
| with gr.Row(): | |
| # Actor list | |
| with gr.Column(scale=1): | |
| gr.Markdown("### Actors") | |
| refresh_actors_btn = gr.Button("Refresh Actor List", size="sm") | |
| actor_list = gr.Dataframe( | |
| headers=["Name", "Type"], | |
| datatype=["str", "str"], | |
| interactive=False, | |
| row_count=(15, "dynamic") | |
| ) | |
| # Quick actions | |
| with gr.Column(scale=1): | |
| gr.Markdown("### Quick Actions") | |
| with gr.Group(): | |
| actor_type = gr.Dropdown( | |
| label="Actor Type", | |
| choices=["Cube", "Sphere", "Cylinder", "PointLight", "SpotLight", "Camera"], | |
| value="Cube" | |
| ) | |
| actor_name = gr.Textbox(label="Name", placeholder="MyActor") | |
| with gr.Row(): | |
| loc_x = gr.Number(label="X", value=0) | |
| loc_y = gr.Number(label="Y", value=0) | |
| loc_z = gr.Number(label="Z", value=0) | |
| spawn_btn = gr.Button("Spawn Actor", variant="primary") | |
| spawn_result = gr.Markdown("") | |
| gr.Markdown("---") | |
| gr.Markdown("### Screenshot") | |
| with gr.Row(): | |
| capture_btn = gr.Button("Capture Screenshot", size="sm") | |
| screenshot_result = gr.Markdown("") | |
| # Event handlers | |
| def handle_refresh_actors(): | |
| response = send_command("list_actors") | |
| if response and response.get("success"): | |
| actors = response.get("actors", []) | |
| if isinstance(actors, list) and actors and isinstance(actors[0], dict): | |
| return [[a.get("name", "?"), a.get("type", a.get("class", "?"))] for a in actors] | |
| elif isinstance(actors, list): | |
| return [[str(a), "Actor"] for a in actors] | |
| error = response.get("error", "Unknown") if response else "Not connected" | |
| return [[error, "—"]] | |
| def handle_spawn(atype, name, x, y, z): | |
| if not name: | |
| return "**Enter an actor name**" | |
| response = send_command("spawn_actor", actor_type=atype, name=name, location=[x, y, z]) | |
| if response and response.get("success"): | |
| return f"**Spawned {atype}: {name}**" | |
| error = response.get("error", "Unknown") if response else "Not connected" | |
| return f"**Failed:** {error}" | |
| def handle_capture(): | |
| response = send_command("capture_screenshot") | |
| if response and response.get("success"): | |
| return f"**Screenshot saved:** {response.get('path', 'Check Unreal project folder')}" | |
| error = response.get("error", "Unknown") if response else "Not connected" | |
| return f"**Failed to capture screenshot:** {error}" | |
| refresh_actors_btn.click(handle_refresh_actors, outputs=[actor_list]) | |
| spawn_btn.click(handle_spawn, inputs=[actor_type, actor_name, loc_x, loc_y, loc_z], outputs=[spawn_result]) | |
| capture_btn.click(handle_capture, outputs=[screenshot_result]) | |
| return {} | |
| # ============================================================================= | |
| # TAB: SKILLS & AGENTS | |
| # ============================================================================= | |
| AGENTS = [ | |
| ("Scene Analyst", "Transform natural language into scene specifications"), | |
| ("Asset Scout", "Discover and catalog project assets"), | |
| ("Pattern Strategist", "Select spatial generation patterns"), | |
| ("Executor", "Execute generation strategies via MCP"), | |
| ("World Organizer", "Automate World Outliner organization"), | |
| ("Orchestrator", "Coordinate multiple agents"), | |
| ("MCP Log Monitor", "Diagnose MCP connection issues"), | |
| ("Project Organizer", "Organize project structure"), | |
| ] | |
| def create_skills_tab(): | |
| """Create Skills & Agents tab""" | |
| with gr.Column(): | |
| gr.Markdown("## Skills & Claude Agents") | |
| with gr.Row(): | |
| # Skills | |
| with gr.Column(scale=1): | |
| gr.Markdown("### Available Skills") | |
| refresh_skills_btn = gr.Button("Refresh Skills", size="sm") | |
| skills_list = gr.Dataframe( | |
| headers=["Name", "Description"], | |
| datatype=["str", "str"], | |
| interactive=False | |
| ) | |
| gr.Markdown("### Execute Skill") | |
| skill_name = gr.Textbox(label="Skill Name") | |
| skill_params = gr.Code(label="Parameters (JSON)", language="json", value="{}") | |
| execute_skill_btn = gr.Button("Execute", variant="primary") | |
| skill_result = gr.TextArea(label="Result", lines=6, interactive=False) | |
| # Agents | |
| with gr.Column(scale=1): | |
| gr.Markdown("### Claude Agents") | |
| gr.Markdown("*Specialized AI agents for complex tasks*") | |
| agents_table = gr.Dataframe( | |
| headers=["Agent", "Purpose"], | |
| datatype=["str", "str"], | |
| value=[[a[0], a[1]] for a in AGENTS], | |
| interactive=False | |
| ) | |
| gr.Markdown(""" | |
| **How to use agents:** | |
| 1. Connect to Unreal via PIN | |
| 2. Use Claude Code or Claude Desktop | |
| 3. Ask Claude to "activate the [Agent Name] agent" | |
| 4. Describe your task | |
| """) | |
| # Event handlers | |
| def handle_refresh_skills(): | |
| response = send_command("list_skills") | |
| if response and response.get("success"): | |
| skills = response.get("skills", []) | |
| return [[s.get("name", "?"), s.get("description", "")[:50]] for s in skills] | |
| return [["Not connected", "—"]] | |
| def handle_execute_skill(name, params_json): | |
| if not name: | |
| return "Enter a skill name" | |
| try: | |
| params = json.loads(params_json) if params_json.strip() else {} | |
| except json.JSONDecodeError as e: | |
| return f"Invalid JSON: {e}" | |
| response = send_command("execute_skill", skill_name=name, params=params) | |
| if response: | |
| return json.dumps(response, indent=2) | |
| return "Failed - not connected" | |
| refresh_skills_btn.click(handle_refresh_skills, outputs=[skills_list]) | |
| execute_skill_btn.click(handle_execute_skill, inputs=[skill_name, skill_params], outputs=[skill_result]) | |
| return {} | |
| # ============================================================================= | |
| # TAB: CLAUDE DESKTOP | |
| # ============================================================================= | |
| def create_claude_desktop_tab(): | |
| """Create Claude Desktop configuration tab""" | |
| with gr.Column(): | |
| gr.Markdown("## Claude Desktop Integration") | |
| gr.Markdown("*Connect Claude Desktop to control Unreal Engine via MCP*") | |
| with gr.Accordion("How It Works", open=True): | |
| gr.Markdown(""" | |
| ### Architecture | |
| ``` | |
| Claude Desktop → umcp-mcp-server.js → ngrok tunnel → Your Unreal Engine | |
| ``` | |
| Claude Desktop uses our custom MCP server (`umcp-mcp-server.js`) which connects | |
| to your Unreal Engine via ngrok. This gives Claude full control over the scene! | |
| """) | |
| gr.Markdown("---") | |
| gr.Markdown("### Generate Configuration") | |
| with gr.Row(): | |
| ngrok_host = gr.Textbox( | |
| label="ngrok Host", | |
| placeholder="e.g., 6.tcp.eu.ngrok.io", | |
| info="The host from your ngrok tunnel URL" | |
| ) | |
| ngrok_port = gr.Textbox( | |
| label="ngrok Port", | |
| placeholder="e.g., 19404", | |
| info="The port from your ngrok tunnel URL" | |
| ) | |
| with gr.Row(): | |
| mcp_server_path = gr.Textbox( | |
| label="Path to umcp-mcp-server.js", | |
| placeholder="/path/to/Kariana-UMCP/umcp-mcp-server.js", | |
| info="Full path to the MCP server script on your machine" | |
| ) | |
| generate_btn = gr.Button("Generate Config", variant="primary") | |
| config_output = gr.Code( | |
| label="claude_desktop_config.json", | |
| language="json", | |
| lines=20 | |
| ) | |
| gr.Markdown(""" | |
| ### Installation Steps | |
| 1. **Copy the configuration** above | |
| 2. **Open your Claude Desktop config file:** | |
| - **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json` | |
| - **Windows:** `%APPDATA%\\Claude\\claude_desktop_config.json` | |
| 3. **Paste the configuration** (merge with existing if needed) | |
| 4. **Restart Claude Desktop** completely (quit and reopen) | |
| 5. **Look for the MCP icon** in Claude Desktop (bottom right) | |
| 6. **Test the connection:** Ask Claude "Can you use handshake to test the Unreal connection?" | |
| """) | |
| # Event handler | |
| def generate_config(host, port, server_path): | |
| if not host or not port: | |
| return '{"error": "Please enter ngrok host and port"}' | |
| if not server_path: | |
| server_path = "/path/to/Kariana-UMCP/umcp-mcp-server.js" | |
| config = { | |
| "mcpServers": { | |
| "umcp-unreal": { | |
| "command": "node", | |
| "args": [server_path], | |
| "env": { | |
| "UMCP_HOST": host.strip(), | |
| "UMCP_PORT": port.strip() | |
| } | |
| } | |
| } | |
| } | |
| return json.dumps(config, indent=2) | |
| generate_btn.click( | |
| generate_config, | |
| inputs=[ngrok_host, ngrok_port, mcp_server_path], | |
| outputs=[config_output] | |
| ) | |
| return {} | |
| # ============================================================================= | |
| # TAB: TEST TOOLS | |
| # ============================================================================= | |
| def create_test_tools_tab(): | |
| """Create Test Tools tab for MCP tool testing""" | |
| with gr.Column(): | |
| gr.Markdown("## MCP Tool Testing") | |
| gr.Markdown("*Test each MCP tool to verify Unreal Engine connection*") | |
| with gr.Row(): | |
| run_all_btn = gr.Button("Run All Tests", variant="primary", size="lg") | |
| test_results = gr.Dataframe( | |
| headers=["Tool", "Status", "Response Time", "Details"], | |
| datatype=["str", "str", "str", "str"], | |
| value=[ | |
| ["ping", "—", "—", "Not tested"], | |
| ["get_server_info", "—", "—", "Not tested"], | |
| ["list_functions", "—", "—", "Not tested"], | |
| ["list_actors", "—", "—", "Not tested"], | |
| ["spawn_actor", "—", "—", "Not tested"], | |
| ["delete_actor", "—", "—", "Not tested"], | |
| ], | |
| interactive=False | |
| ) | |
| gr.Markdown("---") | |
| gr.Markdown("### Individual Tool Test") | |
| with gr.Row(): | |
| with gr.Column(): | |
| tool_select = gr.Dropdown( | |
| label="Select Tool", | |
| choices=[ | |
| "ping", "get_server_info", "list_functions", | |
| "list_actors", "spawn_actor", "delete_actor", | |
| "capture_screenshot", "list_skills" | |
| ], | |
| value="ping" | |
| ) | |
| tool_params = gr.Code( | |
| label="Parameters (JSON)", | |
| language="json", | |
| value="{}", | |
| lines=5 | |
| ) | |
| test_single_btn = gr.Button("Test Tool") | |
| with gr.Column(): | |
| single_result = gr.JSON(label="Result", value={}) | |
| # Event handlers | |
| def run_all_tests(): | |
| results = [] | |
| tests = [ | |
| ("ping", {}), | |
| ("get_server_info", {}), | |
| ("list_functions", {}), | |
| ("list_actors", {}), | |
| ] | |
| for tool_name, params in tests: | |
| start = datetime.now() | |
| try: | |
| response = send_command(tool_name, **params) | |
| elapsed = (datetime.now() - start).total_seconds() * 1000 | |
| if response and (response.get("success") or response.get("server")): | |
| status = "PASS" | |
| details = str(response)[:50] + "..." if len(str(response)) > 50 else str(response) | |
| else: | |
| status = "FAIL" | |
| details = response.get("error", "Unknown error") if response else "No response" | |
| except Exception as e: | |
| elapsed = 0 | |
| status = "ERROR" | |
| details = str(e)[:50] | |
| results.append([tool_name, status, f"{elapsed:.1f}ms", details]) | |
| # Add untested items | |
| results.append(["spawn_actor", "SKIP", "—", "Manual test only"]) | |
| results.append(["delete_actor", "SKIP", "—", "Manual test only"]) | |
| return results | |
| def test_single_tool(tool_name, params_json): | |
| try: | |
| params = json.loads(params_json) if params_json.strip() else {} | |
| except json.JSONDecodeError as e: | |
| return {"error": f"Invalid JSON: {e}"} | |
| response = send_command(tool_name, **params) | |
| return response or {"error": "No response - not connected"} | |
| run_all_btn.click(run_all_tests, outputs=[test_results]) | |
| test_single_btn.click( | |
| test_single_tool, | |
| inputs=[tool_select, tool_params], | |
| outputs=[single_result] | |
| ) | |
| return {} | |
| # ============================================================================= | |
| # TAB: WORKFLOW BUILDER | |
| # ============================================================================= | |
| WORKFLOW_ACTIONS = ["spawn_actor", "delete_actor", "execute_python", "console_command", "capture_screenshot", "wait"] | |
| def create_workflow_tab(): | |
| """Create Workflow Builder tab""" | |
| with gr.Column(): | |
| gr.Markdown("## Workflow Builder") | |
| gr.Markdown("*Create reusable multi-step automation workflows*") | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| workflow_name = gr.Textbox(label="Workflow Name", placeholder="my_workflow") | |
| gr.Markdown("### Add Step") | |
| with gr.Group(): | |
| action_select = gr.Dropdown(label="Action", choices=WORKFLOW_ACTIONS, value="spawn_actor") | |
| param1 = gr.Textbox(label="Parameter 1") | |
| param2 = gr.Textbox(label="Parameter 2") | |
| param3 = gr.Textbox(label="Parameter 3") | |
| add_step_btn = gr.Button("Add Step") | |
| steps_json = gr.Code(label="Workflow Steps", language="json", value="[]", lines=10) | |
| with gr.Row(): | |
| clear_steps_btn = gr.Button("Clear All", size="sm") | |
| with gr.Column(scale=1): | |
| gr.Markdown("### Execute") | |
| execute_workflow_btn = gr.Button("Run Workflow", variant="primary", size="lg") | |
| execution_log = gr.TextArea(label="Execution Log", lines=15, interactive=False) | |
| gr.Markdown("### Templates") | |
| template_lighting = gr.Button("3-Point Lighting", size="sm") | |
| template_scatter = gr.Button("Scatter Objects", size="sm") | |
| # Event handlers | |
| def add_step(action, p1, p2, p3, current): | |
| try: | |
| steps = json.loads(current) if current else [] | |
| except (json.JSONDecodeError, ValueError): | |
| steps = [] | |
| steps.append({"action": action, "params": {"p1": p1, "p2": p2, "p3": p3}}) | |
| return json.dumps(steps, indent=2) | |
| def execute_workflow(name, steps_json): | |
| try: | |
| steps = json.loads(steps_json) if steps_json else [] | |
| except (json.JSONDecodeError, ValueError): | |
| return "Invalid workflow JSON" | |
| if not steps: | |
| return "No steps to execute" | |
| log = [f"Executing: {name or 'Unnamed'}", f"Steps: {len(steps)}", "---"] | |
| for i, step in enumerate(steps): | |
| action = step.get("action", "unknown") | |
| log.append(f"[{i+1}] {action}") | |
| params = step.get("params", {}) | |
| if action == "spawn_actor": | |
| response = send_command("spawn_actor", actor_type="Cube", name=params.get("p1", f"WF_{i}")) | |
| log.append(f" -> {'OK' if response and response.get('success') else 'FAIL'}") | |
| elif action == "wait": | |
| import time | |
| time.sleep(float(params.get("p1", 1))) | |
| log.append(" -> Waited") | |
| else: | |
| log.append(" -> Skipped (demo mode)") | |
| log.append("---") | |
| log.append("Done!") | |
| return "\n".join(log) | |
| def apply_template(template): | |
| if template == "lighting": | |
| return json.dumps([ | |
| {"action": "spawn_actor", "params": {"p1": "KeyLight", "p2": "PointLight", "p3": ""}}, | |
| {"action": "spawn_actor", "params": {"p1": "FillLight", "p2": "PointLight", "p3": ""}}, | |
| {"action": "spawn_actor", "params": {"p1": "BackLight", "p2": "PointLight", "p3": ""}}, | |
| ], indent=2) | |
| else: | |
| return json.dumps([ | |
| {"action": "spawn_actor", "params": {"p1": "Scatter_1", "p2": "Cube", "p3": ""}}, | |
| {"action": "spawn_actor", "params": {"p1": "Scatter_2", "p2": "Cube", "p3": ""}}, | |
| {"action": "spawn_actor", "params": {"p1": "Scatter_3", "p2": "Cube", "p3": ""}}, | |
| ], indent=2) | |
| add_step_btn.click(add_step, inputs=[action_select, param1, param2, param3, steps_json], outputs=[steps_json]) | |
| clear_steps_btn.click(lambda: "[]", outputs=[steps_json]) | |
| execute_workflow_btn.click(execute_workflow, inputs=[workflow_name, steps_json], outputs=[execution_log]) | |
| template_lighting.click(lambda: apply_template("lighting"), outputs=[steps_json]) | |
| template_scatter.click(lambda: apply_template("scatter"), outputs=[steps_json]) | |
| return {} | |
| # ============================================================================= | |
| # TAB: INTRO | |
| # ============================================================================= | |
| def create_intro_tab(): | |
| """Create Intro/Landing tab""" | |
| with gr.Column(): | |
| # Hero + feature card section | |
| gr.Markdown( | |
| """ | |
| <section class="kariana-container kariana-hero-wrapper"> | |
| <div class="kariana-hero-inner"> | |
| <div> | |
| <h1 class="kariana-hero-title"> | |
| AI Assistant for <span class="em">Unreal Engine</span> that powers<br/> | |
| <span class="highlight">Training</span> | |
| </h1> | |
| <p class="kariana-hero-subtitle"> | |
| Your AI copilot for Unreal Engine development in non-gaming industries—complete with scene generation, Blueprint automation, and asset optimization. | |
| </p> | |
| <div class="kariana-hero-actions"> | |
| <button class="kariana-btn-primary">Book a demo</button> | |
| <button class="kariana-btn-secondary">View capabilities</button> | |
| </div> | |
| <p class="kariana-hero-note">MCP 1st Birthday Hackathon · Track: MCP in Action</p> | |
| </div> | |
| <div class="kariana-hero-card"> | |
| <div class="kariana-hero-card-header"> | |
| <span>Share your project with KARIANA...</span> | |
| <span class="kariana-pill-success">Blueprint ready</span> | |
| </div> | |
| <p style="font-size: 0.82rem; color: #9ca3af; margin-bottom: 0.6rem;">Upload your level, materials, and scene assets—then describe the walkthrough you want to build.</p> | |
| <p style="font-size: 0.8rem; color: #d1d5db; opacity: 0.9;">This live demo connects to your Unreal Engine via the KarianaUMCP plugin, exposing 280+ MCP tools for scene control and automation.</p> | |
| </div> | |
| </div> | |
| </section> | |
| """ | |
| ) | |
| # Tealy-style 3-step explainer | |
| gr.Markdown( | |
| """ | |
| <section class="kariana-container kariana-steps-wrapper"> | |
| <div class="kariana-steps-inner"> | |
| <div class="kariana-steps-canvas"> | |
| <div class="kariana-steps-canvas-icons"> | |
| <div class="kariana-steps-icon">✶</div> | |
| <div class="kariana-steps-icon">◎</div> | |
| <div class="kariana-steps-icon">DB</div> | |
| </div> | |
| <div class="kariana-steps-unreal-card"> | |
| <div class="kariana-steps-unreal-logo">U</div> | |
| <div class="kariana-steps-unreal-text"> | |
| Unreal Engine becomes the execution engine for your AI assistants. | |
| KARIANA connects MCP tools directly into your scene. | |
| </div> | |
| </div> | |
| </div> | |
| <div class="kariana-steps-list"> | |
| <article class="kariana-step-card"> | |
| <div class="kariana-step-badge">1</div> | |
| <div> | |
| <h4>Connect Your AI Model</h4> | |
| <p>Choose your preferred AI: Claude, OpenAI (ChatGPT), or a local LLM. Online for power, offline for security—your choice, your control.</p> | |
| </div> | |
| </article> | |
| <article class="kariana-step-card"> | |
| <div class="kariana-step-badge">2</div> | |
| <div> | |
| <h4>Prompt What You Want</h4> | |
| <p>Natural language commands like "Create 50 vehicle variants with different materials" or "Generate a Blueprint for our ADAS camera system". Just describe it.</p> | |
| </div> | |
| </article> | |
| <article class="kariana-step-card"> | |
| <div class="kariana-step-badge">3</div> | |
| <div> | |
| <h4>KARIANA Executes Your Vision</h4> | |
| <p>280+ specialized tools automatically execute workflows inside Unreal Engine. Three days of manual work becomes ten minutes. Professional results, instantly.</p> | |
| </div> | |
| </article> | |
| </div> | |
| </div> | |
| </section> | |
| """ | |
| ) | |
| # Use Cases & Features Sections | |
| gr.Markdown( | |
| """ | |
| <!-- USE CASES --> | |
| <section class="kariana-container kariana-usecases-wrapper"> | |
| <div class="kariana-usecases-grid"> | |
| <div class="kariana-usecase-card"> | |
| <div class="kariana-usecase-icon icon-pink">🏢</div> | |
| <h3 class="kariana-usecase-title">Architecture & Real Estate</h3> | |
| <p class="kariana-usecase-desc">Create photorealistic walkthroughs, interactive floor plans, and client presentations in minutes.</p> | |
| <ul class="kariana-usecase-list"> | |
| <li><span class="check-pink">✓</span> Virtual property tours</li> | |
| <li><span class="check-pink">✓</span> Real-time material swaps</li> | |
| <li><span class="check-pink">✓</span> Lighting simulation</li> | |
| </ul> | |
| </div> | |
| <div class="kariana-usecase-card"> | |
| <div class="kariana-usecase-icon icon-blue">🚗</div> | |
| <h3 class="kariana-usecase-title">Automotive Design</h3> | |
| <p class="kariana-usecase-desc">Visualize vehicle designs, configure interiors, and create marketing assets with speed.</p> | |
| <ul class="kariana-usecase-list"> | |
| <li><span class="check-pink">✓</span> 360° configurators</li> | |
| <li><span class="check-pink">✓</span> Assembly animations</li> | |
| <li><span class="check-pink">✓</span> Showroom experiences</li> | |
| </ul> | |
| </div> | |
| <div class="kariana-usecase-card"> | |
| <div class="kariana-usecase-icon icon-pink">🏥</div> | |
| <h3 class="kariana-usecase-title">Medical & Healthcare</h3> | |
| <p class="kariana-usecase-desc">Build surgical simulations, anatomical visualizations, and patient education experiences.</p> | |
| <ul class="kariana-usecase-list"> | |
| <li><span class="check-pink">✓</span> Surgical training</li> | |
| <li><span class="check-pink">✓</span> Anatomical models</li> | |
| <li><span class="check-pink">✓</span> Procedure walkthroughs</li> | |
| </ul> | |
| </div> | |
| <div class="kariana-usecase-card"> | |
| <div class="kariana-usecase-icon icon-blue">🎓</div> | |
| <h3 class="kariana-usecase-title">Training & Simulation</h3> | |
| <p class="kariana-usecase-desc">Develop immersive training scenarios for industrial, military, and educational applications.</p> | |
| <ul class="kariana-usecase-list"> | |
| <li><span class="check-pink">✓</span> Safety training</li> | |
| <li><span class="check-pink">✓</span> Equipment operation</li> | |
| <li><span class="check-pink">✓</span> Scenario simulation</li> | |
| </ul> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- FEATURES GRID --> | |
| <section class="kariana-container kariana-features-wrapper"> | |
| <div class="kariana-features-grid"> | |
| <div class="kariana-feature-card feat-darkblue"> | |
| <h3>Actor Management</h3> | |
| <p>Spawn, organize, and manipulate actors at scale. Bulk operations, naming conventions, and hierarchical structures.</p> | |
| <div class="feature-img-placeholder">📦</div> | |
| </div> | |
| <div class="kariana-feature-card feat-teal"> | |
| <h3>Blueprint System</h3> | |
| <p>Generate and modify Blueprints through natural language. Event graphs, functions, variables, and interfaces.</p> | |
| <div class="feature-img-placeholder">⚡️</div> | |
| </div> | |
| <div class="kariana-feature-card feat-orange"> | |
| <h3>PCG Automation</h3> | |
| <p>Procedural Content Generation workflows. Landscapes, foliage, and parametric architecture.</p> | |
| <div class="feature-img-placeholder">🌲</div> | |
| </div> | |
| <div class="kariana-feature-card feat-yellow"> | |
| <h3>Material Pipeline</h3> | |
| <p>Create and modify master materials, material instances. PBR workflows and shader optimization.</p> | |
| <div class="feature-img-placeholder">🎨</div> | |
| </div> | |
| <div class="kariana-feature-card feat-purple"> | |
| <h3>Lighting & Rendering</h3> | |
| <p>Automated lighting setup, bake optimization, post-process configuration, and render settings management.</p> | |
| <div class="feature-img-placeholder">💡</div> | |
| </div> | |
| <div class="kariana-feature-card feat-lightteal"> | |
| <h3>Physics Configuration</h3> | |
| <p>Set up collision, physics bodies, constraints, and simulation parameters for accurate behavior.</p> | |
| <div class="feature-img-placeholder">⚛️</div> | |
| </div> | |
| <div class="kariana-feature-card feat-darkorange"> | |
| <h3>Project Organization</h3> | |
| <p>Intelligent folder structures, asset naming conventions, and project-wide refactoring tools.</p> | |
| <div class="feature-img-placeholder">📂</div> | |
| </div> | |
| <div class="kariana-feature-card feat-darkgrey"> | |
| <h3>Blender Integration</h3> | |
| <p>60+ Blender tools with seamless UE bridge. Model preparation, LOD generation, and asset automation.</p> | |
| <div class="feature-img-placeholder">🧊</div> | |
| </div> | |
| <div class="kariana-feature-card feat-lightyellow"> | |
| <h3>Compliance Support</h3> | |
| <p>Documentation generation, audit trails, and configuration management for ISO 26262 and AS9100.</p> | |
| <div class="feature-img-placeholder">📋</div> | |
| </div> | |
| </div> | |
| </section> | |
| """ | |
| ) | |
| return {} | |
| # ============================================================================= | |
| # MAIN APP | |
| # ============================================================================= | |
| def create_app(): | |
| """Create the main Gradio application""" | |
| # Kariana brand colors from kariana.base44.app | |
| kariana_theme = gr.themes.Soft( | |
| primary_hue=gr.themes.Color( | |
| c50="#e6f2f2", c100="#b3d9d9", c200="#80bfbf", | |
| c300="#4da6a6", c400="#1a8c8c", c500="#1a5a5a", | |
| c600="#155050", c700="#104545", c800="#0b3b3b", | |
| c900="#063030", c950="#031818" | |
| ), | |
| secondary_hue="slate", | |
| neutral_hue="slate" | |
| ).set( | |
| button_primary_background_fill="#1a5a5a", | |
| button_primary_background_fill_hover="#155050", | |
| block_title_text_color="#1e3a5f", | |
| body_background_fill="#f8f9fa" | |
| ) | |
| # Kariana CSS for custom components | |
| kariana_css = """ | |
| /* Kariana Brand Colors */ | |
| :root { | |
| --kariana-teal: #1a5a5a; | |
| --kariana-navy: #1e3a5f; | |
| --kariana-orange: #d4a574; | |
| --kariana-cyan: #4a9a9a; | |
| --kariana-pink: #e91e8c; | |
| } | |
| /* Hero Section */ | |
| .kariana-hero-wrapper { background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); padding: 2rem; border-radius: 12px; margin-bottom: 1.5rem; } | |
| .kariana-hero-title { color: #1e3a5f; font-size: 2rem; font-weight: 700; line-height: 1.2; } | |
| .kariana-hero-title .em { color: #1a5a5a; } | |
| .kariana-hero-title .highlight { background: linear-gradient(90deg, #d4a574, #e91e8c); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } | |
| .kariana-hero-subtitle { color: #4a5568; font-size: 1.1rem; margin: 1rem 0; } | |
| .kariana-hero-note { color: #718096; font-size: 0.85rem; margin-top: 1rem; } | |
| /* Buttons */ | |
| .kariana-btn-primary { background: #1a5a5a !important; color: white !important; padding: 0.75rem 1.5rem; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; } | |
| .kariana-btn-primary:hover { background: #155050 !important; } | |
| .kariana-btn-secondary { background: transparent !important; color: #1a5a5a !important; border: 2px solid #1a5a5a !important; padding: 0.75rem 1.5rem; border-radius: 8px; font-weight: 600; cursor: pointer; } | |
| /* Pills/Badges */ | |
| .kariana-pill-success { background: #4a9a9a; color: white; padding: 0.25rem 0.75rem; border-radius: 999px; font-size: 0.75rem; font-weight: 500; } | |
| /* Hero Card */ | |
| .kariana-hero-card { background: #1e3a5f; color: white; padding: 1.5rem; border-radius: 12px; } | |
| .kariana-hero-card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem; } | |
| /* Steps Section */ | |
| .kariana-steps-wrapper { padding: 2rem 0; } | |
| .kariana-step-card { background: white; border: 1px solid #e2e8f0; border-radius: 12px; padding: 1.25rem; margin-bottom: 1rem; } | |
| .kariana-step-badge { background: #1a5a5a; color: white; width: 2rem; height: 2rem; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 700; margin-bottom: 0.75rem; } | |
| /* Use Case Cards */ | |
| .kariana-usecases-wrapper { padding: 2rem 0; } | |
| .kariana-usecases-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; } | |
| .kariana-usecase-card { background: white; border: 1px solid #e2e8f0; border-radius: 12px; padding: 1.5rem; } | |
| .kariana-usecase-icon { font-size: 2rem; margin-bottom: 0.75rem; } | |
| .kariana-usecase-icon.icon-pink { background: linear-gradient(135deg, #fce7f3, #fbcfe8); padding: 0.5rem; border-radius: 8px; display: inline-block; } | |
| .kariana-usecase-icon.icon-blue { background: linear-gradient(135deg, #dbeafe, #bfdbfe); padding: 0.5rem; border-radius: 8px; display: inline-block; } | |
| .kariana-usecase-title { color: #1e3a5f; font-size: 1.1rem; font-weight: 600; margin-bottom: 0.5rem; } | |
| .kariana-usecase-desc { color: #4a5568; font-size: 0.9rem; margin-bottom: 0.75rem; } | |
| .kariana-usecase-list { color: #718096; font-size: 0.85rem; padding-left: 1.25rem; } | |
| /* Feature Cards - matching kariana.base44.app colors */ | |
| .kariana-features-wrapper { padding: 2rem 0; } | |
| .kariana-features-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; } | |
| .kariana-feature-card { padding: 1.25rem; border-radius: 12px; color: white; } | |
| .kariana-feature-card h4 { font-size: 1rem; font-weight: 600; margin-bottom: 0.5rem; } | |
| .kariana-feature-card p { font-size: 0.85rem; opacity: 0.9; } | |
| /* Feature card colors from kariana.base44.app */ | |
| .feat-darkblue, .feat-teal { background: #1a5a5a; } | |
| .feat-orange { background: #d4a574; color: #1e3a5f; } | |
| .feat-yellow, .feat-lightyellow { background: #d4a574; color: #1e3a5f; } | |
| .feat-purple { background: #1e3a5f; } | |
| .feat-lightteal { background: #4a9a9a; } | |
| .feat-darkorange { background: #c49464; color: #1e3a5f; } | |
| .feat-darkgrey { background: #4a5568; } | |
| /* Unreal Card in Steps */ | |
| .kariana-steps-unreal-card { background: #1e3a5f; color: white; padding: 1rem; border-radius: 8px; display: inline-flex; align-items: center; gap: 0.75rem; } | |
| .kariana-steps-unreal-logo { background: white; color: #1e3a5f; width: 2rem; height: 2rem; border-radius: 4px; display: flex; align-items: center; justify-content: center; font-weight: 700; } | |
| /* Container */ | |
| .kariana-container { max-width: 1200px; margin: 0 auto; padding: 0 1rem; } | |
| """ | |
| with gr.Blocks(title="Kariana UMCP", theme=kariana_theme, css=kariana_css) as demo: | |
| gr.Markdown(""" | |
| # Kariana UMCP | |
| **AI-Powered Virtual Production Control for Unreal Engine** | |
| *MCP 1st Birthday Hackathon | Track: MCP in Action* | |
| [🔗 Learn more about this project →](https://kariana.base44.app) | |
| """) | |
| with gr.Row(): | |
| connection_badge = gr.Markdown("**Not Connected** - Go to Setup tab to connect") | |
| with gr.Tabs(): | |
| with gr.Tab("Intro"): | |
| create_intro_tab() | |
| with gr.Tab("Setup"): | |
| create_setup_tab() | |
| with gr.Tab("Monitor"): | |
| create_monitoring_tab() | |
| with gr.Tab("Scene"): | |
| create_scene_tab() | |
| with gr.Tab("Skills"): | |
| create_skills_tab() | |
| with gr.Tab("Workflow"): | |
| create_workflow_tab() | |
| with gr.Tab("Claude Desktop"): | |
| create_claude_desktop_tab() | |
| with gr.Tab("Test Tools"): | |
| create_test_tools_tab() | |
| gr.Markdown(""" | |
| --- | |
| **Links:** [GitHub](https://github.com/kerinzeebart/MCPTest-with-UMCP.it) | | |
| [MCP Hackathon](https://huggingface.co/MCP-1st-Birthday) | |
| *Built with Gradio, FastMCP, and Unreal Engine 5.6* | |
| """) | |
| return demo | |
| # Launch | |
| demo = create_app() | |
| if __name__ == "__main__": | |
| demo.launch() | |