Kimodo Bot commited on
Commit
1f3e499
·
1 Parent(s): 298ec39

Simplify Space app to native Kimodo UI host

Browse files
Files changed (1) hide show
  1. app.py +15 -253
app.py CHANGED
@@ -1,13 +1,7 @@
1
- """Movimento single-character multi-text-prompt loop powered by Qwen on Fireworks."""
2
  from __future__ import annotations
3
 
4
- import json
5
  import os
6
- import re
7
- import urllib.error
8
- import urllib.request
9
- from typing import Generator
10
-
11
  import gradio as gr
12
 
13
  try:
@@ -19,262 +13,30 @@ except Exception:
19
  def _decorator(fn):
20
  return fn
21
  return _decorator
22
- spaces = _SpacesFallback()
23
-
24
- # ---------------------------------------------------------------------------
25
- # Config
26
- # ---------------------------------------------------------------------------
27
- _MODEL = "accounts/fireworks/models/qwen3p6-27b"
28
- _BASE = "https://api.fireworks.ai/inference/v1"
29
-
30
- # Colors cycle per batch: blue → red → yellow → green
31
- _BATCH_COLORS = ["#4a90d9", "#e05252", "#f0b429", "#4caf50"]
32
-
33
- _SYSTEM = """\
34
- You are a motion-description writer for a single humanoid character in a 3D animation system.
35
- Given a scene context and the character's recent motion history, output ONLY a JSON object:
36
-
37
- {"texts": ["<action phrase 1>", ...], "durations": [<seconds float>, ...]}
38
-
39
- Rules:
40
- - Return 3 to 5 short, vivid action phrases that flow naturally from each other.
41
- - Each phrase describes one distinct physical motion (e.g. "walks forward briskly", "pivots left and crouches").
42
- - Each duration is between 2.0 and 8.0 seconds.
43
- - texts and durations must have the same length.
44
- - Do NOT repeat phrases from history.
45
- - Return raw JSON only — no markdown, no explanation.
46
- """
47
-
48
- # ---------------------------------------------------------------------------
49
- # Qwen via Fireworks
50
- # ---------------------------------------------------------------------------
51
-
52
- def _call_qwen(messages: list[dict]) -> str:
53
- api_key = os.environ.get("FIREWORKS_API_KEY", "").strip()
54
- if not api_key:
55
- raise RuntimeError("FIREWORKS_API_KEY is not set")
56
- body = json.dumps({
57
- "model": _MODEL,
58
- "messages": messages,
59
- "max_tokens": 400,
60
- "temperature": 0.85,
61
- }).encode()
62
- req = urllib.request.Request(
63
- f"{_BASE}/chat/completions",
64
- data=body,
65
- headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
66
- method="POST",
67
- )
68
- try:
69
- with urllib.request.urlopen(req, timeout=40) as r:
70
- return json.loads(r.read())["choices"][0]["message"]["content"]
71
- except urllib.error.HTTPError as e:
72
- raise RuntimeError(f"Fireworks {e.code}: {e.read().decode(errors='ignore')}") from e
73
-
74
-
75
- def _parse(raw: str) -> dict:
76
- text = raw.strip()
77
- m = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", text, re.DOTALL)
78
- text = m.group(1) if m else text
79
- s, e = text.find("{"), text.rfind("}")
80
- return json.loads(text[s:e + 1])
81
 
 
82
 
