polats commited on
Commit
8b1ebc8
·
verified ·
1 Parent(s): 2026366

Deploy Karate Wiener (kimodo kata maker)

Browse files
Files changed (5) hide show
  1. app.py +19 -54
  2. docs/comic_model_research.md +88 -0
  3. tabs/dojo.drawer.html +10 -13
  4. tabs/dojo.tab.css +80 -0
  5. tabs/dojo.tab.js +58 -63
app.py CHANGED
@@ -1531,7 +1531,18 @@ def _render_preview_html(
1531
  }}
1532
  dojoSplatBaseBounds = null;
1533
  }}
1534
- function fitDojoSplat(attempt = 0) {{
 
 
 
 
 
 
 
 
 
 
 
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(scale);
 
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 Flux image ready. Starting 360 Flux image...",
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": "Generating TripoSplat...",
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
- <div id="kimodo-dojo-progress" class="kimodo-dojo-progress" aria-label="Dojo generation progress">
32
- <div class="kimodo-dojo-step" data-step="flux-iso"><span></span>Flux isometric</div>
33
- <div class="kimodo-dojo-step" data-step="flux-pano"><span></span>Flux 360</div>
34
- <div class="kimodo-dojo-step" data-step="splat"><span></span>TripoSplat</div>
35
- <div class="kimodo-dojo-step" data-step="viewer"><span></span>Viewer load</div>
36
- </div>
37
- <div id="kimodo-dojo-time" class="kimodo-dojo-time" aria-live="polite"></div>
38
- <div id="kimodo-dojo-detail" class="kimodo-dojo-detail" aria-live="polite"></div>
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
- time: document.getElementById('kimodo-dojo-time'),
44
- detail: document.getElementById('kimodo-dojo-detail'),
45
- previewWrap: document.getElementById('kimodo-dojo-preview-wrap'),
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
- function setStep(step, state) {
165
- const el = document.querySelector('.kimodo-dojo-step[data-step="' + step + '"]');
166
- if (!el) return;
167
- el.classList.remove('active', 'done', 'fail');
168
- if (state) el.classList.add(state);
 
169
  }
170
-
171
- function resetProgress() {
172
- ['flux-iso', 'flux-pano', 'splat', 'viewer'].forEach((s) => setStep(s, ''));
173
  const els = dojoEls();
174
- if (els.time) els.time.textContent = '';
175
- if (els.detail) els.detail.textContent = '';
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
- const img = kind === 'panorama' ? els.previewPano : els.preview;
184
- if (!src || !img || !els.previewWrap) return;
185
- img.src = src;
186
- els.previewWrap.classList.add('on');
 
 
 
 
 
 
 
 
187
  }
188
 
189
  function setBusy(next) {
190
  busy = !!next;
191
  const els = dojoEls();
192
- if (els.generate) els.generate.disabled = busy;
193
- if (els.clear) els.clear.disabled = busy;
194
- if (els.improve) els.improve.disabled = busy;
 
 
 
 
 
 
195
  }
196
 
197
  function waitForResult(previousValue, onStage) {
@@ -295,7 +302,7 @@
295
 
296
  function openCreate(seed) {
297
  showView('create');
298
- resetProgress();
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 ? 'reroll 🎲'
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
- resetProgress();
428
- setStep('flux-iso', 'active');
429
- setStatus('Starting isometric Flux generation...');
430
  writeHidden('dojo-result', '');
431
  writeHidden('dojo-payload', JSON.stringify(cfg));
432
  try {
433
  trigger.click();
434
  const value = await waitForResult('', (stage) => {
435
- setTiming(stage);
436
- if (stage.source_image || stage.isometric_image) showSourceImage(stage.source_image || stage.isometric_image, 'isometric');
437
- if (stage.panorama_image) showSourceImage(stage.panorama_image, 'panorama');
438
- if (stage.stage === 'flux-iso-start' || stage.stage === 'flux-iso-progress') {
439
- setStep('flux-iso', 'active');
440
- setStatus(stage.status || 'Generating isometric Flux image...');
441
- } else if (stage.stage === 'flux-iso-done') {
442
- setStep('flux-iso', 'done'); setStep('flux-pano', 'active');
443
- showSourceImage(stage.source_image || stage.isometric_image, 'isometric');
444
- setStatus(stage.status || 'Isometric ready. Starting 360 Flux...');
445
- } else if (stage.stage === 'flux-pano-start' || stage.stage === 'flux-pano-progress') {
446
- setStep('flux-iso', 'done'); setStep('flux-pano', 'active');
447
- setStatus(stage.status || 'Generating 360 Flux image...');
448
- } else if (stage.stage === 'flux-pano-done') {
449
- setStep('flux-pano', 'done'); setStep('splat', 'active');
450
- showSourceImage(stage.panorama_image, 'panorama');
451
- setStatus(stage.status || '360 ready. Starting TripoSplat...');
452
- } else if (stage.stage === 'splat-start' || stage.stage === 'splat-progress') {
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
  });