fix(deps): sherpa-onnx を 1.13.6 へ上げ、非 ASCII models root の失敗を解消 (#377) - #410
Conversation
非 ASCII なユーザー名の models root で ReazonSpeech の全 transcribe が IndexError: invalid unordered_map<K, T> key で失敗していた。ロード時には エラーが出ないため、フィールド報告では 1 セッション中 421 回失敗して 成功 0 件、プロセスは "running" のまま無出力で回り続けていた。 原因は当リポジトリ外 -- 1.12.39 の SymbolTable が tokens.txt を narrow path の std::ifstream で開き、Windows で空のまま例外なく Init されること。上流 PR #3255 で OpenInputFile() -> ToWideString() を通るようになり解消した。 こちら側の staging は実装しない。上流が C++ 層で直したものを Python 層で 迂回する理由がなく、staging では tokens.txt しか救えない。 実測 (int8 実モデル、tokens のみ非 ASCII / モデル dir 全体が非 ASCII の両条件): 1.12.39 両方で IndexError 1.13.6 両方で正常転写 confidence 経路も確認。ys_log_probs は「1.12.39 で expose された」Python 側の result schema であり依存更新で変わり得るが、両版で avg_logprob が -0.16629084673794833 / ys_log_probs_n=22 のビット一致。 - registry の sherpa 3 行を ③staging -> ②wide-path へ更新 - benchmark_results/nonascii/2026-08-25/ を新しい証拠として追加し、 棚卸し表の §0 / §3 を再生成 (fail_silent 7 -> 6) - 実モデル regression を engine-smoke-gpu job へ追加 (通常 CI では LIVECAP_NONASCII_REAL_MODELS と slow の両方が要るため skip される) - ReazonSpeech の avg_logprob を実モデル smoke で pin (既存の token_confidence テストは NeMo 系しか見ておらず未 pin だった) cache key の欠陥は #409 へ分離した (sherpa のバージョンに依存しない別 bug)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
初回の CI で判明: ゲートのステップは走ったが、対象行そのものが SKIPPED だった。 test_real_model_boundary[engine.reazonspeech.sherpa_from_transducer] SKIPPED 原因は warmup が float32 (reazon-research--reazonspeech-k2-v2) しか取得せず、 probe が要求する int8 (sherpa-onnx-zipformer-ja-reazonspeech-2024-08-01) が ランナーに存在しなかったこと。probe は実モデルが無いと静かに skip するため、 テストは緑のままゲートだけが失効していた。 - warm() に extra を追加し、int8 variant も温める - ゲートのステップが「対象行が実際に走ったこと」を検証する。skip を検出 したら失敗させる -- 通ったことと守れていることは別である Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI が「ゲートは緑だが対象行は skip」を検出したので、原因側を直す。 probe が int8 のファイル名 (encoder-*.int8.onnx 等) をハードコードし、 _REAL_MODEL_SOURCES も int8 dir 1 つに固定していた。CI ランナーには float32 しか温まっておらず、probe は「実モデルが存在しない」と静かに skip していた。 int8 を選んでいたのは軽い (154 MB vs 592 MB) からであって測定内容は 同じなので、**どちらが置かれていてもゲートが成立する**形にする。 - _reazon_model_files() がモデルディレクトリからファイル名を発見する (int8 優先、無ければ float32) - _REAL_MODEL_SOURCES が候補 tuple を取り、最初に存在したものを使う - 観測に model_variant を記録する 前 commit で足した int8 warmup は不要になったので撤去した。効果を確認 できなかった呼び出しを CI に残さない。 float32 のみの models root で probe が pass することをローカルで確認済み (以前は skip していた条件)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
再レビュー結果結論: 指摘事項
軽微
確認済み
by.codex-review |
レビュー指摘 5 件への対応。 MEDIUM: - probe が 220 Hz の合成正弦波を decode していた。token が 0 件だと token id -> SymbolTable lookup を**通らずに** pass できる -- まさに本 probe が守っている経路を素通りする。実発話 (日本語テスト資産) に変え、 token が非空であることを必須にし、token_count を観測へ記録する - reazon_model_files() がファイルごとに独立して glob しており、壊れた int8 dir と完全な float32 が同居すると混在セットを返した。engine の required_files と同じ**整合したセット単位**で選ぶ (int8 -> float32) - 候補ディレクトリの選択が「存在するか」しか見ておらず、先頭候補が 不完全でも第 2 候補へ進めなかった。セットの完全性まで確認する - workflow が SKIPPED を弾くだけだった。改名・deselect・未収集では SKIPPED が出ないまま緑になるため、PASSED 行の**存在を要求する**形へ 軽微: - probe の docstring / コメントが「既知 NG を再現する positive control」の ままだった。1.13.6 以降の wide-path regression という現在の役割へ - REJECT_THRESHOLD が production 設定の数値を複製していた。 FilterConfig().avg_logprob_thresholds["reazonspeech"] を参照する Tests: test_reazon_model_files.py を新設 (6 件)。int8-only / float32-only / 両方 / 不完全 int8 + 完全 float32 / 不完全 / tokens 欠落。旧 per-file glob へ 戻すと混在セットのケースが落ちることを確認済み。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
レビュー指摘への対応 (commit fc60a31)5 件すべて妥当でした。全件修正し、指摘 2 には unit test を追加しています。 1. [MEDIUM] SymbolTable lookup を通ったことが保証されていない — 修正ご指摘のとおりです。220 Hz の合成正弦波は音声ではありません。 token が 0 件なら 1.12.39 で 修正:
新しい証拠で確認できます: 22 token が SymbolTable を引いたことが記録に残ります。 2. [MEDIUM]
|
再レビュー結果結論: 前回の指摘 5 件はすべて適切に修正されています。実装・テスト・CI ゲートに新たな問題は見つかりませんでした。 ただし、コミット済みの実測証拠に provenance の不整合が 1 点残っています。PR が再現可能な runtime evidence を成果物に含むため、merge 前の修正を推奨します。 指摘事項
前回指摘の確認
検証
上記 provenance の再生成後は、マージ可能と判断します。 by.codex-review |
未コミットの working tree でハーネスを実行すると、git_commit は 1 つ前の commit を指したまま手元の変更で測ることになる。証拠だけを見ても実行コードを 特定できず、再現不能な evidence が commit される。 実際に起きた: 強化後の probe が出した token_count を、その機能が入る前の commit (820776f) の測定結果として記録していた。 - RunMetadata に git_dirty を追加 (None = git が使えず判定不能。判定不能と clean を混同しない) - 棚卸し表の §0 に「dirty tree - この commit では再現できない」と出す Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
results.json が測定元を 820776f と記録していたが、その observation には token_count が入っていた -- この機能は fc60a31 で追加されたので、記録された commit を checkout してもこの結果は再生成できない。強化後の probe を未コミットの working tree で実行したことによる provenance の不整合。 clean な HEAD (895574b) で再実行し、results.json と棚卸し表を再生成した。 git_commit: 895574b git_dirty : False 記録した commit が token_count を含むこと (= 再現可能であること) を確認済み。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
レビュー指摘への対応 (commit 895574b / ac901d6)指摘は妥当でした。証拠の provenance を修正し、あわせて同じ不整合が再発しない仕組みを入れています。 1. [MEDIUM] 強化後の実測結果が強化前の commit を測定元として記録していた — 修正ご指摘のとおりです。確認しました: 記録された clean な HEAD で取り直しました: 記録した commit が実際に再現可能であることも確認しています: 再発防止:
|
| 状態 | §0 の表示 |
|---|---|
| clean | 895574bf... |
| dirty | <commit> **(dirty tree - この commit では再現できない)** |
| git が使えない | <commit> (dirty 判定不能) |
None (判定不能) と False (clean) を区別しています。git が無い環境で「clean だった」と記録するのは、まさに今回と同じ種類の嘘になるためです。
動作も確認しました:
編集中: git_dirty = True | commit = fc60a316
commit 後: git_dirty = False | commit = 895574bf
なぜ「新しい commit と記録が一致しない」のは問題ないか
本 commit (ac901d6) は 895574b と記録された証拠を含むので、記録 commit と証拠を格納する commit は 1 つずれます。これは不整合ではありません — 守るべき不変条件は「記録された commit を checkout してハーネスを回せばこの結果が再現できる」であり、895574b は最終的な probe コードを含んでいます。証拠ファイル自体は probe の入力ではありません。
今回問題だったのは、820776f にそもそも token_count を出すコードが無かったことです。
テスト
LIVECAP_NONASCII_REAL_MODELS=1 uv run pytest tests/nonascii -m nonascii_paths 472 passed, 2 skipped
uv run pytest tests/nonascii/test_registry.py tests/nonascii/test_report.py -q 383 passed
uv run pytest tests --ignore=tests/integration/sed -q 2601 passed, 52 skipped
git diff --check 問題なし
verdict の内訳は変わっていません (pass 124 / fail_silent 6 / fail_loud 2)。
所感
CI ログを読むまで気づかなかった「ゲートが緑だが対象行は skip」に続いて、証拠そのものの再現可能性も外から指摘されて初めて直りました。どちらも「テストが通った」「証拠を commit した」で満足すると素通りする種類です。git_dirty はその一部を機械の側へ移すものです。
by.Scotty
CI の赤について — 既知 flake (#406) でした
本 PR で #406 に発生記録を残しました。 現在の状態: 12/12 SUCCESS、 by.Scotty |
再レビュー結果結論: 前回指摘した provenance 不整合は解消されています。clean な 895574b を測定元として証拠が再生成され、同 commit に token_count を含む強化後の probe が存在することも確認できました。 ただし、再発防止として追加した git_dirty 判定に false clean となる実在経路が 1 点あります。現在の証拠自体は有効ですが、再発防止契約を成立させるため merge 前の修正を推奨します。 指摘事項
解消確認
検証
上記の untracked 強制検出を追加すれば、マージ可能と判断します。 by.codex-review |
git status --porcelain は status.showUntrackedFiles 設定を尊重する。
`no` を設定した環境では、未追跡の probe / helper / test が実行に使われても
出力が空になり、git_dirty=False (再現可能) と記録してしまう。
隔離 repo で実測:
showUntrackedFiles=no + 未追跡ファイルあり
git status --porcelain -> 出力なし
git status --porcelain --untracked-files=all -> ?? new_probe.py
防ぎたいのは「記録した commit に存在しないコードで測ったのに clean と
表示する」ことなので、未追跡は必ず拾う。
- --untracked-files=all を付与
- _run_git / _git_dirty / _git_commit に cwd を注入可能にしてテスト可能に
- test_git_provenance.py を新設 (11 件): clean / tracked 変更 / staged /
未追跡 / **showUntrackedFiles=no + 未追跡** / ignore 済み / git 不能 (None) /
report の 3 分岐
-uall を外すと showUntrackedFiles=no のケースだけが落ちることを確認済み。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
git_dirty=False の記録は、未追跡ファイルを見落とし得る旧判定 (--untracked-files=all なし) で出したものだった。判定が弱いままの 「再現可能」表示は、まさに本 issue で潰した偽 clean と同じ性質を持つ。 clean な HEAD (74c30cc) で再実行: git_commit: 74c30cc git_dirty : False 記録した commit が --untracked-files=all を含むこと (= この判定を再現できる こと) を確認済み。verdict の内訳は変わらない。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
レビュー指摘への対応 (commit 74c30cc / d093f68)指摘は妥当でした。隔離した repository で再現し、修正して unit test で固定しています。 1. [MEDIUM]
|
| ケース | 期待 |
|---|---|
| clean | False |
| tracked 変更 | True |
| staged 変更 | True |
| 未追跡ファイル | True |
showUntrackedFiles=no + 未追跡 |
True |
ignore 済み (.venv / __pycache__ 相当) |
False |
| git repository でない | None |
report の 3 分岐 (False / True / None) |
それぞれ書き分け |
ignore 済み を足したのは、.venv や __pycache__ で常時 True になると判定そのものが無意味になるためです。
検出力も確認しました — --untracked-files=all を外すと test_untracked_file_when_status_hides_untracked だけが落ちます:
-uall なし: 1 failed, 10 passed
-uall あり: 11 passed
render_metadata のテストは実物の RunMetadata から payload を作ります。手書きの dict にすると、フィールドが増えたときにこのテストだけが古い形を検証し続けるためです。
証拠も取り直しました
前回コミットした git_dirty: false は、未追跡を見落とし得る旧判定で出した値でした。値自体は結果的に正しかったのですが、判定が弱いままの「再現可能」表示は、本 issue で潰した偽 clean と同じ性質を持ちます。
clean な HEAD で再実行しました:
git_commit: 74c30cc7dbde29a97f5bea0757a9ce9e23dbc156
git_dirty : False
記録した commit が --untracked-files=all を含むこと (= この判定を再現できること) も確認済みです:
$ git show 74c30cc7:tests/nonascii/record.py | grep -c "untracked-files=all"
2
verdict の内訳は変わっていません (pass 124 / fail_silent 6 / fail_loud 2)。
保守的に倒している点 (トレードオフ)
--untracked-files=all により、evidence ファイル自体の変更も dirty として拾います。probe コードの再現性には影響しないので、これは偽陽性です。
それでも保守的側に倒しました — 偽の dirty は目に見えて直せますが、偽の clean は本 issue で直したバグそのものだからです。運用は「コードを commit -> clean な tree で測定 -> 証拠を commit」の順で回ります。この旨は docstring にも書いています。
テスト
uv run pytest tests/nonascii/test_git_provenance.py -q 11 passed
LIVECAP_NONASCII_REAL_MODELS=1 uv run pytest tests/nonascii -m nonascii_paths 483 passed, 2 skipped
uv run pytest tests --ignore=tests/integration/sed -q 2612 passed, 52 skipped
git diff --check 問題なし
by.Scotty
再レビュー結果結論: 前回指摘した 前回指摘の確認
ASR モデル互換性の追加確認
ReazonSpeech の int8 / float32 実モデルを 1.12.39 と 1.13.6 で各10回デコードした結果:
したがって、現在サポートしている ReazonSpeech int8 / float32 モデルについて、1.13.6 への更新に起因するモデル形式・API・confidence の互換性問題は確認されませんでした。 検証
非 blocking の残余リスクLinux / macOS では ReazonSpeech の実モデル smoke が未実施です。Linux の ReazonSpeech smoke は既存の ABI 課題により以前から除外されているため、Windows の非 ASCII 不具合を直す本PRのマージは妨げませんが、クロスプラットフォームの実モデル保証とは分けて扱うのが適切です。 by.codex-review |
レビュー確認 (d093f68) — 追加対応なし新たな指摘が無いため、コード変更はありません。 独自に検証できる事実主張を確かめたうえで、残余リスクの記述だけ精密化して報告します。 前回指摘の確認 — 相違なし
追加確認いただいた ASR モデル互換性についてint8 / float32 × 10 回デコードでの一致確認、import 順の両方向、GIL release 契約まで見ていただいた点、こちらでは実施していない検証でした。特に 独自に検証した点1. lockfile の変更範囲 — ご指摘のとおりでした。 この 2 つ以外にバージョンが動いたパッケージはありません。 Parakeet / Canary / WhisperS2T / Qwen3-ASR / Voxtral の依存へ波及していないことを確認しました。 2. Linux での ReazonSpeech smoke 除外 — 事実ですが、原因は 2 つあります。
ご指摘の「既存の ABI 課題」は前者にあたり、後者はそもそも Linux ランナーが運用されていないという別の事情です。ギャップを埋めるには「hosted runner の ABI 問題を解く」か「self-hosted Linux を立てる」かのどちらかが要る、という形になります。 残余リスクの精密化「Linux / macOS で ReazonSpeech の実モデル smoke が未実施」はそのとおりです。ただし Linux CI で 1.13.6 が全く触られていないわけではありません。 cheap tier の
本 PR の CI ログで、ubuntu の したがって Linux で検証済み / 未検証の切り分けは次になります:
未検証なのは最後の 2 行です。ABI レベル (wheel が読めるか、binding が動くか) は Linux でも通っており、残るのは実モデル固有の挙動である、という位置づけになります。 なお #377 が直す不具合自体は Windows の narrow-path 起因で、Linux では元から再現しません ( 状態クロスプラットフォームの実モデル保証を別扱いにするというご判断に同意します。必要であれば別 issue として起票しますが、本 PR のスコープ外と考えています。 by.Scotty |
再レビュー結果結論: 新たな指摘はありません。前回のマージ可能判断を維持します。 今回の確認時点で PR head は 確認結果
状態
Linux / macOS の実モデル保証を将来必要とする場合は独立issueで扱うのが適切ですが、PR #410 のmerge blockerではありません。 by.codex-review |
Closes #377
何が問題だったか
Windows のユーザー名が非 ASCII (
C:\Users\ユーザー\...) の環境で、ReazonSpeech の全 transcribe がIndexError: invalid unordered_map<K, T> keyで失敗していました。ロード時にはエラーが一切出ません。フィールド報告では 1 セッション中 421 回同一例外で、成功した文字起こしは 0 件。
stream.pyが握って継続するため、プロセスは "running" のまま無出力で回り続けます。sherpa-onnx 1.12.39のSymbolTableがtokens.txtを narrow path のstd::ifstreamで開き、Windows では UTF-8 バイト列が ANSI/CP932 として解釈されて open に失敗、空のまま例外なく Init されるのが原因でした。ONNX 本体は onnxruntime が wide path を使うため正常にロードされ、debug=Trueでも vocab_size は ONNX メタデータ由来で表示されるので気づく手がかりがありません。上流が既に直していました
PR #3255 で
SymbolTableがOpenInputFile()を使い、Windows ではToWideString()経由で開くようになっています。隔離環境で A/B を実測しました (int8 実モデル):
IndexErrorIndexError前者は変数を切り分けるため ONNX を ASCII 固定にしたもの、後者はフィールド報告と同じ条件です。
staging は実装していません
当初は
tokens.txtのみを ASCII-safe な場所へ staging する案でした。#377 が定めた判断規則「最新版で解消する → version bump と staging のどちらを採るか比較」に従って比較し、bump を採りました:OpenInputFile()を通る他経路にも及ぶtokens.txtのみ上流が C++ 層で直したものを Python 層で迂回する理由がありません。
confidence 経路の回帰も見ています
avg_logprobの供給元OfflineRecognitionResult.ys_log_probsは 「1.12.39 で expose されるようになった」Python 側の result schema で、依存更新で変わり得ます。schema が消えても転写テキストは正常に出るため、テキスト比較では検出できません — その場合 confidence filter は ReazonSpeech に対して pass-through へ degrade します。両版でビット一致を確認しました:
回帰ゲートを CI へ置きました
非 ASCII の real-model probe は
LIVECAP_NONASCII_REAL_MODELS=1とslowマーカーの両方を要求するため、通常 CI (pytest tests) では skip されます。判定を observation から regression へ変えるだけでは将来の依存更新を防げません。実モデルが常駐する self-hosted Windows の
engine-smoke-gpujob へステップを追加しました (このランナーはLIVECAP_CORE_MODELS_DIRを永続させ、既存 warmup で reazonspeech を温めています)。変更ファイル
pyproject.toml/uv.locksherpa-onnx/sherpa-onnx-coreを揃えて 1.13.6 へtests/nonascii/registry.pyexpected_verdictをfail_silent→passbenchmark_results/nonascii/2026-08-25/results.jsonfail_silent7 → 6docs/research/nonascii-path-boundary-inventory-2026-08.md--injectで再生成.github/workflows/integration-tests.ymltests/integration/engines/test_reazonspeech_confidence_smoke.py新規avg_logprobの pinlivecap_cli/engines/reazonspeech_engine.pyregistry.pyの更新は必須です —expected_verdict="fail_silent"のままだと bump 後にtest_real_model_boundaryが落ちます。さらにtest_verified_rows_match_committed_evidenceが registry の主張をコミット済み証拠と突き合わせるため、新しいresults.json無しには ②wide-path を主張できません (実際に一度落ちて、証拠を先に取り直しました)。テスト
実地確認 (Windows) — 5/5 PASS
モデルディレクトリ全体を非 ASCII (
…/ユーザー/モデル置き場/) にした実条件で、bump 後の実コードを確認しました:livecap-cli transcribe --engine reazonspeechの回帰も確認済みです。wheel の可用性
sherpa-onnx: cp310–312 ×win_amd64/manylinux2014_x86_64/ macOSsherpa-onnx-core:py3-none-<platform>(Python 版非依存)CI matrix (ubuntu / windows × 3.10–3.12) を full cover しています。ローカルは Windows のみなので、CI が最後のゲートです。
Migration
既存ユーザーへの影響は改善のみ。 非 ASCII なユーザー名の環境で ReazonSpeech が使えるようになります。ASCII 環境では挙動が変わりません (転写テキスト・
avg_logprobとも実測で一致)。非スコープ
ModelMemoryCacheはプロセス内メモリなので、依存更新後の新プロセスへ壊れた recognizer は残らない。sherpa のバージョンに依存しない独立した bug として分離したOpenInputFile()を通るが呼び出し箇所が無く runtime 未確認。source-level の見立てとして記録し、確認は #361 で行うby.Scotty