83
- def _fallback_batch(offset: int) -> dict:
84
- phrases = [
85
- "walks forward at a steady pace",
86
- "turns smoothly to the left",
87
- "pauses and surveys the surroundings",
88
- "steps forward and gestures expressively",
89
- "crouches down then rises back up",
90
- "sidesteps to the right with purpose",
91
- ]
92
- n = len(phrases)
93
- return {
94
- "texts": [phrases[(offset + i) % n] for i in range(3)],
95
- "durations": [3.0, 3.5, 3.0],
96
- }
97
-
98
-
99
- @spaces.GPU(duration=120)
100
- def _generate_next_batch(scene: str, history_json: str) -> tuple[str, str]:
101
- history: list[str] = json.loads(history_json) if history_json else []
102
- user_msg = (
103
- f"Scene: {scene or 'a character moving continuously in 3D space'}\n"
104
- f"Motion history (do not repeat): {json.dumps(history[-12:])}\n\n"
105
- "Generate the next batch of motion prompts."
106
- )
107
- try:
108
- raw = _call_qwen([{"role": "system", "content": _SYSTEM}, {"role": "user", "content": user_msg}])
109
- batch = _parse(raw)
110
- if not isinstance(batch.get("texts"), list) or not isinstance(batch.get("durations"), list):
111
- raise ValueError("Missing texts or durations in response")
112
- n = min(len(batch["texts"]), len(batch["durations"]))
113
- batch["texts"] = batch["texts"][:n]
114
- batch["durations"] = batch["durations"][:n]
115
- except Exception as exc: # noqa: BLE001
116
- batch = _fallback_batch(len(history))
117
- batch["_error"] = str(exc)
118
-
119
- new_history = history + list(batch["texts"])
120
- return json.dumps(batch), json.dumps(new_history)
121
-
122
-
123
- # ---------------------------------------------------------------------------
124
- # Timeline renderer
125
- # ---------------------------------------------------------------------------
126
-
127
- def _render_timeline(segments: list[dict]) -> str:
128
- """Render a Kimodo-style horizontal colored timeline from all segments so far."""
129
- if not segments:
130
- return (
131
- "<div style='padding:20px;color:#6b7280;font-family:monospace;background:#111827;"
132
- "border-radius:10px;text-align:center;font-size:13px'>"
133
- "Click Generate — prompts will appear as a coloured timeline</div>"
134
- )
135
 
136
- total_dur = sum(s["duration"] for s in segments) or 1.0
 
 
 
137
 
138
- blocks = []
139
- for seg in segments:
140
- pct = max(4.0, (seg["duration"] / total_dur) * 100)
141
- color = seg["color"]
142
- dur_label = f"{seg['duration']:.1f}s"
143
- text = seg["text"]
144
- blocks.append(
145
- f"<div style='flex:{pct:.2f};min-width:90px;background:{color};border-radius:6px;"
146
- f"margin-right:4px;padding:7px 10px;box-sizing:border-box;overflow:hidden;"
147
- f"display:flex;flex-direction:column;justify-content:center'>"
148
- f"<span style='color:rgba(255,255,255,0.85);font-size:11px;font-weight:700;"
149
- f"font-family:monospace'>{dur_label}</span>"
150
- f"<span style='color:#fff;font-size:12px;white-space:nowrap;overflow:hidden;"
151
- f"text-overflow:ellipsis;margin-top:3px'>{text}</span>"
152
- f"</div>"
153
- )
154
 
 
 
155
  return (
156
- "<div style='background:#111827;border-radius:10px;padding:14px;overflow-x:auto'>"
157
- "<div style='font-size:11px;color:#6b7280;font-family:monospace;margin-bottom:8px;"
158
- "letter-spacing:0.08em'>PROMPTS</div>"
159
- f"<div style='display:flex;flex-direction:row;align-items:stretch;min-height:68px'>"
160
- + "".join(blocks) +
161
- "</div></div>"
162
  )
163
 
164
 
165
- # ---------------------------------------------------------------------------
166
- # Generator — loops until Gradio cancels on Stop click
167
- # ---------------------------------------------------------------------------
168
-
169
- def generate_loop(
170
- scene: str,
171
- history_json: str,
172
- segments_json: str,
173
- batch_idx_json: str,
174
- ) -> Generator:
175
- history = history_json or "[]"
176
- segments: list[dict] = json.loads(segments_json) if segments_json else []
177
- batch_idx: int = json.loads(batch_idx_json) if batch_idx_json else 0
178
-
179
- while True:
180
- color = _BATCH_COLORS[batch_idx % len(_BATCH_COLORS)]
181
-
182
- # Emit "thinking" status before the GPU call so the user sees feedback
183
- yield (
184
- _render_timeline(segments),
185
- f"⏳ Generating batch {batch_idx + 1}…",
186
- history,
187
- json.dumps(segments),
188
- json.dumps(batch_idx),
189
- )
190
-
191
- batch_json, history = _generate_next_batch(scene, history)
192
- batch = json.loads(batch_json)
193
- texts = batch.get("texts", [])
194
- durations = batch.get("durations", [])
195
- error = batch.get("_error")
196
-
197
- for t, d in zip(texts, durations):
198
- segments.append({"text": t, "duration": d, "color": color})
199
-
200
- status = (
201
- f"✓ Batch {batch_idx + 1} — {len(texts)} prompts via {_MODEL.split('/')[-1]}"
202
- if not error
203
- else f"⚠ Batch {batch_idx + 1} fallback — {error}"
204
- )
205
-
206
- batch_idx += 1
207
- yield (
208
- _render_timeline(segments),
209
- status,
210
- history,
211
- json.dumps(segments),
212
- json.dumps(batch_idx),
213
- )
214
-
215
-
216
- # ---------------------------------------------------------------------------
217
- # UI
218
- # ---------------------------------------------------------------------------
219
-
220
- _KIMODO_SRC = os.environ.get("KIMODO_UI_URL", "https://nvidia-kimodo.hf.space").strip()
221
-
222
- _empty_timeline = _render_timeline([])
223
-
224
  with gr.Blocks(title="Movimento") as demo:
