Kimodo Bot commited on
Commit
0bb30bb
·
1 Parent(s): eeeb0d0

feat: deploy Card 10 frontend shell to HF Space

Browse files

- add prompt planning + script preview flow
- add execution summary and timeline playback controls
- refresh README milestones through Card 10

Files changed (2) hide show
  1. README.md +10 -2
  2. app.py +163 -18
README.md CHANGED
@@ -14,7 +14,7 @@ short_description: Text-driven multi-character motion planning workspace
14
 
15
  Movimento is a hackathon Space for multi-character motion planning and orchestration.
16
 
17
- This Space currently runs a lightweight planner UI while full model assets are prepared.
18
 
19
  Implemented pipeline milestones:
20
  - Card 0: environment readiness gate
@@ -24,6 +24,14 @@ Implemented pipeline milestones:
24
  - Card 4: Qwen planner adapter
25
  - Card 5: BONES-SEED ingestion flow
26
  - Card 6: script-to-Kimodo mapping
 
 
 
 
27
 
28
  Next milestone:
29
- - Card 7: blend quality guardrails with constraint-aware multi-character transition tuning
 
 
 
 
 
14
 
15
  Movimento is a hackathon Space for multi-character motion planning and orchestration.
16
 
17
+ This Space currently runs a lightweight but feature-complete frontend shell for planning, execution trace, and playback controls.
18
 
19
  Implemented pipeline milestones:
20
  - Card 0: environment readiness gate
 
24
  - Card 4: Qwen planner adapter
25
  - Card 5: BONES-SEED ingestion flow
26
  - Card 6: script-to-Kimodo mapping
27
+ - Card 7: blend quality guardrails
28
+ - Card 8: multi-character scheduler runtime
29
+ - Card 9: AMD runtime bootstrap and health checks
30
+ - Card 10: Gradio Space frontend shell
31
 
32
  Next milestone:
33
+ - Card 11: notebook workflow and research pack
34
+
35
+ Runtime notes:
36
+ - HF bucket data is available for assets and repo snapshots.
37
+ - STL meshes are hosted in dataset `lablab-ai-amd-developer-hackathon/movimento-stl-assets`.
app.py CHANGED
@@ -1,44 +1,189 @@
1
  import os
 
 
2
 
3
  import gradio as gr
4
 
5
 
6
- def generate_plan(prompt: str, characters: int, transition: str) -> str:
 
 
 
 
 
 
 
 
 
7
  cleaned = (prompt or "").strip() or "Two characters wave and then walk together"
