8 MB 板の漢字対応を実機で喋らせた(M-104 / M-105 / C-065・C-066)/ 辞書リーダの堅牢化 / hook 2 本 - #14
Draft
ayutaz wants to merge 14 commits into
Draft
8 MB 板の漢字対応を実機で喋らせた(M-104 / M-105 / C-065・C-066)/ 辞書リーダの堅牢化 / hook 2 本#14ayutaz wants to merge 14 commits into
ayutaz wants to merge 14 commits into
Conversation
⚠️ **origin/main に W トラック(ブラウザデモ)が入り、番号が正面衝突していた**ので、 **M/C/D を振り直して**取り込んだ(C-052 の「番号が入れ替わる」形を避けるため)。 11 コミットの詳細な履歴は `backup/8mb-before-rebase` に残してある。 M-94〜M-100 → **M-97〜M-103** C-057/058/060 → **C-059/060/061** C-059 → **削除**(origin の C-058 が同じ発見を独立に記録していた) D-050/051 → **D-051/052**(D-049 は両者とも空き番 = RTF の分母用) ## 中身 **M-97 / M-99 / D-051: 8 MB flash に漢字辞書は載るが、採らない。** 枠 7,143,424 B に対し現行 13,702,320 B。8 MB の動作点は音素誤り 0.89〜1.01%(n=1,495)で 16 MB の 0.63% の 1.4〜1.6 倍。**8 MB の実機が無い**ので D-044 を維持。⚠️ 接続行列 uint8 は 16 MB では買うものが無く、MeCab 一致を 1,696 → 1,693 に落とす。 **M-98 / D-052: PSRAM 無しの板で内部 DRAM を食い潰す穴を塞いだ。** `ids > SAAN_MAX_IDS` の検査が G2P の**後**にあり、喋らない文でも先に 99,668 B 取っていた (低水位 **2,760 B**)。形態素数で NJD 段の前に縛り **40,468 B** に。 **M-100: 辞書リーダの入力検査**(欠陥 5 件。どれも「落ちずに読みだけ静かに変わる」形)。 セクション長を一切見ていない / matrix が無くても成功 / パーティション長を渡す / ベクタが陳腐化する / blob に同一性検査が無い。 新ゲート **`make -C csrc jdict-hard`**(辞書不要 = **K トラックで初めて CI に載る**)は 17 ケース + 陽性対照 15/15 + **ASan 0/2**。 **M-101 / M-102 / M-103: 走っていなかったゲートと、再現コマンドの副作用。** `oj-heap` は `oj_worst.bin` の生成規則が無く **clean な checkout から一度も走れなかった**。 再現コマンドは**共有の成果物を上書きする**(古い M 番号は v2 の ckpt を指す / `CFLAGS_EXTRA` で建てたバイナリが残る)。両方を文書化した。 G25 が n=1,495 で落ちる 1 文は **端末に `normalize_input` が無い**せいと特定(⚠️ **未修正**。 CLAUDE.md の「端末では正規化しない」と衝突するため人の判断が要る)。 **C-059: 「音素の 0.32%」は n=298 の値**で、n=1,495 では **0.63%**。 **C-060: 「行ごとの span は 0..18,656」は誤り**(全体レンジだった。正しくは 1,188..17,342)。 **C-061: 「値が低い方=安全側」は逆**(worst case を 2,667 B 過小に測っていた)。 ## origin/main 側で解消していたもの⚠️ **残タスク「リリース資産の blob を v1 → v2」は不要**だった (origin の **C-057** が「v0.3.0 は既に v2」と訂正済み)。CLAUDE.md はそちらを採った。⚠️ **15.44% → 14.77% の訂正は origin の C-058 が先**。私の同じ発見は落とし、 n=1,495 の 15.59% だけを足した。 ## ゲート(すべて再実行して通過) make -C csrc all-test exit 0 / OK 160 / NG 0 make -C csrc jdict / jdict-hard / oj-heap / kanji-e2e すべて OK check_esp32_template.sh / check_ci_coverage.py / check_partitions.py check_doc_counters.py / check_doc_links.py⚠️ **実機では 1 度も走らせていない。**
## 直した欠陥 **C-062: `docs/measurements.md` の見出しと本文がずれていた。** origin/main を取り込むとき、自分の 7 節を M-94〜M-100 → M-97〜M-103 に振り直して 挿入した点が **origin の M-95 の本文の途中**だったため、 M-95 の §1〜§5 が **M-101 の見出しの下**に、M-101 の本文が **M-96 の下**にあった。⚠️ **all-test も check_doc_counters も check_doc_links も全部通っていた** (番号・件数・アンカーは正しく、本文の所属だけが違った)。C-052 と同じ壊れ方の別の面。 origin/main の M-95 / M-96 を正として取り直し、自分の節は backup/8mb-before-rebase と**本文をバイト比較**して欠落 0 を確認してから並べ直した (差は相互参照の桁数だけで +0〜+4 B)。 **C-063: M-100 の `jdict.c:194` は実体と違った**(正しくは 237 = weak 側の matrix 読み出し)。 行番号は編集でずれるので、位置の意味と日付を添える形に直した。 ## hook **`check_stale_ckpt_export` を追加**(M-102 で実際に踏んだ事故)。 `export_c_weights.py --ckpt runs/v2/...` は**既定で `csrc/student.bin` などを上書きする**ので、 再現コマンドを走らせると出荷している重みが v2 に戻る。 出力先を全部そらしていれば通す(測定の再現は妨げない)。 回帰は**陽性 3 / 陰性 3** を追加し、**94 → 100 ケース**(ドキュメント 8 ファイルの件数も追従)。 ## skill - `writing-gates`: 空虚だったゲートに **3 件**追加(jdict_open の返り値検査 / まとめ N 件の陽性対照 / 生成規則の無い oj_worst.bin)。**節 10〜13 を新設** — ASan(テスト側の余白が検出を殺す)/ 陽性対照はケースと 1:1 / 既定の n が小さいゲートは原理的に盲(G25・C-059)/ ターゲットの同一性に入らないもの(CFLAGS_EXTRA) - `recording-measurements`: 罠の表に **C-059 / C-060 / C-061** を追加。 **「再現コマンドは副作用を持つ」節を新設**(共有の成果物を壊した / 自分のハーネスの不備を 対象の欠陥として報告しかけた 2 回 / 「34 件」は数え間違いで実体 3 件) - `verifying-reports`: **自分の修正を敵対的にレビューさせたら 1/5 しか終わっていなかった**(M-100)。 「再現しない」という自分の結論の方が誤りだった例(M-71 の母集団)も追加 ## その他 - `CLAUDE.md` のゲート表に `jdict-hard` と `check_dict_blob.py` を追加、hook の説明を更新 - `check_doc_counters.py` の英数詞表が Twelve で止まっていたので Twenty まで拡張 - CONTRIBUTING の「空虚だったゲート 12 件」→ 15 件 ## ゲート all-test 172 OK / hook 100 ケース + commit ガード 6 件 / doc_counters / doc_links / sanitize / esp32_template すべて通過。⚠️ **C-062 を検出するゲートは書いていない**(見出しと本文の対応は意味の検査で、機械化できなかった)。
ayutaz
marked this pull request as draft
September 4, 2026 05:17
Owner
Author
|
この PR は 実機で 1 度も測っていない:
確かめてほしいこと(16 MB の M5 CoreS3 で焼ける構成):
|
うち **16 箇所は別の実在する節を指していた**ので、リンク検査にも番号範囲の検査にも 掛からなかった。例: - D-051「M-94(8 MB の枠と動作点)」→ 読者が飛ぶのは **WebAssembly の節** - D-051「C-057(n=298 は 2 倍楽観的)」→ **リリース blob が v1 か v2 か** - README「音素の 0.63% が違う。n=1,495。M-96 §4」→ **人が音を聴いた節** 一括置換はできない(同じ M-96 が、私の旧番号である場合と origin/main の M-96 = 聴取でそのまま正しい場合とが同じファイルに混在する)。 節ごとの話題語と引用の前後 1 行を突き合わせ、食い違う 49 件を列挙して 1 件ずつ判定した。⚠️ **ゲートは書いていない。** 「引用が意図した節を指しているか」は意味の検査で、 C-062 と同じく機械化できなかった。
⚠️ **D-051 の再開条件「8 MB の実機が入る」は、確かめ方として間違っていた。** 実質的なリスクは「書いていない C リーダが正しいか」で、それは 16 MB の板でも QEMU でも確かめられる。ユーザーの指摘で気づいた。 ## 形式 新セクション **`matrixa`**(既存の `matrix` は 1 バイトも触らない = 旧 blob はそのまま動く): 0 u16 lsize / 2 u16 rsize / 4 u16 bits=8 / 6 u16 reserved 8 i16 lo[rsize] / 8+2R u16 span[rsize] / 8+4R u8 q[lsize*rsize]⚠️ **量子化も逆量子化も整数で閉じる**(float スケールにするとホストと C で 値が食い違い、Viterbi の判断が変わる。M-99 §1 / C-060): q = ((v-lo)*510 + span) / (span*2) v' = lo + (q*span*2 + 255) / 510 中間値は最大 255*17,342*2 = 8,844,420 で int32 に収まる。⚠️ **`matrix` と `matrixa` の両方があれば拒む**(`JDICT_ERR_MATRIX`)。 黙って片方を選ぶと、焼き損ねた blob が「動くが読みだけ違う」形になる(M-100 の欠陥そのもの)。 ## 測った値 | | 値 | |---|---:| | 行列セクション | **1,901,645 B**(生 int16 3,792,262 → **−1,890,617**) | | 最大誤差 | **34**(M-99 の記録と一致) | | span の範囲 | 1,188..17,342 / span==0 の行 **0 / 1377** | | 228,000 entries の blob | **7,123,088 B** ≤ 枠 **7,143,424 B**(余り **20,336 B = 0.28%**) |⚠️ **D-051 の見積 7,115,943 B より 7,145 B 大きい**(形式からの算術 vs 実ビルド)。 **余りが薄いので、entries を変えたら必ず測り直すこと。** ## 新しいゲート `make -C csrc matrixa` **同じ逆量子化値を 2 通りの形式で持った blob** を突き合わせる: - G-A1 寸法が一致 - **G-A2 形式が実際に違う**(片方だけ matrixa。⚠️ これが無いと G-A3 は自明に通る) - **G-A3 jdict_trans が全 1,896,129 要素で一致** ← リーダの正しさはここで言える - G-A4 陽性対照: lo[7] を +1 → **1,377 件**(= lsize)が動く⚠️ **「MeCab と 183/184 一致した」はリーダの正しさの証拠にならない。** 量子化そのものが 1 文の分割を変えるので、**両方の形式で同じ 183/184 が出る**。⚠️ **陰性対照(同じ blob を 2 回渡す)で、ゲート自身が SIGSEGV していた。** G-A4 が第2引数を matrixa だと決めつけて NULL を書いていた。しかも **printf がバッファに残って消え、「何も出さずに終了」に見えた**。 `setvbuf` で切り、前提が崩れたら G-A3/G-A4 を走らせないようにした。⚠️ **辞書が要るので all-test には入れていない**(jdict / accent と同じ扱い)。 ## その他 - `esp32/partitions_8mb_kanji.csv`(**model 行なし** = 重みは app の .rodata) - `check_partitions.py` に **`# dict-blob:` 宣言**を足した (既定の 16 MB blob と比べてしまい「入らない」と誤判定していた)。⚠️ `_declared` が値を int に落とすので `raw=True` を追加 ## ゲート all-test 172 OK / jdict OK / matrixa OK(陰性対照つき)/ 8mb・8mb-kana・16mb の パーティション表すべて OK。⚠️ **まだ実機にも QEMU にも載せていない。** 次はそこ。
- `esp32/sdkconfig.kanji8mb`(8 MB flash / partitions_8mb_kanji.csv / PSRAM 無し) - `esp32/partitions_8mb_kanji.csv` に `# dict-blob:` を宣言⚠️ **`-DSAAN_MODEL_RODATA=1` が要る。** 8 MB の表には `model` 行が無い (重みは app の .rodata)ので、付けないと CMake が `Could not find offset of partition model` で止まる。 ## QEMU の実測(8 MB イメージ) | | 値 | |---|---:| | app | **1,021,248 B** / 枠 1,179,648(余り **158,400 B** = 13%) | | 辞書 blob | **7,123,088 B** / 枠 7,143,424(余り **20,336 B**) | | 見出し語 / エントリ | 181,397 / **228,000** | | 起動直後の内部 DRAM | free 103,132 B / 最大ブロック 90,112 | | 辞書 mmap 後 | free **102,556 B** / 低水位 102,556 | | Open JTalk の一時ヒープ上限 | 81,412 B(予算 81,920) |⚠️ **PSRAM 無しで動いている**(QEMU は octal PSRAM を持たない)。 これは AtomS3 のような**PSRAM 無しの 8 MB 板の条件そのもの**である。⚠️ **`esp_partition_mmap` で足りた**(7.1 MB < ROM 実装の 128 ページ = 8 MB 制限)。 16 MB の 13.7 MB では `esp_mmu_map` が要ったが(M-90 §4)、ここでは当たらない。⚠️ **まだ合成していない**(この時点では起動と mmap まで)。
## 確かめられたこと 1. **C リーダが正しい** — 同じ逆量子化値を生 int16 と matrixa の 2 形式で持った blob で `jdict_trans` が**全 1,896,129 要素で一致**(陽性対照 1,377 件 / 陰性対照つき) 2. **枠に入る** — 228,000 entries の blob **7,123,088 B** ≤ 7,143,424 B(余り **20,336 B**)⚠️ D-051 の見積 7,115,943 B より **7,145 B 大きい**(算術 vs 実ビルド) 3. **QEMU で起動し、辞書を mmap し、漢字文から合成まで完走** app 1,021,248 B(余り 13%)/ 内部 DRAM の低水位 **84,716 B**⚠️ **PSRAM 無しで動いている** = AtomS3 の条件そのもの 4. **読みが変わっていない(n=4)** — 4 文とも**漢字経路とかな経路の PCM が bit 一致**。 かな行はホストのフル辞書由来なので、**8 MB 辞書がフル辞書と同じ読みに着いた**。 1 文目は **16 MB 版の基準 checksum `0xa69a7ebbb5ccb05f` とも一致**⚠️ **n=4 では音素の誤り 1.01% は検出できない。** 差が出ないのは当然で、1.01% を否定しない。 ##⚠️ まだ言えないこと - **速度を 1 つも測っていない**(QEMU のサイクルは実機を予測しない。C-055)。 **アフィンの逆量子化が Viterbi を何倍遅くするかは未測定** - **実機に焼いていない**(16 MB の CoreS3 に 8 MB の表を焼けば確かめられるが未実施) - **音は誰も聴いていない**(G32) - **`matrixa` は jdict-hard の入力検査に入れていない**
| ケース | 期待 | 結果 | |---|---|---| | **正しい matrixa 単独**(開けること) | 0 | OK | | matrix と matrixa の**両方**がある | JDICT_ERR_MATRIX | OK | | matrixa の bits = 4(未知の量子化幅) | JDICT_ERR_MATRIX | OK | | matrixa の宣言長 −1 | JDICT_ERR_MATRIX | OK | | matrixa の実体ごと消す(ASan) | JDICT_ERR_MATRIX | OK |⚠️ **「正しい matrixa 単独」が要る。** これが無いと、残り 4 つが全部通っても 「matrixa は常に拒まれる」だけかもしれない(拒否だけを並べたゲートは 『全部拒む』実装で満点を取れる。hook の allow ケースと同じ理由)。 陽性対照は **19 / 19 で leak**(15 → 19。新しい 4 件も含む)。 ゲート: all-test 177 OK / jdict OK / matrixa OK / jdict-hard OK / doc_counters / doc_links / hook 100 ケース すべて通過。
D-051 は「8 MB の実機が入るまで着手しない」とし、 「8 MB 用のパーティション表・C リーダ・ビルド構成は書かない」と決めていた。⚠️ **これは 2 つの別のものを混ぜていた:** | | 8 MB の板が要るか | |---|---| | C リーダが正しいか | **要らない**(16 MB でも QEMU でも) | | 枠に入るか | **要らない**(blob を作って stat) | | 起動して mmap できるか | **要らない**(QEMU が 8 MB を走らせる) | | その板で焼いて鳴らす | **要る** | 唯一の実質的なリスクは C リーダで、それは板と無関係だった。 「板が無いから何も書かない」は、**確かめられることまで止めていた。**⚠️ **D-051 の結論そのものは変わらない。** 16 MB の出荷構成は D-044 のまま (16 MB で ① を使うと音素の誤りが 0.63% → 1.01% に悪化するだけ)。 変わったのは「作らない」で、8 MB 構成は**「作ってあるが既定ではない」**になった。⚠️ **これを検出するゲートは無い**(決定文の論理の穴)。
⚠️ **私が回していなかったゲートで CI が落ちた。** all-test / doc_counters / doc_links / hook / esp32_template は回したが、`check_ci_coverage.py` を忘れていた。 新しい .PHONY ゲート `matrixa` は「CI で回る」でも「回らない理由が書いてある」でも なかったので落ちた。**辞書と pyopenjtalk が要り、ベクタを 2 本生成する**ので CI では回せない(jdict / accent と同じ扱い)。⚠️ **理由を書くだけで通る仕組みなので、除外は「回せない」の証明ではない。** この表は**言い訳を残す場所**であって、免除ではない。
## hook **`check_dict_blob_overwrite` を追加**(M-104 で見えた危険)。 `k1_build_dict.py --matrix affine --out csrc/k1_dict.bin` は **出荷中の 16 MB 辞書を 8 MB 版で置き換える**。⚠️ **落ちない。** `jdict_open` は `matrixa` を正しく受けるので、ファームは普通に起動して 普通に喋り、**読みだけが変わる**(ホストと違う音素 0.63% → 1.01%)。 M-100 で潰した欠陥と同じ形で、**どのゲートも捕まえない**。 別名なら通す / `--matrix` 既定(int16)なら通す / `--out` が無ければ通す。 回帰は**陽性 2 / 陰性 3** を足して **100 → 105 ケース**。 ## skill - `writing-gates`: 空虚だったゲートに 1 件(**「MeCab と 183/184 一致」はリーダの 正しさの証拠にならない** — 量子化が 1 文の分割を変えるので、バグがあっても同じ数が出る)。 **節 14〜16 を新設**: - **ゲート自身が落ちると printf がバッファに残って消える**(`setvbuf` / 前提が崩れたら止める) - **拒否だけを並べたゲートは「全部拒む」実装で満点**(「正しいものが通る」ケースを 1 本) - **ゲートを足したら `check_ci_coverage.py` も回す**(忘れて CI を落とした) - `recording-measurements`: 罠の表に 2 行(**C-065 の「実機が要る」の一括り** / **算術の 7,115,943 B vs 実ビルド 7,123,088 B**)+ 危険信号 3 行 ##⚠️ C-064 が同じセッション中に再発した(自分で) C-064("振り直したのは見出しだけで本文の引用が旧番号")を書いた 30 分後、 範囲表記 `C-001〜C-061` を更新するのに `s.replace("C-061", "C-063")` を掛け、 **skill 本文の「値が低い方=安全側 (C-061)」まで巻き添え**にした。 その後 2 回の更新で **C-065(8 MB の話)を指す**ようになっていた。 **範囲の表記と本文の引用は同じ文字列**なので、一括置換で片方だけは直せない。 `C-001〜C-0NN` の形ごと置換すること。C-064 に追記した。⚠️ **気づいたのは grep の偶然。ゲートは通っていた。** ## docs - `docs/README.md`: 8 MB を「調査」から「**成立した**」に。古い誤り率 0.55〜0.66% を削除 - `esp32/README.md`: **8 MB flash の節を新設**(手順・サイズ・QEMU の値・見ていないもの)。 「8 MB のボードには載らない」を削除。**n=298 の 0.32% → n=1,495 の 0.63%** に訂正 - CONTRIBUTING の「空虚だったゲート 15 件」→ 16 件 ## ゲート all-test 177 OK / jdict-hard OK / hook 105 ケース / ci_coverage / doc_counters / doc_links / sanitize / esp32_template / partitions 3 表 / blob_to_header / kana_g2p / losses **すべて通過**。
ユーザーが M5 CoreS3 を繋いだ(2026-09-05)。M-104 の「実機に焼いていない」を埋めた。 ## 1. 出荷構成(16 MB / M5)に回帰は無い jdict.c に matrixa の枝を足した版を焼いて確認: checksum `0xa69a7ebbb5ccb05f` / |max| 9627 / Σx² 74264237672 / **定常 xRT 0.446〜0.447** / **アンダーラン 0 / 19 チャンク** — すべて基準(M-90)と一致。かな行も同じ checksum。 ## 2. 8 MB 構成が実機で動いた 228,000 entries / blob 7,123,088 B / `esp_partition_mmap` OK / 内部 DRAM free 103,264〜104,112 B / **定常 xRT 0.445** / アンダーラン 0 / 14。 **`0xa69a7ebbb5ccb05f` = 16 MB の基準と bit 一致**、`0x35d40fb53438bf47` = QEMU と一致。 ## 3. **アフィン行列の速度代償 = +0.3%**(今回の主目的)⚠️ 8 MB 版だけでは切り分けられない(entries も同時に変わる)。 **entries を 438,750 に揃え、行列の形式だけ変えた blob**(11,811,696 B)で比較: | 文 | ids | 生 int16 | matrixa | 差 | |---|---:|---:|---:|---:| | 今日は良い天気ですね | 53 | 20.05〜20.08 ms | 20.11〜20.14 ms | **+0.06 ms(+0.3%)** | | 技術の進歩は… | 93 | 40.18〜40.19 ms | 40.21〜40.23 ms | **+0.03 ms(+0.08%)** | **PCM は 2 文とも bit 一致。** 定常 xRT も 0.445 で同じ。⚠️ **差がほぼ 0 だったので、まず測定器を疑った。** 起動ログを取り直して **blob 11,811,696 B / エントリ 438,750** を確認し、アフィン版が実際に載っていることを 確かめてから採用した。→ **その場でログに行列の形式を出すようにした** (`生 int16` / `matrixa = 行ごとアフィン uint8`)。これが無いと 「差し替えたつもりで差し替わっていない」状態を区別できない。 ## 4.⚠️ 8 MB の枠は app の大きさで変わる — **M5Unified を積むと入らない** M-97 の枠 7,143,424 B は **DevKit の app 366,464 B** で計算していた。 **M5Unified を積んだ app は 1,438,496 B** で、dict に回せるのは 6,881,280 B。 **8 MB 辞書 7,123,088 B は 241,808 B 入らない。**⚠️ AtomS3 も M5Stack の板なので、画面やボタンを使うなら同じ壁に当たる。 ## 5. C-066: 「16 MB では買うものが無い」は速度を測る前の判断だった D-051 の 2 は「得るものが無い」と書いたが、実機では **−1,890,624 B / 速度 +0.3% / PCM bit 一致**。⚠️ **決定は維持する**(1.8 MB の使い道が決まっていない)が、 **理由は「得るものが無い」ではなく「得るものの使い道が決まっていない」**である。 **「効果が無い」と「効果を測っていない」を書き分けること。** ## その他 - `-DSAAN_SKIP_I2S=1` を**単独フラグ**にした(それまで SAAN_QEMU からしか立たなかった)。 DAC の無い板で速度と checksum だけ測るのに要る。⚠️ **音は出ない** - **板は出荷構成(M5 CoreS3 / 16 MB / 生 int16)に戻してある**(最終確認済み) ##⚠️ 見ていないもの - **8 MB flash の板そのものでは測っていない**(16 MB の板に 8 MB の表を焼いただけ) - **音を出していない**(§2・§3 は SKIP_I2S)。途切れ・音量・実サンプルレートは未確認 - §3 は 2 文 × n=5 だけ。長い文(44 形態素の上限近く)は未測定 - **PCM が一致した 2 文は「差が出なかった文」**であって、音素の誤り 1.01% を否定しない
## 8 MB の枠は app で変わる — M5 用の表と辞書を別に作った M-97 の枠 7,143,424 B は **DevKit の app(366 KB)**前提だった。 **M5Unified / M5GFX を積むと app は 1,438,576 B** になり、7,123,088 B の辞書は **241,808 B 入らない**。そこで別の枠と辞書を用意した: | | DevKit | **M5Unified** | |---|---:|---:| | factory | 1,179,648 | **1,507,328**(余裕 68,752 = 4.6%) | | dict | 7,143,424 | **6,815,744** | | entries | 228,000 | **213,000** | | blob | 7,123,088 | **6,797,056**(余り 18,688) | | 音素の誤り(n=1,495) | 1.01% | **1.09%** | 新規: `esp32/boards/m5unified/partitions_8mb.csv` / `sdkconfig.8mb`⚠️ **entries は 1 回で当たらなかった。** 214,000 は **6,818,688 B** で **2,944 B 超過** (概算 22.25 B/entry に対し実測 **21.74**)。 **枠の 0.3% を争うときは、概算ではなく作って stat すること。** ## 実機(CoreS3 に 8 MB の表を焼いた)で喋った 見出し語 170,189 / エントリ 213,000 / **`matrixa = 行ごとアフィン uint8`** / 内部 DRAM free **132,031 B** / 漢字 G2P **21.58 ms** / **定常 xRT 0.448** / **アンダーラン 0 / 22・0 / 19** / checksum **`0xa69a7ebb…`** = 基準と一致。 **スピーカーが有効な構成**(SKIP_I2S 無し)。⚠️ **M5 版は USB Serial/JTAG のログが溢れて checksum の後半が落ちる** (画面とスピーカーのタスクが同時に書く)。**先頭 8 桁 = 32 bit** までしか取れていない。⚠️ **音を人が聴いていない。** 鳴っていることしか確かめていない。 ##⚠️ 文一致と編集距離が別方向に動く 228,000 → 213,000 で **文一致は 1117 → 1118 とほぼ同じなのに、音素の誤りは 1.01% → 1.09%**。 C-058 と同じ形で、**編集距離は大きく崩れた少数の文に支配される**。 ## 板の状態 **出荷構成(M5 CoreS3 / 16 MB / 生 int16)に戻してある。** `0xa69a7ebbb5ccb05f` / xRT 0.448 / アンダーラン 0 を確認済み。
古い記述(「速度を測っていない」「実機に焼いていない」「8 MB では入らない」)を、 実機の値に差し替えた。 | ファイル | 直したこと | |---|---| | `docs/README.md` | 「QEMU まで」→ **実機。DevKit と M5 の 2 構成の表**を載せた | | `esp32/README.md` | **表と辞書が板ごとに違う**ことを明示。実機の xRT / アンダーラン / checksum | | `esp32/TESTING.md` | 「8 MB では辞書が入りません」→ **「配布イメージは 16 MB 必須。8 MB でもソースからなら動く」** | | `README.md` / `README.en.md` | 「16 MB 必須」は**配布イメージの話**だと明示し、8 MB の作り方を注記 | | `CLAUDE.md` | 枠の表を **app 別**に作り直し、手順を DevKit / M5 の 2 本に。古い注記 3 行を差し替え | ##⚠️ 特に直したかったところ **「8 MB flash では現行の辞書は入らない」は今も正しいが、 「8 MB では漢字が動かない」と読める書き方が 3 ファイルに残っていた。** 実際は **entries を落とせば動く**(実機で確認済み)。 **配布していないだけ**なので、そう書き分けた。 **枠が app で変わることを全部の表に入れた。** M-97 の 7,143,424 B は DevKit 前提で、 M5Unified を積むと **6,815,744 B** しかない。⚠️ **DevKit 用の辞書を M5 版に焼くと 241,808 B 入らない。表と辞書は必ずセット。** ## 残タスク表に #10 を追加 **8 MB 版を配布するか**(判断)。⚠️ 配るなら **8 MB の実機で 1 度は確かめたい** (いまは 16 MB 板に 8 MB の表を焼いただけ)。 ## ゲート ci_coverage / doc_counters / doc_links / sanitize / esp32_template / hook 105 ケース / blob_to_header / **パーティション表 5 本**(8mb-kana / 16mb / 8mb-kanji / m5-16mb / **m5-8mb**) すべて通過。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
何をしたか
8 MB flash の板で漢字対応が成立するかを、実機なしで確かめた。結果は「成立する」。
C リーダを書き、blob が枠に入り、QEMU で起動して漢字文から合成まで通った。
その過程で見つかった辞書リーダの 5 つの欠陥・走っていなかったゲート・
リベースで壊れていたドキュメントを塞ぎ、skill と hook を最新化した。
origin/mainから 11 コミット / 49 ファイル。1.⚠️ 出発点の誤りを訂正した(C-065)
当初 D-051 は「8 MB の実機が入るまで着手しない」と決め、
「8 MB 用のパーティション表・C リーダ・ビルド構成は書かない」としていた。
これは 2 つの別のものを混ぜていた:
stat)唯一の実質的なリスクは「書いていない C リーダ」で、それは板と無関係だった。
「板が無いから何も書かない」は、確かめられることまで止めていた。
16 MB で ① を使う理由は無い(音素の誤りが 0.63% → 1.01% に悪化するだけ)。
変わったのは「作らない」で、**8 MB 構成は「作ってあるが既定ではない」**になった。
2. 8 MB 対応(M-104)
新セクション
⚠️ 既存の
matrixa(接続行列を行ごとアフィン uint8 で持つ)。matrix(生 int16)は 1 バイトも触っていないので、出荷している 16 MB の blob はそのまま動く。1 つのセクション名に 2 形式を入れないのは、
matrixの長さ検査を厳密なまま残すため。q = ((v-lo)*510 + span)/(span*2)/v' = lo + (q*span*2 + 255)/510。中間値は最大 8,844,420(int32)。matrixとmatrixaの両方があれば拒む(黙って片方を選ぶと「動くが読みだけ違う」形になる)。測った値
.rodataに)余りが薄いので entries を変えたら必ず測り直すこと。
QEMU で通った
esp_partition_mmapで足りる(7.1 MB < ROM 実装の 8 MB 制限。16 MB ではesp_mmu_mapが要った)。-DSAAN_MODEL_RODATA=1が要る(8 MB の表にmodel行が無い)。4 文とも「漢字経路」と「かな経路」の PCM が bit 一致した。
かな行はホストのフル辞書が作った中間表現なので、8 MB 辞書が同じ読みに着いたことになる:
0xa69a7ebbb5ccb05f0x35d40fb53438bf470xcce7fd062fd45a2c0xb1751f2b8332db921 文目は 16 MB 版の基準 checksum とも一致。
⚠️ n=4。 音素の誤り 1.01%(n=1,495)は「平均 1.01% の音素が違う」なので、
4 文で差が出ないのは当然であって 1.01% を否定しない。
新しいゲート
make -C csrc matrixa量子化そのものが 1 文の分割を変えるので、リーダにバグがあっても同じ 183/184 が出る
(実際に両方の形式で同じ数が出た)。だから同じ逆量子化値を 2 形式で持った blob を突き合わせる:
jdict_transが全要素で一致lo[7]を +1しかも printf がバッファに残って消え、「何も出さずに終了」に見えた。
setvbufで切り、前提が崩れたら以降を走らせないようにした。jdict-hardにも 5 ケースを追加(正しい matrixa が開ける / 両方ある / bits=4 / 長さ −1 / 実体消し)。陽性対照は 19 / 19 で leak。
3. 辞書リーダの入力検査(M-100)
壊れた blob を渡すと、落ちずに読みだけが変わる欠陥が 5 つ(louds / classes / char / unk / matrix)。
どれも
jdict_openが 0 を返して通す。matrixしか直していなかった。 敵対的レビューで残り 4 つが出て、そちらの方が悪かった。calloc(total + 64)していて、64 B までの越境が ASan に見えていなかった。4. 走っていなかったゲート(M-101)
make -C csrc oj-heapは clean な checkout で落ちる —oj_worst.binに生成規則が無かったlabel-idsの G25 は n=298 で 298/298、n=1,495 で 1,494/1,495。原因は追った(M-103): 端末に
normalize_input()が無い。5. PSRAM 無しの板の穴(D-052)
Open JTalk の一時ヒープは 197.6 B × ids + 924 B で、最長文で余裕 2,760 B しか無かった。
SAAN_KANJI_MAX_INPUT_TOK(44)で NJD 段の前に形態素数で縛った→ 低水位 2,760 → 40,468 B(checksum 不変)。
6. リベースで壊れていたドキュメント(C-062 / C-064)
自分の 7 節を M-94〜M-100 → M-97〜M-103 に振り直して挿入した点が
origin の M-95 の本文の途中だったため、M-95 の §1〜§5 が M-101 の見出しの下にあった。
さらに、本文の引用 53 箇所が旧番号のままで、うち 16 箇所は別の実在する節を指していた。
all-test/check_doc_counters/check_doc_linksを全部通っていた(番号・件数・アンカーは正しく、本文の所属と引用先だけが違った)。
C-064 を書いた 30 分後、範囲表記
C-001〜C-061を更新するのにreplace("C-061", "C-063")を掛け、skill 本文の「値が低い方=安全側 (C-061)」まで巻き添えにした。2 回の更新を経て C-065(8 MB の話)を指す状態になっていた。
気づいたのは grep の偶然で、ゲートは全部通っていた。
回避策(
C-001〜C-0NNの形ごと置換する)を C-064 に追記した。7. hook
2 本追加した。どちらも「落ちずに間違ったものができあがる」形の事故。
check_stale_ckpt_exportexport_c_weights.py --ckpt runs/v2/...が既定のcsrc/を上書きする。M 番号の再現コマンドを機械的に走らせて実際に出荷中の重みを v2 に戻した(M-102)。出力先をそらせば通すcheck_dict_blob_overwritek1_build_dict.py --matrix affine --out csrc/k1_dict.binが16 MB の辞書を 8 MB 版で置き換える。回帰は 94 → 105 ケース(陽性 5 / 陰性 6 を追加)。
8. skill
writing-gatesoj_worst.bin/ 「183/184 一致」はリーダの証拠にならない)+ 節 10〜16(ASan とテスト側の余白 / 陽性対照はケースと 1:1 / 既定の n が小さいゲートは盲 /CFLAGS_EXTRAはターゲットの同一性に入らない / 落ちると printf が消える / 拒否だけのゲートは「全部拒む」で満点 /check_ci_coverage.pyも回す)recording-measurementsverifying-reports訂正(C-059 〜 C-065)
jdict.c:194は実体 237ゲート
9. ✅ 実機で確かめた(M-105 / 2026-09-05)
ユーザーが M5 CoreS3 を繋いだので、M-104 の「実機に焼いていない」を埋めた。
出荷構成(16 MB / M5)に回帰は無い
jdict.cにmatrixaの枝を足した版を焼いて確認:0xa69a7ebbb5ccb05f/ |max| 9627 / Σx² 742642376728 MB 構成が実機で動いた
228,000 entries / blob 7,123,088 B /
esp_partition_mmapOK /内部 DRAM free 103,264〜104,112 B / 定常 xRT 0.445 / アンダーラン 0 / 14。
0xa69a7ebbb5ccb05f= 16 MB の基準と bit 一致、0x35d40fb53438bf47= QEMU と一致。✅ アフィン行列の速度代償 = +0.3%(PR の最大の穴だった)
entries を 438,750 に揃え、行列の形式だけ変えた blob(11,811,696 B)で比較した:
PCM は 2 文とも bit 一致。 定常 xRT も 0.445 で同じ(合成の段は行列を触らない)。
起動ログを取り直して blob 11,811,696 B / エントリ 438,750 を確認し、
アフィン版が実際に載っていることを確かめてから採用した。
→ その場でログに行列の形式を出すようにした(
生 int16/matrixa = 行ごとアフィン uint8)。✅ M5Unified 版(画面 + スピーカー)も 8 MB で喋った
esp32/partitions_8mb_kanji.csvboards/m5unified/partitions_8mb.csvSKIP_I2S)(概算 22.25 B/entry に対し実測 21.74)。枠の 0.3% を争うときは作って
statする。(C-058 と同じ形。編集距離は大きく崩れた少数の文に支配される)。
M-97 の枠 7,143,424 B は DevKit の app 366,464 B で計算していた。
C-066: 「16 MB では買うものが無い」は速度を測る前の判断だった
D-051 の 2 は「得るものが無い」と書いたが、実機では
⚠️ 決定は維持する(1.8 MB の使い道が決まっていない)が、
−1,890,624 B / 速度 +0.3% / PCM bit 一致。
**理由は「得るものが無い」ではなく「得るものの使い道が決まっていない」**である。
板の状態
出荷構成(M5 CoreS3 / 16 MB / 生 int16 / スピーカー有効)に戻してある。
最終確認で checksum・xRT 0.448・アンダーラン 0 を確認済み。
どう聞こえるかは分からない(G32 と同じ空白)
圧迫してログの後半が落ちる(ファームの欠陥ではない)