Conversation
#653 (durations が ceil 前) に着手する前に、それを検出できるはずの 仕組みが 3 か所とも空振りしていることが分かったので先に塞ぐ。 1. C++ の parity テストが本番コードを 1 行も通っていなかった。 test_phoneme_timing_parity.cpp は golden fixture を読みながら spec アルゴリズムを自身の翻訳単位で再実装しており、コメントにもそう 書いてあった (piper.cpp が onnxruntime を引き込むためリンクできない)。 他 5 ランタイムは本番関数を呼んでいるので、C++ だけが出荷される 挙動について何も検査していなかった。cursor walk を ORT 非依存の timing_helpers.hpp に切り出し、本番実体を直接検査する test_timing_helpers.cpp (16 ケース) を新設した。変異 5 種が 5/5 検出 されることを実測確認している。 fixture をそのまま本番に流すことはできない。fixture は token ベース (`^`, `k`, ..., `$`)、本番は id ベースで PAD/BOS/EOS を skip するため で、これ自体が #697 の内容。糊塗せずテストに記録した。 2. timing-parity.yml の paths: が守る対象を含んでいなかった。 per-runtime-presence job は「parity テストを削除したら fail する — that is intentional」と宣言しているのに、守る 6 ファイルが 1 つも trigger に無く、削除するだけの PR では workflow が起動しなかった。 他 5 ランタイムの timing 実装も未登録だった。5 件 → 18 件に拡張。 同型の穴が他 7 workflow にもあることを走査で確認し #704 に起票。 3. golden fixture の 7 ケースが全て整数 durations だった。実 ONNX 出力 は常に小数なのに代表しておらず、ceil(整数) == 整数 なので frame 量子化 を変えても fixture は 1 バイトも変わらない。実測: Go に math.Ceil を 入れる変異は旧 fixture では ok で素通りし、新 fixture では FAIL する。 小数 2 ケースと 60 エントリの長文ケースを追加し、後から整数化・短縮 されたら --check が fail する anti-vacuity guard も入れた。 併せて cursor の accumulator を float から double にした。格納は float の まま (PhonemeInfo は ABI 公開されており幅を変えると ABI gate が検出する) なので丸めは 1 エントリ 1 回になり累積が消える。1153 frame で float32 は float64 基準から 2.1e-3 ms ずれており、parity テストが宣言する許容誤差 1e-6 ms を大きく超えていた — レプリカが double だったため露見していな かった。 C++ 51/51、6 ランタイムの parity テスト全通過、音声出力は不変。
Required status-check gate (deferred)Head SHA |
Public ABI snapshot diffBootstrap mode: baseline is empty, recording first observation. No ABI changes detected. |
Action SHA drift reportCollected pins (3 actions) — expected_total_pins=3
Summary: total=3, ok=3 |
Distroless trial: webui / cpp-inferenceEach canonical Dockerfile is UNCHANGED; the trial Dockerfile sits beside it as (
|
| metric | canonical | distroless trial | delta |
|---|---|---|---|
| image size (linux/amd64) | 237MB | 232MB | -2.2% |
Smoke entrypoint: /usr/local/bin/piper-plus — passed.
webui
Trial Dockerfile: docker/webui/Dockerfile.distroless. Canonical docker/webui/Dockerfile is UNCHANGED.
| metric | canonical | distroless trial | delta |
|---|---|---|---|
| image size (linux/amd64) | 1.2GB | 807MB | -32.4% |
Smoke entrypoint: /usr/bin/python3 — passed.
Not in these trials (follow-up promotion PRs)
- linux/arm64 buildx coverage is handled by
docker-build.yml(build-distroless-trialsmatrix). - CVE Trivy diff lives in
trivy-container-scan.yml(per-image SARIF upload). - For
webui: full Gradio cold-start under distroless (a realdocker run -p 7860:7860fromwebui-test.yml) is the promotion gate. - For
cpp-inference: end-to-endpiper-plus --model ... --output_file ...with a real model fixture is the promotion gate.
Runtime Parity Deep — audio (informational tier)Pairs compared: 15, failing: 15, runtimes skipped: 0.
|
Multi-Runtime RTF BenchmarkPolicy: warn-only (thresholds: RTF +/-10%, P50 +/-10%, P95 +/-15%) Model:
|
Summary
#653 (ONNX の
durationsがtorch.ceil前の値) に着手する前に、それを検出できるはずの仕組みが 3 か所とも空振りしていることが分かったので先に塞ぐ。実装より先に検出器を直さないと、ceilを入れても入れなくても CI は緑のままになる。Affected Components
Type
Risk Level
Contract Impact
docs/spec/*.tomlは変更していないspec_versionは据え置き。本 PR は既存 spec の記述を実装側で検査可能にするだけで、規定そのものは変えていない。ceilの導入とspec_versionbump は #653 本体で行う。変更内容
piper.cppの walk をsrc/cpp/timing_helpers.hpp(ORT 非依存) に切り出し、extractTimingsFromDurationsは委譲のみtest_timing_helpers.cpp新設floatのまま (PhonemeInfoは ABI 公開)1e-6 msを超える。レプリカがdoubleだったため露見していなかったtiming-parity.ymlの trigger 拡張paths:を 5 → 18 件にper-runtime-presencejob は「parity テストを削除したら fail する」と宣言しているのに、守る 6 ファイルが 1 つも trigger に無く、削除するだけの PR では workflow が起動しなかった。他 5 ランタイムの timing 実装も未登録ceil(整数) == 整数なので frame 量子化を変えても fixture は 1 バイトも変わらない。実 ONNX 出力は常に小数--checkが「小数 2 件以上 / 最長 50 エントリ以上」を検査設計判断
PhonemeInfoの幅は変えない。 accumulator だけdoubleにし、格納はfloatのまま。PhonemeInfoはSynthesisResult経由で shared library から公開されており、幅を変えると struct レイアウトが変わってABI diff (libpiper_plus.so head vs base)gate がabi-dumperの DWARF 情報で検出する。丸めは「1 エントリにつき 1 回」になり、発話全体にわたる累積が消えるので、実用上の精度問題はこれで解消する。^,k, ...,$)、本番は id ベースで PAD/BOS/EOS (id 0/1/2) を skip する。fixture のbasic_konnichiwaは^と$を含み、実モデルではそれぞれ id 1 / 2 になる。Python canonical はエントリを出し、C++ 本番は出さない。この 4 対 2 の分裂は Rust / Go の timing が padding 込みの全エントリを出し total_duration_ms が実 WAV を超過する #697 であり、本 PR で糊塗せずテストのコメントに記録した。ceilを入れたい誘惑があるが、リファクタに混ぜると phoneme timing が実音声に対し系統的に短い — ONNX durations は ceil 前の値(全ランタイム、spec 改訂を伴う) #653 の修正が「fixture の diff」として見えなくなる。本 PR の fixture 値は現行実装のまま。buildPhonemeIdToStringMapがキーの最初の ID しか登録しない (C# では Rust / Go の phoneme timing が音素名ではなくプレースホルダ(ph_0 / p0)を出力 — spec の reverse_map 未実装 #656 で修正済み)、(b) C++ だけ負の duration を 0 に clamp せず cursor が後退する (spec[calculation.negative_handling]違反、他 5 ランタイムは clamp)、(c)2 < id < 128の ASCII フォールバックが?より先に効き、未マップ ID が制御文字になる。Test Plan
cmake -B build -DBUILD_TESTS=ON && cmake --build build --target test_timing_helpers -j8 && ./build/src/cpp/tests/test_timing_helpers→ 16 passedfloatに戻す (4) 促音 overlap 比率 0.3 → 0.4 (5) frame の truncate を round に —src/cpp/timing_helpers.hppを書き換えて再ビルドすると 5/5 で FAILED が出るctest --test-dir build --timeout 300→ 51/51 passed (新規 1 件込み)src/go/piperplus/timing.goのdurationMsにmath.Ceilを入れてcd src/go && go test ./piperplus/ -run Parity -count=1→ FAIL。同じ変異を旧 fixture (git stashでgolden_matrix.jsonとregenerate_timing_fixture.pyを戻す) に対して行うと ok で素通りするdur_frames * frame_time_msをdur_frames.ceil() * frame_time_msに →cargo test -p piper-plus --test test_phoneme_timing_parityが FAILEDuv run python scripts/regenerate_timing_fixture.py --checkが 2/2 で rc=1 かつ理由を出力pytest tests/test_phoneme_timing_parity.py(42 passed) / Rustcargo test --test test_phoneme_timing_parity(3) / Gogo test ./piperplus/ -run Parity/ C++./build/src/cpp/tests/test_phoneme_timing_parity(4) / C#dotnet test --filter TimingWriterParity(9) / JSnode --test test/js/test-phoneme-timing-parity.js(11)uv run python -c "import yaml; d=yaml.safe_load(open('.github/workflows/timing-parity.yml')); on=d[True]; print(len(on['pull_request']['paths']), len(on['push']['paths']))"→18 18noise_scaleに seed 無し)。サイズは常に一致するので durations 自体は決定的total_duration_msの差は 0.000000 ms (この値はemittedSamples由来で cursor を通らないため)total_duration_msが WAV 長と一致すること: 実測 1233.288 ms / 27194 samplespre-commit run --files <変更 8 ファイル>→ rc=0Checklist
Related Issues
#653 の前提。本 PR 単体では
ceilは入れない。対応中に発見し分離:
model_manager.cppの shell injection / path traversal validator は実ドリフト発生中 — 本番は%を許可、レプリカは未追従)paths:が守る対象を含まず gate が起動しない。本 PR はtiming-parity.ymlを解消。同型が他 7 workflow に残存)total_duration_msが実 WAV を超過。PAD/BOS/EOS エントリの有無が 6 ランタイムで 4 対 2 に割れている)