Spaces:
Running on Zero
Running on Zero
docs(phase-02): phase execution report - 10/11 plans, findings, measurements, deploy blocker
Browse files
.planning/phases/02-japanese-language-core/02-EXECUTION-REPORT.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Phase 02 β Japanese Language Core: Execution Report
|
| 2 |
+
|
| 3 |
+
status: blocked-on-deploy
|
| 4 |
+
phase: 02-japanese-language-core
|
| 5 |
+
plans_complete: 10 of 11
|
| 6 |
+
commits: 54 (`df2c231`..`5411e5e`)
|
| 7 |
+
written: 2026-09-12
|
| 8 |
+
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
## What the phase set out to do
|
| 12 |
+
|
| 13 |
+
Give the learner a page where Japanese is *readable*, not just audible: every line broken into
|
| 14 |
+
whole words, furigana over the kanji at a level the learner picks, a tap on any word explaining
|
| 15 |
+
it, and an English reveal per line. Requirements JPN-01 through JPN-04.
|
| 16 |
+
|
| 17 |
+
Phase 1 had shipped the voice-and-avatar loop. Phase 2 adds the language layer behind the same
|
| 18 |
+
`gr.HTML` bridge, with no new transport and no GPU on the turn path.
|
| 19 |
+
|
| 20 |
+
---
|
| 21 |
+
|
| 22 |
+
## Status
|
| 23 |
+
|
| 24 |
+
Ten of eleven plans are complete, committed and locally green. The eleventh (`02-10`) is an
|
| 25 |
+
owner-gated close-out that cannot run until the build is on the Space.
|
| 26 |
+
|
| 27 |
+
| Wave | Plan | What it built | Status |
|
| 28 |
+
|------|------|---------------|--------|
|
| 29 |
+
| 1 | 02-01 | LFS arming, stack pins, JLPT + compact JMdict data sets | β Complete |
|
| 30 |
+
| 2 | 02-02 | Morphemes β whole-word units; ruby alignment; reading overrides | β Complete |
|
| 31 |
+
| 2 | 02-03 | Ranked JMdict headword lookup; JLPT level derivation (both axes) | β Complete |
|
| 32 |
+
| 2 | 02-04 | OPUS-MT jaβen converted to CTranslate2 int8; translate() | β Complete |
|
| 33 |
+
| 3 | 02-05 | The canonical `analyze(text)`; 28-sentence golden set | β Complete |
|
| 34 |
+
| 4 | 02-06 | Three server functions; tokens on every directive; warm-up | β Complete |
|
| 35 |
+
| 5 | 02-07 | Furigana on the page: three modes, level picker, persistence | β Complete |
|
| 36 |
+
| 6 | 02-08 | Word-lookup popover, zero bridge calls per tap | β Complete |
|
| 37 |
+
| 7 | 02-09 | Inline EN reveal with per-line cache and state machine | β Complete |
|
| 38 |
+
| 8 | 02-11 | Nine licence rows verified; on-page credits; docs/LANGUAGE.md | β Complete |
|
| 39 |
+
| 9 | 02-10 | Deployed suite, latency record, phone rows, requirement marks | β¬ **Blocked** |
|
| 40 |
+
|
| 41 |
+
**Blocker:** the build has not been pushed to the Space. Local `master` is 54 commits ahead of
|
| 42 |
+
`space/main`, which still runs `df2c231`. The four LFS objects the build needs **were uploaded
|
| 43 |
+
successfully** (`git lfs push space master`, 87 MB); only the ref update remains.
|
| 44 |
+
|
| 45 |
+
---
|
| 46 |
+
|
| 47 |
+
## What shipped
|
| 48 |
+
|
| 49 |
+
### Server side β `src/japanese_avatar/nlp/`
|
| 50 |
+
|
| 51 |
+
| Module | Role |
|
| 52 |
+
|--------|------|
|
| 53 |
+
| `tokenizer.py` | Sudachi mode-C morphemes; **per-thread** instance under a build lock |
|
| 54 |
+
| `units.py` | Morphemes β whole-word units (joining rules A1βA7) |
|
| 55 |
+
| `ruby.py` | Surface + reading β per-kanji-run ruby spans, with an honest fallback |
|
| 56 |
+
| `overrides.py` | Committed reading-override table |
|
| 57 |
+
| `jmdict.py` | Compact JMdict loader; ranked headword lookup |
|
| 58 |
+
| `levels.py` | JLPT level derivation on the word and kanji axes |
|
| 59 |
+
| `analyzer.py` | The one canonical `analyze(text)` β 14-key token records |
|
| 60 |
+
| `translate.py` | CTranslate2 int8 translator, singleton + warm-up |
|
| 61 |
+
| `warm.py` | Whole-language warm-up with per-component timings and RSS |
|
| 62 |
+
|
| 63 |
+
### Browser side β `avatar/`
|
| 64 |
+
|
| 65 |
+
`transcript.js` owns rendering and all three reading features; `transcript.css` styles them;
|
| 66 |
+
`transcript-harness.html` exercises the module with no server at all. `host.js` owns *policy*
|
| 67 |
+
(which line, which cache, when to call) and holds the single `avatar.translate()` call site in
|
| 68 |
+
the codebase. `avatar.js`, `avatar-iframe.js` and `stage.html` were changed by **zero lines** β
|
| 69 |
+
the transport seam Phase 1 established held for the whole phase.
|
| 70 |
+
|
| 71 |
+
### Data, all pinned and hash-checked
|
| 72 |
+
|
| 73 |
+
| Asset | Pin | Size |
|
| 74 |
+
|-------|-----|------|
|
| 75 |
+
| JLPT vocabulary (yomitan-jlpt-vocab) | tag `2025.08.01.0` | 8,293 rows, 7,748 unique ids |
|
| 76 |
+
| Kanji levels (kanji-data) | commit `00fd7079β¦` | 2,211 kanji |
|
| 77 |
+
| Compact JMdict projection | jmdict-eng `3.6.2` | 218,672 entries, 7.66 MB gz (LFS) |
|
| 78 |
+
| OPUS-MT jaβen β CTranslate2 int8 | Hub rev `0770961aβ¦` | 77,339,435 B (LFS) |
|
| 79 |
+
|
| 80 |
+
---
|
| 81 |
+
|
| 82 |
+
## Four findings that a green test suite would not have shown
|
| 83 |
+
|
| 84 |
+
These are the substance of the phase. Each was found by a check that was *built to be able to
|
| 85 |
+
fail*, and each is fixed and pinned.
|
| 86 |
+
|
| 87 |
+
### 1. The Japanese tokenizer is not safe to share between threads
|
| 88 |
+
|
| 89 |
+
`sudachipy.Tokenizer` takes a mutable borrow for the whole `tokenize()` call. A second thread
|
| 90 |
+
gets `RuntimeError: Already borrowed` β and the server's own error path swallowed it and returned
|
| 91 |
+
an empty token list so the turn would still *speak*. The visible symptom was an avatar line that
|
| 92 |
+
rendered without furigana, occasionally, with tests green on both sides. Five of eight concurrent
|
| 93 |
+
calls were being lost.
|
| 94 |
+
|
| 95 |
+
Plan 02-07 is what made it reachable: it analyses the learner's line while the turn's own analyse
|
| 96 |
+
stage runs. The `Dictionary` stays process-wide (the mmapped ~110 MB); `get_tokenizer()` is now
|
| 97 |
+
`threading.local` under a build lock. 288 of 288 concurrent tokenisations clean.
|
| 98 |
+
|
| 99 |
+
**The guard is mutation-tested.** Reverting to a shared instance makes
|
| 100 |
+
`tests/test_analyzer.py::test_analyze_is_thread_safe` fail with the real exception β the test
|
| 101 |
+
cannot pass against the broken implementation.
|
| 102 |
+
|
| 103 |
+
### 2. A word card could cover the word it explained
|
| 104 |
+
|
| 105 |
+
On a phone column, a card clamped back into the viewport could land on top of its own anchor. The
|
| 106 |
+
next tap then hit the card: the dismiss handler returned early because the target was inside the
|
| 107 |
+
popover, and the open handler found no word under the pointer. The tap was swallowed in total
|
| 108 |
+
silence. Placement now picks the side with more room and caps the card's height to that room.
|
| 109 |
+
|
| 110 |
+
### 3. The transcript panel had never actually scrolled
|
| 111 |
+
|
| 112 |
+
`#transcript-text` was an auto-height block, so *any* card anchored to the last line hung below
|
| 113 |
+
the visible area β the plan's phone must-have was unreachable on the real page. It is now a fixed
|
| 114 |
+
40vh scrolling column (335 px on a Pixel 7). Side effect worth knowing: `host.js`'s long-standing
|
| 115 |
+
`el.scrollTop = el.scrollHeight` does something for the first time; on an auto-height element it
|
| 116 |
+
had always been a no-op.
|
| 117 |
+
|
| 118 |
+
### 4. Two acceptance checks could not fail
|
| 119 |
+
|
| 120 |
+
In plan 02-09, the grep asserting the translate call existed was matching a **comment**, because
|
| 121 |
+
the real call was chained across two lines. Deleting the actual call would have left both the grep
|
| 122 |
+
and the seam test green. A second check asserted the absence of something that had existed since
|
| 123 |
+
the previous plan. Both are now exact enough to break when the code they guard is removed.
|
| 124 |
+
|
| 125 |
+
A third, in 02-11, was met by substitute evidence rather than literally: the dictionary credit
|
| 126 |
+
string is defined **once** and interpolated into both the footer and the About panel, so the
|
| 127 |
+
plan's "appears at least twice" grep returns 1. Retyping the string to satisfy the grep would
|
| 128 |
+
create exactly the drift the machine-readable credits block exists to prevent. The rehearsal's own
|
| 129 |
+
printout β all five credits present across footer, About panel and `GET /config` β is recorded in
|
| 130 |
+
the summary in its place.
|
| 131 |
+
|
| 132 |
+
---
|
| 133 |
+
|
| 134 |
+
## Corrections to the research, made against primary sources
|
| 135 |
+
|
| 136 |
+
The phase refused to inherit numbers it could not reproduce.
|
| 137 |
+
|
| 138 |
+
| Claim in research/plan | Measured truth |
|
| 139 |
+
|---|---|
|
| 140 |
+
| 7,747 unique JLPT ids | **7,748**, all of which resolve in JMdict |
|
| 141 |
+
| 505 ids on more than one level | **447** ids; the 505 counted *rows*, not ids |
|
| 142 |
+
| jmdict-eng top-level `version` is the release tag | It is `"3.6.2"`; the plan's assertion would have rejected the correct file |
|
| 143 |
+
| OPUS-MT rights holder "Language Technology Research Group" | The Hub org's own name is **"Helsinki-NLP Research Group"**, University of Helsinki |
|
| 144 |
+
| SudachiDict licence resolvable from the GitHub API | API returns `license: null` (its file is `LICENSE-2.0.txt`); Apache-2.0 rests on the repo's own `LICENSE`/`LEGAL`/`README` plus PyPI. `LEGAL` also discloses bundled **UniDic** (BSD-3-Clause) and **NEologd**, which the plan did not mention |
|
| 145 |
+
|
| 146 |
+
All nine Phase 2 licence rows were verified against primary sources on 2026-09-07. Every one of
|
| 147 |
+
the fourteen cited URLs returned 200. Nothing was unreachable; nothing was guessed.
|
| 148 |
+
|
| 149 |
+
### Honest level results that look like bugs and are not
|
| 150 |
+
|
| 151 |
+
`ζ₯ζ¬θͺ` and `γͺγ` badge as `N1+ / beyond lists` because they sit on none of the five pinned
|
| 152 |
+
lists. `γγγ«γ‘γ―` is N3. `γͺγ` badges N3 because the N5 list's `γͺγ` row points at the archaic
|
| 153 |
+
copula, not the verb. `γγ` resolves to its own JMdict entry `2820690`, not `θ―γ`. These are
|
| 154 |
+
upstream list truths, recorded in the golden set rather than special-cased away.
|
| 155 |
+
|
| 156 |
+
---
|
| 157 |
+
|
| 158 |
+
## Numbers
|
| 159 |
+
|
| 160 |
+
### Local suites at the end of the phase
|
| 161 |
+
|
| 162 |
+
| Suite | Result |
|
| 163 |
+
|---|---|
|
| 164 |
+
| Quick loop (`tests`, no e2e, no mt) | **343 passed**, 17 deselected, 35.66 s (budget 40 s) |
|
| 165 |
+
| Whole `tests/e2e/` | **39 passed**, 25 skipped, 0 failed |
|
| 166 |
+
| Translation marker (`-m mt`) | 17 passed |
|
| 167 |
+
| `ruff check` / `ruff format --check` | clean, 58 files |
|
| 168 |
+
|
| 169 |
+
### Measured behaviour
|
| 170 |
+
|
| 171 |
+
| Measurement | Value |
|
| 172 |
+
|---|---|
|
| 173 |
+
| Warm analysis, 36-mora sentence | 0.14β0.19 ms |
|
| 174 |
+
| Ranked lookup, 10,000 iterations | 22.7 ms (budget 50) |
|
| 175 |
+
| Compact JMdict load | 0.56β0.70 s gz; 2.0β2.4 s to indexed entries |
|
| 176 |
+
| Translator warm-up | 0.35 s |
|
| 177 |
+
| Translation, idle, per sentence | 22β286 ms |
|
| 178 |
+
| Language warm-up, fresh process | tokenizer 0.04 s, JMdict 2.4 s, translator 0.34 s; RSS +467 MB |
|
| 179 |
+
| Ruby `rt` height | 11 px; line height always-vs-never 32.39 / 29.39 |
|
| 180 |
+
| Word card, requests per tap | `[]` under both transports and deployed |
|
| 181 |
+
| EN reveal, deployed rehearsal | 2,062 / 2,438 ms tapβshown; cached re-show `[]` requests |
|
| 182 |
+
|
| 183 |
+
**Every "deployed" number above was measured against a local `DISABLE_GPU=1 app.py` on loopback.**
|
| 184 |
+
They are rehearsals awaiting the owner's push, and they are labelled that way in every summary.
|
| 185 |
+
Real Space numbers are plan 02-10's job.
|
| 186 |
+
|
| 187 |
+
### A caveat on browser timings
|
| 188 |
+
|
| 189 |
+
Under headless SwiftShader this machine is badly starved. The learner line's analyse round trip
|
| 190 |
+
measures 14.6β22.7 s of wall clock against 0.02β0.19 ms of server work, because Gradio serves it
|
| 191 |
+
around a 12β19 s synthesis. Phase 1's lesson β record timings, do not assert tight budgets β
|
| 192 |
+
applied throughout, and three absolute ceilings that flake under load were logged rather than
|
| 193 |
+
chased.
|
| 194 |
+
|
| 195 |
+
---
|
| 196 |
+
|
| 197 |
+
## Deferred, with owners
|
| 198 |
+
|
| 199 |
+
Six items in `deferred-items.md`. The three that matter:
|
| 200 |
+
|
| 201 |
+
1. **First-turn wait behind the language warm-up on the Space is unmeasured.** Locally the warm-up
|
| 202 |
+
finishes 1.2 s before the first frame so a tap cannot overlap it, but the Space's CPU is 3β6Γ
|
| 203 |
+
slower. 02-10 should time a "Say hello" tapped immediately after `ready`.
|
| 204 |
+
2. **The learner's furigana costs a second round trip.** The avatar echoes the same text in
|
| 205 |
+
Phase 2, so one analysis could serve both lines β but Phase 3 makes the two lines differ, so
|
| 206 |
+
the split shape was kept deliberately.
|
| 207 |
+
3. **Three wall-clock ceilings flake under whole-suite load** and pass in isolation (Phase 1's
|
| 208 |
+
tap-to-running ceiling, the WASM ASR fallback, the JMdict lookup budget). All are machine-speed
|
| 209 |
+
constants on a shared laptop, not product properties.
|
| 210 |
+
|
| 211 |
+
---
|
| 212 |
+
|
| 213 |
+
## What 02-10 still needs
|
| 214 |
+
|
| 215 |
+
1. **The push** β `git push space master:main`. The LFS objects are already uploaded.
|
| 216 |
+
2. Deployed suite on the pushed revision, with `DISABLE_GPU` read back.
|
| 217 |
+
3. Real translation latency into `docs/LATENCY.md Β§ Translation` with the revision SHA, plus the
|
| 218 |
+
container's cores, memory and post-warm-up RSS into `docs/HOSTING.md`.
|
| 219 |
+
4. **Phone rows** β furigana legibility, popover thumb reach and translation quality judged on a
|
| 220 |
+
real device, recorded with who judged what. These need a human and a phone.
|
| 221 |
+
5. JPN-01 through JPN-04 marked complete **only** for rows that actually passed on the pushed
|
| 222 |
+
revision. Nothing has been marked yet, deliberately, across all ten plans.
|
| 223 |
+
|
| 224 |
+
Note that Phase 1's `AVTR-01` also still awaits an owner phone retest, and the same push serves
|
| 225 |
+
both.
|