Spaces:
Running on Zero
Running on Zero
docs(02-06): complete server functions, directive tokens and language bridge plan
Browse files- 02-06-SUMMARY.md: measured warm-up seconds and RSS (+467 MB bare, +558 MB
in-app vs +410 expected), system.dic / model.bin bytes, translate ms under
both transports, e2e totals, the strict-autoplay A/B against the pre-plan
commit, three auto-fixed deviations
- deferred-items.md: the Phase 1 tap-to-running sanity ceiling this machine no
longer meets; first-turn wait behind the language warm-up to measure on the Space
- STATE.md: plan 7 of 11, metrics row, four decisions, session
- ROADMAP.md: phase 02 plan progress
.planning/ROADMAP.md
CHANGED
|
@@ -59,7 +59,7 @@ Decimal phases appear between their surrounding integers in numeric order.
|
|
| 59 |
- [x] 02-03-PLAN.md — Ranked JMdict lookup + JLPT word/kanji level derivation (N1+, name) (TDD) [wave 2]
|
| 60 |
- [x] 02-04-PLAN.md — OPUS-MT ja-en → CTranslate2 int8 under LFS, conversion script, translate() runtime (mt marker) [wave 2]
|
| 61 |
- [x] 02-05-PLAN.md — The canonical analyze(text) + the 28-sentence golden fixture set (SC-1) [wave 3]
|
| 62 |
-
- [
|
| 63 |
- [ ] 02-07-PLAN.md — Furigana: transcript.js renderer, three-mode control + level picker, persistence, three-layer numbers (JPN-02) [wave 5]
|
| 64 |
- [ ] 02-08-PLAN.md — Word-lookup popover with published numbers, desktop + Pixel 7 rows (JPN-03) [wave 6]
|
| 65 |
- [ ] 02-09-PLAN.md — Inline EN reveal with per-line cache, translation latency harness (JPN-04) [wave 7]
|
|
|
|
| 59 |
- [x] 02-03-PLAN.md — Ranked JMdict lookup + JLPT word/kanji level derivation (N1+, name) (TDD) [wave 2]
|
| 60 |
- [x] 02-04-PLAN.md — OPUS-MT ja-en → CTranslate2 int8 under LFS, conversion script, translate() runtime (mt marker) [wave 2]
|
| 61 |
- [x] 02-05-PLAN.md — The canonical analyze(text) + the 28-sentence golden fixture set (SC-1) [wave 3]
|
| 62 |
+
- [x] 02-06-PLAN.md — tokens on the directive; analyze / translate / language_info server functions; warm-up on load; turn-loop + facade surface; parity [wave 4]
|
| 63 |
- [ ] 02-07-PLAN.md — Furigana: transcript.js renderer, three-mode control + level picker, persistence, three-layer numbers (JPN-02) [wave 5]
|
| 64 |
- [ ] 02-08-PLAN.md — Word-lookup popover with published numbers, desktop + Pixel 7 rows (JPN-03) [wave 6]
|
| 65 |
- [ ] 02-09-PLAN.md — Inline EN reveal with per-line cache, translation latency harness (JPN-04) [wave 7]
|
.planning/STATE.md
CHANGED
|
@@ -3,13 +3,13 @@ gsd_state_version: 1.0
|
|
| 3 |
milestone: v1.0
|
| 4 |
milestone_name: milestone
|
| 5 |
status: unknown
|
| 6 |
-
stopped_at: "Completed 02-
|
| 7 |
-
last_updated: "2026-09-07T00:
|
| 8 |
progress:
|
| 9 |
total_phases: 6
|
| 10 |
completed_phases: 1
|
| 11 |
total_plans: 22
|
| 12 |
-
completed_plans:
|
| 13 |
---
|
| 14 |
|
| 15 |
# Project State
|
|
@@ -24,7 +24,7 @@ See: .planning/PROJECT.md (updated 2026-08-08)
|
|
| 24 |
## Current Position
|
| 25 |
|
| 26 |
Phase: 02 (japanese-language-core) — EXECUTING
|
| 27 |
-
Plan:
|
| 28 |
|
| 29 |
## Performance Metrics
|
| 30 |
|
|
@@ -66,6 +66,7 @@ Plan: 6 of 11
|
|
| 66 |
| Phase 02 P03 | 11 | 2 tasks | 5 files |
|
| 67 |
| Phase 02 P04 | 17 | 2 tasks | 13 files |
|
| 68 |
| Phase 02 P05 | 12 | 2 tasks | 7 files |
|
|
|
|
| 69 |
|
| 70 |
## Accumulated Context
|
| 71 |
|
|
@@ -145,6 +146,10 @@ Recent decisions affecting current work:
|
|
| 145 |
- [Phase 02]: Three plan-assumed word levels corrected to what the pinned lists say and pinned by JMdict id, not patched: 日本語 1464530 is N1+ (on no list; kanji all N5 - the two-axis example), ない/なかった resolve to 無い 1529520 which no list carries (N1+), こんにちは 1289400 is N3; 人気 read ひとけ is the separate entry 1367020 (N1+). A level-alias table is the honest fix if the product wants otherwise (02-10 review), never a weaker lookup
|
| 146 |
- [Phase 02]: levels.is_kanji IS ruby.is_kanji (02-03's parallel-wave copy removed): one predicate decides both ruby and the kanji-axis gate, so supplementary-plane kanji (𠮷) reach the axis as unlisted instead of being skipped
|
| 147 |
- [Phase 02]: The fixed sample sentence set (SC-1) follows the golden_timeline pattern: 28 sentences / 115 records generated by the real pipeline, 18 EXPECTED facts checked before writing (exit 1, nothing written otherwise), frozen LF byte-for-byte with data pins read from the environment; JPN-01 still bound to 02-10's rows
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
|
| 149 |
### Pending Todos
|
| 150 |
|
|
@@ -166,8 +171,8 @@ None yet.
|
|
| 166 |
|
| 167 |
## Session Continuity
|
| 168 |
|
| 169 |
-
Last session: 2026-09-07T00:
|
| 170 |
-
Stopped at: Completed 02-
|
| 171 |
Resume file: None
|
| 172 |
|
| 173 |
**Roadmap-level open decision:** the Phase 1 latency trigger (p50 7.1 s, ~90 % server synthesis) fired; recorded in `docs/LATENCY.md` and in 02-CONTEXT.md § Deferred. Phase 2 adds a CPU translation model to the same container — measure on the Space. Consider `/gsd:insert-phase` for streaming/synthesis relocation before Phase 3 lengthens turns.
|
|
|
|
| 3 |
milestone: v1.0
|
| 4 |
milestone_name: milestone
|
| 5 |
status: unknown
|
| 6 |
+
stopped_at: "Completed 02-06-PLAN.md (wave 4): analyze/translate/language_info server functions + tokens on the directive, lock-serialised language warm-up on Blocks.load (+467 MB local vs +410 expected), analyze/translate/languageInfo on the shared turn loop, parity under both transports; quick loop 308 passed / 33.4 s; whole e2e 26 passed. Next 02-07."
|
| 7 |
+
last_updated: "2026-09-07T00:52:28.909Z"
|
| 8 |
progress:
|
| 9 |
total_phases: 6
|
| 10 |
completed_phases: 1
|
| 11 |
total_plans: 22
|
| 12 |
+
completed_plans: 17
|
| 13 |
---
|
| 14 |
|
| 15 |
# Project State
|
|
|
|
| 24 |
## Current Position
|
| 25 |
|
| 26 |
Phase: 02 (japanese-language-core) — EXECUTING
|
| 27 |
+
Plan: 7 of 11
|
| 28 |
|
| 29 |
## Performance Metrics
|
| 30 |
|
|
|
|
| 66 |
| Phase 02 P03 | 11 | 2 tasks | 5 files |
|
| 67 |
| Phase 02 P04 | 17 | 2 tasks | 13 files |
|
| 68 |
| Phase 02 P05 | 12 | 2 tasks | 7 files |
|
| 69 |
+
| Phase 02 P06 | 38 | 3 tasks | 10 files |
|
| 70 |
|
| 71 |
## Accumulated Context
|
| 72 |
|
|
|
|
| 146 |
- [Phase 02]: Three plan-assumed word levels corrected to what the pinned lists say and pinned by JMdict id, not patched: 日本語 1464530 is N1+ (on no list; kanji all N5 - the two-axis example), ない/なかった resolve to 無い 1529520 which no list carries (N1+), こんにちは 1289400 is N3; 人気 read ひとけ is the separate entry 1367020 (N1+). A level-alias table is the honest fix if the product wants otherwise (02-10 review), never a weaker lookup
|
| 147 |
- [Phase 02]: levels.is_kanji IS ruby.is_kanji (02-03's parallel-wave copy removed): one predicate decides both ruby and the kanji-axis gate, so supplementary-plane kanji (𠮷) reach the axis as unlisted instead of being skipped
|
| 148 |
- [Phase 02]: The fixed sample sentence set (SC-1) follows the golden_timeline pattern: 28 sentences / 115 records generated by the real pipeline, 18 EXPECTED facts checked before writing (exit 1, nothing written otherwise), frozen LF byte-for-byte with data pins read from the environment; JPN-01 still bound to 02-10's rows
|
| 149 |
+
- [Phase 02]: warm_language() is a lock around an lru_cache'd body: lru_cache alone lets two concurrent first callers load the compact JMdict twice (CPython calls the function outside any lock), proven by a two-thread test; every server function and turn's analyze stage call it first so a request during Blocks.load waits for the singletons instead of racing a second +300 MB load
|
| 150 |
+
- [Phase 02]: Language warm-up measured: local bare process tokenizer 0.04 s / JMdict 2.4 s / translator 0.34 s, RSS +467 MB vs research +410 (02-03's +312 MB JMdict accounts for it); inside app.py +558 MB because warm_synthesizer shares the Blocks.load window; rss_mb() reads /proc on the Space, the Windows working set via ctypes locally (the GetCurrentProcess pseudo-handle must be declared HANDLE)
|
| 151 |
+
- [Phase 02]: Bridge helpers are requireBridge() + checkResult(), not one callBridge(name, payload): every turn-loop call site keeps its literal one-payload bridge.x({...}) for the seam grep and dispatchTurn keeps 'await bridge.' on the line the thinking-before-first-await guard reads; transports changed by 0 lines
|
| 152 |
+
- [Phase 02]: A full-suite timing failure (strict-autoplay tap-to-running 11.5 s > Phase 1's 10 s sanity ceiling) was A/B'd against the pre-plan commit in a throwaway worktree on the same machine (6.2 s baseline vs 6.1 s HEAD inline) and a stamped probe showed the load warm-up finishing before the first frame: not a regression; the machine-speed ceiling is logged to deferred-items, the load-bearing relative assertion held every run
|
| 153 |
|
| 154 |
### Pending Todos
|
| 155 |
|
|
|
|
| 171 |
|
| 172 |
## Session Continuity
|
| 173 |
|
| 174 |
+
Last session: 2026-09-07T00:52:28.896Z
|
| 175 |
+
Stopped at: Completed 02-06-PLAN.md (wave 4): analyze/translate/language_info server functions + tokens on the directive, lock-serialised language warm-up on Blocks.load (+467 MB local vs +410 expected), analyze/translate/languageInfo on the shared turn loop, parity under both transports; quick loop 308 passed / 33.4 s; whole e2e 26 passed. Next 02-07.
|
| 176 |
Resume file: None
|
| 177 |
|
| 178 |
**Roadmap-level open decision:** the Phase 1 latency trigger (p50 7.1 s, ~90 % server synthesis) fired; recorded in `docs/LATENCY.md` and in 02-CONTEXT.md § Deferred. Phase 2 adds a CPU translation model to the same container — measure on the Space. Consider `/gsd:insert-phase` for streaming/synthesis relocation before Phase 3 lengthens turns.
|
.planning/phases/02-japanese-language-core/02-06-SUMMARY.md
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
phase: 02-japanese-language-core
|
| 3 |
+
plan: 06
|
| 4 |
+
subsystem: ui
|
| 5 |
+
tags: [gradio, gr.HTML, server_functions, bridge, turn-loop, facade, warm-up, rss, playwright, parity, sc-4, tdd]
|
| 6 |
+
|
| 7 |
+
# Dependency graph
|
| 8 |
+
requires:
|
| 9 |
+
- phase: 02-japanese-language-core
|
| 10 |
+
plan: 05
|
| 11 |
+
provides: "analyzer.analyze(text) -> 14-key records, analyzer.warmup(); the 28-sentence golden fixture the parity assertions are drawn from"
|
| 12 |
+
- phase: 02-japanese-language-core
|
| 13 |
+
plan: 04
|
| 14 |
+
provides: "translate.translate() / warmup() / model_bytes() / MAX_TEXT_CHARS; the LFS CT2 int8 model; the mt marker"
|
| 15 |
+
- phase: 02-japanese-language-core
|
| 16 |
+
plan: 03
|
| 17 |
+
provides: "jmdict.load() (218,672 entries, meta.version) and levels; jmdict.COMPACT_PATH"
|
| 18 |
+
- phase: 01-voice-avatar-loop-skeleton
|
| 19 |
+
plan: 08
|
| 20 |
+
provides: "one-payload server functions that never raise, warm_synthesizer on Blocks.load, AVATAR_SURFACE as data, turn-loop.js as the only turn implementation, the parity suite"
|
| 21 |
+
- phase: 01-voice-avatar-loop-skeleton
|
| 22 |
+
plan: 11
|
| 23 |
+
provides: "the strict-autoplay layer of the parity suite and its tap-to-running sanity ceiling (re-measured here)"
|
| 24 |
+
provides:
|
| 25 |
+
- "src/japanese_avatar/nlp/warm.py: warm_language() - lock-serialised, lru_cache-idempotent warm-up of tokenizer + compact JMdict + CT2 translator with per-component seconds, RSS before/after/delta beside EXPECTED_WARM (+410 MB), CPU_CORES/MEMORY, errors[]; rss_mb() via /proc, psutil or the Windows working set"
|
| 26 |
+
- "src/japanese_avatar/ui/blocks.py: analyze(payload), translate(payload), language_info(payload) server functions (one positional, {error} on every failure); turn() attaches tokens inside an analyze stage; warm_language_on_load bound to Blocks.load; server_functions=[turn, greeting, analyze, translate, language_info]"
|
| 27 |
+
- "src/japanese_avatar/voice/models.py: AvatarDirective.tokens: list[dict] = field(default_factory=list)"
|
| 28 |
+
- "avatar/turn-loop.js: analyze(text), translate(text, lineId), languageInfo() implemented once for both transports; requireBridge()/checkResult() shared with dispatchTurn; tokens on the turn event; 9 language debug keys initialised at construction"
|
| 29 |
+
- "avatar/facade.js: AVATAR_SURFACE += analyze, translate, languageInfo (15 names)"
|
| 30 |
+
- "tests/test_server_functions.py (14 tests; 3 mt): payload shapes, {error} paths, language_info shape with and without the real warm, lock serialisation, failing component, tokens on the directive, every server function waits for warm_language()"
|
| 31 |
+
- "tests/test_no_gpu_on_turn_path.py: transformers banned; reachability walked from turn/analyze/translate/language_info with same-named definitions merged"
|
| 32 |
+
- "tests/test_transport_seam.py (+12 items): language surface declared once, lives only in turn-loop.js, debug keys, tokens inside emit('turn'), one payload per bridge call"
|
| 33 |
+
- "tests/e2e/test_facade_parity.py::test_language_bridge_under_both_transports[inline|iframe]"
|
| 34 |
+
affects: [02-07, 02-08, 02-09, 02-10, 02-11, phase-3-tutor-turns]
|
| 35 |
+
|
| 36 |
+
# Tech tracking
|
| 37 |
+
tech-stack:
|
| 38 |
+
added: []
|
| 39 |
+
patterns:
|
| 40 |
+
- "Every language server function calls warm_language() before touching a singleton: after the first call it is a lock acquire plus a cached dict; during Blocks.load it makes the request wait for the singletons instead of racing lru_cache misses into a second +300 MB JMdict load"
|
| 41 |
+
- "The warm-up measurement is the lru_cache'd return value of the locked body - one read-only dict, never a module-level mutable - so language_info can report it without a half-filled cache"
|
| 42 |
+
- "Bridge helpers split in two (requireBridge + checkResult) rather than one callBridge(name, payload): every call site keeps its literal one-payload `bridge.x({...})` for the seam grep and dispatchTurn keeps `await bridge.` on the line the thinking-before-await guard reads"
|
| 43 |
+
- "Quick-loop tests that only need the analyzer redirect blocks.warm_language to a dictionary-only stub (fixture dictionary_only_warm); the real warm-up is exercised under the mt marker and by the parity suite, and one counting test proves each server function calls it"
|
| 44 |
+
- "A timing-bound failure in the full suite is A/B'd against the pre-plan commit in a throwaway worktree on the same machine before it is called a flake or a regression"
|
| 45 |
+
|
| 46 |
+
key-files:
|
| 47 |
+
created:
|
| 48 |
+
- src/japanese_avatar/nlp/warm.py
|
| 49 |
+
- tests/test_server_functions.py
|
| 50 |
+
modified:
|
| 51 |
+
- src/japanese_avatar/ui/blocks.py
|
| 52 |
+
- src/japanese_avatar/voice/models.py
|
| 53 |
+
- avatar/turn-loop.js
|
| 54 |
+
- avatar/facade.js
|
| 55 |
+
- tests/test_directive.py
|
| 56 |
+
- tests/test_no_gpu_on_turn_path.py
|
| 57 |
+
- tests/test_transport_seam.py
|
| 58 |
+
- tests/e2e/test_facade_parity.py
|
| 59 |
+
|
| 60 |
+
key-decisions:
|
| 61 |
+
- "warm_language() = with _WARM_LOCK: return _warm_once() where _warm_once is lru_cache(maxsize=1): proven by test_warm_language_first_call_is_serialised (two concurrent first callers, each component ran exactly once, same dict object) - lru_cache alone runs the body twice because CPython calls the function outside any lock"
|
| 62 |
+
- "rss_mb() gained a Windows working-set route (psapi.GetProcessMemoryInfo through ctypes) so this machine measures instead of printing None; the pseudo-handle must be declared HANDLE or it is truncated to a 32-bit int and the call fails with 'The handle is invalid' (measured, fixed)"
|
| 63 |
+
- "The version pins language_info reports are READ, not typed: sudachipy.__version__, importlib.metadata for the dictionary, jmdict.load().meta['version'], and data/jlpt/README.md + data/mt/README.md for the two pins that live in scripts/ (the package must not import scripts/)"
|
| 64 |
+
- "analyze/turn share one _analyze_stage(text, timings) so `stage(\"analyze\")` appears once; analyze() turns its exception into {error}, turn() turns it into tokens=[] with a logged traceback so the avatar still speaks"
|
| 65 |
+
- "The quick loop stays translator-free: analyze/turn tests run with blocks.warm_language stubbed to a dictionary-only warm (the session analyzer fixture already holds the JMdict), and test_server_functions_wait_for_warm_language proves the real call sites with a counter; the real warm runs under mt (3 tests) and in the browser suite"
|
| 66 |
+
- "The one full-suite failure (test_gesture_unlocks_audio_under_both_transports[inline], tap-to-running 11,457 ms > 10,000 ms sanity ceiling) was A/B'd: the pre-plan commit 16f28bd in a worktree reads 6,160 ms inline / 2,976 ms iframe on the same machine minutes later, HEAD reads 6,084 / 4,228 ms - the same starved profile either way; not a regression, the Phase 1 ceiling (written at 0.7-3.5 s) is logged to deferred-items for re-measurement"
|
| 67 |
+
|
| 68 |
+
patterns-established:
|
| 69 |
+
- "Warm-up components are separate module functions (_warm_tokenizer/_warm_jmdict/_warm_translator) so a test can substitute a slow or failing one and prove the lock or the never-raise rule without touching the real singletons"
|
| 70 |
+
- "A parity test parks a slow promise on window and waits for the event it emits with the turn budget, instead of awaiting page.evaluate with no timeout"
|
| 71 |
+
|
| 72 |
+
requirements-completed: [] # JPN-01 / JPN-04 are bound to 02-10's rows (02-VALIDATION.md); not marked here
|
| 73 |
+
|
| 74 |
+
# Metrics
|
| 75 |
+
duration: 38min
|
| 76 |
+
completed: 2026-09-07
|
| 77 |
+
---
|
| 78 |
+
|
| 79 |
+
# Phase 02 Plan 06: Server Functions, Directive Tokens and the Language Bridge Summary
|
| 80 |
+
|
| 81 |
+
**The language core reaches the browser the way the voice does: three payload-taking, never-raising server functions (`analyze`, `translate`, `language_info`) behind the `gr.HTML` bridge, three surface methods implemented once in `turn-loop.js` so inline and iframe get them together, the analyzer's tokens on every directive and on the `turn` event, and the tokenizer + compact JMdict + CT2 translator warmed on `Blocks.load` under a lock with their cost measured (local: 0.04 s + 2.4 s + 0.34 s, +467 MB bare / +558 MB in the app beside the researched +410) - proven end to end under both transports before any DOM is written.**
|
| 82 |
+
|
| 83 |
+
## Performance
|
| 84 |
+
|
| 85 |
+
- **Duration:** 38 min (started 2026-09-07T00:10:44Z, last task commit `f743d45` at ~00:41Z, verification and A/B to 00:49Z)
|
| 86 |
+
- **Tasks:** 3 (Task 1 TDD: RED + GREEN; Tasks 2-3 feature/test)
|
| 87 |
+
- **Files:** 10 (2 created, 8 modified), +1,110 / -30 lines
|
| 88 |
+
- **Quick loop** (`pytest tests --ignore=tests/e2e -m "not mt"`): **308 passed, 17 deselected, 33.4 s** pytest (was 293 / 14 / 28.8 s after 02-05; budget 40 s). The +15 items and +4.6 s are the 11 quick-loop server-function tests (two turn syntheses, one 50 ms-per-component lock test) plus the 4 new SC-4 walks.
|
| 89 |
+
- **`mt` subset** (`tests/test_server_functions.py -m mt`): 3 passed in 6.6 s; `translate('駅はどこですか。') -> 'Where is the station?'` in **36.9-45.8 ms** server-side, idle.
|
| 90 |
+
- **Parity file** (`tests/e2e/test_facade_parity.py`): **12 passed in 174.9 s** (10 existing + 2 new).
|
| 91 |
+
- **Whole e2e, one invocation** (`pytest tests/e2e/`): **26 passed, 18 skipped (all `deployed`, need `--space-url`), 0 failed, 239.9 s**. No `test_asr_standalone` re-run was needed.
|
| 92 |
+
- **Whole suite, one invocation** (`pytest tests -m "not deployed"`): **366 passed, 1 failed, 18 deselected, 282.9 s** - the failure is the Phase 1 strict-autoplay sanity ceiling under load, A/B'd against the pre-plan commit and found identical (see Deviations / Issues).
|
| 93 |
+
|
| 94 |
+
### Measured (the plan's `<output>` list)
|
| 95 |
+
|
| 96 |
+
| Fact | Measured | Expected (research / plan) |
|
| 97 |
+
|---|---|---|
|
| 98 |
+
| `system.dic` path | `.venv/Lib/site-packages/sudachidict_core/resources/system.dic` (`importlib.resources.files("sudachidict_core") / "resources" / "system.dic"`) | same layout |
|
| 99 |
+
| `system.dic` bytes | **217,466,039** | ~217 MB |
|
| 100 |
+
| `model.bin` bytes | **77,339,435** | 77,339,435 |
|
| 101 |
+
| compact JMdict bytes / entries | 7,662,785 / **218,672** | 218,672 |
|
| 102 |
+
| warm-up, bare process (`python -c`, fresh) | tokenizer **0.04 s**, jmdict **2.37-2.41 s**, translator **0.34 s**, total **2.75-2.80 s** | 0.03 / 2.0-2.4 / 0.35 (02-05, 02-04) |
|
| 103 |
+
| warm-up, inside `app.py` on `Blocks.load` (headless suite running) | tokenizer 0.06-2.08 s, jmdict **2.60-4.50 s**, translator 0.39-0.48 s, total **4.99-5.17 s** | - |
|
| 104 |
+
| RSS delta, bare process (Windows working set) | **+466.5 / +469 MB** (126 -> 593 MB; 141 -> 609 MB) | **+~410 MB** = 62 mmap + 241 JMdict + 103 CT2 |
|
| 105 |
+
| RSS delta, inside `app.py` | **+558 MB** (137-138 -> 695-696 MB) | the extra ~90 MB is `warm_synthesizer` running concurrently on the same `Blocks.load` and landing inside the window |
|
| 106 |
+
| `languageInfo().rss_mb` after a turn (app process) | 699-700 MB | - |
|
| 107 |
+
| `CPU_CORES` / `MEMORY` | `None` / `None` locally (the Space sets them; 02-10 records them) | - |
|
| 108 |
+
| `translate` under **inline**, in the browser suite | **1,038 ms** wall / 838 ms server | 22-286 ms idle (02-04); headless SwiftShader + two app processes starve the CPU (Phase 1 lesson) |
|
| 109 |
+
| `translate` under **iframe**, in the browser suite | **977 ms** wall / 872 ms server | same |
|
| 110 |
+
| `analyze` in the browser suite | 37.9 ms first call / 9.1 ms (inline); 0.8 ms (iframe) server-side; 96-190 ms wall | 0.14-0.19 ms warm, idle (02-05) - the first call after warm-up pages the mmap in |
|
| 111 |
+
| `analyze_ms` on the `turn` directive | 0.29 ms (inline) / 2.25 ms (iframe); 122 ms on the very first analysis after a fresh warm-up | - |
|
| 112 |
+
| greeting synthesis, headless | 7,973-18,987 ms (`synthesis_ms`) | Phase 1: 10-19 s headless vs 1.6 s headed |
|
| 113 |
+
| pins reported | sudachipy 0.6.11, sudachidict_core 20260723, jmdict 3.6.2+20260831182826, jlpt_vocab 2025.08.01.0, opus_mt_revision 0770961a39ba6bd66305b149c3f4110bcafca2e6 | all read from the environment / data READMEs |
|
| 114 |
+
|
| 115 |
+
The research's +410 MB is exceeded by ~57 MB in a bare process. 02-03 already measured the compact JMdict at +312 MB here (research said 241), which on its own accounts for the difference; the Windows working set also counts touched pages of the 217 MB `system.dic` mmap. The Space's number (`/proc/self/status` VmRSS) is 02-10's to record via `languageInfo()`.
|
| 116 |
+
|
| 117 |
+
## Accomplishments
|
| 118 |
+
|
| 119 |
+
- **Server side (Task 1).** `analyze` / `translate` / `language_info` accept a dict, a list or a plain string (`_unpack`'s tolerance through a shared `_text_only(payload, what)`), refuse empty / whitespace / `None` / `[]` / >200 chars with `{"error": ...}` (wording `Nothing to analyse.` / `Nothing to translate.` / `Keep it to 200 characters (N given).`), and wrap every exception into `{"error": "analysis failed: ..."}` / `"translation failed: ..."` / `"language_info failed: ..."` with a logged traceback. `translate` echoes `line_id` (dict key or second list element). `turn()` gains `tokens` through `_tokens_for_turn` -> `_analyze_stage` (one `stage("analyze")`); a broken analyzer yields `tokens: []` and the avatar still speaks (`test_turn_speaks_when_analysis_fails`). `build_blocks` registers all five and binds `warm_language_on_load` beside `warm_synthesizer`.
|
| 120 |
+
- **`nlp/warm.py`.** `EXPECTED_WARM` recorded beside every measurement, `_WARM_LOCK` around the `lru_cache`'d body, three substitutable component functions, `_timed()` so a failing component logs, records `None` seconds and an `errors` entry, and never raises (`test_warm_language_survives_a_failing_component`). One INFO line: `language warm-up tokenizer 0.04s jmdict 2.37s translator 0.34s; CPU_CORES=None MEMORY=None rss=593 MB (delta 467 MB, expected +~410)`.
|
| 121 |
+
- **SC-4 carried forward.** `transformers` joins `torch` / `spaces` in the banned imports; the reachability walk starts from each of the four server functions and, as a strengthening, merges every same-named definition (previously `setdefault` kept only the first `analyze` / `translate` it met - the nlp module's, never the server function's). `zerogpu_probe` is unreachable from all four; `warm_language` is reachable from all four (the "every server function waits" property, asserted statically as well as by counter).
|
| 122 |
+
- **Browser side (Task 2).** `analyze(text)` rejects `analyze: text is required` before any bridge call, stamps `analyzeCount` / `lastAnalyzeMs`, returns `{tokens, timings}`; `translate(text, lineId)` returns `{text, lineId, timings, ms}` and stamps `translateCount` / `lastTranslateMs` / `lastTranslation` / `lastTranslateError`; `languageInfo()` caches into `state.languageInfo`. `dispatchTurn` now uses the same `requireBridge` / `checkResult` pair, sets `lastTokens` / `lastTokenCount` from the directive and emits `tokens` on the `turn` event; its `setThinking(true)`-before-`await bridge.` order and `stagePort.unlockAudio()`-first statement are unchanged (both guards green). `avatar.js`, `avatar-iframe.js`, `stage.html`: **0 lines changed** since `16f28bd`.
|
| 123 |
+
- **Parity (Task 3).** Under both transports against live objects: 食べました -> one tappable unit, lemma 食べる, reading たべました, N5, gloss present; 今日はいい天気ですね。 -> surfaces tile the text, 今日 reads きょう, は is not tappable; 駅はどこですか。 -> "Where is the station?" filed under `L1`, `translateCount 1`, `lastTranslateMs > 0`; `languageInfo()` -> 218,672 entries, 77,339,435-byte model, 217,466,039-byte dictionary, `warm.total_s` a number, `warm.errors == []`, the five pins; the `turn` event's `tokens` has the one 食べる unit and `lastTokenCount == 1`; `analyze('')` rejects with `analyze: text is required` and `translate('あ' x 201)` rejects with `Keep it to 200 characters (201 given).` - the server's `{error}` surfacing as a rejection carrying its words, never `undefined`. `test_turn_surface_is_live_under_both_transports` still passes with 15 names (the no-arg probes reject for ordinary reasons; `languageInfo()` simply succeeds).
|
| 124 |
+
|
| 125 |
+
## Task Commits
|
| 126 |
+
|
| 127 |
+
| Task | Commit | Type |
|
| 128 |
+
|---|---|---|
|
| 129 |
+
| 1 RED - failing server-function, directive and SC-4 tests | `ac630fa` | test |
|
| 130 |
+
| 1 GREEN - warm.py, blocks.py server functions + warm-up, tokens field | `a60327b` | feat |
|
| 131 |
+
| 2 - turn-loop.js / facade.js surface + seam guards | `969caf4` | feat |
|
| 132 |
+
| 3 - language bridge parity under both transports | `f743d45` | test |
|
| 133 |
+
|
| 134 |
+
All commits `--no-verify`, explicit paths only; nothing pushed; no `git add -A`.
|
| 135 |
+
|
| 136 |
+
## Files Created/Modified
|
| 137 |
+
|
| 138 |
+
- `src/japanese_avatar/nlp/warm.py` (new, 203 lines) - `EXPECTED_WARM`, `_WARM_LOCK`, `rss_mb()` + `_windows_working_set_mb()`, `_warm_tokenizer/_warm_jmdict/_warm_translator`, `_timed`, `_warm_once` (lru_cache), `warm_language`
|
| 139 |
+
- `src/japanese_avatar/ui/blocks.py` - imports; `_text_only`, `_line_id`, `_analyze_stage`, `_tokens_for_turn`, `analyze`, `translate`, `_system_dic_bytes`, `_pin_from_readme`, `_pins` (lru_cache), `language_info`, `warm_language_on_load`; `turn()` analyze stage + `tokens=`; `server_functions=[turn, greeting, analyze, translate, language_info]`; second `blocks.load`
|
| 140 |
+
- `src/japanese_avatar/voice/models.py` - `field` import; `tokens: list[dict] = field(default_factory=list)` as the last field, docstring line
|
| 141 |
+
- `avatar/turn-loop.js` - header paragraph; 9 state keys; `requireBridge` / `checkResult`; `dispatchTurn` refactor + tokens; `analyze` / `translate` / `languageInfo`
|
| 142 |
+
- `avatar/facade.js` - three doc lines, three surface names
|
| 143 |
+
- `tests/test_server_functions.py` (new, 14 tests) - as listed in `provides`
|
| 144 |
+
- `tests/test_directive.py` - `DIRECTIVE_KEYS` += `tokens`
|
| 145 |
+
- `tests/test_no_gpu_on_turn_path.py` - docstring, `BANNED_IMPORT_ROOTS`, `SERVER_ENTRY_POINTS`, `_reachable_from(entry)` with merged definitions, `test_gpu_probe_unreachable_from_every_server_function[4]`
|
| 146 |
+
- `tests/test_transport_seam.py` - `LANGUAGE_SURFACE`, `LANGUAGE_DEBUG_KEYS`, 5 new tests (12 items), parametrisation extended
|
| 147 |
+
- `tests/e2e/test_facade_parity.py` - `EAT` / `WEATHER` / `STATION`, `REJECTION`, `LANGUAGE_DEBUG`, `_unit`, `test_language_bridge_under_both_transports[2]`
|
| 148 |
+
|
| 149 |
+
## Decisions Made
|
| 150 |
+
|
| 151 |
+
See `key-decisions` in the frontmatter. The two that shape later plans: every server function (and `turn`'s analyze stage) blocks on `warm_language()`, so a request during `Blocks.load` waits for the singletons - correct by construction, and it means a visitor who taps "Say hello" before the load warm-up finishes waits for it (locally the warm-up finished at t = 9.6 s, before the first rendered frame at 10.8 s, so there was no overlap; on the 3-6x slower Space that ordering is 02-10's to measure). And the quick loop stays translator-free by stubbing `blocks.warm_language` in the analyzer-only tests, with the real call sites proven by a counter and the real warm-up proven under `mt` and in the browser.
|
| 152 |
+
|
| 153 |
+
## Deviations from Plan
|
| 154 |
+
|
| 155 |
+
### Auto-fixed Issues
|
| 156 |
+
|
| 157 |
+
**1. [Rule 1 - Bug] The Windows RSS fallback silently returned `None`**
|
| 158 |
+
- **Found during:** Task 1 GREEN - the acceptance one-liner printed `ok None` although a ctypes working-set route had been added.
|
| 159 |
+
- **Issue:** `GetCurrentProcess()` returns the pseudo-handle `(HANDLE)-1`; without a declared `restype` ctypes truncates it to a 32-bit `int`, and `GetProcessMemoryInfo` fails with `[WinError 6] The handle is invalid`, which the "measurement only" `except` turned into `None`.
|
| 160 |
+
- **Fix:** declared `GetCurrentProcess.restype = HANDLE` and `GetProcessMemoryInfo.argtypes/restype`; the bare-process delta is now a number (+467 MB) instead of "unreadable".
|
| 161 |
+
- **Files modified:** `src/japanese_avatar/nlp/warm.py`
|
| 162 |
+
- **Committed in:** `a60327b`
|
| 163 |
+
|
| 164 |
+
**2. [Rule 2 - Correctness of the guard] SC-4's call graph kept only the FIRST definition per name**
|
| 165 |
+
- **Found during:** Task 1, reading `_reachable_from_turn` before parametrising it.
|
| 166 |
+
- **Issue:** `functions.setdefault(node.name, node)` over `sorted(SRC.rglob("*.py"))` binds `analyze` to `nlp/analyzer.py`'s and `translate` to `nlp/translate.py`'s definition, so a walk "from `analyze`" would never have seen the server function's calls (`warm_language`, `_analyze_stage`, ...). The plan says "extend, do not weaken"; this would have been a silent weakening.
|
| 167 |
+
- **Fix:** `functions: dict[str, list[FunctionDef]]`; the walk unions the calls of every definition of a name. Still an over-approximation (the safe direction).
|
| 168 |
+
- **Files modified:** `tests/test_no_gpu_on_turn_path.py`
|
| 169 |
+
- **Committed in:** `ac630fa`
|
| 170 |
+
|
| 171 |
+
**3. [Rule 3 - Blocking] The plan's single `callBridge(name, payload)` helper conflicts with two existing seam guards**
|
| 172 |
+
- **Found during:** Task 2 design.
|
| 173 |
+
- **Issue:** `test_thinking_engages_before_the_first_await` asserts `"await bridge." in body[await_at]` for dispatchTurn's first await, and the plan's own new `test_bridge_calls_take_one_payload` greps the literal `bridge.analyze({` / `bridge.translate({` / `bridge.language_info(`. A generic `bridge[name](payload)` helper satisfies neither.
|
| 174 |
+
- **Fix:** two helpers - `requireBridge(name, without)` (get + throw `no host bridge: ...`) and `checkResult(result, what)` (`undefined` / `{error}`) - used by dispatchTurn and the three new methods; every call site keeps its literal one-payload call. Same behaviour, same messages (`the host returned nothing for this turn - see its log` preserved verbatim).
|
| 175 |
+
- **Files modified:** `avatar/turn-loop.js`
|
| 176 |
+
- **Committed in:** `969caf4`
|
| 177 |
+
|
| 178 |
+
### Scope notes (not deviations)
|
| 179 |
+
|
| 180 |
+
- `tests/test_server_functions.py` has 14 tests where the plan enumerated 10: added `test_language_info_never_raises`, `test_warm_language_survives_a_failing_component`, `test_rss_mb_is_a_number_or_none`, `test_turn_speaks_when_analysis_fails` and the plan's acceptance-listed `test_server_functions_wait_for_warm_language`. Each pins a rule the plan states in prose (never raise; startup must not take the page down; the turn must still speak).
|
| 181 |
+
- `warm_language()`'s dict carries an `errors: list[str]` key beyond the plan's list (empty on success) so a failed component is visible in `languageInfo()` rather than only in a log the browser cannot see. `test_language_bridge_under_both_transports` asserts it is `[]`.
|
| 182 |
+
- The quick-loop `analyze` tests stub `blocks.warm_language` (see Decisions) - the plan's `test_analyze_accepts_the_bridge_payload_shapes` would otherwise have loaded the translator into the quick loop, which the `mt` marker exists to prevent.
|
| 183 |
+
|
| 184 |
+
---
|
| 185 |
+
|
| 186 |
+
**Total deviations:** 3 auto-fixed (1 Rule 1, 1 Rule 2, 1 Rule 3). No architectural change; no scope creep; nothing pushed; `state` commands not run before this SUMMARY; requirements not marked.
|
| 187 |
+
|
| 188 |
+
## Issues Encountered
|
| 189 |
+
|
| 190 |
+
- **One failure in the whole-suite run, investigated to a conclusion rather than re-run to green.** `pytest tests -m "not deployed"` (366 passed, 1 failed): `test_gesture_unlocks_audio_under_both_transports[inline]`, `assert 11457.0 <= 10000` on `running_after_click_ms` - the strict-autoplay layer's *absolute sanity ceiling* (its comment: 0.7 s idle, "up to ~3.5 s with two Gradio apps synthesising under SwiftShader"). The same test passed in the e2e-only run (26 passed) and in two isolated re-runs (8,288 ms; 6,084 ms). Because this plan adds a post-load CPU burst to the app process (JMdict parse + translator load) and a lock wait to `turn()`, it could plausibly have been the cause, so two measurements were taken instead of assuming: (a) a stamped probe of a fresh app + headless page showed the language warm-up finishing at t = 9.62 s, *before* the first frame (10.78 s) and the tap, tap-to-running 1.53 s, and the 20.0 s response entirely `synthesis_ms` = 18,987 ms (Phase 1's headless 10-19 s range) with `analyze_ms` 122 ms; (b) an A/B in a detached worktree at the pre-plan commit `16f28bd`, same venv, same machine, minutes apart: **baseline 6,160 ms inline / 2,976 ms iframe (response 34.9 s / 14.6 s); HEAD 6,084 ms / 4,228 ms (16.6 s / 21.6 s)**. Identical profile with and without this plan's code: the machine is 2-3x slower today than when the ceiling was written (RAM 31.4 GiB, 10.1 GiB free at 67 % load, 16 logical CPUs). Recorded in `deferred-items.md` for re-measurement; the ceiling was not touched (Phase 1's test, out of scope). The relative, load-bearing assertion (`running` observed before the server answered) held in every run.
|
| 191 |
+
- A Git Bash heredoc that carried backticks (the `release tag \`...\`` regex) failed to parse; the `blocks.py` and JS patches were applied from scratchpad Python scripts instead.
|
| 192 |
+
- `git` printed its usual CRLF-will-be-replaced warning for the appended `test_facade_parity.py`; the committed blob is LF (`core.autocrlf=input`), as in 02-04/02-05.
|
| 193 |
+
- `ruff format` reflowed the seam test's `LANGUAGE_DEBUG_KEYS` list and one long return in the parity test; `pytest -q` over `addopts="-q"` hides counts, so counts here were taken with `-o addopts="--strict-markers"`.
|
| 194 |
+
- `psutil` is not installed in the venv (deliberately: not a dependency); the Windows route made the local measurement possible without adding one. On the Space `/proc/self/status` is the route.
|
| 195 |
+
|
| 196 |
+
## Known Stubs
|
| 197 |
+
|
| 198 |
+
None. `tokens` on the directive is the analyzer's real output (or `[]` only after a logged analysis failure, which the parity suite shows does not happen); `languageInfo()` reports measured sizes, a measured warm-up and pins read from the environment; `cpu_cores` / `memory` are `None` locally because the container variables are the Space's, not placeholders - 02-10 records them from the deployed `languageInfo()`.
|
| 199 |
+
|
| 200 |
+
## User Setup Required
|
| 201 |
+
|
| 202 |
+
None. Nothing pushed to the `space` remote; no external service touched.
|
| 203 |
+
|
| 204 |
+
## Next Phase Readiness
|
| 205 |
+
|
| 206 |
+
- **02-07 (furigana) / 02-08 (popover):** the `turn` event now carries `tokens` (same records as `analyze()`), `state.lastTokens` / `lastTokenCount` are on `getDebug()`, and `window.Avatar.analyze(text)` tokenises the learner's own lines (`{tokens, timings}`; rejects `analyze: text is required` on empty). Glosses ride inside each token - no lookup round trip. `host.js` does not yet call any of the three methods; the seam test's host-call assertion (`test_host_glue_is_neither_a_transport_nor_the_turn_loop`) deliberately does not require it until those plans wire it.
|
| 207 |
+
- **02-09 (translation reveal):** `window.Avatar.translate(text, lineId)` -> `{text, lineId, timings, ms}`; rejects with the server's `{error}` words (`Keep it to 200 characters (N given).`) and sets `lastTranslateError`; `translateCount` / `lastTranslateMs` / `lastTranslation` are on `getDebug()`. The per-line cache is the host page's (D-17); the loop holds none. Local wall time in the browser suite was ~1.0 s under SwiftShader load vs 37-46 ms idle - design the loading state for seconds, as research says.
|
| 208 |
+
- **02-10 (deployed):** `languageInfo()` right after `ready` blocks on the load warm-up and returns `warm` (seconds per component, `rss_before_mb` / `rss_mb` / `rss_delta_mb` from `/proc`, `expected_rss_delta_mb: 410`, `errors`), `cpu_cores` / `memory` from the container, `rss_mb` now, sizes and pins - record them in `docs/HOSTING.md § Phase 2 container record` and `docs/LATENCY.md § Translation`. **Measure the first "Say hello" tapped right after `ready`:** `turn()` waits on `warm_language()` before its analyze stage, so on the 3-6x slower Space a fast first tap may wait for the JMdict load (locally the warm-up finished 1.2 s before the first frame; the Space's ordering is unknown). If it bites, the levers are ordering `warm_language_on_load` before `warm_synthesizer`, or a non-blocking first analysis (tokens `[]` on the very first line) - a product call, not this plan's.
|
| 209 |
+
- **02-11 (docs):** the warm-up log line format and the `languageInfo()` shape are in `nlp/warm.py` / `blocks.language_info` docstrings; the RSS expectation table is `EXPECTED_WARM`.
|
| 210 |
+
- **Quick-loop budget:** 33.4 s of 40 s; this plan added ~4.6 s. Anything new that needs a turn should reuse `test_server_functions.py`'s stubbed-synthesis pattern rather than synthesise again.
|
| 211 |
+
|
| 212 |
+
---
|
| 213 |
+
*Phase: 02-japanese-language-core*
|
| 214 |
+
*Completed: 2026-09-07*
|
| 215 |
+
|
| 216 |
+
## Self-Check: PASSED
|
| 217 |
+
|
| 218 |
+
All 2 created files (`nlp/warm.py`, `tests/test_server_functions.py`) and 8 modified files present on disk; commits `ac630fa`, `a60327b`, `969caf4`, `f743d45` present in `git log`; `warm.py` is 203 lines; `tests/test_server_functions.py` holds 14 `def test_` (3 `mt`); `AVATAR_SURFACE` has 15 names; `avatar.js` / `avatar-iframe.js` / `stage.html` differ from `16f28bd` by 0 lines; quick loop 308 passed / 17 deselected / 33.4 s; parity file 12 passed; whole e2e 26 passed / 18 skipped / 0 failed; repo-wide `ruff check . && ruff format --check .` clean (55 files). The SUMMARY's test count was corrected from an initial over-claim of 17 to the measured 14 during this check. Nothing pushed; `requirements mark-complete` not run (JPN-01 / JPN-04 are 02-10's rows).
|
.planning/phases/02-japanese-language-core/deferred-items.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Deferred items - Phase 02
|
| 2 |
+
|
| 3 |
+
Out-of-scope discoveries logged during execution, not fixed in the plan that found them.
|
| 4 |
+
|
| 5 |
+
## Logged by 02-06 (2026-09-07)
|
| 6 |
+
|
| 7 |
+
- **`tests/e2e/conftest.py::UNLOCK_BEFORE_RESPONSE_MS = 10_000` no longer clears this machine under
|
| 8 |
+
full-suite load.** The strict-autoplay layer's *absolute* tap-to-`running` sanity ceiling (Phase 1,
|
| 9 |
+
plan 01-11) was written when the number measured 0.7 s idle and "up to ~3.5 s with two Gradio apps
|
| 10 |
+
synthesising under SwiftShader". On 2026-09-07 the pre-plan commit `16f28bd` (a detached worktree,
|
| 11 |
+
same venv, same browser) reads **6,160 ms inline / 2,976 ms iframe** in isolation, HEAD reads
|
| 12 |
+
6,084 / 4,228 ms, and the whole-suite run hit 11,457 ms on inline once. The relative, load-bearing
|
| 13 |
+
assertion (`running` before the server's `turn` answer) held every time; the ceiling is a
|
| 14 |
+
machine-speed constant, not a product property. Options: re-measure and raise the ceiling with the
|
| 15 |
+
new measurement recorded beside it, or make the ceiling a multiple of the observed
|
| 16 |
+
`response_after_click_ms`. Owner of the test is Phase 1's strict-autoplay proof, so it was not
|
| 17 |
+
touched from 02-06.
|
| 18 |
+
- **First-turn wait behind the language warm-up on the Space.** `turn()` (and `analyze` /
|
| 19 |
+
`translate` / `language_info`) block on `warm_language()`; locally the load warm-up finishes 1.2 s
|
| 20 |
+
before the first rendered frame, so a tap cannot overlap it, but the Space's CPU is 3-6x slower
|
| 21 |
+
and its warm-up ordering against the browser's VRM boot is unmeasured. 02-10's deployed run should
|
| 22 |
+
time a "Say hello" tapped immediately after `ready` and record `languageInfo().warm`. Levers if it
|
| 23 |
+
bites: bind `warm_language_on_load` before `warm_synthesizer`, or let the very first analysis be
|
| 24 |
+
non-blocking (tokens `[]` on the first line).
|