Skip to content

fix(deps): sherpa-onnx を 1.13.6 へ上げ、非 ASCII models root の失敗を解消 (#377) - #410

Merged
Mega-Gorilla merged 8 commits into
mainfrom
fix/issue-377-sherpa-onnx-1136
Aug 25, 2026
Merged

Mega-Gorilla merged 8 commits into
mainfrom
fix/issue-377-sherpa-onnx-1136

Conversation

@Mega-Gorilla

Copy link
Copy Markdown
Owner

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 実モデル):

sherpa-onnx tokens のみ非 ASCII モデル dir 全体が非 ASCII
1.12.39 IndexError IndexError
1.13.6 OK OK

前者は変数を切り分けるため ONNX を ASCII 固定にしたもの、後者はフィールド報告と同じ条件です。

staging は実装していません

当初は tokens.txt のみを ASCII-safe な場所へ staging する案でした。#377 が定めた判断規則「最新版で解消する → version bump と staging のどちらを採るか比較」に従って比較し、bump を採りました:

version bump tokens-only staging
実測 1.13.6 で解消 未実装
範囲 C++ 層で wide path 化 → 同じ OpenInputFile() を通る他経路にも及ぶ tokens.txt のみ
保守 上流が持つ こちらが staging・lease・reaper・寿命を持ち続ける

上流が C++ 層で直したものを Python 層で迂回する理由がありません。

confidence 経路の回帰も見ています

avg_logprob の供給元 OfflineRecognitionResult.ys_log_probs は 「1.12.39 で expose されるようになった」Python 側の result schema で、依存更新で変わり得ます。schema が消えても転写テキストは正常に出るため、テキスト比較では検出できません — その場合 confidence filter は ReazonSpeech に対して pass-through へ degrade します。

両版でビット一致を確認しました:

1.12.39   avg_logprob = -0.16629084673794833   ys_log_probs_n = 22
1.13.6    avg_logprob = -0.16629084673794833   ys_log_probs_n = 22

回帰ゲートを CI へ置きました

非 ASCII の real-model probe は LIVECAP_NONASCII_REAL_MODELS=1 と slow マーカーの両方を要求するため、通常 CI (pytest tests) では skip されます。判定を observation から regression へ変えるだけでは将来の依存更新を防げません。

実モデルが常駐する self-hosted Windows の engine-smoke-gpu job へステップを追加しました (このランナーは LIVECAP_CORE_MODELS_DIR を永続させ、既存 warmup で reazonspeech を温めています)。

変更ファイル

ファイル 内容
pyproject.toml / uv.lock sherpa-onnx / sherpa-onnx-core を揃えて 1.13.6 へ
tests/nonascii/registry.py sherpa 3 行を ③staging → ②wide-path へ。expected_verdict を fail_silent → pass
benchmark_results/nonascii/2026-08-25/results.json 新しい証拠。fail_silent 7 → 6
docs/research/nonascii-path-boundary-inventory-2026-08.md §0 / §3 は自動生成なので --inject で再生成
.github/workflows/integration-tests.yml real-model regression ゲート
tests/integration/engines/test_reazonspeech_confidence_smoke.py 新規 avg_logprob の pin
livecap_cli/engines/reazonspeech_engine.py 「現 依存版」を主張する docstring のみ更新 (歴史的記述は書き換えない)

registry.py の更新は必須です — expected_verdict="fail_silent" のままだと bump 後に test_real_model_boundary が落ちます。さらに test_verified_rows_match_committed_evidence が registry の主張をコミット済み証拠と突き合わせるため、新しい results.json 無しには ②wide-path を主張できません (実際に一度落ちて、証拠を先に取り直しました)。

テスト

uv run pytest tests --ignore=tests/integration/sed -q                          2595 passed, 52 skipped
LIVECAP_NONASCII_REAL_MODELS=1 uv run pytest tests/nonascii -m nonascii_paths     471 passed, 2 skipped
uv run pytest tests/nonascii/test_registry.py tests/nonascii/test_report.py -q    383 passed

実地確認 (Windows) — 5/5 PASS

モデルディレクトリ全体を非 ASCII (…/ユーザー/モデル置き場/) にした実条件で、bump 後の実コードを確認しました:

