Spaces:
Running on Zero
Running on Zero
Deploy Karate Wiener (kimodo kata maker)
Browse files- app.py +45 -1
- tabs/README.md +68 -0
- tabs/templates/example.drawer.html +8 -0
- tabs/templates/example.tab.css +11 -0
- tabs/templates/example.tab.html +5 -0
- tabs/templates/example.tab.js +37 -0
app.py
CHANGED
|
@@ -127,6 +127,30 @@ CLOTHING_CATALOG = [
|
|
| 127 |
DEFAULT_CLOTHING = {"head": "beanie_green", "face": "nerdy_glasses",
|
| 128 |
"torso_over": "kimono", "legs": "kimono_trousers"}
|
| 129 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
APP_CSS = """
|
| 131 |
#main-preview {
|
| 132 |
min-width: 0;
|
|
@@ -2252,6 +2276,8 @@ def _drawers_html() -> str:
|
|
| 2252 |
'data-drawer="kimodo-compose-drawer" title="Storyboard composer: sequence stances into a kata">COMPOSE</button>',
|
| 2253 |
'<button id="kimodo-clothing-toggle" class="kimodo-left-tab" type="button" '
|
| 2254 |
'data-drawer="kimodo-clothing-drawer" title="Add clothing to the citizen">CLOTHING</button>',
|
|
|
|
|
|
|
| 2255 |
'</div>',
|
| 2256 |
# Actions library (clip cards populated in a later phase).
|
| 2257 |
'<aside id="kimodo-actions-drawer" class="kimodo-drawer" aria-label="Actions library">',
|
|
@@ -2311,6 +2337,8 @@ def _drawers_html() -> str:
|
|
| 2311 |
f'{html.escape(item["label"])}</button>'
|
| 2312 |
)
|
| 2313 |
parts.append("</aside>")
|
|
|
|
|
|
|
| 2314 |
# Fixed bottom-of-page Build & Play — shown only while a sequence is storyboarded.
|
| 2315 |
parts.append('<button id="kimodo-compose-launch" type="button">▶ Build & Play</button>')
|
| 2316 |
parts.append("</div>")
|
|
@@ -3248,6 +3276,9 @@ _COMPOSE_JS = r"""
|
|
| 3248 |
"""
|
| 3249 |
|
| 3250 |
|
|
|
|
|
|
|
|
|
|
| 3251 |
DRAWER_JS = f"""
|
| 3252 |
(() => {{
|
| 3253 |
let attempts = 0;
|
|
@@ -3333,6 +3364,9 @@ DRAWER_JS = f"""
|
|
| 3333 |
if (open && drawer.id === 'kimodo-stances-drawer') refreshStances();
|
| 3334 |
// Build the storyboard composer (palette + strip) when its drawer opens.
|
| 3335 |
if (open && drawer.id === 'kimodo-compose-drawer') composeOpen();
|
|
|
|
|
|
|
|
|
|
| 3336 |
// Shift the viewer camera to keep the character centered beside the open drawer.
|
| 3337 |
sendViewportInset();
|
| 3338 |
}}
|
|
@@ -3417,6 +3451,8 @@ DRAWER_JS = f"""
|
|
| 3417 |
{_KATA_JS}
|
| 3418 |
{_STANCES_JS}
|
| 3419 |
{_COMPOSE_JS}
|
|
|
|
|
|
|
| 3420 |
|
| 3421 |
window.addEventListener('message', (event) => {{
|
| 3422 |
const msg = event.data || {{}};
|
|
@@ -3451,7 +3487,8 @@ DRAWER_JS = f"""
|
|
| 3451 |
"""
|
| 3452 |
APP_HEAD = (
|
| 3453 |
'<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>'
|
| 3454 |
-
|
|
|
|
| 3455 |
)
|
| 3456 |
|
| 3457 |
|
|
@@ -4085,6 +4122,13 @@ with gr.Blocks(title="Kimodo ZeroGPU", css=_BLOCKS_CSS) as demo:
|
|
| 4085 |
fn=_generate_button_label, inputs=[anonymous_client_id], outputs=[btn], api_name=False,
|
| 4086 |
)
|
| 4087 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4088 |
# Display model drives the persistent viewer live via postMessage (no reload).
|
| 4089 |
# Clothing is handled by the fixed browser drawer so it can behave like kata's drawers.
|
| 4090 |
model_dd.change(
|
|
|
|
| 127 |
DEFAULT_CLOTHING = {"head": "beanie_green", "face": "nerdy_glasses",
|
| 128 |
"torso_over": "kimono", "legs": "kimono_trousers"}
|
| 129 |
|
| 130 |
+
# --- Pluggable side tabs ---------------------------------------------------
|
| 131 |
+
# A NEW side tab is added by DROPPING FILES into ./tabs — NO app.py edit needed,
|
| 132 |
+
# so multiple people can build tabs in parallel without touching shared code:
|
| 133 |
+
# tabs/<name>.tab.html -> the rail button (.kimodo-left-tab[data-drawer=…])
|
| 134 |
+
# tabs/<name>.drawer.html -> the slide-in <aside id="kimodo-<name>-drawer"> panel
|
| 135 |
+
# tabs/<name>.tab.css -> styles for that tab
|
| 136 |
+
# tabs/<name>.tab.js -> behavior; runs INSIDE the drawer-JS scope, so it can
|
| 137 |
+
# call the shared helpers (previewFrame, fetchManifest,
|
| 138 |
+
# fetchRecord, loadIntoViewer, setOpen, sendViewportInset…).
|
| 139 |
+
# All matching files are globbed + concatenated in sorted filename order at import.
|
| 140 |
+
# The drawer manager auto-discovers the new button+aside from the DOM; the tab hooks
|
| 141 |
+
# its "drawer opened" behavior WITHOUT editing setOpen via:
|
| 142 |
+
# window.addEventListener('kimodo-drawer-open', (e) => {
|
| 143 |
+
# if (e.detail === 'kimodo-<name>-drawer') { /* refresh / build */ }
|
| 144 |
+
# });
|
| 145 |
+
# Tabs needing a Python/@GPU backend are the only case that still edits app.py — at
|
| 146 |
+
# the "NEW-TAB BACKEND EXTENSION POINT" marker near the compose wiring.
|
| 147 |
+
# See tabs/README.md + tabs/templates/ for a complete copy-paste example.
|
| 148 |
+
_TABS_DIR = Path(__file__).parent / "tabs"
|
| 149 |
+
def _read_tabs(pattern: str) -> str:
|
| 150 |
+
if not _TABS_DIR.is_dir():
|
| 151 |
+
return ""
|
| 152 |
+
return "".join(p.read_text(encoding="utf-8") for p in sorted(_TABS_DIR.glob(pattern)))
|
| 153 |
+
|
| 154 |
APP_CSS = """
|
| 155 |
#main-preview {
|
| 156 |
min-width: 0;
|
|
|
|
| 2276 |
'data-drawer="kimodo-compose-drawer" title="Storyboard composer: sequence stances into a kata">COMPOSE</button>',
|
| 2277 |
'<button id="kimodo-clothing-toggle" class="kimodo-left-tab" type="button" '
|
| 2278 |
'data-drawer="kimodo-clothing-drawer" title="Add clothing to the citizen">CLOTHING</button>',
|
| 2279 |
+
# Pluggable tabs drop a rail button here (tabs/<name>.tab.html).
|
| 2280 |
+
_read_tabs("*.tab.html"),
|
| 2281 |
'</div>',
|
| 2282 |
# Actions library (clip cards populated in a later phase).
|
| 2283 |
'<aside id="kimodo-actions-drawer" class="kimodo-drawer" aria-label="Actions library">',
|
|
|
|
| 2337 |
f'{html.escape(item["label"])}</button>'
|
| 2338 |
)
|
| 2339 |
parts.append("</aside>")
|
| 2340 |
+
# Pluggable tabs drop their slide-in <aside> panel here (tabs/<name>.drawer.html).
|
| 2341 |
+
parts.append(_read_tabs("*.drawer.html"))
|
| 2342 |
# Fixed bottom-of-page Build & Play — shown only while a sequence is storyboarded.
|
| 2343 |
parts.append('<button id="kimodo-compose-launch" type="button">▶ Build & Play</button>')
|
| 2344 |
parts.append("</div>")
|
|
|
|
| 3276 |
"""
|
| 3277 |
|
| 3278 |
|
| 3279 |
+
# Pluggable side-tab behavior, concatenated into the drawer-JS scope (see _read_tabs).
|
| 3280 |
+
_EXTRA_TABS_JS = _read_tabs("*.tab.js")
|
| 3281 |
+
|
| 3282 |
DRAWER_JS = f"""
|
| 3283 |
(() => {{
|
| 3284 |
let attempts = 0;
|
|
|
|
| 3364 |
if (open && drawer.id === 'kimodo-stances-drawer') refreshStances();
|
| 3365 |
// Build the storyboard composer (palette + strip) when its drawer opens.
|
| 3366 |
if (open && drawer.id === 'kimodo-compose-drawer') composeOpen();
|
| 3367 |
+
// Pluggable tabs (tabs/*.tab.js) hook their open behavior off this event,
|
| 3368 |
+
// so a new tab never has to edit setOpen. detail = the opened drawer's id.
|
| 3369 |
+
if (open) window.dispatchEvent(new CustomEvent('kimodo-drawer-open', {{ detail: drawer.id }}));
|
| 3370 |
// Shift the viewer camera to keep the character centered beside the open drawer.
|
| 3371 |
sendViewportInset();
|
| 3372 |
}}
|
|
|
|
| 3451 |
{_KATA_JS}
|
| 3452 |
{_STANCES_JS}
|
| 3453 |
{_COMPOSE_JS}
|
| 3454 |
+
// --- Pluggable tabs (tabs/*.tab.js) — run in this same scope, after the helpers. ---
|
| 3455 |
+
{_EXTRA_TABS_JS}
|
| 3456 |
|
| 3457 |
window.addEventListener('message', (event) => {{
|
| 3458 |
const msg = event.data || {{}};
|
|
|
|
| 3487 |
"""
|
| 3488 |
APP_HEAD = (
|
| 3489 |
'<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>'
|
| 3490 |
+
# APP_CSS + any pluggable-tab styles (tabs/<name>.tab.css), then the drawer JS.
|
| 3491 |
+
f"<style>{APP_CSS}{_read_tabs('*.tab.css')}</style><script>{DRAWER_JS}</script>"
|
| 3492 |
)
|
| 3493 |
|
| 3494 |
|
|
|
|
| 4122 |
fn=_generate_button_label, inputs=[anonymous_client_id], outputs=[btn], api_name=False,
|
| 4123 |
)
|
| 4124 |
|
| 4125 |
+
# ===================== NEW-TAB BACKEND EXTENSION POINT =====================
|
| 4126 |
+
# Front-end-only tabs need NOTHING here (drop files in tabs/). A tab that needs
|
| 4127 |
+
# a Python/@GPU backend adds its hidden gr components + .click wiring BELOW this
|
| 4128 |
+
# line (mirror the compose_btn pattern: a hidden #<name>-btn the tab JS clicks,
|
| 4129 |
+
# outputs routed to [preview, meta, file_out]). Append only — don't edit above.
|
| 4130 |
+
# ==========================================================================
|
| 4131 |
+
|
| 4132 |
# Display model drives the persistent viewer live via postMessage (no reload).
|
| 4133 |
# Clothing is handled by the fixed browser drawer so it can behave like kata's drawers.
|
| 4134 |
model_dd.change(
|
tabs/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Pluggable side tabs
|
| 2 |
+
|
| 3 |
+
A side tab is added by **dropping files into this folder** — you do **not** edit
|
| 4 |
+
`app.py` (unless your tab needs a Python/@GPU backend). This lets several tabs be
|
| 5 |
+
built in parallel with zero shared-code conflicts.
|
| 6 |
+
|
| 7 |
+
## The 4 files (replace `<name>` with your tab's slug, e.g. `effects`)
|
| 8 |
+
|
| 9 |
+
| File | What it is | Globbed into |
|
| 10 |
+
|------|------------|--------------|
|
| 11 |
+
| `<name>.tab.html` | the rail button | the left tab rail |
|
| 12 |
+
| `<name>.drawer.html` | the slide-in `<aside>` panel | the drawer container |
|
| 13 |
+
| `<name>.tab.css` | styles for your tab | the page `<style>` |
|
| 14 |
+
| `<name>.tab.js` | behavior | the drawer-JS scope (shared helpers available) |
|
| 15 |
+
|
| 16 |
+
All four are optional, but a useful tab has at least `.drawer.html` + `.tab.html`.
|
| 17 |
+
Files load in **sorted filename order** at import.
|
| 18 |
+
|
| 19 |
+
## ID conventions (the drawer manager auto-discovers these from the DOM)
|
| 20 |
+
|
| 21 |
+
- Rail button: `id="kimodo-<name>-toggle"`, `class="kimodo-left-tab"`,
|
| 22 |
+
`data-drawer="kimodo-<name>-drawer"`.
|
| 23 |
+
- Drawer panel: `<aside id="kimodo-<name>-drawer" class="kimodo-drawer">` (add
|
| 24 |
+
`kimodo-drawer--wide` for a wider panel). Opening it is handled for you
|
| 25 |
+
(one drawer open at a time, tab-rail shift, camera re-centers beside it).
|
| 26 |
+
|
| 27 |
+
## Showing your tab
|
| 28 |
+
|
| 29 |
+
Minimal mode currently **hides every rail tab except COMPOSE**. Your `.tab.css`
|
| 30 |
+
must un-hide yours:
|
| 31 |
+
|
| 32 |
+
```css
|
| 33 |
+
#kimodo-<name>-toggle { display: block !important; }
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
## Hooking "drawer opened" (no setOpen edit)
|
| 37 |
+
|
| 38 |
+
```js
|
| 39 |
+
window.addEventListener('kimodo-drawer-open', (e) => {
|
| 40 |
+
if (e.detail !== 'kimodo-<name>-drawer') return;
|
| 41 |
+
// build/refresh your panel here
|
| 42 |
+
});
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
## Shared helpers your `.tab.js` can call (same scope)
|
| 46 |
+
|
| 47 |
+
- `fetchManifest()` → `[{id, prompt, ...}]` of all saved clips/stances.
|
| 48 |
+
- `fetchRecord(id)` → a clip record (incl. `posed_joints`, `global_quats_xyzw`).
|
| 49 |
+
- `loadIntoViewer(rec, id, label)` → play a clip in the 3D viewer (set
|
| 50 |
+
`rec.num_joints` + `rec.bone_names = SMPLX22_BONES` first if missing).
|
| 51 |
+
- `previewFrame()` → the viewer iframe (for custom `postMessage`).
|
| 52 |
+
- `setOpen(drawerEl, bool)`, `sendViewportInset()`, `stitchPath(ids)`,
|
| 53 |
+
`DATASET_BASE`, `KIMODO_BACKEND`.
|
| 54 |
+
|
| 55 |
+
## Tabs that need a Python/@GPU backend
|
| 56 |
+
|
| 57 |
+
Front-end-only tabs need nothing in `app.py`. If you need server generation, add a
|
| 58 |
+
hidden `gr` component + `.click` wiring **below** the
|
| 59 |
+
`NEW-TAB BACKEND EXTENSION POINT` marker in `app.py` (mirror the `compose_btn`
|
| 60 |
+
pattern: a hidden `#<name>-btn` your JS clicks; outputs → `[preview, meta, file_out]`).
|
| 61 |
+
That marker is the **only** shared line a backend tab touches — append only.
|
| 62 |
+
|
| 63 |
+
## Deploy
|
| 64 |
+
|
| 65 |
+
`tabs/` ships automatically with the Space (no ignore rule). `tabs/templates/` is a
|
| 66 |
+
subfolder, so its example files are **not** loaded (the globs are non-recursive).
|
| 67 |
+
|
| 68 |
+
See `tabs/templates/` for a complete, copy-paste example tab.
|
tabs/templates/example.drawer.html
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!-- The slide-in panel. Copy to tabs/<name>.drawer.html. Use class
|
| 2 |
+
"kimodo-drawer" (add " kimodo-drawer--wide" for a wider panel). -->
|
| 3 |
+
<aside id="kimodo-example-drawer" class="kimodo-drawer" aria-label="Example tab">
|
| 4 |
+
<h2>Example tab</h2>
|
| 5 |
+
<div class="kimodo-drawer-sub">A starter tab — lists saved clips; click one to
|
| 6 |
+
play it in the viewer. Replace this with your own UI.</div>
|
| 7 |
+
<div id="kimodo-example-list"></div>
|
| 8 |
+
</aside>
|
tabs/templates/example.tab.css
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Styles for the example tab. Copy to tabs/<name>.tab.css.
|
| 2 |
+
The un-hide rule is REQUIRED: minimal mode hides every rail tab but COMPOSE. */
|
| 3 |
+
#kimodo-example-toggle { display: block !important; color: #9fe6c0; }
|
| 4 |
+
#kimodo-example-toggle.active { background: #1f3a2c !important; border-color: #3a6 !important; color: #9fe6c0 !important; }
|
| 5 |
+
|
| 6 |
+
.kimodo-ex-item {
|
| 7 |
+
display: block; width: 100%; text-align: left; margin-bottom: 6px;
|
| 8 |
+
padding: 8px 10px; font-size: 12px; cursor: pointer; border-radius: 7px;
|
| 9 |
+
background: #26262b; color: #e3e3e8; border: 1px solid #3a3a40;
|
| 10 |
+
}
|
| 11 |
+
.kimodo-ex-item:hover { background: #30303a; }
|
tabs/templates/example.tab.html
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!-- Rail button. Copy to tabs/<name>.tab.html and rename ids/label.
|
| 2 |
+
NOTE: files under tabs/templates/ are NOT loaded (globs are non-recursive);
|
| 3 |
+
move a copy up to tabs/ to activate it. -->
|
| 4 |
+
<button id="kimodo-example-toggle" class="kimodo-left-tab" type="button"
|
| 5 |
+
data-drawer="kimodo-example-drawer" title="Example pluggable tab">EXAMPLE</button>
|
tabs/templates/example.tab.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Behavior for the example tab. Copy to tabs/<name>.tab.js.
|
| 2 |
+
// This block is concatenated INTO the drawer-JS scope, AFTER the shared helpers,
|
| 3 |
+
// so it can call them directly (fetchManifest, fetchRecord, loadIntoViewer, …).
|
| 4 |
+
// Wrap in an IIFE to keep your locals from colliding with other tabs.
|
| 5 |
+
(function () {
|
| 6 |
+
let loaded = false;
|
| 7 |
+
|
| 8 |
+
async function refreshExample() {
|
| 9 |
+
const list = document.getElementById('kimodo-example-list');
|
| 10 |
+
if (!list) return;
|
| 11 |
+
list.textContent = 'loading…';
|
| 12 |
+
let items = [];
|
| 13 |
+
try { items = await fetchManifest(); } catch (e) { list.textContent = 'failed to load'; return; }
|
| 14 |
+
list.innerHTML = '';
|
| 15 |
+
for (const m of items.slice(0, 25)) {
|
| 16 |
+
const b = document.createElement('button');
|
| 17 |
+
b.type = 'button'; b.className = 'kimodo-ex-item';
|
| 18 |
+
b.textContent = (m.prompt || m.id).slice(0, 48);
|
| 19 |
+
b.onclick = async () => {
|
| 20 |
+
try {
|
| 21 |
+
const rec = await fetchRecord(m.id);
|
| 22 |
+
// The viewer skips clips whose num_joints != 22; derive them if missing.
|
| 23 |
+
if (!rec.num_joints) rec.num_joints = (rec.posed_joints && rec.posed_joints[0]) ? rec.posed_joints[0].length : 22;
|
| 24 |
+
if (!rec.bone_names || !rec.bone_names.length) rec.bone_names = SMPLX22_BONES;
|
| 25 |
+
loadIntoViewer(rec, m.id, m.prompt || m.id);
|
| 26 |
+
} catch (e) { console.error('example load failed', e); }
|
| 27 |
+
};
|
| 28 |
+
list.appendChild(b);
|
| 29 |
+
}
|
| 30 |
+
loaded = true;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
// Build the panel when THIS drawer opens (no setOpen edit needed).
|
| 34 |
+
window.addEventListener('kimodo-drawer-open', (e) => {
|
| 35 |
+
if (e.detail === 'kimodo-example-drawer' && !loaded) refreshExample();
|
| 36 |
+
});
|
| 37 |
+
})();
|