225
-
226
- gr.HTML("""
227
- <div style="background:linear-gradient(135deg,#0d3b66,#1b6ca8);padding:16px 22px;
228
- border-radius:10px;margin-bottom:10px">
229
- <h2 style="color:#fff;margin:0;font-size:22px">Movimento</h2>
230
- <p style="color:#c8e6ff;margin:4px 0 0;font-size:13px">
231
- Single character &middot; <strong>Qwen3&#8209;p6&nbsp;27B</strong> on Fireworks AI &middot;
232
- Generates motion prompts continuously — click&nbsp;<strong>Stop</strong> to pause
233
- </p>
234
- </div>
235
- """)
236
-
237
- history_state = gr.State("[]")
238
- segments_state = gr.State("[]")
239
- batch_idx_state = gr.State("0")
240
-
241
- scene_box = gr.Textbox(
242
- label="Scene / character context",
243
- value="A lone figure moving through an empty plaza",
244
- lines=2,
245
- )
246
-
247
- with gr.Row():
248
- generate_btn = gr.Button("▶ Generate", variant="primary", scale=4, min_width=160)
249
- stop_btn = gr.Button("⏹ Stop", variant="stop", scale=1, min_width=100)
250
- clear_btn = gr.Button("✕ Clear", variant="secondary", scale=1, min_width=100)
251
-
252
- timeline_html = gr.HTML(_empty_timeline)
253
- status_line = gr.Textbox(label="Status", interactive=False, lines=1)
254
-
255
- gr.HTML(
256
- "<div style='border:1px solid #c0d8ea;border-radius:10px;overflow:hidden;margin-top:14px'>"
257
- f"<iframe src='{_KIMODO_SRC}' style='width:100%;border:0' height='820' "
258
- "loading='lazy' title='Kimodo 3D'></iframe></div>"
259
- )
260
-
261
- # Wiring
262
- click_event = generate_btn.click(
263
- fn=generate_loop,
264
- inputs=[scene_box, history_state, segments_state, batch_idx_state],
265
- outputs=[timeline_html, status_line, history_state, segments_state, batch_idx_state],
266
- )
267
-
268
- stop_btn.click(fn=None, cancels=[click_event])
269
-
270
- def _clear():
271
- return _render_timeline([]), "", "[]", "[]", "0"
272
-
273
- clear_btn.click(
274
- fn=_clear,
275
- inputs=[],
276
- outputs=[timeline_html, status_line, history_state, segments_state, batch_idx_state],
277
- )
278
 
279
 
280
  if __name__ == "__main__":
 
1
+ """Movimento Space: lightweight host for NVIDIA Kimodo native UI."""
2
  from __future__ import annotations
3
 
 
4
  import os
 
 
 
 
 
5
  import gradio as gr
6
 
7
  try:
 
13
  def _decorator(fn):
14
  return fn
15
  return _decorator
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
+ spaces = _SpacesFallback()
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
+ # Keep a GPU-decorated function so HF Spaces startup checks pass on zero-a10g.
21
+ @spaces.GPU(duration=60)
22
+ def _gpu_healthcheck() -> str:
23
+ return "ok"
24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
+ def _viewer_html() -> str:
27
+ src = os.environ.get("KIMODO_UI_URL", "https://nvidia-kimodo.hf.space").strip()
28
  return (
29
+ "<div style='border:1px solid #d9e7ef;border-radius:12px;overflow:hidden;'>"
30
+ f"<iframe src='{src}' title='Kimodo Native UI' style='width:100%;border:0' "
31
+ "height='900' loading='lazy'></iframe>"
32
+ "</div>"
 
 
33
  )
34
 
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  with gr.Blocks(title="Movimento") as demo:
37
+ gr.Markdown("# Movimento")
38
+ gr.Markdown("Native NVIDIA Kimodo UI is embedded below.")
39
+ gr.HTML(_viewer_html())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
 
42
  if __name__ == "__main__":