[PASS] 0. resource 層が非 ASCII root を解決し is_ascii=False を報告する
[PASS] int8:    非 ASCII root で転写が成功し初回 / cache hit が一致
[PASS] int8:    avg_logprob=-0.1366 が閾値 -0.40 を上回る
[PASS] float32: 非 ASCII root で転写が成功し初回 / cache hit が一致
[PASS] float32: avg_logprob=-0.1663 が閾値 -0.40 を上回る

livecap-cli transcribe --engine reazonspeech の回帰も確認済みです。

wheel の可用性

  • sherpa-onnx: cp310–312 × win_amd64 / manylinux2014_x86_64 / macOS
  • sherpa-onnx-core: py3-none-<platform> (Python 版非依存)

CI matrix (ubuntu / windows × 3.10–3.12) を full cover しています。ローカルは Windows のみなので、CI が最後のゲートです。

Migration

既存ユーザーへの影響は改善のみ。 非 ASCII なユーザー名の環境で ReazonSpeech が使えるようになります。ASCII 環境では挙動が変わりません (転写テキスト・avg_logprob とも実測で一致)。

非スコープ

#409 cache key v2。ModelMemoryCache はプロセス内メモリなので、依存更新後の新プロセスへ壊れた recognizer は残らない。sherpa のバージョンに依存しない独立した bug として分離した
#392 post-load decode ヘルスチェック。bump 後も有効 — 上流が再び壊れたときに黙って原文が出るのを防ぐ多層防御
#361 hotwords。上流実装では同じ OpenInputFile() を通るが呼び出し箇所が無く runtime 未確認。source-level の見立てとして記録し、確認は #361 で行う

by.Scotty

Mega-Gorilla and others added 3 commits August 25, 2026 17:05
非 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>
@Mega-Gorilla

Copy link
Copy Markdown
Owner Author

再レビュー結果

結論: sherpa-onnx / sherpa-onnx-core の 1.13.6 への更新方針は適切で、最新 commit 820776f の CI も 12/12 checks PASS しています。ただし、今回追加した回帰ゲートには false green / unrelated failure につながる点が残っています。以下 3 点は merge 前の修正を推奨します。

指摘事項

  1. [MEDIUM] real-model probe が SymbolTable lookup を実際に通ったことを保証していません

    tests/nonascii/probes/native_models.py:203-217 は 220 Hz の合成信号を decode した後、stream.result.text が str であることだけを返しています。出力 token が 0 件の場合、問題の token ID -> SymbolTable lookup を通らないまま pass できます。

    probe 入力が確実に token を生成する音声であることを担保し、stream.result.tokens が非空であることを必須にしてください。あわせて token_count を observation に記録すると、本ゲートが修正対象経路を実際に通ったことを確認できます。

  2. [MEDIUM] _reazon_model_files() が int8 / float32 の不整合な組を生成できます

    tests/nonascii/probes/native_models.py:25-50 は encoder / decoder / joiner を個別に探索しています。そのため、int8 encoder だけが残った不完全なディレクトリと完全な float32 ファイルが共存すると、次の混在セットを返します。

    encoder-*.int8.onnx
    decoder-*.onnx
    joiner-*.onnx
    

    ローカルの最小確認でもこの組が返りました。各ファイルを独立に glob するのではなく、ReazonSpeech engine が要求する整合した既知セットを単位に、int8 一式 -> float32 一式の順で選択してください。あわせて int8-only / float32-only / incomplete-int8 + complete-float32 の unit test を追加するのが安全です。

    また、tests/nonascii/test_probes.py:149 は最初の「存在するディレクトリ」を選ぶため、先頭候補が不完全でも完全な第 2 候補へ進みません。候補選択時にファイルセットの完全性まで確認してください。

  3. [MEDIUM] workflow は SKIPPED を拒否するだけで、対象 test の実行成功を証明していません

    .github/workflows/integration-tests.yml:333-340 は sherpa_from_transducer.*SKIPPED のみを検出します。対象 test が改名・未収集・deselect された場合、SKIPPED が出ないため緑になります。

    対象 node ID を直接実行するか、出力に次が存在しなければ fail させてください。

    test_real_model_boundary[engine.reazonspeech.sherpa_from_transducer] PASSED
    

軽微

  • tests/nonascii/probes/native_models.py:157-161 の docstring と :201 / :210 のコメントが、現在も「既知 NG を再現する positive control」「decode で IndexError」と説明しています。1.13.6 以降の wide-path regression という現在の役割へ更新してください。
  • tests/integration/engines/test_reazonspeech_confidence_smoke.py:49 の REJECT_THRESHOLD = -0.40 は production 設定と重複しています。「現在の filter で clean sample が通る」ことが目的なら、FilterConfig().avg_logprob_thresholds["reazonspeech"] を参照した方が将来の再調整で乖離しません。

