v0.3.0 — 実機で漢字を喋り、実時間の要件を満たした
2026-09-03。モデルの重みは v0.1.0 から 1 bit も変わっていません(v3 = Stage 3 を 80,000 step)。
変わったのは端末側のコードと配布物です。
何が変わったか
1. スタックチャン(M5Stack CoreS3)で漢字かな交じり文を喋ります
ESP32-S3 の上だけで G2P が完結します。 ホスト側の前処理は要りません。
辞書 13.7 MB を flash から読み、形態素解析(Viterbi)→ アクセント規則 → NJD の各段 →
フルコンテキストラベル → 音素 ID まで、すべて端末の C 実装です。
! の印は要らなくなりました。 行をそのまま打つと、端末が 3 通りに振り分けます。
| 打った行 | 経路 |
|---|---|
今日は良い天気ですね。 |
辞書(漢字経路) |
コンニチハ |
辞書 |
きょ][おわよ][いて][んきです°ね |
かな(中間表現) |
きょ][おわよ][んきです°ね。(中間表現に 。 が混ざる) |
位置つきで拒否(黙って別経路に回さない) |
判定はかな G2P のトークナイザそのもので、ホスト側の scripts/to_intermediate.py と
held-out 298 文 + 中間表現 298 行で 596/596 一致します。
2. 実時間の要件(RTF ≤ 0.5)を満たしました
| v0.2.0 相当(9/2 夜) | v0.3.0 | |
|---|---|---|
| 満チャンク 1 pull の xRT | 0.926 | 0.446 |
| 1 step | 18,378,513 cyc | 11,659,500 cyc |
| アンダーラン | 1 / 14 | 0 |
| 鳴らし始めまで | 719 ms | 384 ms |
| 内部 RAM の空き(起動直後) | 99,987 B | 132,039 B |
| arena(静的確保) | 212,992 B | 180,224 B |
出力の波形は 1 bit も変わっていません(checksum 0xa69a7ebbb5ccb05f が 9/2 から同一)。
速くなったのは、捨てられる出力を計算しない・token を持ち越す・GELU のコード生成を直す・
D-cache の行を 64 B にする、といった出力を変えない変更の積み重ねです。
3. 音を聴きました
これまでの一致はすべて「OpenJTalk と同じ出力か」という機械的な照合で、
日本語として通用するかは誰も確かめていませんでした。実機の音を聴いて確認しました。
**「品質を測った」ではなく「破綻していないことを人が 1 度確かめた」**と読んでください。
⚠️ v0.2.0 から必ず入れ替えてください
配布していた int8 の重みを、現行コードが拒みます
v0.2.0 の saanotts-jp-v3-int8.bin は blob v1(643,936 B)で、
現行コードは起動時に SAAN_ERR_VERSION で止まります。v0.3.0 は v2(654,032 B)です。
blob v2 は int8 の conv 重みを [cout][k][align16(cin)] に 0 埋めして並べ直したもので、
PIE 命令に転置コピー無しで渡せるようにしたものです(+10,096 B)。数値は同じです。
配布していた firmware が、native USB だけの板で操作できませんでした
v0.1.1 / v0.2.0 のイメージはコンソール入力が UART0 でした。CoreS3 や AtomS3 は
USB-シリアル変換を持たないので、起動はするのに かな> に何を打っても届きません。
v0.3.0 のイメージは USB Serial/JTAG 入力です。
配布物
| 資産 | 大きさ | 何に使うか |
|---|---|---|
m5-cores3-firmware-kanji-16mb.bin |
16 MB | M5Stack CoreS3 / スタックチャン。焼くだけ。漢字 + スピーカー + 画面 |
esp32s3-firmware-kanji-16mb-usbjtag.bin |
16 MB | ESP32-S3 DevKit(16 MB flash)。漢字。I2S DAC で発声 |
esp32s3-firmware-w8a8-pie-usbjtag.bin |
8 MB | 同(8 MB flash)。かな中間表現のみ(辞書なし) |
saanotts-jp-v3-int8.bin |
654,032 B | 端末用の重み。blob v2 |
saanotts-jp-v3-fp32.bin |
2,249,792 B | ホストで動かす / ゴールデンテスト用 |
golden-v3-int8.bin / golden-v3-fp32.bin |
各 779,584 B | 参照出力(make -C csrc int8-golden / test) |
saanotts-jp-v3-stage4.pt |
2,744,874 B | PyTorch のチェックポイント(上の blob はここから再現できます) |
k1-dict-438750.bin |
13,702,320 B | 漢字辞書(438,750 entries)。firmware に同梱済み |
SHA256SUMS.txt に全資産のハッシュがあります。
焼き方(スタックチャン)
# ⚠️ 先に元のファームを退避してください
esptool.py --chip esp32s3 -p /dev/cu.usbmodemXXXX read_flash 0 0x1000000 backup.bin
esptool.py --chip esp32s3 -p /dev/cu.usbmodemXXXX write_flash 0x0 m5-cores3-firmware-kanji-16mb.bin--flash_mode qio を渡さないでください。 ヘッダが QIO になると ROM ローダが読めず、
ets_loader.c 78 を繰り返してブートループします。そのまま焼けば bootloader が起動後に
qio_mode: Enabling default flash chip QIO で切り替えます。
焼けたら USB Serial/JTAG(115200)で繋ぎ、かな> に日本語の行をそのまま打ってください。
起動時に見える行
esp_psram: Found 8MB PSRAM device
saanotts: W8A8 + PIE 有効 / int8 blob を確認
saan_dict: esp_mmu_map OK: vaddr 0x3c960000
saan_dict: 辞書 OK: 見出し語 355768 / エントリ 438750 / 行列 1377x1377
saanotts: 起動直後: 内部 DRAM free 132039 B
今日は良い天気ですね。 を打つと 0xa69a7ebbb5ccb05f が出ます(この値は QEMU とも一致します)。
⚠️ できないこと・分かっていない食い違い
- 音の品質は測っていません。 聴いたのは 1 名・数文・対照なしです。
- 辞書を削ったので、ホストと音素の 0.32%が違います(n=298)。落ちた語は無音にならず、
短く切り直されて誤読されます(上毛→上+毛)。 - 端末に載らない後処理が 3 段あります(「何」の読み推定 / Sudachi の漢字読み修正 / 踊り字)。
中間表現で 2.00% [1.15, 3.45] の文が食い違います。 - アクセントの起伏が教師より 19% 大きい(
magnitude_ratio1.193)。過剰強調かは未判断です。 - 発話全体で見た RTF は 0.55〜0.71 で、まだ 0.5 を超えます(最初の 38 フレーム分の助走が
初回 pull に乗るため)。要件の分母が定義されていないので、両方を記録しています。 - I2S の実サンプルレートの誤差は未測定です(ESP32-S3 に APLL がありません)。
- 動作を確認した実機は M5Stack CoreS3 1 台だけです。
開発者向け
コマンドの名前が変わりました。 工程番号(K1〜K7)で付いていたファイル名を責務ベースにしました。
| 変更前 | 変更後 |
|---|---|
make -C csrc k2 / k4 / k4b / k5 / k6 / k7 |
jdict / accent / njd-rules / oj-heap / kanji-e2e / label-ids |
csrc/k1dict.c k4_accent.c k4b_njd.c k7_label2ids.c |
jdict.c accent.c njd_rules.c label_ids.c |
ESP32-S3 では W8A8 + PIE が既定になりました(-DSAAN_ENABLE_PIE=1 は不要)。
W8A32 で測るときは -DSAAN_ENABLE_PIE=0 を明示してください。
CI が増えました: 参照実装との一致(リリースの fp32 資産を落として実行・陽性対照つき)と、
ゲートが CI で回るかの機械検査を足しました。
訂正(この版で見つけた欠陥)
- C-055: 命令数が減っても実機で遅くなることがあります。GELU の最適化で objdump の命令数は
70 → 59 に減ったのに、実機では 118 → 211 cyc/要素と倍になっていました
(符号処理がスタック往復に落ちた)。QEMU でもマイクロベンチでも見えませんでした。 - C-056: ゴールデンテストが NaN を「完全一致」と報告して通していました。
出力の 27,136 サンプル中 26,368 が NaN なのにPearson 1.000000 / SNR inf dB / 一致と出ていました。
比較関数の 3 つの分岐が揃って NaN を「一致」側に落としていたためです。修正し、
毎回走る陽性対照を組み込みました。
全文は docs/decisions.md にあります。