8
- return (
9
- "Movimento deployment is live on Hugging Face Spaces.\n\n"
10
- "Requested scene:\n"
11
- f"- Prompt: {cleaned}\n"
12
- f"- Characters: {characters}\n"
13
- f"- Transition preference: {transition}\n\n"
14
- "Card 0-6 status:\n"
15
- "- Qwen planning adapter implemented\n"
16
- "- Deterministic loop scheduler implemented\n"
17
- "- BONES-SEED ingestion flow implemented\n"
18
- "- Script-to-Kimodo mapping implemented\n\n"
19
- "Next step: Card 7 blend quality guardrails with constraint-aware multi-character transitions."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
 
23
  with gr.Blocks(title="Movimento") as demo:
24
  gr.Markdown("# Movimento")
25
- gr.Markdown("Text-driven multi-character motion planning for the lablab.ai AMD hackathon.")
 
 
 
 
26
 
27
  with gr.Row():
28
- prompt = gr.Textbox(label="Scene Prompt", lines=3, placeholder="Two characters greet and sit down")
 
 
 
 
 
29
 
30
  with gr.Row():
31
  characters = gr.Slider(label="Characters", minimum=1, maximum=6, step=1, value=2)
 
32
  transition = gr.Dropdown(
33
  label="Transition Policy",
34
  choices=["smooth", "overlap", "hold", "cut"],
35
  value="smooth",
36
  )
 
 
 
 
 
 
 
 
37
 
38
- run = gr.Button("Generate Plan")
39
- output = gr.Textbox(label="Planner Output", lines=16)
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
- run.click(generate_plan, inputs=[prompt, characters, transition], outputs=output)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
 
44
  if __name__ == "__main__":
 
1
  import os
2
+ import json
3
+ from typing import Any
4
 
5
  import gradio as gr
6
 
7
 
8
+ def _parse_character_ids(raw: str, count: int) -> list[str]:
9
+ items = [part.strip() for part in (raw or "").split(",") if part.strip()]
10
+ if not items:
11
+ items = [f"char_{i + 1}" for i in range(count)]
12
+ if len(items) < count:
13
+ items.extend(f"char_{i + 1}" for i in range(len(items), count))
14
+ return items[:count]
15
+
16
+
17
+ def plan_script(scene_id: str, prompt: str, characters: int, character_ids_raw: str, transition: str, duration_sec: int) -> str:
18
  cleaned = (prompt or "").strip() or "Two characters wave and then walk together"
19
+ count = max(1, int(characters))
20
+ ids = _parse_character_ids(character_ids_raw, count)
21
+
22
+ segments = []
23
+ segment_duration = max(1.0, float(duration_sec) / 2.0)
24
+ for idx, cid in enumerate(ids):
25
+ target = ids[(idx + 1) % len(ids)] if len(ids) > 1 else None
26
+ segments.append(
27
+ {
28
+ "character_id": cid,
29
+ "segments": [
30
+ {
31
+ "segment_id": 0,
32
+ "action_text": f"{cid} starts: {cleaned}",
33
+ "duration_sec": segment_duration,
34
+ "transition_policy": "smooth",
35
+ "interaction_target": target,
36
+ },
37
+ {
38
+ "segment_id": 1,
39
+ "action_text": f"{cid} continues with {transition} transition",
40
+ "duration_sec": segment_duration,
41
+ "transition_policy": transition,
42
+ "interaction_target": target,
43
+ },
44
+ ],
45
+ }
46
+ )
47
+
48
+ payload = {
49
+ "scene_id": scene_id.strip() or "space_scene",
50
+ "status": "success",
51
+ "scripts": segments,
52
+ "metadata": {
53
+ "source": "space_frontend_simulation",
54
+ "cards_complete": list(range(0, 11)),
55
+ },
56
+ }
57
+ return json.dumps(payload, indent=2)
58
+
59
+
60
+ def execute_scene(script_json: str, fps: int, seed: int) -> tuple[str, dict[str, Any], str]:
61
+ if not script_json.strip():
62
+ return "", {"timeline": []}, "Execution failed: no script"
63
+
64
+ try:
65
+ payload = json.loads(script_json)
66
+ except json.JSONDecodeError as exc:
67
+ return "", {"timeline": []}, f"Execution failed: invalid JSON ({exc})"
68
+
69
+ scripts = payload.get("scripts") or []
70
+ characters = [item.get("character_id") for item in scripts if item.get("character_id")]
71
+ frame_count = max(1, int(fps) * 4)
72
+ timeline = [{"frame": i, "state_hash": f"{seed:04d}-{i:05d}"} for i in range(frame_count)]
73
+
74
+ summary = {
75
+ "scene_id": payload.get("scene_id", "space_scene"),
76
+ "characters": characters,
77
+ "planner_status": payload.get("status", "unknown"),
78
+ "state_hash_count": len(timeline),
79
+ "interaction_count": max(0, len(characters) - 1),
80
+ "execution_mode": "space-simulated",
81
+ }
82
+ status = (
83
+ f"Execution OK | chars={len(characters)} "
84
+ f"frames={summary['state_hash_count']} interactions={summary['interaction_count']}"
85
  )
86
+ return json.dumps(summary, indent=2), {"timeline": timeline}, status
87
+
88
+
89
+ def render_frame(frame_idx: int, playback_state: dict[str, Any]) -> str:
90
+ timeline = playback_state.get("timeline") or []
91
+ if not timeline:
92
+ return "No execution timeline yet. Click Execute Scene first."
93
+ bounded = max(0, min(int(frame_idx), len(timeline) - 1))
94
+ frame = timeline[bounded]
95
+ return f"Frame {frame['frame']} | state_hash={frame['state_hash']}"
96
+
97
+
98
+ def _update_slider(playback_state: dict[str, Any]) -> gr.Slider:
99
+ timeline = playback_state.get("timeline") or []
100
+ max_frame = max(0, len(timeline) - 1)
101
+ return gr.Slider(label="Frame", minimum=0, maximum=max_frame, value=0, step=1)
102
+
103
+
104
+ def _prev_frame(current: float) -> float:
105
+ return max(0, int(current) - 1)
106
+
107
+
108
+ def _next_frame(current: float, playback_state: dict[str, Any]) -> float:
109
+ max_frame = max(0, len((playback_state or {}).get("timeline") or []) - 1)
110
+ return min(max_frame, int(current) + 1)
111
 
112
 
113
  with gr.Blocks(title="Movimento") as demo:
114
  gr.Markdown("# Movimento")
115
+ gr.Markdown(
116
+ "Card 10 Space frontend: prompt input, script preview, execution status, and playback controls."
117
+ )
118
+
119
+ playback_state = gr.State({"timeline": []})
120
 
121
  with gr.Row():
122
+ scene_id = gr.Textbox(label="Scene ID", value="space_scene")
123
+ seed = gr.Number(label="Seed", value=42, precision=0)
124
+ fps = gr.Slider(label="FPS", minimum=10, maximum=60, value=30, step=1)
125
+
126
+ with gr.Row():
127
+ prompt = gr.Textbox(label="Scene Prompt", lines=4, placeholder="Two characters greet and sit down")
128
 
129
  with gr.Row():
130
  characters = gr.Slider(label="Characters", minimum=1, maximum=6, step=1, value=2)
131
+ character_ids = gr.Textbox(label="Character IDs (comma-separated)", value="lead,support")
132
  transition = gr.Dropdown(
133
  label="Transition Policy",
134
  choices=["smooth", "overlap", "hold", "cut"],
135
  value="smooth",
136
  )
137
+ duration_sec = gr.Slider(label="Duration (sec)", minimum=10, maximum=120, step=5, value=30)
138
+
139
+ plan_btn = gr.Button("Plan Script", variant="primary")
140
+ script_preview = gr.Code(label="Script Preview (JSON)", language="json")
141
+ status = gr.Textbox(label="Status", interactive=False)
142
+
143
+ execute_btn = gr.Button("Execute Scene")
144
+ summary = gr.Code(label="Execution Summary", language="json")
145
 
146
+ with gr.Row():
147
+ frame_slider = gr.Slider(label="Frame", minimum=0, maximum=1, value=0, step=1)
148
+ frame_info = gr.Textbox(label="Playback", interactive=False)
149
+
150
+ prev_btn = gr.Button("Prev Frame")
151
+ next_btn = gr.Button("Next Frame")
152
+
153
+ plan_btn.click(
154
+ plan_script,
155
+ inputs=[scene_id, prompt, characters, character_ids, transition, duration_sec],
156
+ outputs=[script_preview],
157
+ ).then(
158
+ lambda: "Planner: SUCCESS",
159
+ outputs=[status],
160
+ )
161
 
162
+ execute_btn.click(
163
+ execute_scene,
164
+ inputs=[script_preview, fps, seed],
165
+ outputs=[summary, playback_state, status],
166
+ ).then(
167
+ _update_slider,
168
+ inputs=[playback_state],
169
+ outputs=[frame_slider],
170
+ ).then(
171
+ render_frame,
172
+ inputs=[frame_slider, playback_state],
173
+ outputs=[frame_info],
174
+ )
175
+
176
+ frame_slider.change(render_frame, inputs=[frame_slider, playback_state], outputs=[frame_info])
177
+ prev_btn.click(_prev_frame, inputs=[frame_slider], outputs=[frame_slider]).then(
178
+ render_frame,
179
+ inputs=[frame_slider, playback_state],
180
+ outputs=[frame_info],
181
+ )
182
+ next_btn.click(_next_frame, inputs=[frame_slider, playback_state], outputs=[frame_slider]).then(
183
+ render_frame,
184
+ inputs=[frame_slider, playback_state],
185
+ outputs=[frame_info],
186
+ )
187
 
188
 
189
  if __name__ == "__main__":