確認済み

  • CI: 12/12 checks PASS
  • self-hosted Windows: ReazonSpeech confidence smoke PASSED
  • self-hosted Windows: engine.reazonspeech.sherpa_from_transducer PASSED
  • ローカル: 415 passed, 10 deselected
  • git diff --check: 問題なし

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>
@Mega-Gorilla

Copy link
Copy Markdown
Owner Author

レビュー指摘への対応 (commit fc60a31)

5 件すべて妥当でした。全件修正し、指摘 2 には unit test を追加しています。

1. [MEDIUM] SymbolTable lookup を通ったことが保証されていない — 修正

ご指摘のとおりです。220 Hz の合成正弦波は音声ではありません。 token が 0 件なら stream.result.text は "" で、これは str なので pass します — つまり token id -> SymbolTable の lookup を通らずに緑になる、まさに本 probe が守っている経路を素通りする形でした。

1.12.39 で fail_silent を再現できていたのは、その正弦波でたまたま token が出ていたからにすぎず、保証ではありませんでした。

修正:

  • 入力を実発話へ (tests/assets/audio/ja/jsut_basic5000_0001.wav。ReazonSpeech は日本語モデルなので日本語資産)
  • stream.result.tokens が非空であることを必須にし、空なら ProbeSkipped で「本 probe は修正対象経路を検証できていない」と明示
  • token_count を観測へ記録

新しい証拠で確認できます:

sherpa real: pass | variant: int8 | tokens: 22

22 token が SymbolTable を引いたことが記録に残ります。

2. [MEDIUM] _reazon_model_files() が混在セットを返し得る — 修正

こちらもご指摘どおりです。ファイルごとに独立して glob していたため、壊れた int8 dir と完全な float32 が同居すると encoder-*.int8.onnx + decoder-*.onnx + joiner-*.onnx を返しました。

int8 の decoder は量子化されず .int8 が付かないため、per-file glob でも「正しく見える」錯覚があったのが原因です。実際の int8 セットが encoder .int8 + decoder 素 + joiner .int8 なので、混在との区別がつきません。

reazonspeech_engine.py:236-248 の required_files と同じ整合したセット単位に変更し、int8 一式 -> float32 一式の順で「全部そろっているか」を見るようにしました。

2b の候補選択もご指摘どおりでした。test_probes.py は「存在するディレクトリ」を採用しており、先頭候補が不完全でも第 2 候補へ進めません。_real_model_is_usable() を追加してセットの完全性まで確認します (判定は probe 側の reazon_model_files() を再利用 — ファイル名をここに書くと二重管理になるため)。

unit test を追加しました (tests/nonascii/test_reazon_model_files.py、6 件):

test_int8_only
test_float32_only
test_int8_is_preferred_when_both_are_present
test_incomplete_int8_falls_back_to_complete_float32   <- ご指摘のケース
test_incomplete_directory_is_rejected
test_missing_tokens_is_rejected

検出力も確認しています — 旧 per-file glob へ戻すと test_incomplete_int8_falls_back_to_complete_float32 だけが落ちます。

3. [MEDIUM] workflow が実行成功を証明していない — 修正

そのとおりです。SKIPPED を弾くだけでは、改名・deselect・未収集のときに SKIPPED が出ないまま緑になります。SKIPPED の不在を条件にしていたのが誤りで、PASSED の存在を要求すべきでした。

$target = 'test_real_model_boundary[engine.reazonspeech.sherpa_from_transducer] PASSED'
if (-not $out.Contains($target)) {
  throw "#377 の real-model regression が PASSED していない - skip / 改名 / 未収集のいずれか。..."
}

軽微 1: probe の docstring / コメント — 修正

「positive control — 既知 NG を再現する」「decode で IndexError」のままでした。1.13.6 以降は regression ゲートという現在の役割へ書き換え、1.12.39 までの挙動は経緯として残しています。

軽微 2: 閾値の重複 — 修正

REJECT_THRESHOLD = -0.40 は production 設定の複製でした。FilterConfig().avg_logprob_thresholds["reazonspeech"] を参照します。ご指摘のとおり、見たいのは「現在の filter で clean sample が通ること」なので参照が正しく、#334 PR-4 のような再調整でも乖離しません。

