WolfDavid's picture
docs(01-05): record the spike verdict - CONFIRMED, inline transport - and close the plan
199a6c3
|
Raw
History Blame
11.5 kB
# Resume here — Japanese Learning Avatar
> ## ✅ 2026-09-05 (latest) — 01-05 CLOSED: spike CONFIRMED, `AVATAR_TRANSPORT=inline`, Space at `487b8cf`
>
> Commit `7a26764` poses the arms out of the authored T-pose in `avatar/vrm-stage.js`
> (upper arm 1.2 rad about Z toward the body, forearm a further 0.2 rad, applied once on the
> normalized bones at mount) and publishes `armDown.{left,right}` — the downward component
> of the world-space shoulder→elbow direction on the RAW skeleton, read after every render.
> Measured **-0.932 both sides** (= sin 1.2). Local screenshot: `docs/evidence/2026-09-05-local-rest-pose.png`
> (before: `docs/evidence/2026-09-05-deployed-tpose.png`).
>
> **Tests now assert `armDown < -0.7` at all three layers:** `test_stage_standalone.py::test_arms_rest_at_sides`,
> `test_facade_parity.py::test_arms_rest_at_sides_under_both_transports` (proves the field
> crosses the iframe postMessage boundary), and the deployed
> `test_avatar_loop.py::test_arms_at_sides`. Mutation-tested: `ARM_REST_DROP = 0` fails the
> standalone test with `armDown=0.000`. Each waits for the first rendered frame via
> `breathValue !== 0` — `ready` fires *before* the first frame and that frame compiles every
> MToon shader (>1.5 s measured headless), so a fixed sleep read boot-time zeros.
>
> **Verified:** `ruff` clean; 82 quick-loop; full `pytest tests/e2e/` **17 passed** as a whole
> (5 deployed skipped without `--space-url`).
>
> **Watched, not fixed:** two later full-suite runs each dropped one to three
> `test_asr_standalone.py` tests (`speech_ja.wav` read as rms 0.00057 → `rms-floor`;
> `test_tier_wasm_fallback`). The ASR harness never imports `vrm-stage.js`, and the file passes
> 7/7 alone. Signature matches Chromium's `--use-file-for-fake-audio-capture=…%noloop`
> finishing the WAV before the page's `getUserMedia` attaches when the fleet boots slowly.
> Pre-existing; not caused by this change.
>
> **Deployed and verified.** Pushed `41ee90e..487b8cf`; Space RUNNING in 106 s. Deployed suite
> `pytest tests/e2e/test_avatar_loop.py --space-url https://wolfdavid-japanese-learning-avatar.hf.space`
> **5 passed, twice**, incl. the new `test_arms_at_sides` (live `armDown` -0.932 both sides).
> Live screenshot: `docs/evidence/2026-09-05-deployed-rest-pose.png` — relaxed A-pose.
>
> **Verdict written:** `docs/SPIKE-VERDICT.md` — CONFIRMED, evidence table complete, including
> the huggingface.co Space-page embed read from inside the cross-origin iframe
> (`docs/evidence/2026-09-05-hub-embed.png`). `01-05-SUMMARY.md` closes the plan; **DPLY-01 is
> complete**. `HANDOFF.json` is gone — nothing is blocked any more.
>
> **Next:** `/gsd:execute-phase 1` resumes at **01-08**, the plan that makes the avatar speak.
> One human row is still open and does not block 01-08: **AVTR-01's mobile check** — load the
> Space on a real phone, note device / OS / browser / renders / smooth-or-slideshow, and put it in
> the verdict's *Mobile* line.
>
> **"It does not speak" is still expected.** `dispatchTurn()` throws until plan 01-08.
> **`window.Avatar.__debug` is still a stale snapshot** — always `await window.Avatar.getDebug()`.
> <details><summary>Earlier banner (2026-09-05, before the fix) — kept for the diagnosis</summary>
>
> ## ⚠ 2026-09-05 — READ THIS FIRST: the deployed avatar is in a T-pose
>
> The Space is **live and working** at
> <https://huggingface.co/spaces/WolfDavid/japanese-learning-avatar> (revision `41ee90e`,
> `RUNNING` on `zero-a10g`) — but the avatar stands with **both arms straight out**.
> Screenshot evidence: `docs/evidence/2026-09-05-deployed-tpose.png`.
>
> **This is NOT the two-three.js-instances bug.** The screenshot shows correct materials,
> hair, face and toon shading, and the instruments report `threeInstanceCount = 1`,
> `vrmMetaTitle = 'VRM1_Constraint_Twist_Sample'`, `vrmSpecVersion = '1'`. A plain-glTF
> fallback would have no `vrm.meta` at all.
>
> **Root cause: nothing ever applies an idle POSE.** A VRM's authored rest pose *is* a
> T-pose. Phase 1's idle life only adds tiny offsets — spine 0.012 rad, hips 0.02 rad,
> head 0.05–0.08 rad, all under 1.5° — and **never touches the arms**. No code in this
> repo has ever posed them.
>
> **The fix** (detail in `.planning/HANDOFF.json` → `the_fix`): hand-pose
> `leftUpperArm`/`rightUpperArm` about Z by ~±1.2 rad at mount in `avatar/vrm-stage.js`,
> before the render loop starts. Zero new dependencies, ~10 lines, composes with the
> existing additive idle. The alternative is a `.vrma` clip via
> `@pixiv/three-vrm-animation@3.5.5`, which is better long-term but adds a dependency and
> an asset to license — better as a later polish plan.
>
> **Then add a deployed assertion that the arms are DOWN.** AVTR-01 passed every existing
> check while looking visibly wrong. That is the real lesson: do not mark AVTR-01 complete.
>
> **"It does not speak" is expected, not a bug.** `dispatchTurn()` throws
> `not wired yet - plan 01-08 implements it`. Plan **01-08** is the one that makes it talk.
>
> **Diagnostic trap — cost real time this session:** `window.Avatar.__debug` is a *stale
> snapshot*, refreshed only inside `getDebug()`. Polling it in a rAF loop returns frozen
> values forever and looks exactly like a dead render loop. Always
> `await window.Avatar.getDebug()` per sample; copy `IDLE_SAMPLER` from
> `tests/e2e/test_avatar_loop.py`.
>
> </details>
**Updated 2026-09-05 (T-pose fix). Originally written 2026-08-27.**
Working tree is clean. Everything is committed. The fix IS deployed: the Space runs revision `487b8cf`. Local master carries only docs commits on top.
To pick back up, run `/gsd:resume-work` in this directory. It reads `.planning/HANDOFF.json`
first, which carries the same state in machine-readable form.
---
## One-line status
Phase 01 is **7 of 10 plans complete and unblocked**: the Space is deployed, the spike is CONFIRMED, and 01-08 is next.
---
## Why nothing was pushed
This repo's **only** git remote is `space` — the public Hugging Face Space at
`WolfDavid/japanese-learning-avatar`. There is no GitHub remote. Branch `master` has no upstream.
So `git push` here does not mean "back up my work." It means **publish the app publicly and
trigger a Space rebuild** — which is precisely plan 01-05 Task 1, the gate that is waiting on you.
Pushing right now would also produce a **broken** public build. There is no `README.md` in this
repo; the Space's front-matter lives only on the HF side and was auto-generated wrong when the
Space was created:
| Front-matter key | Currently on HF | Must become | Why |
|---|---|---|---|
| `sdk_version` | `6.26.0` | `6.22.0` | must match `gradio==6.22.0` in `requirements.txt` |
| `python_version` | `'3.12'` | `3.12.12` | ZeroGPU provides only 3.10.13 and 3.12.12 |
Writing that corrected README is part of plan 01-05. Push after 01-05 runs, not before.
**42 commits are unpushed by design, not by omission.**
If what you actually want is an off-machine backup, that is a *different* action from deploying:
add a private GitHub remote and push there. That was not done unprompted because the standing
rule for this account is to ask before pushing nested repos.
---
## Where the work stands
| Plan | What it is | State |
|---|---|---|
| 01-01 | Hosting decision + Space creation + VRM sourcing | Complete |
| 01-02 | Toolchain, Git LFS, package skeleton, test scaffolding | Complete |
| 01-03 | Avatar stage, shared facade + turn loop, both transports | Complete (resumed from the crash) |
| 01-04 | VOICEVOX TTS, `requirements.txt`, ground-truth fixtures | Complete |
| 01-06 | Mora-to-viseme timeline builder | Complete |
| 01-07 | Push-to-talk mic gate + tiered browser ASR | Complete |
| 01-05 | Space manifest, deploy the spike, verdict | Complete — CONFIRMED, inline |
| **01-08** | **Turn-loop wiring — closes the round trip** | **NEXT** (`/gsd:execute-phase 1`) |
| 01-09 | Full deployed E2E suite + `LICENSES.md` | Blocked behind 01-08 |
| 01-10 | Vendor modules, latency harness (user-gated) | Blocked behind 01-09 |
The dependency chain is real, not a preference: `01-08 depends_on [01-05, 01-06, 01-07]`,
`01-09 depends_on [01-08]`, `01-10 depends_on [01-09]`. Nothing proceeds until 01-05 does.
### Verified green at handoff
- 82 quick-loop tests in ~12.8 s
- 15 end-to-end browser tests
- `ruff check .` and `ruff format --check .` both clean
**Run `pytest tests/e2e/` as a whole before trusting a green phase.** Per-file runs hid two real
defects this session — each plan's own verification passed while the full suite failed.
---
## What deploying would actually test
The risky part is already retired. three.js + `@pixiv/three-vrm` render inside a real
Gradio 6.22.0 `gr.HTML` component locally, with `threeInstanceCount === 1` and
`mountCount === 1`, and the iframe fallback boots identically under `AVATAR_TRANSPORT=iframe`.
What remains genuinely deployment-specific: CDN reach from `*.hf.space`, `set_static_paths`
behind the Space proxy, cold-start behaviour, and mobile.
Also note: **1 of 2 free ZeroGPU slots is already consumed** by this Space. The remaining slot is
the last free Gradio Space this account can create without PRO — budget it against the other
HF-profile projects. `cpu-basic` is not a fallback; it returns HTTP 402 for Gradio Spaces.
---
## Traps found the hard way — do not re-derive these
`01-RESEARCH.md` is **wrong** on these points. Copy from
`src/japanese_avatar/ui/avatar_component.py` and `avatar/asr.js` instead.
1. **`js_on_load` cannot use top-level `await`.** Gradio 6.22.0 compiles it into a plain
non-async `Function`, so the documented snippet throws `SyntaxError` and `window.Avatar`
never exists. Use an async IIFE with its own `.catch`.
2. **Custom props are `**kwargs`, not `props={...}`.** The dict form creates one prop literally
named `props`, so `props.vrmUrl` comes back `undefined`.
3. **ASR `dtype:'q8'` cannot create an ONNX session on the WASM backend at all** — for every
Whisper size tested. It works on WebGPU, so the prescribed default would ship green on a dev
machine and dead on exactly the browsers the WASM tier exists to serve. **`q4` is the only
quantisation working on both tiers.**
4. **The prescribed WebGPU→WASM fallback does not fall back.** A failed WebGPU init poisons the
ONNX Runtime Web backend registry for the whole page. `avatar/asr.js` probes
`requestAdapter()` first so the doomed call is never issued.
## Two spec corrections carried forward
- **Frame quantisation:** the correct form is `round(round(sec * 93.75) / speed)`, **not**
`round(sec / speed * 93.75)`. Measured 24/24 correct vs 8/24. Already shipped in `visemes.py`.
- **The slow/long duration ratio is 1.3410852713178294, not exactly 1/0.75** — 4 frames out from
what `01-VALIDATION.md` and plan 01-06 assert. The plan's stated truth was written from a wrong
assumption. **Update `01-VALIDATION.md` when 01-10 touches it**, so the phase verifier does not
read this as an unmet must-have.
## Standing warning about requirements
Phase 1 plan frontmatter **over-claims shared requirements**. AVTR-01, AVTR-02, VOIC-02/03/04/05,
DPLY-01 and DPLY-04 all appear in the `requirements:` field of plans that do not actually satisfy
them. Never blind-run `requirements mark-complete` — verify the acceptance test has really run.
Three executors this session correctly declined to mark requirements complete for this reason.