# 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()`. >
Earlier banner (2026-09-05, before the fix) — kept for the diagnosis > > ## ⚠ 2026-09-05 — READ THIS FIRST: the deployed avatar is in a T-pose > > The Space is **live and working** at > (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`. > >
**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.