Spaces:
Running on Zero
Running on Zero
Deploy Karate Wiener (kimodo kata maker)
Browse files- app.py +19 -54
- docs/comic_model_research.md +88 -0
- tabs/dojo.drawer.html +10 -13
- tabs/dojo.tab.css +80 -0
- tabs/dojo.tab.js +58 -63
app.py
CHANGED
|
@@ -1531,7 +1531,18 @@ def _render_preview_html(
|
|
| 1531 |
}}
|
| 1532 |
dojoSplatBaseBounds = null;
|
| 1533 |
}}
|
| 1534 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1535 |
if (!dojoSplatRoot) return;
|
| 1536 |
const robust = dojoSplatBaseBounds && dojoSplatBaseBounds.robust ? dojoSplatBaseBounds.robust : null;
|
| 1537 |
const rawMax = robust && robust.footprint > 0 ? robust.footprint : (dojoSplatBaseBounds && dojoSplatBaseBounds.maxDim > 0 ? dojoSplatBaseBounds.maxDim : 1.0);
|
|
@@ -1541,7 +1552,8 @@ def _render_preview_html(
|
|
| 1541 |
const localCenterX = robust ? (robust.minX + robust.maxX) * 0.5 : 0;
|
| 1542 |
const localCenterZ = robust ? (robust.minZ + robust.maxZ) * 0.5 : 0;
|
| 1543 |
const floorY = (dojoFloorSurfaceY - dojoFloorThickness) - localFloorY * scale;
|
| 1544 |
-
dojoSplatRoot.scale.setScalar(
|
|
|
|
| 1545 |
dojoSplatRoot.position.set(-localCenterX * scale, floorY, -localCenterZ * scale);
|
| 1546 |
dojoSplatRoot.updateMatrixWorld(true);
|
| 1547 |
resizeFloorGrid(rawMax * scale);
|
|
@@ -1572,7 +1584,7 @@ def _render_preview_html(
|
|
| 1572 |
dojoSplatRoot.scale.setScalar(1.0);
|
| 1573 |
dojoSplatRoot.position.set(0.95, 0.05, -0.85);
|
| 1574 |
scene.add(dojoSplatRoot);
|
| 1575 |
-
fitDojoSplat();
|
| 1576 |
}} catch (err) {{
|
| 1577 |
console.error('dojo splat load failed', err);
|
| 1578 |
postDojoStatus('Splat load failed: ' + (err && err.message ? err.message : err));
|
|
@@ -2587,6 +2599,7 @@ def _render_preview_html(
|
|
| 2587 |
}}
|
| 2588 |
_camAnimTick(dt); // glide the camera into a new framing
|
| 2589 |
_placeAnimTick(); // grow/shrink place skeletons
|
|
|
|
| 2590 |
controls.update();
|
| 2591 |
renderer.render(scene, camera);
|
| 2592 |
requestAnimationFrame(tick);
|
|
@@ -3429,75 +3442,27 @@ def generate_dojo_scene(payload_json: str | dict | None) -> str:
|
|
| 3429 |
if not iso_path:
|
| 3430 |
raise RuntimeError("Klein returned an unsupported image payload.")
|
| 3431 |
isometric_image = _result_data_url(iso_result, "image/png")
|
|
|
|
| 3432 |
yield emit({
|
| 3433 |
"ok": True,
|
| 3434 |
"done": False,
|
| 3435 |
"source": "klein-triposplat",
|
| 3436 |
"stage": "flux-iso-done",
|
| 3437 |
-
"status": "Isometric
|
| 3438 |
"source_image": isometric_image,
|
| 3439 |
"isometric_image": isometric_image,
|
| 3440 |
"isometric_prompt": iso_prompt,
|
| 3441 |
-
"panorama_prompt": pano_prompt,
|
| 3442 |
**base,
|
| 3443 |
})
|
| 3444 |
-
|
| 3445 |
-
yield emit({
|
| 3446 |
-
"ok": True,
|
| 3447 |
-
"done": False,
|
| 3448 |
-
"source": "klein-triposplat",
|
| 3449 |
-
"stage": "flux-pano-start",
|
| 3450 |
-
"status": "Generating 360 Flux image...",
|
| 3451 |
-
"source_image": isometric_image,
|
| 3452 |
-
"isometric_image": isometric_image,
|
| 3453 |
-
"isometric_prompt": iso_prompt,
|
| 3454 |
-
"panorama_prompt": pano_prompt,
|
| 3455 |
-
**base,
|
| 3456 |
-
})
|
| 3457 |
-
pano_job = klein_client.submit(pano_prompt, seed, api_name="/generate")
|
| 3458 |
-
for update in _dojo_wait_for_job(pano_job, {
|
| 3459 |
-
"ok": True,
|
| 3460 |
-
"source": "klein-triposplat",
|
| 3461 |
-
"stage": "flux-pano-progress",
|
| 3462 |
-
"status": "Generating 360 Flux image...",
|
| 3463 |
-
"source_image": isometric_image,
|
| 3464 |
-
"isometric_image": isometric_image,
|
| 3465 |
-
"isometric_prompt": iso_prompt,
|
| 3466 |
-
"panorama_prompt": pano_prompt,
|
| 3467 |
-
**base,
|
| 3468 |
-
}):
|
| 3469 |
-
yield emit(update)
|
| 3470 |
-
pano_result = pano_job.result()
|
| 3471 |
-
panorama_image = _result_data_url(pano_result, "image/png")
|
| 3472 |
-
if not panorama_image:
|
| 3473 |
-
raise RuntimeError("Klein returned an unsupported 360 image payload.")
|
| 3474 |
-
yield emit({
|
| 3475 |
-
"ok": True,
|
| 3476 |
-
"done": False,
|
| 3477 |
-
"source": "klein-triposplat",
|
| 3478 |
-
"stage": "flux-pano-done",
|
| 3479 |
-
"status": "360 Flux image ready. Starting TripoSplat...",
|
| 3480 |
-
"source_image": isometric_image,
|
| 3481 |
-
"isometric_image": isometric_image,
|
| 3482 |
-
"panorama_image": panorama_image,
|
| 3483 |
-
"image": panorama_image,
|
| 3484 |
-
"isometric_prompt": iso_prompt,
|
| 3485 |
-
"panorama_prompt": pano_prompt,
|
| 3486 |
-
**base,
|
| 3487 |
-
})
|
| 3488 |
-
|
| 3489 |
yield emit({
|
| 3490 |
"ok": True,
|
| 3491 |
"done": False,
|
| 3492 |
"source": "klein-triposplat",
|
| 3493 |
"stage": "splat-start",
|
| 3494 |
-
"status": "
|
| 3495 |
"source_image": isometric_image,
|
| 3496 |
"isometric_image": isometric_image,
|
| 3497 |
-
"panorama_image": panorama_image,
|
| 3498 |
-
"image": panorama_image,
|
| 3499 |
"isometric_prompt": iso_prompt,
|
| 3500 |
-
"panorama_prompt": pano_prompt,
|
| 3501 |
**base,
|
| 3502 |
})
|
| 3503 |
tripo_client = Client(TRIPOSPLAT_SPACE, token=token)
|
|
|
|
| 1531 |
}}
|
| 1532 |
dojoSplatBaseBounds = null;
|
| 1533 |
}}
|
| 1534 |
+
// Animate the splat scaling up from ~0 to its fit scale on first load, so it grows
|
| 1535 |
+
// in instead of popping. Ticked each frame by _dojoSplatGrowTick.
|
| 1536 |
+
let _dojoSplatGrow = null;
|
| 1537 |
+
function _dojoSplatGrowTick(dt) {{
|
| 1538 |
+
if (!_dojoSplatGrow || !dojoSplatRoot) return;
|
| 1539 |
+
_dojoSplatGrow.t += dt;
|
| 1540 |
+
const k = Math.min(1, _dojoSplatGrow.t / _dojoSplatGrow.dur);
|
| 1541 |
+
const e = 1 - Math.pow(1 - k, 3); // easeOutCubic
|
| 1542 |
+
dojoSplatRoot.scale.setScalar(Math.max(0.0001, _dojoSplatGrow.to * e));
|
| 1543 |
+
if (k >= 1) _dojoSplatGrow = null;
|
| 1544 |
+
}}
|
| 1545 |
+
function fitDojoSplat(attempt = 0, grow = false) {{
|
| 1546 |
if (!dojoSplatRoot) return;
|
| 1547 |
const robust = dojoSplatBaseBounds && dojoSplatBaseBounds.robust ? dojoSplatBaseBounds.robust : null;
|
| 1548 |
const rawMax = robust && robust.footprint > 0 ? robust.footprint : (dojoSplatBaseBounds && dojoSplatBaseBounds.maxDim > 0 ? dojoSplatBaseBounds.maxDim : 1.0);
|
|
|
|
| 1552 |
const localCenterX = robust ? (robust.minX + robust.maxX) * 0.5 : 0;
|
| 1553 |
const localCenterZ = robust ? (robust.minZ + robust.maxZ) * 0.5 : 0;
|
| 1554 |
const floorY = (dojoFloorSurfaceY - dojoFloorThickness) - localFloorY * scale;
|
| 1555 |
+
if (grow) {{ _dojoSplatGrow = {{ t: 0, dur: 0.7, to: scale }}; dojoSplatRoot.scale.setScalar(0.0001); }}
|
| 1556 |
+
else {{ _dojoSplatGrow = null; dojoSplatRoot.scale.setScalar(scale); }}
|
| 1557 |
dojoSplatRoot.position.set(-localCenterX * scale, floorY, -localCenterZ * scale);
|
| 1558 |
dojoSplatRoot.updateMatrixWorld(true);
|
| 1559 |
resizeFloorGrid(rawMax * scale);
|
|
|
|
| 1584 |
dojoSplatRoot.scale.setScalar(1.0);
|
| 1585 |
dojoSplatRoot.position.set(0.95, 0.05, -0.85);
|
| 1586 |
scene.add(dojoSplatRoot);
|
| 1587 |
+
fitDojoSplat(0, true); // grow in from ~0 so the splat doesn't pop
|
| 1588 |
}} catch (err) {{
|
| 1589 |
console.error('dojo splat load failed', err);
|
| 1590 |
postDojoStatus('Splat load failed: ' + (err && err.message ? err.message : err));
|
|
|
|
| 2599 |
}}
|
| 2600 |
_camAnimTick(dt); // glide the camera into a new framing
|
| 2601 |
_placeAnimTick(); // grow/shrink place skeletons
|
| 2602 |
+
_dojoSplatGrowTick(dt); // grow the dojo splat in on first load
|
| 2603 |
controls.update();
|
| 2604 |
renderer.render(scene, camera);
|
| 2605 |
requestAnimationFrame(tick);
|
|
|
|
| 3442 |
if not iso_path:
|
| 3443 |
raise RuntimeError("Klein returned an unsupported image payload.")
|
| 3444 |
isometric_image = _result_data_url(iso_result, "image/png")
|
| 3445 |
+
panorama_image = None # panorama flux disabled for now — iso → splat only
|
| 3446 |
yield emit({
|
| 3447 |
"ok": True,
|
| 3448 |
"done": False,
|
| 3449 |
"source": "klein-triposplat",
|
| 3450 |
"stage": "flux-iso-done",
|
| 3451 |
+
"status": "Isometric image ready. Converting to 3D...",
|
| 3452 |
"source_image": isometric_image,
|
| 3453 |
"isometric_image": isometric_image,
|
| 3454 |
"isometric_prompt": iso_prompt,
|
|
|
|
| 3455 |
**base,
|
| 3456 |
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3457 |
yield emit({
|
| 3458 |
"ok": True,
|
| 3459 |
"done": False,
|
| 3460 |
"source": "klein-triposplat",
|
| 3461 |
"stage": "splat-start",
|
| 3462 |
+
"status": "Converting to 3D (TripoSplat)...",
|
| 3463 |
"source_image": isometric_image,
|
| 3464 |
"isometric_image": isometric_image,
|
|
|
|
|
|
|
| 3465 |
"isometric_prompt": iso_prompt,
|
|
|
|
| 3466 |
**base,
|
| 3467 |
})
|
| 3468 |
tripo_client = Client(TRIPOSPLAT_SPACE, token=token)
|
docs/comic_model_research.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Open-weight image models ≤32B for the Weiner 4-koma pipeline
|
| 2 |
+
|
| 3 |
+
Research date: 2026-06-12. Method: deep-research workflow (5 search angles, 22 sources,
|
| 4 |
+
108 claims extracted, 18 adversarially verified 3-0). Claims marked *(unverified)* had
|
| 5 |
+
their verification cut off by a rate limit — they come from official/primary sources
|
| 6 |
+
but were not independently confirmed.
|
| 7 |
+
|
| 8 |
+
## Use-case requirements
|
| 9 |
+
|
| 10 |
+
Generate a 2×2-grid 4-panel comic in ONE image (claymation/3D-render style, recurring
|
| 11 |
+
sausage-sensei mascot). Speech-bubble text composited programmatically afterward (in-model
|
| 12 |
+
text = bonus only). Script comes from a separate LLM → prompt adherence for multi-panel
|
| 13 |
+
scene layout is the critical capability. Character consistency within the grid (free, by
|
| 14 |
+
construction) and across generations (LoRA and/or image-reference conditioning).
|
| 15 |
+
Must run on HF ZeroGPU and be license-clean for a public hackathon Space.
|
| 16 |
+
|
| 17 |
+
## Infrastructure correction (verified 3-0)
|
| 18 |
+
|
| 19 |
+
ZeroGPU is no longer H200/70GB: it now runs **RTX Pro 6000 Blackwell** — `large`
|
| 20 |
+
(default) = half GPU / **48GB VRAM** at 1× quota, `xlarge` = full GPU / 96GB at 2× quota.
|
| 21 |
+
Max `@spaces.GPU(duration=...)` remains effectively ~120s per call.
|
| 22 |
+
Source: https://huggingface.co/docs/hub/spaces-zerogpu
|
| 23 |
+
|
| 24 |
+
## Quality bar
|
| 25 |
+
|
| 26 |
+
- Nano Banana 2 (Gemini 3.1 Flash Image Preview): Elo 1258, #4 on Artificial Analysis arena (verified).
|
| 27 |
+
- **HiDream-O1-Image-1.5: Elo 1264, #3 — ABOVE Nano Banana 2** (verified). Open-weight
|
| 28 |
+
availability NOT confirmed (HiDream-I1 was MIT). **Watch item: if weights are open, test it.**
|
| 29 |
+
|
| 30 |
+
## Candidates
|
| 31 |
+
|
| 32 |
+
### Qwen-Image-Edit-2509 / 2511 (~20B MMDiT + Qwen2.5-VL encoder)
|
| 33 |
+
- License: **Apache 2.0** (verified).
|
| 34 |
+
- **Multi-image reference editing** (person+person, person+product, person+scene) —
|
| 35 |
+
condition generation on mascot reference images; character lock WITHOUT training (verified).
|
| 36 |
+
- 2509 explicitly improved identity/character preservation across pose/style changes (verified).
|
| 37 |
+
- 2511 (unverified, official Qwen blog): further character-consistency gains; bakes popular
|
| 38 |
+
community LoRAs into the base model.
|
| 39 |
+
- Known best-in-class in-model text rendering (bonus: maybe legible bubbles for free).
|
| 40 |
+
- 20B bf16 is tight-but-fine on 48GB with standard offload/quantization; official ZeroGPU
|
| 41 |
+
Spaces exist; diffusers support mature.
|
| 42 |
+
|
| 43 |
+
### FLUX.2-klein 4B (+ 9B)
|
| 44 |
+
- 4B: **Apache 2.0** (verified). 9B: FLUX Non-Commercial License (verified) → avoid for public Space.
|
| 45 |
+
- ~13GB bf16; distilled 4-step variant = seconds per image (verified).
|
| 46 |
+
- **LoRA in ~1hr on a single RTX 4090 (<24GB) via ostris/ai-toolkit**, diffusers-loadable (verified).
|
| 47 |
+
- Unified t2i + editing + multi-reference per BFL blog/card *(unverified — votes lost to rate limit)*.
|
| 48 |
+
- We already have klein infra (tiny-army sidecar Space + klein starter repo with LoRA tooling).
|
| 49 |
+
|
| 50 |
+
### Z-Image / Z-Image-Turbo (Tongyi, 6B)
|
| 51 |
+
- **Apache 2.0** (verified). 6B family (verified).
|
| 52 |
+
- Turbo: 8 DiT forward passes, sub-second on H800, fits 16GB consumer VRAM (verified).
|
| 53 |
+
- Already integrated in tiny-army's local image path.
|
| 54 |
+
- No verified reference/editing variant or comic-layout evidence; distilled few-step models
|
| 55 |
+
typically trade away complex-prompt adherence → risk for 4-panel layouts.
|
| 56 |
+
|
| 57 |
+
### FLUX.2-dev (~32B + Mistral Small 3.1 24B encoder)
|
| 58 |
+
- At/over the param cap; bf16 >80GB, ~62GB offloaded, ~20GB at 4-bit (verified).
|
| 59 |
+
- Native multi-reference up to 10 images (verified) — best built-in character conditioning.
|
| 60 |
+
- FLUX dev line is non-commercial-licensed → **ruled out** for a public hackathon Space.
|
| 61 |
+
|
| 62 |
+
### SD 3.5 / HiDream-I1 / CogView4
|
| 63 |
+
- Superseded on quality/adherence by the above within the same size envelope; no
|
| 64 |
+
verified advantage for this use case. Not shortlisted.
|
| 65 |
+
|
| 66 |
+
## Ranking for this use case
|
| 67 |
+
|
| 68 |
+
1. **Qwen-Image-Edit-2511 (or 2509) — the comic engine.** Apache 2.0, the only verified
|
| 69 |
+
≤32B model with multi-image character-reference conditioning (mascot consistency with
|
| 70 |
+
zero training: feed a Weiner reference sheet each call), strongest prompt adherence
|
| 71 |
+
reputation for complex multi-panel scenes, and text rendering good enough that in-model
|
| 72 |
+
bubbles might work. Cost: heaviest of the shortlist; needs the 48GB slice + offload and
|
| 73 |
+
~tens of seconds per comic (within 120s).
|
| 74 |
+
2. **FLUX.2-klein 4B — the on-theme Build Small pick.** Apache 2.0, trivially fast
|
| 75 |
+
(4-step distilled), cheap LoRA pipeline already in-house → Weiner LoRA locks the mascot.
|
| 76 |
+
Lower ceiling on per-panel scene complexity than 20B Qwen; the fine-tune narrows the
|
| 77 |
+
style/character gap.
|
| 78 |
+
3. **Z-Image-Turbo 6B — speed fallback.** Apache 2.0, near-instant, already integrated;
|
| 79 |
+
use if quota pressure dominates and layouts prove simple enough.
|
| 80 |
+
|
| 81 |
+
## Recommended architecture
|
| 82 |
+
|
| 83 |
+
- **A/B test first** (same 10 scripts through Qwen-Edit-2511-with-reference-sheet and
|
| 84 |
+
klein-4B-with/without-LoRA; blank-bubble prompting + programmatic text composite).
|
| 85 |
+
- Production default: Qwen-Image-Edit-2511 + Weiner reference sheet (no training needed).
|
| 86 |
+
- Featured small-model tier: klein 4B + Weiner LoRA (trained on a reference corpus we can
|
| 87 |
+
bootstrap with the bigger model) — the "fine-tuned the 4B open model on our mascot" story.
|
| 88 |
+
- Watch item: HiDream-O1-Image-1.5 weights — if open, benchmark immediately.
|
tabs/dojo.drawer.html
CHANGED
|
@@ -28,29 +28,26 @@
|
|
| 28 |
<button id="kimodo-dojo-generate" class="kimodo-dojo-primary" type="button">Generate Scene</button>
|
| 29 |
</div>
|
| 30 |
|
| 31 |
-
<
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
<div
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
<div id="kimodo-dojo-preview-wrap" class="kimodo-dojo-preview-wrap">
|
| 40 |
-
<figure><figcaption>Isometric</figcaption><img id="kimodo-dojo-preview" alt="Flux isometric result" /></figure>
|
| 41 |
-
<figure><figcaption>360</figcaption><img id="kimodo-dojo-preview-pano" alt="Flux 360 result" /></figure>
|
| 42 |
</div>
|
| 43 |
<div id="kimodo-dojo-status" class="kimodo-drawer-sub kimodo-dojo-status">Ready.</div>
|
| 44 |
|
| 45 |
<details class="kimodo-dojo-adv">
|
| 46 |
<summary>Advanced settings</summary>
|
| 47 |
<div class="kimodo-dojo-adv-body">
|
| 48 |
-
<div class="kimodo-cz-seg kimodo-dojo-mode" id="kimodo-dojo-mode" aria-label="Dojo scene type">
|
| 49 |
<button type="button" class="kimodo-cz-segbtn active" data-mode="splat">Splat</button>
|
| 50 |
<button type="button" class="kimodo-cz-segbtn" data-mode="panorama">Panorama</button>
|
| 51 |
</div>
|
| 52 |
<div class="kimodo-dojo-settings" aria-label="Dojo room settings">
|
| 53 |
-
<label class="kimodo-dojo-check">
|
| 54 |
<input id="kimodo-dojo-skybox" type="checkbox" />
|
| 55 |
<span>Use panorama as skybox in splat</span>
|
| 56 |
</label>
|
|
|
|
| 28 |
<button id="kimodo-dojo-generate" class="kimodo-dojo-primary" type="button">Generate Scene</button>
|
| 29 |
</div>
|
| 30 |
|
| 31 |
+
<!-- Generation stage: the flux isometric image fades in, then a "converting to 3D"
|
| 32 |
+
scan effect plays over it while TripoSplat runs. Status text sits below. -->
|
| 33 |
+
<div id="kimodo-dojo-stage" class="kimodo-dojo-stage" hidden>
|
| 34 |
+
<div id="kimodo-dojo-iso" class="kimodo-dojo-iso">
|
| 35 |
+
<img id="kimodo-dojo-iso-img" class="kimodo-dojo-iso-img" alt="Generated isometric scene" />
|
| 36 |
+
<div class="kimodo-dojo-iso-scan" aria-hidden="true"></div>
|
| 37 |
+
<div class="kimodo-dojo-iso-3d" aria-hidden="true">converting to 3D…</div>
|
| 38 |
+
</div>
|
|
|
|
|
|
|
|
|
|
| 39 |
</div>
|
| 40 |
<div id="kimodo-dojo-status" class="kimodo-drawer-sub kimodo-dojo-status">Ready.</div>
|
| 41 |
|
| 42 |
<details class="kimodo-dojo-adv">
|
| 43 |
<summary>Advanced settings</summary>
|
| 44 |
<div class="kimodo-dojo-adv-body">
|
| 45 |
+
<div class="kimodo-cz-seg kimodo-dojo-mode kimodo-dojo-panoctl" id="kimodo-dojo-mode" aria-label="Dojo scene type">
|
| 46 |
<button type="button" class="kimodo-cz-segbtn active" data-mode="splat">Splat</button>
|
| 47 |
<button type="button" class="kimodo-cz-segbtn" data-mode="panorama">Panorama</button>
|
| 48 |
</div>
|
| 49 |
<div class="kimodo-dojo-settings" aria-label="Dojo room settings">
|
| 50 |
+
<label class="kimodo-dojo-check kimodo-dojo-panoctl">
|
| 51 |
<input id="kimodo-dojo-skybox" type="checkbox" />
|
| 52 |
<span>Use panorama as skybox in splat</span>
|
| 53 |
</label>
|
tabs/dojo.tab.css
CHANGED
|
@@ -391,3 +391,83 @@
|
|
| 391 |
.kimodo-dojo-adv[open] > summary::after { content: " ▴"; }
|
| 392 |
.kimodo-dojo-adv-body { padding: 0 10px 8px; }
|
| 393 |
.kimodo-dojo-adv-body .kimodo-dojo-mode { margin-top: 2px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
.kimodo-dojo-adv[open] > summary::after { content: " ▴"; }
|
| 392 |
.kimodo-dojo-adv-body { padding: 0 10px 8px; }
|
| 393 |
.kimodo-dojo-adv-body .kimodo-dojo-mode { margin-top: 2px; }
|
| 394 |
+
|
| 395 |
+
/* ===== Panorama disabled for now: hide its controls ===== */
|
| 396 |
+
.kimodo-dojo-panoctl { display: none !important; }
|
| 397 |
+
|
| 398 |
+
/* ===== Generation stage: flux iso image + 3D-conversion effect ===== */
|
| 399 |
+
.kimodo-dojo-stage { margin-top: 10px; }
|
| 400 |
+
.kimodo-dojo-iso {
|
| 401 |
+
position: relative;
|
| 402 |
+
width: 100%;
|
| 403 |
+
aspect-ratio: 1 / 1;
|
| 404 |
+
border-radius: 8px;
|
| 405 |
+
overflow: hidden;
|
| 406 |
+
background: #15151a;
|
| 407 |
+
border: 1px solid #34343c;
|
| 408 |
+
}
|
| 409 |
+
.kimodo-dojo-iso.generating::before {
|
| 410 |
+
content: '';
|
| 411 |
+
position: absolute;
|
| 412 |
+
inset: 0;
|
| 413 |
+
background: linear-gradient(110deg, #1b1b20 30%, #2a2620 50%, #1b1b20 70%);
|
| 414 |
+
background-size: 220% 100%;
|
| 415 |
+
animation: kimodo-dojo-shimmer 1.3s linear infinite;
|
| 416 |
+
}
|
| 417 |
+
.kimodo-dojo-iso-img {
|
| 418 |
+
position: absolute;
|
| 419 |
+
inset: 0;
|
| 420 |
+
width: 100%;
|
| 421 |
+
height: 100%;
|
| 422 |
+
object-fit: cover;
|
| 423 |
+
opacity: 0;
|
| 424 |
+
transform: scale(1.06);
|
| 425 |
+
filter: blur(14px);
|
| 426 |
+
transition: opacity .9s ease, filter .9s ease, transform .9s ease;
|
| 427 |
+
}
|
| 428 |
+
.kimodo-dojo-iso.has-img .kimodo-dojo-iso-img {
|
| 429 |
+
opacity: 1;
|
| 430 |
+
transform: scale(1);
|
| 431 |
+
filter: blur(0);
|
| 432 |
+
}
|
| 433 |
+
.kimodo-dojo-iso-scan,
|
| 434 |
+
.kimodo-dojo-iso-3d {
|
| 435 |
+
opacity: 0;
|
| 436 |
+
transition: opacity .3s ease;
|
| 437 |
+
pointer-events: none;
|
| 438 |
+
}
|
| 439 |
+
.kimodo-dojo-iso.converting .kimodo-dojo-iso-scan,
|
| 440 |
+
.kimodo-dojo-iso.converting .kimodo-dojo-iso-3d { opacity: 1; }
|
| 441 |
+
.kimodo-dojo-iso-scan {
|
| 442 |
+
position: absolute;
|
| 443 |
+
inset: 0;
|
| 444 |
+
background:
|
| 445 |
+
repeating-linear-gradient(0deg, rgba(255,210,138,0.05) 0 2px, transparent 2px 6px),
|
| 446 |
+
linear-gradient(180deg, transparent 0%, rgba(255,210,138,0.55) 50%, transparent 100%);
|
| 447 |
+
background-size: 100% 100%, 100% 22%;
|
| 448 |
+
background-repeat: no-repeat, no-repeat;
|
| 449 |
+
background-position: 0 0, 0 -22%;
|
| 450 |
+
mix-blend-mode: screen;
|
| 451 |
+
}
|
| 452 |
+
.kimodo-dojo-iso.converting .kimodo-dojo-iso-scan { animation: kimodo-dojo-scan 1.6s ease-in-out infinite; }
|
| 453 |
+
.kimodo-dojo-iso-3d {
|
| 454 |
+
position: absolute;
|
| 455 |
+
left: 0;
|
| 456 |
+
right: 0;
|
| 457 |
+
bottom: 0;
|
| 458 |
+
padding: 6px;
|
| 459 |
+
text-align: center;
|
| 460 |
+
font-size: 10.5px;
|
| 461 |
+
font-weight: 700;
|
| 462 |
+
letter-spacing: .06em;
|
| 463 |
+
color: #ffe0ad;
|
| 464 |
+
background: linear-gradient(0deg, rgba(0,0,0,0.62), transparent);
|
| 465 |
+
}
|
| 466 |
+
@keyframes kimodo-dojo-shimmer { from { background-position: 120% 0; } to { background-position: -120% 0; } }
|
| 467 |
+
@keyframes kimodo-dojo-scan {
|
| 468 |
+
0% { background-position: 0 0, 0 -22%; }
|
| 469 |
+
100% { background-position: 0 0, 0 122%; }
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
/* Improve/reroll button loading state (matches move-gen green loader). */
|
| 473 |
+
.kimodo-dojo-improve.loading { background: #243a2c !important; color: #8fcaa6 !important; border-color: #2f5a3e !important; }
|
tabs/dojo.tab.js
CHANGED
|
@@ -40,11 +40,9 @@
|
|
| 40 |
generate: document.getElementById('kimodo-dojo-generate'),
|
| 41 |
clear: document.getElementById('kimodo-dojo-clear'),
|
| 42 |
status: document.getElementById('kimodo-dojo-status'),
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
preview: document.getElementById('kimodo-dojo-preview'),
|
| 47 |
-
previewPano: document.getElementById('kimodo-dojo-preview-pano')
|
| 48 |
};
|
| 49 |
}
|
| 50 |
|
|
@@ -161,37 +159,46 @@
|
|
| 161 |
if (frame && frame.contentWindow) frame.contentWindow.postMessage({ kind: 'dojo-room-settings', settings }, '*');
|
| 162 |
}
|
| 163 |
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
if (
|
|
|
|
| 169 |
}
|
| 170 |
-
|
| 171 |
-
function resetProgress() {
|
| 172 |
-
['flux-iso', 'flux-pano', 'splat', 'viewer'].forEach((s) => setStep(s, ''));
|
| 173 |
const els = dojoEls();
|
| 174 |
-
if (els.
|
| 175 |
-
if (els.
|
| 176 |
-
if (els.previewWrap) els.previewWrap.classList.remove('on');
|
| 177 |
-
if (els.preview) els.preview.removeAttribute('src');
|
| 178 |
-
if (els.previewPano) els.previewPano.removeAttribute('src');
|
| 179 |
}
|
| 180 |
-
|
| 181 |
-
function showSourceImage(src, kind) {
|
| 182 |
const els = dojoEls();
|
| 183 |
-
|
| 184 |
-
if (
|
| 185 |
-
|
| 186 |
-
els.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
}
|
| 188 |
|
| 189 |
function setBusy(next) {
|
| 190 |
busy = !!next;
|
| 191 |
const els = dojoEls();
|
| 192 |
-
if (els.generate)
|
| 193 |
-
|
| 194 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
}
|
| 196 |
|
| 197 |
function waitForResult(previousValue, onStage) {
|
|
@@ -295,7 +302,7 @@
|
|
| 295 |
|
| 296 |
function openCreate(seed) {
|
| 297 |
showView('create');
|
| 298 |
-
|
| 299 |
const els = dojoEls();
|
| 300 |
improvedByAya = false;
|
| 301 |
if (seed) {
|
|
@@ -375,7 +382,7 @@
|
|
| 375 |
const els = dojoEls();
|
| 376 |
if (!els.improve || improving) return;
|
| 377 |
const has = !!(els.prompt && els.prompt.value.trim());
|
| 378 |
-
els.improve.textContent = improvedByAya ? '
|
| 379 |
: (has ? '✦ Improve with tiny-aya' : 'let tiny-aya decide 🎲');
|
| 380 |
}
|
| 381 |
|
|
@@ -424,40 +431,32 @@
|
|
| 424 |
const trigger = document.querySelector('#dojo-btn button, #dojo-btn');
|
| 425 |
if (!payloadInput || !trigger) { setStatus('Dojo backend controls are missing.'); return; }
|
| 426 |
setBusy(true);
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
setStatus('
|
| 430 |
writeHidden('dojo-result', '');
|
| 431 |
writeHidden('dojo-payload', JSON.stringify(cfg));
|
| 432 |
try {
|
| 433 |
trigger.click();
|
| 434 |
const value = await waitForResult('', (stage) => {
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
if (
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
setStep('flux-iso', 'done'); setStep('flux-pano', 'done'); setStep('splat', 'active');
|
| 454 |
-
setStatus(bestProgressText(stage) || stage.status || 'Generating TripoSplat...');
|
| 455 |
-
} else if (stage.stage === 'splat-done') {
|
| 456 |
-
setStep('splat', 'done'); setStep('viewer', 'active');
|
| 457 |
-
setStatus(stage.status || 'TripoSplat ready. Loading viewer...');
|
| 458 |
-
} else if (stage.stage === 'fallback') {
|
| 459 |
-
setStep('flux-iso', 'fail'); setStep('flux-pano', 'fail'); setStep('splat', 'fail');
|
| 460 |
-
setStatus(stage.warning || stage.error || 'Fallback generated.');
|
| 461 |
}
|
| 462 |
});
|
| 463 |
const scene = JSON.parse(value);
|
|
@@ -533,11 +532,7 @@
|
|
| 533 |
window.addEventListener('message', (event) => {
|
| 534 |
const msg = event.data || {};
|
| 535 |
if (msg.kind === 'dojo-status' && msg.text) {
|
| 536 |
-
if (/loaded in viewer/i.test(msg.text))
|
| 537 |
-
setStep('viewer', 'done');
|
| 538 |
-
setTiming({ stage: 'viewer-done', job_status: 'loaded' });
|
| 539 |
-
}
|
| 540 |
-
if (/failed/i.test(msg.text)) setStep('viewer', 'fail');
|
| 541 |
setStatus(msg.text);
|
| 542 |
}
|
| 543 |
});
|
|
|
|
| 40 |
generate: document.getElementById('kimodo-dojo-generate'),
|
| 41 |
clear: document.getElementById('kimodo-dojo-clear'),
|
| 42 |
status: document.getElementById('kimodo-dojo-status'),
|
| 43 |
+
stage: document.getElementById('kimodo-dojo-stage'),
|
| 44 |
+
iso: document.getElementById('kimodo-dojo-iso'),
|
| 45 |
+
isoImg: document.getElementById('kimodo-dojo-iso-img')
|
|
|
|
|
|
|
| 46 |
};
|
| 47 |
}
|
| 48 |
|
|
|
|
| 159 |
if (frame && frame.contentWindow) frame.contentWindow.postMessage({ kind: 'dojo-room-settings', settings }, '*');
|
| 160 |
}
|
| 161 |
|
| 162 |
+
// ---- Generation stage: iso image loads in, then a 3D-conversion scan ----
|
| 163 |
+
function stageReset() {
|
| 164 |
+
const els = dojoEls();
|
| 165 |
+
if (els.stage) els.stage.hidden = true;
|
| 166 |
+
if (els.iso) els.iso.classList.remove('generating', 'has-img', 'converting');
|
| 167 |
+
if (els.isoImg) els.isoImg.removeAttribute('src');
|
| 168 |
}
|
| 169 |
+
function stageGenerating() { // iso not ready yet → shimmer placeholder
|
|
|
|
|
|
|
| 170 |
const els = dojoEls();
|
| 171 |
+
if (els.stage) els.stage.hidden = false;
|
| 172 |
+
if (els.iso) { els.iso.classList.add('generating'); els.iso.classList.remove('has-img', 'converting'); }
|
|
|
|
|
|
|
|
|
|
| 173 |
}
|
| 174 |
+
function stageIsoReady(src) { // iso image arrived → fade/deblur it in
|
|
|
|
| 175 |
const els = dojoEls();
|
| 176 |
+
if (!src || !els.iso || !els.isoImg) return;
|
| 177 |
+
if (els.stage) els.stage.hidden = false;
|
| 178 |
+
els.iso.classList.remove('generating');
|
| 179 |
+
if (els.isoImg.getAttribute('src') !== src) {
|
| 180 |
+
els.isoImg.onload = () => els.iso.classList.add('has-img');
|
| 181 |
+
els.isoImg.src = src;
|
| 182 |
+
}
|
| 183 |
+
if (els.isoImg.complete && els.isoImg.naturalWidth) els.iso.classList.add('has-img');
|
| 184 |
+
}
|
| 185 |
+
function stageConverting(on) { // 3D-conversion scan overlay
|
| 186 |
+
const els = dojoEls();
|
| 187 |
+
if (els.iso) els.iso.classList.toggle('converting', !!on);
|
| 188 |
}
|
| 189 |
|
| 190 |
function setBusy(next) {
|
| 191 |
busy = !!next;
|
| 192 |
const els = dojoEls();
|
| 193 |
+
if (els.generate) {
|
| 194 |
+
els.generate.disabled = busy;
|
| 195 |
+
els.generate.innerHTML = busy ? '<span class="spin">⟳</span> Generating…' : 'Generate Scene';
|
| 196 |
+
}
|
| 197 |
+
if (els.improve) {
|
| 198 |
+
els.improve.disabled = busy || improving;
|
| 199 |
+
if (busy) { els.improve.classList.add('loading'); els.improve.innerHTML = '<span class="spin">⟳</span> generating…'; }
|
| 200 |
+
else { els.improve.classList.remove('loading'); updateImproveBtn(); }
|
| 201 |
+
}
|
| 202 |
}
|
| 203 |
|
| 204 |
function waitForResult(previousValue, onStage) {
|
|
|
|
| 302 |
|
| 303 |
function openCreate(seed) {
|
| 304 |
showView('create');
|
| 305 |
+
stageReset();
|
| 306 |
const els = dojoEls();
|
| 307 |
improvedByAya = false;
|
| 308 |
if (seed) {
|
|
|
|
| 382 |
const els = dojoEls();
|
| 383 |
if (!els.improve || improving) return;
|
| 384 |
const has = !!(els.prompt && els.prompt.value.trim());
|
| 385 |
+
els.improve.textContent = improvedByAya ? 'Reroll with tiny-aya 🎲'
|
| 386 |
: (has ? '✦ Improve with tiny-aya' : 'let tiny-aya decide 🎲');
|
| 387 |
}
|
| 388 |
|
|
|
|
| 431 |
const trigger = document.querySelector('#dojo-btn button, #dojo-btn');
|
| 432 |
if (!payloadInput || !trigger) { setStatus('Dojo backend controls are missing.'); return; }
|
| 433 |
setBusy(true);
|
| 434 |
+
stageReset();
|
| 435 |
+
stageGenerating();
|
| 436 |
+
setStatus('Generating the dojo image…');
|
| 437 |
writeHidden('dojo-result', '');
|
| 438 |
writeHidden('dojo-payload', JSON.stringify(cfg));
|
| 439 |
try {
|
| 440 |
trigger.click();
|
| 441 |
const value = await waitForResult('', (stage) => {
|
| 442 |
+
const st = stage.stage || '';
|
| 443 |
+
const iso = stage.isometric_image || stage.source_image;
|
| 444 |
+
if (st === 'flux-iso-start' || st === 'flux-iso-progress') {
|
| 445 |
+
stageGenerating();
|
| 446 |
+
setStatus(stage.status || 'Generating the dojo image…');
|
| 447 |
+
} else if (st === 'flux-iso-done') {
|
| 448 |
+
stageIsoReady(iso);
|
| 449 |
+
setStatus(stage.status || 'Image ready — converting to 3D…');
|
| 450 |
+
} else if (st === 'splat-start' || st === 'splat-progress') {
|
| 451 |
+
if (iso) stageIsoReady(iso);
|
| 452 |
+
stageConverting(true);
|
| 453 |
+
setStatus(bestProgressText(stage) || stage.status || 'Converting to 3D…');
|
| 454 |
+
} else if (st === 'splat-done') {
|
| 455 |
+
stageConverting(false);
|
| 456 |
+
setStatus(stage.status || 'Loading the scene into the viewer…');
|
| 457 |
+
} else if (st === 'fallback') {
|
| 458 |
+
stageConverting(false);
|
| 459 |
+
setStatus(stage.warning || stage.error || 'Fallback scene generated.');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 460 |
}
|
| 461 |
});
|
| 462 |
const scene = JSON.parse(value);
|
|
|
|
| 532 |
window.addEventListener('message', (event) => {
|
| 533 |
const msg = event.data || {};
|
| 534 |
if (msg.kind === 'dojo-status' && msg.text) {
|
| 535 |
+
if (/loaded in viewer/i.test(msg.text)) stageConverting(false);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 536 |
setStatus(msg.text);
|
| 537 |
}
|
| 538 |
});
|