作業中に自分で壊した点

セット定義を差し替える際、スライス置換で onnxruntime_session_path の @probe(...) デコレータまで削ってしまい、ハーネスが 未登録の probe_id として検出しました。復元し、全 @probe 登録が HEAD と一致することを静的に照合したうえで証拠を取り直しています。

テスト

uv run pytest tests/nonascii/test_reazon_model_files.py -q                        6 passed
LIVECAP_NONASCII_REAL_MODELS=1 uv run pytest tests/nonascii -m nonascii_paths   471 passed, 2 skipped
uv run pytest tests --ignore=tests/integration/sed -q                          2601 passed, 52 skipped
uv run pytest tests/integration/engines/test_reazonspeech_confidence_smoke.py     1 passed

results.json と棚卸し表は強化後の probe で取り直しました (verdict の内訳は変わらず pass 124 / fail_silent 6 / fail_loud 2)。

by.Scotty

@Mega-Gorilla

Copy link
Copy Markdown
Owner Author

再レビュー結果

結論: 前回の指摘 5 件はすべて適切に修正されています。実装・テスト・CI ゲートに新たな問題は見つかりませんでした。

ただし、コミット済みの実測証拠に provenance の不整合が 1 点残っています。PR が再現可能な runtime evidence を成果物に含むため、merge 前の修正を推奨します。

指摘事項

  1. [MEDIUM] 強化後の実測結果が、強化前の commit を測定元として記録しています

    benchmark_results/nonascii/2026-08-25/results.json:6 と docs/research/nonascii-path-boundary-inventory-2026-08.md:66 は、測定元を 820776f と記録しています。

    一方、同じ results.json の ReazonSpeech observation には今回追加した token_count: 22 が含まれています。このコードは commit fc60a31 で追加されたため、記録された 820776f を checkout してもこの結果は再生成できません。

    つまり、強化後の probe を未コミットの working tree で実行したため、git rev-parse HEAD だけが旧 commit を記録した状態です。現 schema には dirty tree / patch 情報もなく、証拠だけを見ると実行コードを特定できません。

    推奨対応: commit fc60a31 を HEAD にした状態でハーネスを再実行し、results.json と棚卸し表を再生成してください。これにより git_commit が fc60a31... となり、証拠と実行コードが一致します。将来的には dirty tree を拒否するか git_dirty を記録すると、同じ不整合を防げます。

前回指摘の確認

  • 実発話を使用し、tokens 非空を必須化、token_count を記録: 解消
  • int8 / float32 を整合したセット単位で選択: 解消
  • 不完全な第 1 候補から完全な第 2 候補への fallback: 解消
  • CI が対象テストの明示的な PASSED 行を要求: 解消
  • docstring / 閾値 SSOT の修正: 解消

検証

  • CI: 12/12 checks PASS
  • self-hosted Windows の対象行: test_real_model_boundary[engine.reazonspeech.sherpa_from_transducer] PASSED
  • self-hosted non-ASCII suite: 9 passed, 2 skipped, 469 deselected
  • ローカル: 389 passed
  • uv lock --check: 成功
  • git diff --check: 問題なし

上記 provenance の再生成後は、マージ可能と判断します。

by.codex-review

Mega-Gorilla and others added 2 commits August 25, 2026 18:54
未コミットの 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>
@Mega-Gorilla

Copy link
Copy Markdown
Owner Author

レビュー指摘への対応 (commit 895574b / ac901d6)

指摘は妥当でした。証拠の provenance を修正し、あわせて同じ不整合が再発しない仕組みを入れています。

1. [MEDIUM] 強化後の実測結果が強化前の commit を測定元として記録していた — 修正

ご指摘のとおりです。確認しました:

results.json の git_commit : 820776fec1624789a9f435ba0a50ad205117bd91
token_count を追加した commit: fc60a31   (git log -S "token_count" で確認)

記録された 820776f を checkout してもこの結果は再生成できません。 強化後の probe を未コミットの working tree で実行したため、git rev-parse HEAD だけが 1 つ前の commit を返していました。

clean な HEAD で取り直しました:

git_commit: 895574bff871da27fd0ef313137379ca8c485a53
git_dirty : False

記録した commit が実際に再現可能であることも確認しています:

