"""Card 10: Gradio Space frontend shell for prompt->script->execution flow.""" from __future__ import annotations import json import os import time from dataclasses import dataclass from pathlib import Path from typing import Any, Optional import gradio as gr from kimodo.model import DEFAULT_MODEL, load_model from kimodo.pipeline.scheduler_runtime import run_scheduled_scene from kimodo.planner import QwenPlannerAdapter from kimodo.runtime import runtime_health_report from kimodo.schemas import CharacterDefinition, CharacterGenerationState, GeneratorRequest, PlannerRequest, PlannerResponse from .gradio_theme import get_gradio_theme @dataclass class FrontendConfig: execution_mode: str default_model: str default_scene_id: str = "space_scene" class _FakeKimodoModel: """Fast fallback model for cold-start demo flow.""" def __call__(self, prompts, num_frames, **kwargs): return { "posed_joints": [[0.0]], "global_rot_mats": [[0.0]], "foot_contacts": [[0.0]], "prompts": prompts, "num_frames": num_frames, "meta": kwargs, } _MODEL_CACHE: dict[str, Any] = {} def build_kimodo_iframe_html(space_url: str, *, height_px: int = 760) -> str: """Build embeddable iframe HTML for the upstream Kimodo UI.""" url = (space_url or "").strip() or "https://nvidia-kimodo.hf.space" height = max(480, int(height_px)) return ( "
" ) def _parse_character_ids(raw: str, count: int) -> list[str]: items = [part.strip() for part in (raw or "").split(",") if part.strip()] if not items: items = [f"char_{i+1}" for i in range(count)] if len(items) < count: items.extend(f"char_{i+1}" for i in range(len(items), count)) return items[:count] def _build_planner_request(scene_id: str, prompt: str, character_ids: list[str], duration_limit_sec: float) -> PlannerRequest: return PlannerRequest( scene_id=scene_id, user_prompt=prompt, duration_limit_sec=duration_limit_sec, characters=[CharacterDefinition(character_id=item, skeleton_type="soma") for item in character_ids], ) def _planner_response_to_generator_request(response: PlannerResponse, seed: int) -> GeneratorRequest: characters: list[CharacterGenerationState] = [] for character_id, segments in response.scripts.items(): characters.append( CharacterGenerationState( character_id=character_id, skeleton_type="soma", segments=segments, ) ) return GeneratorRequest( scene_id=response.scene_id, characters=characters, seed=seed, num_samples=1, ) def _get_or_load_model(config: FrontendConfig, requested_model: str, requested_device: Optional[str]) -> Any: if config.execution_mode == "simulate": return _FakeKimodoModel() cache_key = f"{requested_model}:{requested_device or 'auto'}" if cache_key in _MODEL_CACHE: return _MODEL_CACHE[cache_key] report = runtime_health_report(requested_device) model = load_model(requested_model, device=report.selected_device) _MODEL_CACHE[cache_key] = model return model def plan_script( scene_id: str, prompt: str, character_count: int, character_ids_raw: str, duration_limit_sec: float, ) -> tuple[str, str]: start = time.time() character_ids = _parse_character_ids(character_ids_raw, int(character_count)) request = _build_planner_request(scene_id.strip() or "space_scene", prompt, character_ids, duration_limit_sec) adapter = QwenPlannerAdapter() response = adapter.plan(request) payload = json.dumps(response.model_dump(), indent=2) elapsed_ms = int((time.time() - start) * 1000) status = f"Planner: {response.status.upper()} in {elapsed_ms} ms | characters={len(response.scripts)}" return payload, status def execute_script( planned_script_json: str, seed: int, fps: int, requested_device: str, execution_mode: str, model_name: str, ) -> tuple[str, dict[str, Any], str]: if not planned_script_json.strip(): return "", {"timeline": []}, "Execution failed: script preview is empty" try: response = PlannerResponse.model_validate_json(planned_script_json) except Exception as exc: # pylint: disable=broad-except return "", {"timeline": []}, f"Execution failed: invalid planner JSON ({exc})" try: config = FrontendConfig(execution_mode=execution_mode, default_model=model_name) model = _get_or_load_model(config, model_name, requested_device) request = _planner_response_to_generator_request(response, seed=seed) result = run_scheduled_scene(model, request, fps=float(fps), seed=seed) summary = { "scene_id": response.scene_id, "characters": list(result.outputs.keys()), "errors": result.errors, "state_hash_count": len(result.state_hashes), "interaction_count": len(result.interactions), "completed_segments": result.completed_segments, } timeline = [ { "frame": index, "state_hash": state_hash, } for index, state_hash in enumerate(result.state_hashes) ] status = ( f"Execution: OK | chars={len(summary['characters'])} " f"frames={summary['state_hash_count']} interactions={summary['interaction_count']}" ) return json.dumps(summary, indent=2), {"timeline": timeline}, status except Exception as exc: # pylint: disable=broad-except return "", {"timeline": []}, f"Execution failed: {exc}" def render_frame(frame_idx: int, playback_state: dict[str, Any]) -> str: timeline = playback_state.get("timeline") or [] if not timeline: return "No execution timeline yet. Click Execute Scene first." bounded = max(0, min(int(frame_idx), len(timeline) - 1)) frame = timeline[bounded] return f"Frame {frame['frame']} | state_hash={frame['state_hash']}" def create_app() -> gr.Blocks: theme, css = get_gradio_theme(remove_gradio_footer=True) execution_mode = os.environ.get("SPACE_EXECUTION_MODE", "simulate").strip().lower() default_model = os.environ.get("DEFAULT_MODEL", DEFAULT_MODEL) kimodo_ui_url = os.environ.get("KIMODO_UI_URL", "https://nvidia-kimodo.hf.space").strip() app_css = css + """ :root { --brand-primary: #0d3b66; --brand-accent: #f95738; --brand-muted: #faf6f1; } .movimento-hero { background: linear-gradient(130deg, var(--brand-muted) 0%, #e5f4f9 100%); border: 1px solid #d9e7ef; border-radius: 14px; padding: 18px; margin-bottom: 12px; } .movimento-hero h1 { color: var(--brand-primary); margin: 0; } .movimento-hero p { margin: 6px 0 0 0; color: #264653; } """ with gr.Blocks(title="Movimento", css=app_css, theme=theme) as demo: gr.HTML( """Prompt -> Qwen script plan -> scheduled execution trace. Built for lablab.ai x AMD.