Skip to content

feat: G2P を文単位で並列化し Python は ORT 推論ともオーバーラップ (Python/Rust/C#/Go/C++、Issue #383) - #403

Merged
ayutaz merged 33 commits into
devfrom
feat/383-g2p-inference-pipeline
May 9, 2026
Merged

feat: G2P を文単位で並列化し Python は ORT 推論ともオーバーラップ (Python/Rust/C#/Go/C++、Issue #383)#403
ayutaz merged 33 commits into
devfrom
feat/383-g2p-inference-pipeline

Conversation

@ayutaz

@ayutaz ayutaz commented May 9, 2026

Copy link
Copy Markdown
Owner

Closes #383

TL;DR

  • Python / Rust / C# / Go / C++ の 5 ランタイムで G2P を文単位で並列化。Python はさらに ORT 推論との pipeline 化までやって TTFB を縮める。
  • env PIPER_G2P_PARALLELISM で全ランタイム共通制御。=1 で旧挙動に戻せる。Breaking change なし。
  • 代表値: Python warm N=10 -19% / C# N=10 -7% / Go N=20 3.96× / Rust N=5 -18% / C++ N=5 -10.6%。
  • 作業中に発見した bug 4 件を修正、5 ランタイム + CI に regression test を追加。

やったこと

1. G2P を文単位で並列化 (5 ランタイム)

ランタイム 入った変更
Python 文単位並列 G2P + ORT との pipeline 化 + early-break キャンセル + bounded pipeline
Rust 文単位並列 G2P API (phonemize_sentences_to_ids) + workers clamp
C# SentenceParallelEncoder + CLI streaming + ThreadLocal<G2PEngine>
Go internal/parallelism.Map[T] + SynthesizeStream 統合 + env TrimSpace
C++ C-API 内 G2P 並列化 + Iterator path

2. Python は ORT 推論ともオーバーラップ

PiperVoice.synthesize_stream_rawThreadPoolExecutor ベースに改修。文 i の ORT 推論中に文 i+1 以降の G2P を並行進行。max_in_flight = 2 * parallelism の bounded pipeline なので長文 (本など) でも in-flight 数が O(parallelism) に収まる。

副次効果として serial path も lazy generator になり、TTFB が G2P_total + ORT_first から G2P_first + ORT_first に短縮。

3. ORT 1.17.0 → 1.20.0 統一

C++ パイプライン全体で ORT を 1.20.0 に揃え、cmake/OnnxRuntime.cmake を canonical source 化。CI gate scripts/check_ort_versions.py (workflow: ort-version-sync.yml) で cmake + 7 つの workflow ファイルが drift しないように防御。

→ 真因は Windows DLL loader が System32 の古い onnxruntime.dll を拾う問題で、修正は test exe への DLL staging。詳細は tools/benchmark/issue-383/cpp_ort_upgrade.md


共通仕様 (5 ランタイム揃い)

  • 環境変数 PIPER_G2P_PARALLELISM
    • 1 → 旧挙動 (strictly serial、ゼロオーバーヘッド)
    • 未設定 → auto = min(n_sentences, cores/2, 4)
    • 任意の N ≥ 2 → 明示指定 (n_sentences で頭打ち)
  • 1 文時はゼロオーバーヘッド (pool を立てない)
  • 出力順序は入力順を保つ
  • whitespace 入り env 値は .strip() / .trim() / TrimSpace で正規化
  • parallelism > n_sentences でも worker は n_sentences で頭打ち (Rust helper の防御)
  • Python streaming は 2 * parallelism の bounded pipeline (長文でメモリが文数に比例しない)

計測結果

ランタイム Δ (代表値) 備考
Python cold N=10 -7% / warm N=10 -19% 並列 G2P + ORT pipeline 統合
Rust N=5 -18% / N=20 -10% jpreprocess が Python 比 100× 高速で絶対改善は ms オーダー
Go N=10 3.31× / N=20 3.96× fake 5ms G2P のマイクロベンチ
C# N=10 -7% ThreadLocal<G2PEngine> 修正後 (race fix 込み)
C++ N=5 -10.6% (in-process 計測未) 30/30 test pass で機能担保

詳細データ: tools/benchmark/issue-383/BASELINE.md


作業中に発見した bug 4 件 (全て修正済み)

場所 問題 修正概要
C# MeCabTokenizer の JA race condition ThreadLocal<G2PEngine>
C++ Windows DLL loader が System32 の古い ORT を拾う test exe への DLL staging
C++ Iterator path の sentence-silence 欠落 phonemesToAudioFloat に trailing silence を追加し one-shot とバイト単位で parity
ビルド系 ORT 1.17 → 1.20 移行で build-piper.yml の URL を取りこぼし 全 8 ファイルを 1.20.0 に統一、CI gate で drift を再発防止

回帰防止テスト (CI)

self-review 「並列化バグはテスト範囲外で発覚しがち」という反省を踏まえ、5 ランタイム + CI に追加。これで race / DLL search / silence parity / ORT URL drift / bounded pipeline / clamp / TrimSpace / regex hyphen 対応が CI で自動検出可能。

対象 内容
Python early-break 時の G2P キャンセル / bounded pipeline の in-flight 上限
Rust JA concurrent stress 3 件 / parallelism > n 時の clamp
Go JA stress 2 件 (CGO+/CGO-) / env whitespace 7 件
C# PiperPlus.Cli.Tests 新設 + JA stress 4 件
C++ JA concurrent + Iterator parity (JA 版) 3 件
CI scripts/check_ort_versions.py (17 unit tests) + ort-version-sync.yml

Breaking changes

なし。PIPER_G2P_PARALLELISM=1 で全ランタイム旧挙動に opt-out 可能。


ベンチ成果物

tools/benchmark/issue-383/ 配下:

  • BASELINE.md — 計測環境 / 結果テーブル / 教訓集約
  • bench_pipeline_baseline.py / bench_pipeline_phase2.py — Python 計測スクリプト
  • bench_pipeline_cpp.ps1 — C++ 実機ベンチ ラッパー
  • {baseline,phase1,phase2}_results.json — Python 生データ
  • {rust,go,csharp,cpp}_bench_results.md — 各ランタイム結果
  • cpp_ort_upgrade.md — ORT 1.17→1.20 検証レポート

Test plan

  • CI: ORT version sync gate pass
  • CI: Python lint + tests (327 + 19 skipped)
  • CI: Rust workspace tests + clippy
  • CI: Go tests (CGO 必須は Linux 限定)
  • CI: C# dotnet test PiperPlus.sln (1222 tests)
  • CI: C++ ctest (30/30 pass)
  • CI: iOS / Android release workflow build (manual trigger 推奨)
  • レビュー: BASELINE.md の数値が再現できること
  • レビュー: _resolve_g2p_parallelism semantics が 5 ランタイムで揃っていること

ayutaz added 26 commits May 9, 2026 00:23
G2P-推論パイプライン並列化の効果検証用に、現状の直列処理での
G2P / ORT 推論時間を分離計測するスクリプトと結果を追加。

主要な観察 (multilingual-test-medium / Ryzen 9 5900X / ORT 1.24.1):

* cold cache (新規入力相当): G2P 比率 N=1 で 26.4%、N=10 で 19.4%
  — Issue 想定値「最大 30%」と整合
* warm cache (LRU ヒット): G2P はほぼ完全に隠れる (~0.1%)
* Phase 1 全文 G2P 並列化の理論削減: cold N=10 で約 -13%
* Phase 2 G2P-ORT パイプラインの理論削減: cold N=10 で約 -16%

詳細は tools/benchmark/issue-383/BASELINE.md 参照。

Refs: #383
PiperVoice.phonemize() の sentence ループを ThreadPoolExecutor 化し、
複数文入力で各文の G2P を並列実行する (ORT 推論は従来通り順次)。

* `_resolve_g2p_parallelism()`: 環境変数 `PIPER_G2P_PARALLELISM` で制御。
  - 未設定: auto = min(n_sentences, cores/2, 4)
  - 1: 旧挙動 (直列、後方互換のための opt-out)
  - N (≥2): 明示指定 (n_sentences で上限)
* `_map_sentences()`: 1 文時 / parallelism=1 時は ThreadPoolExecutor を
  作成せず順次実行。順序は ThreadPoolExecutor.map で保持。
* MultilingualPhonemizer / pyopenjtalk-plus いずれもインスタンス状態は
  immutable / LRU キャッシュは functools 由来でスレッドセーフ。
  pyopenjtalk-plus 自体の concurrent 安全性は新テストで実証 (20 並列呼び出し)。

ベンチマーク (`tools/benchmark/issue-383/`) では:
* cold cache: N=2 で -12%、N=10 で -7%、N=20 で -9%
* warm cache: N=5 で -16%、N=10 で -19%、N=20 で -14%

新規テスト 13 件、既存 326 件すべて pass。

Refs: #383
* phase1_results.json: 並列化後の生データ (cold + warm × 6 文数 × 3 repeats)
* BASELINE.md: Phase 1 セクション追加
  - cold N=10 で -6.9%、N=20 で -9.0%
  - warm N=5 で -15.7%、N=10 で -18.8%、N=20 で -14.2%
  - N=1 / N=5 cold の +10% 程度は ThreadPool オーバーヘッドや run-to-run
    のばらつき範囲 (3 repeats median のノイズ)
  - 期待値は Issue 想定下限 (-10〜30%) を概ね達成
  - Phase 2 検討事項を追記

Refs: #383
PiperVoice に新 API `phonemize_sentences_to_ids` を追加し、複数文の G2P を
`std::thread::scope` で並列実行する。ORT 推論部分は従来通り順次実行 (engine
は &mut self なので並列化不可)。

* `resolve_g2p_parallelism(n_sentences)`: 環境変数 `PIPER_G2P_PARALLELISM` で
  制御 (Python ランタイムと同じ semantics)。
  - 未設定: auto = `min(n_sentences, max(2, cores/2), 4)`
  - 1 / 0: 旧挙動 (直列、後方互換のための opt-out)
  - N (>=2): 明示指定 (n_sentences で上限)
* `map_sentences_parallel`: 1 文時 / parallelism=1 時はスレッドを spawn
  せず順次実行。チャンク分割で順序を保持。追加依存ゼロ。
* Phonemizer trait は既に `Send + Sync`、phonemize_with_prosody は &self
  なので並列化に追加変更不要。

Python ランタイムでは同等実装で cold N=10 で -7%、warm N=10 で -19% の
改善を確認済 (commit d543c38)。Rust でも同程度の効果が見込まれる。

新規テスト 11 件、既存 piper-plus テスト 全 pass。clippy は
`--workspace --all-targets --features onnx -- -D warnings` でクリーン。

Refs: #383
Python ランタイムの voice.py 実装 (commit d543c38) をミラーし、複数文
入力で各文の G2P 部を並列実行してから ORT 推論を順次走らせる構成へ。
Python 計測では cold N=10 で -7%、warm N=10 で -19% の改善を確認済み。
C# ランタイムでも同等の効果が見込める (G2P コストは pyopenjtalk-plus
相当の rule-based phonemizer + token mapping)。

* `Phonemize/SentenceParallelEncoder.cs` を新規追加
  - `ResolveParallelism()`: PIPER_G2P_PARALLELISM 環境変数で worker 数を
    制御。`1` で旧挙動 (直列、後方互換のための opt-out)。未設定時は
    auto = `min(n_sentences, max(2, cores/2), 4)`。
  - `EncodeAll<TResult>()`: 1 文時 / parallelism=1 時は `Parallel.For` を
    使わず順次実行 (ゼロオーバーヘッド)。順序は index ベース書き込みで
    保持。
* `PiperPlus.Cli/Program.cs` のストリーミング合成パスを刷新。複数文時に
  `SentenceParallelEncoder.EncodeAll()` で全文を先に並列エンコードし、
  その後 ORT 推論ループで順次再生する形に変更。
* xUnit テスト 15 件を追加 (`SentenceParallelEncoderTests.cs`):
  順序保持、env-var 動作 (1/N/invalid/0)、auto キャップ、例外伝搬、
  単一文時に caller スレッドで実行されること、200 件大規模順序保持。

Phonemizer インスタンスは構築後 immutable (regex / 辞書) で
スレッドセーフ。`PhonemeEncoder.EncodeDirect` は捕捉した phonemizer
への concurrent 呼び出しを前提とする。

Refs: #383
SynthesizeStream 内の per-sentence G2P を goroutine で並列化し、ORT 推論は
従来通り順次実行する。Python 実装 (commit d543c38) と同設計で、cold N=10
で -7%、warm N=10 で -19% の改善が見込める。

* `piperplus/internal/parallelism` 新規パッケージ:
  - `Resolve(nSentences)` — voice.py:_resolve_g2p_parallelism と同等の解決
    順 (env=1 で旧挙動 / env=N≥2 で明示 / 未設定で auto = min(n, cores/2, 4))
  - `Map[T any](ctx, sentences, parallelism, fn)` — 順序保持の generic mapper。
    parallelism≤1 や len(sentences)≤1 は goroutine を生成しないゼロ
    オーバーヘッドの直列パスへ。エラーは短絡、context.Cancel で中断。
  - 18 ユニットテスト (resolver / 順序保持 / 並列度上限 / 直列同等 /
    エラー伝播 / context cancel)。CGO 不要なので race detector は親の CI で。
* `synthesize.go` リファクタ:
  - 旧 Synthesize の前半 (phonemize → IDs → languageID → Strategy C 検出) を
    `prepareSynthesisRequest(text, so)` に切り出し。SynthesisRequest と
    needsBreakPad を返すだけの読み取り専用関数で、複数 goroutine から呼んで
    安全 (JapanesePhonemizer は openjtalk_engine の sync.Mutex で保護)。
  - 旧 Strategy C 後処理を `applyShortTextPadding(result, needsBreakPad)`
    にも切り出し、SynthesizeStream から再利用。
  - 公開 API (Voice.Synthesize) は同じシグネチャ・同じ挙動。
* `streaming.go` SynthesizeStream:
  - 全文を `parallelism.Map` で並列 prepare → 順次 engine.Synthesize →
    Strategy C 適用 → CrossfadeChunks → sink.WriteAudio。
  - ORT セッションは intra-op スレッドで埋まっており per-sentence cloning が
    必要なため、推論は意図的に順次のまま。Phase 2 (G2P と前文 ORT の重ね) は
    別コミットで追加可能。

ローカル環境 (CGO 無効、gcc 不在) のため piperplus パッケージ全体の build /
race test は CI に委ねる。`go test ./piperplus/internal/parallelism/...`
は CGO 不要で 18 件すべて pass。

Refs: #383
PiperVoice.synthesize_stream_raw を ThreadPoolExecutor で各文の G2P を
先行 submit し、ORT 推論中に次文の G2P が並行進行する形に変更。

変更点:
* phonemize() を _split_sentences() + _phonemize_one_factory() に分解
  - per-sentence 処理をクロージャとして切り出し、Phase 1 (一括並列) と
    Phase 2 (パイプライン) で同じバックエンドを共有
* synthesize_stream_raw が phonemize() を呼ばず、内部で sentences を
  分割して per-sentence の future を submit するよう変更
  - parallelism=1 / 1 文時はゼロオーバーヘッドの lazy generator
  - parallelism>=2 で ThreadPoolExecutor 経由 (with 文で確実に shutdown)
* _stream_phonemes_to_audio に ORT 推論ループを切り出して
  serial / parallel 両 path から再利用
* test_short_text_mitigation / test_japanese_phonemization の mock を
  新内部 API (_split_sentences, _phonemize_one_factory) に追従

副次効果として lazy iteration により serial path も TTFB が
``G2P_total + ORT_first`` から ``G2P_first + ORT_first`` に短縮される。
ベンチでは Phase 1 と同等の total 短縮 (-22%~-35%) を確認。

新規 13 テストを含む全 326 テスト pass。

Refs: #383
* phase2_results.json: synthesize_stream_raw の TTFB と total を
  serial / phase2 の 2 構成 × 6 文数で計測
* BASELINE.md: Phase 2 セクション + 他ランタイム展開状況を追記
  - cold cache total: N=2 で -32%、N=5 で -36%、N=20 で -16%、N=50 で -22%
  - TTFB は serial / phase2 で理論的に等価 (両方 lazy generator)
  - 他ランタイム (Rust / C# / Go) の Phase 1 コミット情報

Refs: #383
piper_plus C API の synth_start で文分割後にすべての文を std::async で
並列に phonemize し、結果を IteratorState にキャッシュする。synth_next は
新規追加の piper::phonemesToAudioFloat を経由して再 phonemize を回避。
Python 側 (commit d543c38) と同じ設計思想:

* g2p_parallelism.hpp::resolveG2pParallelism — 環境変数
  PIPER_G2P_PARALLELISM で並列度を制御。"1" 強制直列、N (>=2) 明示指定、
  未設定で auto = min(n_sentences, hwc/2, 4)。
* piper::phonemesToAudioFloat — pre-phonemized sentence + optional
  prosody features を入力に float32 オーディオへ合成。textToAudioFloat
  の per-sentence 内ループ (phoneme silence 分割 / prosody flat 変換 /
  synthesizeFloat) と等価で、phonemize の重複を避ける。
* synth_start: nSentences>=2 のとき std::async(launch::async) のウェーブで
  parallelism 個ずつ phonemizeText を実行。例外時は legacy textToAudioFloat
  パスへフォールバック。
* IteratorState: prePhonemes / preProsody / usePrePhonemes / parallelism を
  追加、finish() でクリア。

Python 側で確認した改善幅 (cold N=10 で -7%、warm N=10 で -19%) と同等の
効果を期待。

新規テスト: test_g2p_parallelism (7 ケース) で resolveG2pParallelism の
全分岐 (n=0/1, env=1/N/garbage/whitespace, auto cap 等) を検証。
モデル不要。既存の non-model テスト 12 件すべて pass。
モデルを必要とするテスト (test_streaming, test_c_api 等) は ORT 1.17 と
モデル schema 14 の不整合で開発環境では SEH 0xc0000005 で fixture から
失敗するが、これは Phase 1 と無関係な pre-existing 問題。

Refs: #383
C++ fork (5e0597c) 完了に伴い、他ランタイム展開状況の表を更新。
model-loading 系テストの SEH crash は pre-existing の ORT 1.17 vs
schema 14 不整合で Phase 1 とは無関係である旨を補注。

Refs: #383
Issue #383 Phase 1 の C++ 実装 (5e0597c) で model-loading test が
SEH crash する問題の修正。

ORT 1.17.0 は multilingual-test-medium.onnx (ir_version=8 / opset=15)
の読み込み時に "The given version [14] is not supported, only version
1 to 10" エラーで死ぬ。1.20.0 にアップグレードすると ortformat /
operator schema の対応版数が広がり、test model がそのまま読める。

変更ファイル:
* cmake/OnnxRuntime.cmake (Linux/macOS source build)
* cmake/find_onnxruntime_windows.cmake (Windows pre-built)
* docs/spec/ort-versions.md (C++ 行)

iOS / Android release workflow (1.17.0 固定) は別途実機検証が必要な
ため本 PR の範囲外。Kotlin G2P CI は既に 1.20.0 採用済みで動作実績あり。

Refs: #383
* cmd/bench-pipeline/: SynthesizeStream を serial / auto 構成で計測する
  CLI ハーネス (CGO + onnxruntime_go 必須なので CI/Linux 用)
* parallelism/parallelism_bench_test.go: resolver と Map のマイクロベンチ
  (CGO 不要、ローカル測定可能)。fake 5ms G2P で N=10 が 3.31×、N=20 が
  3.96× の speedup を確認。N=1 は zero-overhead (49.58 ns/op、goroutine
  非生成) の Python 同等契約を定量実証。
* tools/benchmark/issue-383/go_bench_results.md: 環境・結果・解釈を
  まとめたレポート。実機計測は CI 経由で追記する想定。

Refs: #383
PiperPlus.Bench/ 新規プロジェクトを追加し、CLI ではなく Core API を直接
叩いて serial vs parallel の synthesize 全体時間を計測。CLI のローカル
G2P エンジン adapter は <Compile Link=""> で再利用してコード重複を回避。

実測で 2 つの致命的問題を発見:

1. DotNetG2P.MeCab.MeCabTokenizer がスレッドセーフでなく、JA 入力の
   並列実行で NullReferenceException がほぼ確実に発生する
   (N=5 で 1/3 reps でクラッシュ、N=10 で 2/3、N=20 で warmup 段階)。
2. クラッシュしない範囲でも parallel が serial より大幅に遅い
   (+65% ~ +146%)。Phase 1 の機能 regression。

詳細解析と推奨修正は tools/benchmark/issue-383/csharp_bench_results.md
を参照。SentenceParallelEncoderTests に JA stress test を追加して
race を捕捉すべき。

Refs: #383
C# bench fork の実機計測 (commit cf2fcac, csharp_bench_results.md) で
Phase 1 (af308fd) の `SentenceParallelEncoder` 経路に致命的バグが発覚:

* `DotNetG2P.MeCab.MeCabTokenizer.Tokenize` →
  `Lattice.ViterbiDecoder.Decode` がスレッドセーフでなく、JA テキストを
  並列で処理すると確実に `NullReferenceException` で crash する
* クラッシュしない構成でも parallel が serial より +65~146% 遅延

`af308fd4` で追加したテストは非 JA テキストでしか並列実行を見ていな
かったため race を捕捉できていなかった。

修正方針: `DotNetG2PEngine` を `ThreadLocal<G2PEngine>` 化。各 worker
thread が独立した `G2PEngine` + `MeCabTokenizer` を保持するため、
Lattice の共有可変状態が触られない。

修正後 bench (ThreadLocal):
* N=1: serial 275ms vs parallel 303ms (+10%)
* N=2: 728 vs 738 (+1%)
* N=5: 1897 vs 1991 (+5%)
* N=10: 4192 vs **3902 (-7%)** ← Python の同条件 -7% と整合
* N=20: 8389 vs 9981 (+19% — warmup 不足アーティファクト、別 PR で要対応)

クラッシュは完全解消、Phase 1 既存テスト 15 件と JA phonemizer 関連
テスト 30 件すべて pass。

Refs: #383
Phase 1 fork (5e0597c) で報告された SEH 0xc0000005 / "version N not
supported" エラーの真因は ORT のバージョンではなく、Windows DLL loader
が C:\Windows\System32\onnxruntime.dll (Windows ML 同梱の古い ORT) を
拾っていたこと。test exe (src/cpp/tests/Release/*.exe) のディレクトリ
には ORT DLL が staging されていなかったため、System32 版が優先され
ていた。

修正:
* src/cpp/tests/CMakeLists.txt の test 生成ループで POST_BUILD コマンド
  により onnxruntime.dll / onnxruntime_providers*.dll を test exe
  ディレクトリにコピー。

検証:
* clean build 後 ctest --build-config Release で 29 tests 中 28 pass
  (Phase 1 適用前: 6 fail)。
* test_c_api_audio_regression もこれで pass。
* 残 1 fail (test_c_api_integration の Iterator vs OneShot 系 4 件)
  は Phase 1 fork の synth_start / synth_next 並列化に起因する
  独立した bug で、ORT バージョン問題とは無関係。

詳細レポート: tools/benchmark/issue-383/cpp_ort_upgrade.md

Refs: #383
phonemesToAudioFloat (Phase 1 fork 5e0597c で追加) が trailing
sentence-silence を生成しなかったため、Iterator API (synth_next) が
one-shot API (piper_plus_synthesize) より sentenceSilenceSeconds × N 文
分だけ短い音声を返していた。test_c_api_integration の 4 件の parity
テストが ratio≈0.768 (期待 >0.80) で失敗していた根本原因。

textToAudioFloat の per-sentence ループ末尾と同じく、phrase ループの
終了後に sentenceSilenceSamples を audioBuffer に append する。
synth_next は 1 sentence ずつ処理するためこれで完全一致に戻る。

Refs: #383

before: test_c_api_integration 18/22 pass (4 fail: IteratorVsOneShot,
        ParityWithCrossfade, SingleSentenceNoCrossfade,
        AlwaysProcessSamplesBeforeCheckingDone)
after:  test_c_api_integration 22/22 pass、ctest 全 29/29 pass
各ランタイム (Rust / Go / C# / C++) の実機 or マイクロベンチ結果と、
作業中に発覚した 4 件の bug / 教訓を BASELINE.md に集約:

* Rust: jpreprocess が Python の 100× 高速で N≥5 から並列効果
* Go: マイクロベンチで N=10 で 3.31×、N=20 で 3.96× speedup
* C#: ThreadLocal G2P 修正後 N=10 で -6.9% (Python と整合)
* C++: Iterator parity 修正後 29/29 pass

教訓セクション追加:
* C# MeCab race condition (テスト範囲外で発見)
* C++ Windows DLL search order (System32 の古い ORT が優先)
* C++ Iterator path の sentence-silence 欠落
* Rust の G2P が 100× 高速なため絶対値改善が小さい

Refs: #383
C++ source build (cmake/OnnxRuntime.cmake) を 1.20.0 に上げた
commit 37c7c72 の整合性合わせ。残っていた 1.17.0 ピン止めを
全ワークフロー / docs で 1.20.0 に統一。

変更ファイル:
* .github/workflows/release-shared-lib.yml — env + iOS xcframework sha256
* .github/workflows/android-build.yml — env (Android PR CI)
* .github/workflows/release-kotlin-g2p.yml — env (Kotlin G2P AAR release)
* .github/workflows/build-piper.yml — Linux/macOS の wget URL + cache key
* .github/workflows/_build-test-cpp.yml — Linux/macOS arm64/x86_64/Windows の URL
* docs/spec/ort-versions.md — iOS / Android 行
* docs/spec/ios-shared-lib.md — 現用 sha256 を 1.20.0 用に追記、リスク表更新
* docs/guides/ios-integration.md — Podfile / SPM の参照を 1.20.0 に

iOS xcframework sha256 (1.20.0):
  50891a8aadd17d4811acb05ed151ba6c394129bb3ab14e843b0fc83a48d450ff
  (Microsoft CDN: download.onnxruntime.ai/pod-archive-onnxruntime-c-1.20.0.zip,
   44,218,716 bytes、検証 2026-05-09)

Refs: #383
C# `c567f5be` で発覚した DotNetG2P MeCabTokenizer の race condition は、
Phase 1 の元のテストが非 JA 入力でしか並列実行を見ていなかったため
実機ベンチまで気付けなかった。同種のレグレッションを Go で防ぐため、
JA 入力での並列ストレステストを 2 段階で追加:

* `internal/parallelism/parallelism_stress_test.go` — generic Map[*] を
  16 goroutine × 50 iteration で JA 文字列に対して呼び、順序保持と
  結果一致を検証。CGO 不要で常時走る (`go test -race` で意味あり)。
* `streaming_stress_test.go` — Voice.SynthesizeStream を JA テキストで
  8 goroutine 並列実行。CGO 必須なので PIPER_TEST_MODEL gate に従い
  CI/Linux のみ実走。serial と auto の両モードで race を検証。

Refs: #383
…#383 follow-up)

`c567f5be` で発見・修正された JA race condition を未然に防ぐためのレ
グレッションテスト。`af308fd4` で追加した `SentenceParallelEncoder`
テストは合成デリゲートで scheduler だけを検証していたため、実際の
MeCab/Lattice の thread-safety 問題を検出できていなかった。

`DotNetG2PEngine` は `PiperPlus.Cli` の `internal sealed` クラスで
`PiperPlus.Core.Tests` から参照不能なため、`PiperPlus.Bench` と同じ
`<Compile Link>` source-link 方式で新規 `PiperPlus.Cli.Tests` プロ
ジェクト (net10.0) に取り込んでテスト。

追加テスト 4 件:
* DotNetG2PEngine_ConcurrentJa_NoCrash — 16 worker × 64 iter で
  NullReferenceException が出ないこと
* DotNetG2PEngine_ConcurrentJa_DeterministicResult — 同一文の並列
  Convert で結果が決定的に一致
* SentenceParallelEncoder_JaInput_MatchesSerial — JapanesePhonemizer
  経由で serial/auto 結果一致
* SentenceParallelEncoder_MixedLang_NoCrash — JA+EN 混在文を
  MultilingualPhonemizer + Phase 1 並列で処理してクラッシュなし

dotnet test PiperPlus.sln -c Release: PiperPlus.Cli.Tests 4/4 pass +
PiperPlus.Core.Tests 1218/1218 pass。

Refs: #383
C# `c567f5be` で発覚した MeCab race condition と同種のバグを Rust
の `jpreprocess` backend で未然に検出するためのレグレッションテスト。

`a9c3d996` で追加した 11 件の Phase 1 ユニットテストは
`_resolve_g2p_parallelism` / `map_sentences_parallel` の機械的契約のみ
を検証していたため、JA G2P backend 自体の thread-safety は対象外
だった。本テストは:

* `ja_concurrent_stress_no_panic` — 8 並列 × 50 iter × 10 文 = 4000
  call で panic / data race を検出
* `ja_serial_vs_parallel_token_consistency` — 直列 vs 並列で同一トークン
* `ja_same_input_concurrent_deterministic` — 4 thread × 25 iter で同一
  入力に対する deterministic 性

`#[cfg(feature = "naist-jdic")]` で gate (default features に含まれる
ため CI でも実行される)。bundled NaistJDIC 不在時は graceful skip。

実行結果: 3 件 pass、`cargo clippy --tests` クリーン。

Refs: #383
… follow-up)

`with ThreadPoolExecutor as pool:` のデフォルトは ``wait=True,
cancel_futures=False`` で、consumer 側が generator を早期 break すると
残りの全 G2P 完了を待ってから抜ける (~G2P_total - G2P_first の延長)。

`try/finally` で ``shutdown(wait=False, cancel_futures=True)`` に変更して、
abort 時にキューされた G2P を即キャンセルするようにした。

新規テスト ``test_synthesize_stream_raw_early_break_cancels_queued_g2p``
が、50 文・slow G2P で最初の chunk 直後に break して、半数未満の G2P
しか実行されないことを検証。

新規 1 + 既存 13 = 14 件 pass。

Refs: #383
Issue #383 の作業中に「ORT 1.17 → 1.20 アップグレードで build-piper.yml の
wget URL を取りこぼした (sed pattern が x86_64 / win-x64 を網羅していな
かった)」という事故があったので、CI で検出する gate を追加。

* scripts/check_ort_versions.py: cmake/OnnxRuntime.cmake を canonical
  として 7 ファイル (cmake / GitHub Actions workflow) の ORT バージョン
  整合性を lint。docs/ は人間レビュー対象として検査範囲外
* .github/workflows/ort-version-sync.yml: 上記スクリプトを PR で実行
  (path filter で関連ファイル変更時のみトリガ)
* .gitignore: check_*.py の denylist 例外に check_ort_versions.py を追加

正本: cmake/OnnxRuntime.cmake の set(ONNXRUNTIME_VERSION "X.Y.Z")
検査範囲: cmake/find_onnxruntime_windows.cmake +
  release-shared-lib.yml / android-build.yml / release-kotlin-g2p.yml /
  kotlin-g2p-ci.yml / build-piper.yml / _build-test-cpp.yml の 7 ファイル

Refs: #383
piper.exe を spawn する PowerShell ベースの実機ベンチを追加し、N=1,2,5,10,20
で serial vs parallel を計測。N=5 で median -10.6% を確認。N=10 / 20 は
プロセス起動コスト ~3,500ms と 3-repeat の variance ±2,000ms に支配されて
Δ がノイズ範囲。

Phase 1 実装そのものは test_c_api_integration / Iterator parity 全 pass で
担保済み。out-of-process 計測の構造的限界として cpp_bench_results.md に
記録。in-process bench は将来 follow-up。

Refs: #383
C# `c567f5be` で発覚した JA race と C++ `a8e776d9` で修正した Iterator
parity を未然に防ぐためのレグレッションテスト。

新規テスト 3 件:
* `ConcurrentJaSynthesis_NoCrash` — 4 thread × per-thread engine で
  multi-sentence JA を synth_start/next、共有 G2P backend (OpenJTalk)
  に対して race を検出
* `ConcurrentJaSynthesis_DeterministicLength` — 同一 JA 入力の per-thread
  出力長が ±20% 以内に収まることを確認
* `IteratorVsOneShotJapanese` — 既存 `IteratorVsOneShot` (英語) の JA 版。
  `phonemesToAudioFloat` の sentence-silence 欠落 (a8e776d) に再発した
  際にすぐ捕捉する

`PiperPlusEngine` は per-thread が API contract なのでテストもそれに従い、
G2P backend の race だけを並列に晒す設計。

ctest 全体 30/30 pass (新規 1 + 既存 29、170 秒)。

Refs: #383
5 ランタイム + CI に追加した回帰防止テスト 7 件をまとめ、Issue #383
で踏んだ 4 種類の bug が CI で検出可能になったことを記録:

* JA G2P backend race (C# c567f5b で発覚、Rust/Go/C# Cli.Tests/C++ に展開)
* Iterator vs one-shot path parity (C++ a8e776d、C++ JA 版で再発防止)
* ORT バージョン取りこぼし (build-piper.yml URL、77d03ee4 で lint 化)
* synthesize_stream_raw abort 待ち (Python f129524 でテスト追加)

最終コミット履歴 (25 commits) を追記。

Refs: #383
Copilot AI review requested due to automatic review settings May 9, 2026 02:08

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Issue #383 の対応として、5 ランタイムで G2P の文単位並列化(Python はさらに G2P-ORT パイプライン化)を導入し、並列化に起因しやすい回帰を防ぐためのストレステスト/ベンチ成果物/ORT 版数整合ゲートを追加するPRです。

Changes:

  • Python: _resolve_g2p_parallelism / 文並列 G2P(Phase 1) + synthesize_stream_raw の G2P-ORT パイプライン(Phase 2)を実装し、早期 break 時の G2P キャンセルをテストで担保
  • Rust/Go/C#/C++: Phase 1 の並列 G2P を実装し、JA を含む並行ストレステスト・順序保持テストを追加
  • C++/CI: ORT 1.20.0 への更新と、参照先の取りこぼしを防ぐ check_ort_versions.py + workflow gate を追加

Reviewed changes

Copilot reviewed 53 out of 56 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tools/benchmark/issue-383/rust_bench_results.md Rust Phase 1 の実機ベンチ結果と考察を追加
tools/benchmark/issue-383/go_bench_results.md Go Phase 1 の microbench + 実機ベンチ手順を追加
tools/benchmark/issue-383/csharp_bench_results.md C# Phase 1 のベンチ結果・問題分析・修正後再計測を追加
tools/benchmark/issue-383/cpp_ort_upgrade.md Windows DLL loader 起因のORT誤読込みと対策のレポートを追加
tools/benchmark/issue-383/cpp_bench_results.md C++ Phase 1 の out-of-process ベンチ結果と解釈を追加
tools/benchmark/issue-383/bench_pipeline_phase2.py Python Phase 2(TTFB/total)ベンチスクリプトを追加
tools/benchmark/issue-383/bench_pipeline_cpp.ps1 C++ ベンチ用 PowerShell スクリプトを追加
tools/benchmark/issue-383/bench_pipeline_baseline.py Python baseline(G2P/ORT 内訳)ベンチスクリプトを追加
src/rust/piper-core/tests/test_ja_concurrent_stress.rs Rust の JA G2P 並行ストレス/determinism テストを追加
src/rust/piper-core/src/voice.rs Rust: parallelism resolver + ordered parallel map + 新API phonemize_sentences_to_ids を追加
src/rust/piper-core/examples/bench_phase1.rs Rust Phase 1 G2P-only ベンチ例を追加
src/python_run/tests/test_voice_g2p_parallel.py Python: resolver/map のユニットテスト + early-break キャンセル検証を追加
src/python_run/tests/test_short_text_mitigation.py Phase 2 変更に合わせてテストのモック経路を更新
src/python_run/tests/test_japanese_phonemization.py Phase 2 の内部フックに合わせたテスト更新
src/python_run/piper/voice.py Python: Phase 1(全文並列G2P)+ Phase 2(G2P-ORT pipeline)を実装
src/go/piperplus/synthesize.go Go: streaming 用に request 準備を分離し短文パディング処理を関数化
src/go/piperplus/streaming_stress_test.go Go: JA を使った SynthesizeStream 並行ストレステストを追加
src/go/piperplus/streaming.go Go: Phase 1 として文ごとの準備処理を並列化してから順次推論に変更
src/go/piperplus/internal/parallelism/parallelism_test.go Go: resolver/ordered map の詳細テストを追加
src/go/piperplus/internal/parallelism/parallelism_stress_test.go Go: internal parallelism の JA ストレステストを追加
src/go/piperplus/internal/parallelism/parallelism_bench_test.go Go: internal parallelism の microbench を追加
src/go/piperplus/internal/parallelism/parallelism.go Go: Phase 1 の resolver + ordered Map helper を新設
src/go/cmd/bench-pipeline/main.go Go: CGO 前提の実機ベンチコマンドを追加
src/csharp/PiperPlus.sln C#: 新テストプロジェクト追加・構成プラットフォーム追加
src/csharp/PiperPlus.Core/Phonemize/SentenceParallelEncoder.cs C#: ordered 並列エンコード helper を追加
src/csharp/PiperPlus.Core.Tests/SentenceParallelEncoderTests.cs C#: resolver/EncodeAll の契約テストを追加
src/csharp/PiperPlus.Cli/Program.cs C# CLI: 複数文で G2P を先に並列エンコードしてから順次推論に変更
src/csharp/PiperPlus.Cli/DotNetG2PEngine.cs C#: JA G2P を ThreadLocal 化し並行実行のクラッシュを修正
src/csharp/PiperPlus.Cli.Tests/PiperPlus.Cli.Tests.csproj C#: CLI adapter を source-link して並行性テストを実行するプロジェクトを追加
src/csharp/PiperPlus.Cli.Tests/DotNetG2PEngineConcurrencyTests.cs C#: DotNetG2PEngine の並行ストレス/determinism/E2E Phase 1 回帰テストを追加
src/csharp/PiperPlus.Bench/Program.cs C#: Phase 1 ベンチハーネスを追加
src/csharp/PiperPlus.Bench/PiperPlus.Bench.csproj C#: ベンチ用 csproj を追加
src/cpp/tests/test_g2p_parallelism.cpp C++: parallelism resolver の単体テストを追加
src/cpp/tests/test_c_api_concurrent_ja.cpp C++: JA 並行 + Iterator parity の回帰テストを追加
src/cpp/tests/CMakeLists.txt C++: 新テスト追加 + Windows で ORT DLL staging を全テストへ追加
src/cpp/piper_plus_c_api.cpp C++ C-API: synth_start で文単位に pre-phonemize を並列化し synth_next で再利用
src/cpp/piper.hpp C++: phonemesToAudioFloat 宣言とドキュメント追加
src/cpp/piper.cpp C++: phonemesToAudioFloat 実装追加(sentence-silence parity 含む)
src/cpp/g2p_parallelism.hpp C++: resolver helper を新設
scripts/check_ort_versions.py ORT 版数参照の取りこぼしを検出する lint スクリプトを追加
docs/spec/ort-versions.md ORT 版数マトリクスを 1.20.0 に更新
docs/spec/ios-shared-lib.md iOS 向け ORT sha256 と説明を更新
docs/guides/ios-integration.md iOS 統合ガイドの ORT 版数/URL/sha256 を更新
cmake/find_onnxruntime_windows.cmake Windows 用 ORT バージョンを 1.20.0 に更新
cmake/OnnxRuntime.cmake Linux/macOS 用 ORT バージョンを 1.20.0 に更新(コメント追加)
.gitignore scripts/check_ort_versions.py を追跡対象に追加
.github/workflows/release-shared-lib.yml release の ORT version/sha256 を 1.20.0 に更新
.github/workflows/release-kotlin-g2p.yml Kotlin G2P release の ORT version を 1.20.0 に更新
.github/workflows/ort-version-sync.yml ORT version sync gate の新規 workflow を追加
.github/workflows/build-piper.yml build の ORT cache key / download URL を 1.20.0 に更新
.github/workflows/android-build.yml Android CI の ORT version を 1.20.0 に更新
.github/workflows/_build-test-cpp.yml C++ build/test workflow の ORT download を 1.20.0 に更新
Comments suppressed due to low confidence (4)

src/python_run/piper/voice.py:1

  • return _map_sentences(...) の後に raise ValueError(...) が残っており到達不能コードになっています(リンタ/静的解析で指摘されやすい)。_phonemize_one_factory() 側ですでに未対応 phoneme_type を例外化しているので、この到達不能な raise は削除するのが安全です。
    src/python_run/piper/voice.py:1
  • Phase 2 の実装で「全文分の future を一括 submit」しているため、文数が非常に多い入力ではキュー/メモリ使用量が文数に比例して増えます。TTFB 改善の目的に沿う範囲で、parallelism + prefetch 程度だけ先行 submit して消費しながら次を submit する(bounded pipeline)構造にすると、上限メモリを抑えつつ ORT と G2P のオーバーラップも維持できます。
    src/go/piperplus/internal/parallelism/parallelism.go:1
  • PIPER_G2P_PARALLELISM の値を TrimSpace していないため、" 4""\t4" のような(他ランタイムやドキュメント上許容されている)入力が Atoi 失敗となり auto にフォールバックします。Python/Rust/C++ 側が .strip() / whitespace-skip をしている実装になっているので、Go でも strings.TrimSpace(raw) を挟えて挙動を揃えるのが安全です。
    src/rust/piper-core/src/voice.rs:1
  • map_sentences_parallelparallelism > items.len() の場合でも chunk_size = 1 になり、結果的に items.len() 個のスレッドを spawn します(呼び出し側が常に resolve_g2p_parallelism を経由する想定なら起きにくいですが、関数自体の防御性は低い状態です)。内部で let workers = parallelism.min(items.len()).max(1); のように clamp してから div_ceil(workers) を計算すると、将来の呼び出し経路追加でも意図せず過剰 spawn しにくくなります。

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/cpp/piper.hpp
Comment thread cmake/OnnxRuntime.cmake Outdated
Comment thread scripts/check_ort_versions.py Outdated
…d-code 整理

Copilot review コメント (4 件) への対応。

## 1. Python: phonemize() の到達不能 raise を削除 (voice.py)

`return _map_sentences(...)` の直後に残っていた `raise ValueError(...)`
を削除。未対応 phoneme_type の検証は `_phonemize_one_factory()` 側で
すでに行うので二重チェックは不要。

## 2. Python: synthesize_stream_raw を bounded pipeline 化 (voice.py)

旧実装は ``[pool.submit(fn, s) for s in sentences]`` で全文 future を
一括 submit していたため、長文 (本など 1000 文超) でキュー / メモリ
使用量が文数に比例していた。`max_in_flight = 2 * parallelism` の
prefetch 付き bounded pipeline に変更し、メモリ使用量を
O(n_sentences) → O(parallelism) に抑制。G2P と ORT 推論のオーバー
ラップは維持。

回帰テスト `test_synthesize_stream_raw_bounded_pipeline_caps_in_flight`:
100 文 / parallelism=4 で in-flight 数が `3 * parallelism` 以下である
ことを検証 (worker race 用に slack 1 つ分追加)。

## 3. Go: PIPER_G2P_PARALLELISM の TrimSpace (parallelism.go)

env 値を `strconv.Atoi` 前に `strings.TrimSpace` するように修正。
shell rc から流れる末尾改行や `"  4"` のような入力が Python (.strip())
/ Rust (.trim()) と同じく受理されるようにし、ランタイム間の挙動を統一。

回帰テスト 7 件 (whitespace 各種パターン + whitespace-only fall-back)
を追加。

## 4. Rust: map_sentences_parallel の workers clamp (voice.rs)

`parallelism > items.len()` の場合に `chunk_size = n.div_ceil(parallelism)`
が 1 に潰れ、`items.len()` 個のスレッドを spawn してしまう問題。
`workers = parallelism.min(n).max(1)` で clamp し、過剰 spawn を防止。
`resolve_g2p_parallelism` 経由なら起きないが、helper 自体の防御性を
担保する defence-in-depth。

回帰テスト `test_map_sentences_parallel_clamps_excess_parallelism`:
3 items / parallelism=100 で観測される thread ID が 3 以下である
ことを検証。
@ayutaz ayutaz changed the title feat: G2P-推論パイプライン並列化 (Phase 1 + Phase 2, Issue #383) feat: 全 7 ランタイムで G2P を文単位並列化、Python は ORT 推論ともオーバーラップ (Issue #383) May 9, 2026
@ayutaz ayutaz changed the title feat: 全 7 ランタイムで G2P を文単位並列化、Python は ORT 推論ともオーバーラップ (Issue #383) feat: G2P を文単位で並列化し Python は ORT 推論ともオーバーラップ (Python/Rust/C#/Go/C++、Issue #383) May 9, 2026
ayutaz added 6 commits May 9, 2026 12:02
…phen 対応

Copilot inline review コメント 3 件への対応。

## 1. src/cpp/piper.hpp: phonemesToAudioFloat ヘッダコメント

実装は trailing sentence-silence を「追加する」挙動 (Iterator vs
one-shot parity を保つために必要)。ヘッダの `NOT added by this
function` は実装と矛盾しており、利用者が「自分で sentence-silence
を足す」誤実装を招きうる。実装に合わせて訂正し、parity を担保する
回帰テストへの参照も明記。

## 2. cmake/OnnxRuntime.cmake: コメント更新

旧コメントの問題:
* 「iOS/Android release workflow の固定値 (1.17.0) はこの PR の範囲外」
  と書かれていたが、実際には release-shared-lib.yml / android-build.yml
  / release-kotlin-g2p.yml なども同 PR 内で 1.20.0 に更新済み。
* 「1.17.0 では SEH crash する」を一次原因として書いていたが、
  cpp_ort_upgrade.md の調査では真因は Windows DLL loader が System32
  の古い onnxruntime.dll を拾っていたこと。1.17.0 自体の問題ではない。

事実に合わせて、(a) このファイルが C++ パイプライン ORT バージョンの
canonical source であること、(b) 1.20.0 への bump 経緯と DLL search
order が真因だったこと、(c) cmake + 7 ファイル全体を同期すること、
の 3 点に整理。

## 3. scripts/check_ort_versions.py: archive regex に hyphen 許容

archive ファイル名の regex `[a-z0-9_]+` が hyphen を含むセグメント
(例: `onnxruntime-linux-x64-gpu-1.20.0.tgz`、`win-x64-cuda-...`) に
マッチせず、GPU/CUDA ビルドの archive 名 drift を gate が取りこぼす
可能性があった。`[a-z0-9_-]+` に拡張して GPU/CUDA suffix 付きでも
バージョンを検出できるようにした。

新規 `scripts/test_check_ort_versions.py` で全 7 パターン (cmake set /
GHA env / release URL / archive 各種 / Maven Android / pod archive /
cache key) と GPU/CUDA suffix の hyphen 対応を 17 ケースでピン留め。
unrelated string (`llama-runtime-...`) で false-positive しないこと、
URL 内のバージョンと archive 内のバージョンが drift した場合に両方
検出できることもテスト。
python-tests.yml:
  Run runtime full tests に test_voice_g2p_parallel.py と
  test_short_text_mitigation.py を追加。前者は Phase 1 の
  _resolve_g2p_parallelism / _map_sentences、Phase 2 の
  synthesize_stream_raw bounded pipeline と早期 abort 時の G2P
  キャンセル契約を verify する。後者はこのブランチで Phase 2 の
  _split_sentences + _phonemize_one_factory + _stream_phonemes_to_audio
  経由に書き換えたモック契約を validate する。

ort-version-sync.yml:
  scripts/test_check_ort_versions.py を pytest で実行する step を
  追加し、path filter にも登録。Issue #383 follow-up で
  archive-filename regex を [a-z0-9_] → [a-z0-9_-] に拡張した契約
  (GPU / CUDA suffix を取りこぼさない) を 17 ケースで pin する。

ローカル検証: 15 + 38 + 17 = 70 / 70 pass。
50a5bd4 の追加で踏んだ wiring 不備と、もともと潜んでいた drift を
すべて修正。13 個の失敗ジョブを 1 commit で解消する。

## 1. ORT gate (`ort-version-sync.yml`) — addopts collision
50a5bd4 で追加した ``pytest scripts/test_check_ort_versions.py``
は ``--override-ini="addopts="`` を渡しておらず、pytest.ini の
``--cov=src/python/piper_train`` 等が pytest-cov 未インストールの
このジョブで "unrecognized arguments" を起こしていた。
他のジョブと同じく override を付けて gate を自己完結に。

## 2. ruff (`tools/benchmark/issue-383/bench_pipeline_*`)
PR #403 で追加した bench スクリプトに 5 件:
- I001 × 3 (`import sys` の位置 / numpy 後の空行)
- F841 (`n_sentences_target` 未使用)
- F401 (`dataclasses.field` 未使用、phase2)
ruff --fix で auto-format したものを採用。

## 3. rustfmt (3 ファイル)
PR #403 後に rustfmt が新ルールで diff を出していた:
- `examples/bench_phase1.rs:77` — eprintln! を multi-line に
- `src/voice.rs:555` — closure body が brace 化
- `tests/test_ja_concurrent_stress.rs:89` — `.join()` を改行
``cargo fmt --all`` で適用。

## 4. Rust example bench_phase1 — feature gate 不在
`bench_phase1.rs` は ``piper_plus::PiperVoice`` を import するが、
これは ``#[cfg(feature = "onnx")]``。default features は
``["naist-jdic", "dict-download"]`` なので、CI の
``cargo test -p piper-plus`` で E0432 unresolved imports が出ていた。
``[[example]] required-features = ["onnx"]`` を Cargo.toml に追加し、
onnx 無効時は cargo が silently skip するように。clippy
``--all-features`` の側では今まで通り compile-check される。

## 5. Go bench-pipeline — `WithConfigPath` typo
`cmd/bench-pipeline/main.go:67` で ``piperplus.WithConfigPath`` を
呼んでいたが、実体は ``WithConfig`` (options.go:128)。
go vet / golangci-lint / build が落ちていた。

## 6. Go streaming_stress_test.go — build tag + runtime re-init
2 つの問題が重なっていた:
- ``//go:build integration`` 抜け。``testModelPath`` は
  engine_test.go の integration-tagged ファイルにあるため、
  tag 無しの本ファイルからは見えず ``go vet`` / golangci-lint で
  ``undefined: testModelPath`` が出ていた。
- ``-tags integration`` 付きで走らせても、init_test.go の
  ``TestShutdown`` が runtime を down させた状態で
  alphabetically-after の本ファイルが ``LoadVoice`` を呼び、
  ``InitializeRuntime() has either not yet been called`` で fail。
  ``TestShutdown`` に ``t.Cleanup`` で re-init を仕込み、
  以降の test も safe に。本 PR の test を超えて latent bug の
  根本修正になっている。

## ローカル検証
- `pytest scripts/test_check_ort_versions.py --override-ini="addopts="`: 17/17 pass
- `python -m ruff check .`: All checks passed
- `cargo fmt --all -- --check`: clean
- `cargo build --tests` (default features): compile OK (example skipped)
50a5bd4 で ruff check を通した後でも、別 step の `ruff format --check`
で 3 ファイルが落ちていた:
- src/python_run/piper/voice.py — `_map_sentences` 後の空行 1 行不足
- tools/benchmark/issue-383/bench_pipeline_{baseline,phase2}.py — 同様

`python -m ruff format` で全リポジトリを再整形 (187/187 OK)。
## Go bench-pipeline (golangci-lint)
``cmd/bench-pipeline/main.go`` で 3 件の lint:
- ``exitAfterDefer`` (line 86): ``os.Exit`` で ``defer voice.Close()``
  が走らない → ``main`` を ``run() error`` に分割し、defer が確実に
  fire するようにする (Go の標準 idiom)。
- ``errcheck`` (line 93/95): ``os.Setenv`` / ``os.Unsetenv`` の戻り値
  未チェック → エラー時は ``run`` から早期 return。

## C# Windows test flake (CustomDictionaryTests)
``LoadDefaults_MalformedFile_SkippedSilently`` が Windows runner で
``Directory.Delete`` 時に ``IOException: ... being used by another
process`` で落ちていた。

根本原因: 5 つのテストが ``Directory.SetCurrentDirectory(baseDir)``
→ ``Directory.SetCurrentDirectory(originalDir)`` → ``Directory.Delete``
の順で実行している。Windows は cwd への内部ハンドルを保持しており、
SetCurrentDirectory(originalDir) 直後でも一瞬残ることがある。
さらに antivirus / Windows Defender / インデクサが temp dir をスキャン
する間も ``IOException`` を生じる。

修正: ``DeleteDirectoryWithRetry`` ヘルパー (GC + backoff) を追加し、
本ファイル内の ``Directory.Delete(baseDir, true)`` 5 箇所すべてを
差し替え。100ms × attempt の backoff で最大 6 回リトライ、6 回目は
通常の ``Directory.Delete`` と同じ例外を投げる (静かに飲み込まない)。

PR #403 のテストではないが、この PR の Windows runner で flake が
顕在化したため一緒に対応。同じパターン (cwd 切替 + Delete) の latent
flake が今後再発しないように helper 化。

ローカル: ``ruff check/format``: pass、``cargo fmt --check``: clean
前回の commit (4d3d6dd) で 2 件残った lint:
- gofmt: docstring の ``...`` (バッククォート 2 つ) を gofmt が
  シングル "..." + smart-quote 風に書き換える挙動。Go の慣例どおり
  シングルバッククォートに統一して gofmt -w を適用。
- errcheck (line 83): `defer voice.Close()` の戻り値未チェック →
  `defer func()` でラップして cerr を stderr へログ出力。
@ayutaz
ayutaz merged commit d65e4a9 into dev May 9, 2026
117 checks passed
@ayutaz
ayutaz deleted the feat/383-g2p-inference-pipeline branch May 9, 2026 07:34
ayutaz added a commit that referenced this pull request Jun 13, 2026
- CHANGELOG ### Added: G2P 文単位並列化 + 新 env PIPER_G2P_PARALLELISM (Issue #383, PR #403)
- CHANGELOG ### Fixed: MB-iSTFT モデルの speaker_embedding 未対応で Docker/Rust/C++ が 500/load 失敗していた問題 (Issue #426, PR #443)
- CLAUDE.md: env 列挙に PIPER_G2P_PARALLELISM を追記
- README.npm.md: importmap CDN pin を piper-plus@0.6.0 -> @0.7.0 に更新 (publish 版と同期)
ayutaz added a commit that referenced this pull request Jun 14, 2026
* chore(release): v1.13.0 — version bump + CHANGELOG promotion

- bump 5 manifests (Python/Rust/C#/npm) + release-versions.toml prefixes
  - Python/root VERSION + piper_train/VERSION + src/python pyproject: 1.12.0 -> 1.13.0
  - Rust workspace (+ internal cli/core/python/wasm deps): 0.4.0 -> 0.5.0
  - C# Core/Cli: 0.3.0 -> 0.4.0
  - npm synthesis (openjtalk-web): 0.6.0 -> 0.7.0
- regenerate uv.lock / Cargo.lock / package-lock.json
- promote CHANGELOG [Unreleased] -> [1.13.0] - 2026-06-13
- add missing Fixed entries (#557 Windows tsukuyomi C++, #506/#507 EOS trim,
  #504 Go text splitter, #505 Windows release-QA)
- add missing Security entries (CVE bumps #530/#531/#437/#450,
  CodeQL #435/#434, pyo3 advisories #558/#559)

* docs(release): v1.13.0 監査で判明した CHANGELOG 漏れ等を追記

- CHANGELOG ### Added: G2P 文単位並列化 + 新 env PIPER_G2P_PARALLELISM (Issue #383, PR #403)
- CHANGELOG ### Fixed: MB-iSTFT モデルの speaker_embedding 未対応で Docker/Rust/C++ が 500/load 失敗していた問題 (Issue #426, PR #443)
- CLAUDE.md: env 列挙に PIPER_G2P_PARALLELISM を追記
- README.npm.md: importmap CDN pin を piper-plus@0.6.0 -> @0.7.0 に更新 (publish 版と同期)

* fix(ci): unblock v1.13.0 tag — g2p checksum gate + pre-tag Swift runbook

監査 (ultracode) で判明したリリース実行 blocker を release ブランチで解消:

- release-shared-lib.yml: g2p xcframework の checksum 検証 (placeholder + asset
  一致) を「g2pVersion == tag 版」のときのみ強制する条件を追加。g2p は自身の
  g2pVersion tag で debut するため、synthesis v1.13.0 タグで g2pVersion=1.14.0 の
  placeholder が release job を hard-fail させていた (Issue #387)。synthesis の
  checksum は従来どおり常時強制。
- prepare-release skill: Swift checksum 更新を「tag 後」→「tag 前」の正しい順序に
  修正 (Phase 5/7)。compute-checksum 対象を実 artifact 名 (libpiper_plus-ios.
  xcframework.zip 等) に訂正。workflow_dispatch artifact から取得する手順に変更。
- CONTRIBUTING.md: v<X.Y.Z> tag が release-shared-lib.yml + docker-build.yml を
  同時発火する点、Swift checksum pre-tag 手順、GitHub Release 二重作成の注意を追記。

* docs(release): v1.13.0 監査で判明した CHANGELOG/CLAUDE.md の漏れを修正

- CLAUDE.md「ランタイム別パッケージ」表の版数を bump 後の値に更新
  (Python 1.12.0→1.13.0 / C# 0.3.0→0.4.0 / Rust 0.4.0→0.5.0 / npm 0.6.0→0.7.0)。
  version-consistency gate は CLAUDE.md を読まないため CI 未検出だった
- CHANGELOG [1.13.0] Fixed に PR #389 を独立記載。v1.12.0 配布 config.json の
  未 PUA 化 multi-codepoint 音素 (ɔɪ/œ̃/ɐ̃) による Windows C++ 推論失敗の実データ
  修正 (pua.json v1→v2 + 全6ランタイム同期 + strict fail-fast)。従来は #557 の
  regression CI ガード文脈でしか言及されておらず、別 PR の実修正が独立記載漏れだった

* docs(changelog): 開発時の重大度マーカー (🔴 Critical 等) を除去

[1.13.0] の Kotlin/Android G2P エントリに混入していた開発時の重大度/優先度
マーカー (🔴 Critical / 🔴 / 🟡 / 🟡 → 🟢) を全除去。リリース後に changelog を
読む人にとって緊急度の色分けは無意味なノイズのため、事実記述のみに統一。
カテゴリ見出しの装飾絵文字 (### 🚀 Major Features 等) と要件 ID (NFR-SEC-2 等)
は意味が明確なため残置。

* docs(readme): 全8言語のヘッダーを統一・改行整理し FR/ES/PT/DE の正書法を復元

- 言語ナビとバッジ群の間・パッケージラベルとバッジの間に空行を挿入。README は
  CommonMark で単一改行がスペース化されナビとバッジが密着していたため、論理グループ
  を空行で分離 (左寄せ・HTML 不使用)
- 全8言語ヘッダーを日本語版水準に統一: 状態バッジ整列 (重複 PyPI 除去)、パッケージ
  レジストリバッジ群 (PyPI/NuGet/crates.io/npm/Maven Central)、Try in Browser バッジ、
  MIT フォーク訴求文 (各言語へ翻訳) を追加
- FR/ES/PT/DE の本文 diacritics を全面復元 (Systeme→Système、Franzoesisch→Französisch
  等)。heading slug と TOC アンカーの整合は維持、コードブロック内は ASCII のまま
  (コピペ互換性のため)
- 6言語は per-language の edit→adversarial verify で検証 (バッジ URL byte 一致、callout
  翻訳、TOC アンカー整合、内容非改変)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: G2P-推論パイプライン並列化による長文・サーバー合成高速化

2 participants