$ git show 895574bf:tests/nonascii/probes/native_models.py | grep -c token_count
1

再発防止: git_dirty を記録する

ご提案の「dirty tree を拒否するか git_dirty を記録する」を採用しました。記録する方を選んでいます — 開発中の探索的な実行を止める理由はなく、問題は「dirty で走ったことが証拠から分からない」ことだからです。

RunMetadata に追加し、棚卸し表の §0 にも出します:

状態 §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

@Mega-Gorilla

Copy link
Copy Markdown
Owner Author

CI の赤について — 既知 flake (#406) でした

ac901d6 の初回 CI で tests (3.12) が 1 件落ちましたが、本 PR とは無関係の既知 flake です。再実行でコード変更なしに 12/12 SUCCESS になりました。

FAILED tests/nonascii/test_harness_selftest.py::TestReaperLiveness::test_live_session_is_not_reaped_even_when_old
確認 結果
#406 が指すテスト 完全に同一
本 PR が reaper に触れているか 触れていない (test_harness_selftest.py / roots.py は diff に無い)
同ブランチの直前 CI 3 回連続 12/12 SUCCESS
再実行 12/12 SUCCESS

本 PR で record.py に git status --porcelain を 1 回追加しているため、reaper の liveness テストがタイミング依存であることを踏まえて影響を検討しましたが、この呼び出しは session fixture の構築時に 1 度走るだけで、reaper の判定区間 (holder 起動 -> 未回収確認 -> kill -> 回収確認) の内側には入りません。

#406 に発生記録を残しました。

現在の状態: 12/12 SUCCESS、ac901d6。 前回レビューの provenance 指摘は解消済みです。

by.Scotty

@Mega-Gorilla

Copy link
Copy Markdown
Owner Author

再レビュー結果

結論: 前回指摘した provenance 不整合は解消されています。clean な 895574b を測定元として証拠が再生成され、同 commit に token_count を含む強化後の probe が存在することも確認できました。

ただし、再発防止として追加した git_dirty 判定に false clean となる実在経路が 1 点あります。現在の証拠自体は有効ですが、再発防止契約を成立させるため merge 前の修正を推奨します。

指摘事項

  1. [MEDIUM] git status の設定によって、未追跡の実行コードを dirty と判定できません

    tests/nonascii/record.py:207 の _git_dirty() は次を実行しています。

    git status --porcelain
    

    このコマンドは status.showUntrackedFiles 設定を尊重します。そのため、開発者が次を設定している環境では、未追跡の新しい probe / helper / test が実行に使われても出力が空になり、git_dirty=False と記録されます。

    git config status.showUntrackedFiles no
    

    隔離したローカル repository で確認しました。

    untracked: new_probe.py
    git status --porcelain
    -> 出力なし
    
    git status --porcelain --untracked-files=all
    -> ?? new_probe.py
    

    今回防ぎたいのは「記録された commit に存在しないコードで測定したのに clean と表示する」ことなので、未追跡ファイルは必ず検出する必要があります。

    推奨修正:

    _run_git("status", "--porcelain", "--untracked-files=all")
    

    あわせて、少なくとも次を unit test で固定してください。

    • clean -> False
    • tracked / staged change -> True
    • untracked file + status.showUntrackedFiles=no -> True
    • git 実行不能 -> None
    • report 表示の False / True / None の 3 分岐

解消確認

  • results.json: git_commit=895574bff871da27fd0ef313137379ca8c485a53
  • results.json: git_dirty=false
  • 895574b の probe に token_count が存在: 確認済み
  • 棚卸し表と results.json の run_id / commit: 一致
  • 前回までの SymbolTable / model variant / explicit PASSED gate 指摘: 解消済み

検証

  • CI: 12/12 checks PASS
  • ローカル対象テスト: 389 passed
  • 現在の working tree に対する _git_dirty(): false
  • git diff --check: 問題なし

上記の untracked 強制検出を追加すれば、マージ可能と判断します。

by.codex-review

Mega-Gorilla and others added 2 commits August 25, 2026 21:10
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>
@Mega-Gorilla

Copy link
Copy Markdown
Owner Author

レビュー指摘への対応 (commit 74c30cc / d093f68)

指摘は妥当でした。隔離した repository で再現し、修正して unit test で固定しています。

1. [MEDIUM] status.showUntrackedFiles により未追跡コードを dirty と判定できない — 修正

ご指摘の経路を実測で再現しました:

$ git config status.showUntrackedFiles no      # 未追跡ファイル 1 件がある状態
$ git status --porcelain
[]                                              <- 空。偽の clean
$ git status --porcelain --untracked-files=all
[?? new_probe.py]                               <- 検出できる

防ぎたいのは「記録した commit に存在しないコードで測ったのに clean と表示する」ことなので、未追跡は必ず拾う必要があります。 未追跡の probe / helper / test はまさにその代表例で、git_dirty を入れた意味が消えていました。

--untracked-files=all を付与しました (record.py)。

unit test で固定 (11 件)

ご提示の 5 ケースをすべて含めています。_run_git / _git_dirty / _git_commit に cwd を注入可能にし、隔離 repo で検証できるようにしました。

ケース 期待
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

@Mega-Gorilla

Copy link
Copy Markdown
Owner Author

再レビュー結果

結論: 前回指摘した git_dirty の偽 clean 経路は適切に修正されています。追加で ASR モデル互換性も確認し、新たな blocking finding はありません。PR #410 はマージ可能と判断します。

前回指摘の確認

  • git status --porcelain --untracked-files=all により、status.showUntrackedFiles=no が設定されていても未追跡の probe / helper を検出: 解消
  • clean / tracked変更 / staged変更 / 未追跡 / showUntrackedFiles=no / ignored / git判定不能を隔離 repository で検証: 11 tests で固定
  • False / True / None の metadata 表示を区別: 解消
  • 修正を含む commit 74c30cc の clean tree で証拠を再生成し、git_commit=74c30cc... / git_dirty=false を記録: provenance 整合
  • 記録された commit 自体に --untracked-files=all が含まれることも確認済み

ASR モデル互換性の追加確認

sherpa-onnx 1.13.6 の影響範囲は ReazonSpeech に限定され、lockfile 上も変更は sherpa-onnx / sherpa-onnx-core の同時 bump のみです。Parakeet / Canary / WhisperS2T / Qwen3-ASR / Voxtral の依存版には波及していません。

ReazonSpeech の int8 / float32 実モデルを 1.12.39 と 1.13.6 で各10回デコードした結果:

  • 両モデルとも load / decode / process終了に成功し、ハングなし
  • 転写結果の UTF-8 bytes、token数 (22)、avg_logprob がバージョン間で一致
  • OfflineRecognizer.from_transducer() の使用引数と ys_log_probs schema は互換
  • onnxruntime -> sherpa_onnx / sherpa_onnx -> onnxruntime の両 import 順で実モデル転写に成功
  • 実装済み timeout / upstream の GIL release 契約にも退行なし

したがって、現在サポートしている ReazonSpeech int8 / float32 モデルについて、1.13.6 への更新に起因するモデル形式・API・confidence の互換性問題は確認されませんでした。

検証

  • CI: 12/12 checks PASS
  • ローカル: 400 passed
  • uv lock --check: 成功
  • git diff --check: 問題なし
  • ReazonSpeech confidence smoke: 1 passed(前回追加確認)

非 blocking の残余リスク

Linux / macOS では ReazonSpeech の実モデル smoke が未実施です。Linux の ReazonSpeech smoke は既存の ABI 課題により以前から除外されているため、Windows の非 ASCII 不具合を直す本PRのマージは妨げませんが、クロスプラットフォームの実モデル保証とは分けて扱うのが適切です。

by.codex-review

@Mega-Gorilla

Copy link
Copy Markdown
Owner Author

レビュー確認 (d093f68) — 追加対応なし

新たな指摘が無いため、コード変更はありません。 独自に検証できる事実主張を確かめたうえで、残余リスクの記述だけ精密化して報告します。

前回指摘の確認 — 相違なし

git_dirty の偽 clean 経路、証拠の provenance、11 件の unit test による固定について、ご確認いただいた内容と当方の実装は一致しています。

追加確認いただいた ASR モデル互換性について

int8 / float32 × 10 回デコードでの一致確認、import 順の両方向、GIL release 契約まで見ていただいた点、こちらでは実施していない検証でした。特に onnxruntime -> sherpa_onnx / sherpa_onnx -> onnxruntime の両 import 順は、ネイティブ拡張の初期化順が絡む領域で、私の確認範囲から漏れていました。

独自に検証した点

1. lockfile の変更範囲 — ご指摘のとおりでした。

$ git diff origin/main -- uv.lock | grep -B4 "^[+-]version" | grep "^ name" | sort -u
 name = "sherpa-onnx"
 name = "sherpa-onnx-core"

この 2 つ以外にバージョンが動いたパッケージはありません。 Parakeet / Canary / WhisperS2T / Qwen3-ASR / Voxtral の依存へ波及していないことを確認しました。

2. Linux での ReazonSpeech smoke 除外 — 事実ですが、原因は 2 つあります。

根拠
hosted runner の CPU smoke test_smoke_engines.py:77 — 「ReazonSpeech on CPU is disabled due to sherpa-onnx/onnxruntime ABI issues on hosted runners. See PR #34」
self-hosted の GPU smoke integration-tests.yml の matrix コメント — 「self-hosted Linux runner は未運用のため windows のみ」

ご指摘の「既存の ABI 課題」は前者にあたり、後者はそもそも Linux ランナーが運用されていないという別の事情です。ギャップを埋めるには「hosted runner の ABI 問題を解く」か「self-hosted Linux を立てる」かのどちらかが要る、という形になります。

残余リスクの精密化

「Linux / macOS で ReazonSpeech の実モデル smoke が未実施」はそのとおりです。ただし Linux CI で 1.13.6 が全く触られていないわけではありません。

cheap tier の sherpa.from_transducer.diff probe (native_models.py:107-135) は:

  • import sherpa_onnx する
  • OfflineRecognizer.from_transducer() をネイティブに呼ぶ (意図的に壊した ONNX で、ASCII / 非 ASCII のエラー署名を比較する)
  • cheap tier なので通常 CI (pytest tests) で走る

本 PR の CI ログで、ubuntu の tests (3.10/3.11/3.12) に + sherpa-onnx==1.13.6 が入り、この probe が通っていることを確認しました。

したがって Linux で検証済み / 未検証の切り分けは次になります:

Linux CI
wheel の解決・インストール 検証済み (3.10 / 3.11 / 3.12)
import sherpa_onnx 検証済み
ネイティブ from_transducer() の呼び出し 検証済み (不正 ONNX 経路)
実モデルの load + decode 未検証
avg_logprob / ys_log_probs schema 未検証

未検証なのは最後の 2 行です。ABI レベル (wheel が読めるか、binding が動くか) は Linux でも通っており、残るのは実モデル固有の挙動である、という位置づけになります。

なお #377 が直す不具合自体は Windows の narrow-path 起因で、Linux では元から再現しません (SymbolTable の std::ifstream は POSIX では UTF-8 バイト列をそのまま扱えるため)。

状態

CI          12/12 SUCCESS
tip         d093f68
mergeState  CLEAN

クロスプラットフォームの実モデル保証を別扱いにするというご判断に同意します。必要であれば別 issue として起票しますが、本 PR のスコープ外と考えています。

by.Scotty

@Mega-Gorilla

Copy link
Copy Markdown
Owner Author

再レビュー結果

結論: 新たな指摘はありません。前回のマージ可能判断を維持します。

今回の確認時点で PR head は d093f68 のままで、前回レビュー後のコード差分はありません。投稿された補足説明を、実装・workflow・CI と照合しました。

確認結果

  • Linux hosted CI では sherpa-onnx 1.13.6 の wheel 解決・install・import が Python 3.10 / 3.11 / 3.12 で通っている: 説明どおり
  • cheap tier の sherpa.from_transducer.diff は Linux CI でも native OfflineRecognizer.from_transducer() を呼ぶ: 説明どおり
  • 一方、Linux / macOS での ReazonSpeech 実モデル load + decode、および ys_log_probs / avg_logprob は未検証: 残余リスクの切り分けが正確
  • 本 issue の障害は Windows の narrow-path 読み込みに固有であり、Linux実モデルの既存カバレッジ不足は本PRの修正スコープ外: 妥当
  • Windowsでは int8 / float32 の実モデル、confidence、非ASCII rootを確認済みで、本修正の回帰ゲートは成立している

状態

  • CI: 12/12 checks PASS
  • merge state: CLEAN
  • 前回ローカル検証: 400 passed
  • ASR互換性A/B: int8 / float32とも転写結果・token数・avg_logprob一致、ハングなし

Linux / macOS の実モデル保証を将来必要とする場合は独立issueで扱うのが適切ですが、PR #410 のmerge blockerではありません。

by.codex-review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant