Skip to content

feat(android): Android システム TTS エンジンを追加 - #604

Open
ayutaz wants to merge 22 commits into
devfrom
feat/android-tts-engine
Open

feat(android): Android システム TTS エンジンを追加#604
ayutaz wants to merge 22 commits into
devfrom
feat/android-tts-engine

Conversation

@ayutaz

@ayutaz ayutaz commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Summary

Android のシステム TTS エンジンとして piper-plus を提供する。任意のアプリやアクセシビリティ機能から、オフラインで 6 言語 (ja/en/zh/es/fr/pt) の音声合成を利用できるようになる。

背景として、Android で piper-plus のモデルを sherpa-onnx 経由で使おうとしたユーザーが断念した事例がある。調査の結果、sherpa-onnx には日本語 G2P が存在せず、ONNX の入出力を互換化しても生テキストからの合成が成立しないことが判明したため、自前のエンジンを提供する方針を採った。

実装後にテスト充足度を監査したところ、テストの分布がリスクの分布と逆になっていた。壊れると音が壊れる境界 (PCM のバイト詰め、8 引数の JNI ブリッジ、SynthesisCallback の呼び出しプロトコル、manifest 配線) がいずれもカバレッジ 0 で、そこから production バグが 2 件見つかった。修正とテストを併せて含む。

Affected Components

  • Python (src/python/, pyproject.toml)
  • Rust (src/rust/)
  • C# (src/csharp/)
  • C++ (src/cpp/, CMakeLists.txt) — JNI ラッパー (android/piper-plus/src/main/cpp/) の変更を含む
  • Go (src/go/)
  • WASM/npm (src/wasm/)
  • Docker (docker/)
  • CI/CD (.github/workflows/)
  • Documentation (docs/, README*)

Type

  • Bug fix
  • New feature
  • Refactoring
  • Documentation
  • CI/CD
  • Dependencies

Risk Level

  • patch — bug fix / internal refactor / docs only
  • minor — new feature / additive API / no breaking change
  • major — breaking change (API removal, schema migration, behavior reversal)

既存 API は追加のみ。PiperPlus.synthesize(text, speakerId) はそのまま維持し、オプション指定版をオーバーロードとして追加している。

Contract Impact

  • None (Python/runtime-internal change only)

docs/spec/*.toml の変更なし。音素化・推論・文分割はすべて既存の libpiper_plus.so に委譲しており、契約に関わるロジックを Kotlin 側で再実装していない。

変更内容

機能名 動作 これがないと起こること
PiperPlusTtsService TextToSpeechService を実装し、Android の TextToSpeech API から piper-plus を呼べるようにする システム TTS エンジンとして選択できず、他アプリから利用できない
LocaleResolver ISO-639-3 言語コードを language_id に解決し、可用性 (LANG_AVAILABLE / LANG_MISSING_DATA / LANG_NOT_SUPPORTED) を判定する 言語切替ができない。非対応言語の要求に正しく応答できない
SynthesisParams システム設定の speechRate (100 = 等速) を length_scale に変換する ユーザーの読み上げ速度設定が無視される
SynthOptions (AAR) AAR から language_id / length_scale / noise_scale / noise_w / sentence_silence_sec を指定できる JNI が speakerId しか通しておらず、多言語合成と速度制御が原理的に不可能
JNI の options 対応関数 PiperPlusSynthOptions を組み立てて C API に渡す (_reserved[5] はゼロ埋め) 同上。C API 側には既に存在するパラメータが Kotlin から到達しない
ModelPaths モデル・設定・辞書のパス解決とインストール判定 モデルの有無を判定できず、未配置状態でネイティブ層がクラッシュする
EngineHolder モデル単位で PiperPlus インスタンスをキャッシュし、切替時に確実に解放する 合成のたびにモデルロード (数百 ms) が発生する。インスタンスがリークする
3 ABI 対応 AAR の abiFiltersarm64-v8a / armeabi-v7a / x86_64 に拡張 arm64 以外の端末で動作しない
CI の APK ビルド 既存の結合 artifact を jniLibs に展開し、APK ビルドまで実行 ネイティブビルドと APK ビルドが一度も検証されない
piper_plus_synth_abort() 途中で放棄した iterator を解放する。synth_start は engine を busy にしたまま返り、解放するのは synth_next が終端に達したときだけ 読み上げ停止を 1 回押すと、プロセスが死ぬまで以降の全合成が ERR_BUSY で失敗する
catch (LinkageError) System.loadLibrary の失敗を捕まえて ERROR_SERVICE にする 非対応 ABI の端末で TTS サービスごとクラッシュする。設計書 §8 が「クラッシュさせない」と規定した経路
CI trigger の修正 pathsandroid/** を追加し、JVM テストを native ビルドに従属しない独立 job に分離 Kotlin / JNI だけを変更した PR でテストが 1 件も実行されない。マージ後は既存の 30 件も二度と走らない
SynthesisSession / PcmEmitter 合成本体を Service から切り出す。停止フラグの所有権も移す SynthesisRequest が final + 全 getter スタブのため、JVM テストが 1 分岐にしか到達できない
PiperPlusNativeBridgeTest Kotlin external 宣言と piper_plus_jni.cpp の C++ 定義をソース照合 末尾 4 引数がすべて Float で、並べ替えても JNI シグネチャが変わらずリンクも通る (length_scalenoise_scale が入れ替わると常時 2.5 倍速)
ManifestWiringTest android:name が実在の TextToSpeechService に解決すること、intent-filter が正しいこと タイポしても assembleDebug は成功し、端末の設定画面にエンジンが現れない
SynthOptionsTest の実質化 既定値を piper_plus_default_options() の実ソースと突き合わせ 従来はテスト名に反して Kotlin リテラルを Kotlin リテラルで読み返していた

設計判断

  • 既存 AAR に依存し、Kotlin 側を Service とパラメータ変換に限定した。 ONNX Runtime を Kotlin から直接叩く構成も検討したが、その場合 scales 決定・文分割・短テキスト戦略を Kotlin で再実装することになり、docs/spec/*.toml の契約を C++ と 2 箇所で守る必要が生じる。実装の二重化を避けることを優先した。
  • PiperPlusEngine インターフェースを挟んだ。 PiperPlus を直接持つと JVM ユニットテストで JNI のロードが走ってしまう。この境界により、エミュレータなしで生存管理のロジックを検証できる。
  • 既定モデルを css10-6lang にした。 つくよみちゃんはキャラクター固有の利用規約を持つため初回起動時に同意フローが必要になる。既定を規約なしのモデルにすることで導線を短くした。モデル ID は piper-core の組込みレジストリ (model_download.rs) の名前と揃え、端末上のディレクトリ名にもそのまま使っている。
  • pitch は未対応とした。 Android TTS はユーザー設定の pitch を渡してくるが、VITS に対応するパラメータがない。無視する旨をドキュメントに明記し、設定画面でも示す方針とした。
  • 中断処理に takeWhile を使った。 collect 内の早期 return では上流の生成が止まらず、onStop() 後もチャンクを作り続けてしまう。
  • CI は既存の package-android job の結合 artifact を使う。 この job が既に <abi>/*.so という jniLibs レイアウトへ再編成しているため、ABI ごとの手動コピーは不要だった。CMakeLists.txtlibpiper_plus.soIMPORTED として参照する以上、AAR のビルド前に配置されている必要がある。
  • piper_plus_synth_abort() を新設した。 Kotlin の finally で残りを drain する案は、takeWhile が節約しようとした CPU をそのまま焼くので却下した。C API に解放専用の入口を作り、何も進行していないときに呼んでも安全にすることで、無条件のクリーンアップ経路に置けるようにした。
  • catch (Throwable) にはしなかった。 runBlocking を囲む位置で Throwable を握ると OutOfMemoryError まで飲み込み、キャンセルの制御も曖昧になる。ExceptionLinkageError の 2 節に分けた。
  • テスト可能性のために production を分割した。 android.speech.tts.SynthesisRequest は final かつ全 getter が RuntimeException("Stub!") を投げるスタブで、reflection でも中身を詰められない。Robolectric や mockk を入れる案もあったが、request をほどいた形の seam を切れば手書きの fake で足りるため、依存を増やさない方を選んだ。本 PR で追加したテストの新規 dependency はゼロ。
  • 書いたテストにミューテーションを入れて検証した。 通ることは価値の証明にならない。バイト順の反転、takeWhile の書き換え、停止フラグのリセット除去、JNI 引数の並べ替え、manifest のタイポなど 23 種類の変異を production に入れ、対応するテストが実際に落ちることを確認している (23/23 検出)。
  • androidTest を削除した。 PiperPlusTtsServiceTest は名前に反して PiperPlusTtsService を一度も参照せず、assert が LocaleResolverTest と重複していた。CI では assembleDebugAndroidTest すら呼ばれずコンパイルもされないため rot しており、さらにモデルを配置済みの端末では fail する。実行されないテストは、あるだけで「守られている」という誤った印象を与える分、無いより有害と判断した。
  • モデルのアプリ内ダウンロードと設定画面は本 PR に含めない。 本 PR は「Android で piper-plus が喋る」ことを成立させる範囲に絞り、配布性の作り込みは後続で対応する。

Test Plan

  • ユニットテストが通ることを確認する (61 件)
cd android
./gradlew :piper-plus:testDebugUnitTest :piper-plus-tts-engine:testDebugUnitTest
  • C API のテストが通ることを確認する (test_c_api 123 件 / test_c_api_integration 23 件)
cmake -B build -DBUILD_TESTS=ON && cmake --build build -j --target test_c_api test_c_api_integration
./build/src/cpp/tests/test_c_api
./build/src/cpp/tests/test_c_api_integration   # リポジトリルートから実行 (test/models/ を探す)
  • テストが変異を検出することを確認する (任意)
# 例: PCM のバイト順を反転させて PcmEmitterTest が落ちること
cd android
./gradlew :piper-plus-tts-engine:testDebugUnitTest --tests '*PcmEmitterTest'
  • JNI の C++ が構文エラーなくコンパイルできることを確認する
CLANG=~/Library/Android/sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/darwin-x86_64/bin/clang++
"$CLANG" -fsyntax-only -std=c++17 --target=aarch64-linux-android24 \
  -Isrc/cpp android/piper-plus/src/main/cpp/piper_plus_jni.cpp
echo "exit=$?"   # 0 であること
  • CI の Build TTS engine APK job が green であることを確認する (ネイティブビルド + 3 ABI の jniLibs 配置 + APK ビルド)
  • 実機で APK をインストールし、設定 → 言語と入力 → テキスト読み上げ に「piper-plus TTS」が現れることを確認する
  • 読み上げ中に停止ボタンを押し、その後の読み上げが正常に鳴ることを確認する (本 PR の修正点)
  • モデル未配置の状態で読み上げを実行し、クラッシュせずエラーとして扱われることを確認する
  • モデルと辞書を配置したうえで日本語の読み上げが鳴ることを確認する
adb push css10-ja-6lang-fp16.onnx /sdcard/
adb shell run-as com.piperplus.tts mkdir -p files/models/css10-6lang
# model.onnx / model.onnx.json を files/models/css10-6lang/ へ、
# OpenJTalk 辞書を files/open_jtalk_dic/ へ配置する
  • 読み上げ速度を変更して、実際に速さが変わることを確認する

Checklist

  • Tests pass locally
  • No GPL/LGPL dependencies added (License Policy)
  • Documentation updated (if applicable)

設計書 docs/design/android-tts-engine-design.md と実装計画 docs/design/android-tts-engine-plan.md を追加。CHANGELOG.md の Unreleased に追記済み。

Related Issues

なし

ayutaz added 14 commits July 25, 2026 16:21
Android のシステム TTS エンジンとして piper-plus を提供する設計。
sherpa-onnx 経由での実現可能性を PoC で検証した結果、ONNX の入力
シグネチャと metadata は解決できる一方、sherpa-onnx に日本語 G2P が
存在しないため生テキストからの合成が成立しないことが判明した。
このため自前の TTS エンジンを提供する方針とする。

既存の android/piper-plus AAR (JNI + libpiper_plus.so) に依存し、
Kotlin 側は TextToSpeechService とモデル管理 UI に限定することで、
推論・G2P の実装が C++ と二重化することを避ける。
TDD の 9 タスクに分解。各タスクは失敗するテスト → 最小実装 → 検証 →
コミットのサイクルを持ち、独立してレビュー可能な単位にした。

設計書の §9.1 / §12 もあわせて修正。CMakeLists.txt が libpiper_plus.so を
IMPORTED として宣言しているため、M1 の本質は「リリース配布」ではなく
「CI 内での jniLibs への供給」だった。リリースアセットとしての配布は
外部の AAR 利用者向けの別価値なので M5 に移した。

モデル管理 UI (M4) と配布 (M5) は後続計画として範囲外に切り出し、
M3 完了時点で「Android で 6 言語が喋る」ことを検証できる構成にしている。
つくよみちゃんはキャラクター固有の利用規約を持つため、初回起動時に
同意フローが必要になる。既定を css10-6lang にすることで導線を短くできる。

モデル ID は piper-core の組込みレジストリ (model_download.rs) の
モデル名と揃え、端末上のディレクトリ名にもそのまま使う。
ライセンス同意フローはつくよみちゃん固有の記述から、モデルごとの
ライセンス表示という一般形に書き換えた。
.gitignore には android/piper-plus-g2p/ の成果物しか登録されておらず、
新モジュールと android/piper-plus/ のビルド生成物、およびマシン固有の
local.properties が git に入る状態だった。

あわせて AGP 8.x が JDK 17 以上を要求する点を Step 8 の前提として明記。
Task 9: 既存の package-android job が jniLibs レイアウト (<abi>/*.so) の
結合 artifact を既に作っていたため、ABI ごとの手動コピーは不要だった。
download-artifact の展開先を指定するだけで済む形に書き換えた。

Task 6: .so が無いと Gradle が native ビルドをスキップし、C++ の構文
エラーが CI まで発見できない。NDK の clang による構文チェック手順を
ステップとして追加した。
@github-actions

Copy link
Copy Markdown
Contributor

Action SHA drift report

Collected pins (3 actions) — expected_total_pins=3

Action Pinned SHA Resolved Status
dawidd6/action-download-artifact 8a33849 (commit-only) OK
dawidd6/action-download-artifact 8a33849 (commit-only) OK
mymindstorm/setup-emsdk 6ab9eb1 (commit-only) OK

Summary: total=3, ok=3

Android TTS エンジンの設計書と実装計画に含まれるコードブロックが
audit に登録されていなかったため drift していた。
@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Required status-check gate (deferred)

Head SHA 2ec5a04 is no longer the branch tip (latest: 2412e37). Waiting for the new commit's spoke runs to complete before re-evaluating.

@github-actions

Copy link
Copy Markdown
Contributor

Bundle size gate

Status Ecosystem Package Baseline Observed Delta Tolerance
SKIP npm piper-plus n/a 59.3 KiB n/a ±3%
SKIP npm @piper-plus/g2p n/a 71.1 KiB n/a ±3%
SKIP nuget PiperPlus.Core n/a 111.6 KiB n/a ±5%
SKIP nuget PiperPlus.Cli n/a 125.38 MiB n/a ±5%
SKIP cargo piper-plus n/a n/a n/a ±5%
SKIP maven piper-plus-g2p-android n/a 2.92 MiB n/a ±5%

Summary: 0 fail / 6 skip / 0 ok

SKIP means the artifact was not built in this job, or the baseline is a placeholder. The gate never fails on SKIP.

ayutaz added 3 commits July 25, 2026 17:44
MD040: 図表用のコードフェンスに text 言語を指定。
MD032: **Files:** / **Interfaces:** 直後のリストに空行を追加 (18 箇所)。

あわせて設計書のモデル一覧で、編集時に重複していたリード文を統合した。
TDD 充足度の監査で、テストの分布がリスクの分布と逆になっていることが判明した。
30 件のユニットテストは純関数に集中する一方、最大の新規複雑度である
PiperPlusTtsService (156 行) はテストコード中に識別子すら現れず、
壊れると音が壊れる境界 (PCM のバイト詰め / 8 引数 JNI / callback プロトコル /
manifest 配線) はすべてカバレッジ 0 だった。その未検証領域から
production バグが 2 件見つかったため、修正とテストを併せて入れる。

production バグ:

- 打ち切られた stream がネイティブ iterator を解放せず、以降の全合成が
  ERR_BUSY になる。synth_start は engine を busy にしたまま返り、解放するのは
  synth_next が終端に達したときだけだった。読み上げ停止を 1 回押すと
  プロセスが死ぬまで TTS が沈黙する。piper_plus_synth_abort() を新設し
  PiperPlus.synthesizeStream の finally から呼ぶ
- System.loadLibrary の失敗は LinkageError なので catch (Exception) を
  すり抜け、非対応 ABI の端末でサービスごとクラッシュしていた。
  設計書 §8 が「クラッシュさせない」と規定した経路

CI:

- android-build.yml の paths が android/** を含まず、Kotlin / JNI だけを
  変更した PR ではテストが 1 件も実行されていなかった。マージ後は既存の
  30 件も二度と走らない状態だった
- JVM ユニットテストを kotlin-unit-tests job に分離し、3 ABI の
  ネイティブビルド (45-60 分) への従属を外した

テスト可能性のための分割:

- SynthesisRequest は final かつ全 getter がスタブのため、Service を直接
  呼ぶ形では JVM テストが 1 分岐にしか到達できない。合成本体を
  SynthesisSession と PcmEmitter に切り出し、停止フラグの所有権も移した
- JNI 呼び出しと NativeEngine を named argument 化。末尾 4 引数がすべて
  Float で、位置引数のままだと並べ替えても型検査を通る
- lateinit を by lazy に変更し、super.onCreate() の順序依存を構造的に解消

テスト (30 → 61 件、新規 dependency ゼロ):

- PcmEmitterTest / SynthesisSessionTest — 中核ロジック
- PiperPlusNativeBridgeTest — Kotlin external 宣言と C++ 定義をソース照合
- ManifestWiringTest — Service 配線と intent-filter
- SynthOptionsTest — 既定値を piper_plus_default_options() の実ソースと突き合わせ
  (従来は Kotlin リテラルを Kotlin リテラルで読み返していた)
- C++ 統合テスト 2 件 — 放棄された iterator の解放と冪等性

いずれも production に変異を入れて落ちることを確認済み (23/23 検出)。

削除:

- androidTest の PiperPlusTtsServiceTest — Service を一度も参照せず
  LocaleResolverTest と assert が重複。CI ではコンパイルもされず、
  モデル配置済みの端末では fail する
- ModuleSanityTest — assertTrue(true)
@github-actions

Copy link
Copy Markdown
Contributor

Public ABI snapshot diff

Bootstrap mode: baseline is empty, recording first observation.

No ABI changes detected.

docs/spec/language-id-map-contract.toml は「Drift is catastrophic — every
multilingual checkpoint becomes unrecoverable」と警告し、9 つのランタイム
ミラーを gate しているが、本ブランチが追加した Kotlin の表だけが
どの gate にも載っていなかった。

check_language_id_map_contract.py の sweep は src/ しか walk しないため、
android/ は forbidden-pattern sweep の対象にもならない。さらに Kotlin 側は
Android framework が ISO-639-3 を渡してくる関係でキーが "jpn" 形式であり、
sweep が探す '"ja": 0' 形式とは構造的に一致しない。

通り抜ける drift の例: 将来モデルが言語を途中挿入した場合 (ko=2 で
zh→3…pt→6)、canonical 側の不変条件 (ja_is_zero / en_is_one /
values_consecutive_from_zero / values_unique) はすべて満たすため契約検査は
通り、登録済み 9 ミラーは gate に従って更新される。一方 Kotlin だけが
"zho" to 2 のまま残り、中国語が韓国語の emb_lang に流れる。クラッシュせず
「それっぽいが違う」音声になるため、発見が遅れる。

- kotlin_iso3_map extractor を追加。mapOf リテラルを括弧の対応をとって
  切り出し、[iso639_3_aliases] で canonical キーに正規化してから比較する
- 未知の ISO-639-3 コード、および zho/cmn が異なる id を指す状態を拒否する
- parity-hub.yml の paths と pre-commit の files 正規表現に LocaleResolver.kt
  を追加。canonical 側だけを変更する PR でも発火するよう、gate 登録を
  JVM テストではなく contract 側で行う

変異 5 種 (spa/fra 取り違え / cmn だけ別 id / jpn 削除 / 未知コード追加 /
シンボル rename / pt を連番から外す) がすべて検出されることを確認済み。
@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Runtime Parity Deep — audio (informational tier)

Pairs compared: 15, failing: 14, runtimes skipped: 0.

A B Tier Result Detail
cpp csharp sha256 ⚠️ b03f2dc42b4c vs daee8d1f99bd
cpp csharp peak_rms ⚠️ Δrms=0.15320 (≤ 0.005)
cpp csharp snr ⚠️ frame count differs: 3129 vs 6615
cpp go sha256 ⚠️ b03f2dc42b4c vs 5226f7680924
cpp go peak_rms Δrms=0.00255 (≤ 0.005)
cpp python sha256 ⚠️ b03f2dc42b4c vs 9a183d7810aa
cpp python peak_rms ⚠️ Δrms=0.07016 (≤ 0.005)
cpp python snr ⚠️ SNR=-0.16 dB (≥ 60.0)
cpp rust sha256 ⚠️ b03f2dc42b4c vs 76a08aa923e6
cpp rust peak_rms ⚠️ Δrms=0.13611 (≤ 0.005)
cpp rust snr ⚠️ frame count differs: 3129 vs 7539
cpp wasm sha256 ⚠️ b03f2dc42b4c vs 8112e9a02f2e
cpp wasm peak_rms ⚠️ Δrms=0.42930 (≤ 0.005)
cpp wasm snr ⚠️ frame count differs: 3129 vs 3328
csharp go sha256 ⚠️ daee8d1f99bd vs 5226f7680924
csharp go peak_rms ⚠️ Δrms=0.15574 (≤ 0.005)
csharp go snr ⚠️ frame count differs: 6615 vs 3129
csharp python sha256 ⚠️ daee8d1f99bd vs 9a183d7810aa
csharp python peak_rms ⚠️ Δrms=0.22335 (≤ 0.005)
csharp python snr ⚠️ frame count differs: 6615 vs 3129
csharp rust sha256 ⚠️ daee8d1f99bd vs 76a08aa923e6
csharp rust peak_rms ⚠️ Δrms=0.01709 (≤ 0.005)
csharp rust snr ⚠️ frame count differs: 6615 vs 7539
csharp wasm sha256 ⚠️ daee8d1f99bd vs 8112e9a02f2e
csharp wasm peak_rms ⚠️ Δrms=0.27611 (≤ 0.005)
csharp wasm snr ⚠️ frame count differs: 6615 vs 3328
go python sha256 ⚠️ 5226f7680924 vs 9a183d7810aa
go python peak_rms ⚠️ Δrms=0.06761 (≤ 0.005)
go python snr ⚠️ SNR=-2.28 dB (≥ 60.0)
go rust sha256 ⚠️ 5226f7680924 vs 76a08aa923e6
go rust peak_rms ⚠️ Δrms=0.13866 (≤ 0.005)
go rust snr ⚠️ frame count differs: 3129 vs 7539
go wasm sha256 ⚠️ 5226f7680924 vs 8112e9a02f2e
go wasm peak_rms ⚠️ Δrms=0.43185 (≤ 0.005)
go wasm snr ⚠️ frame count differs: 3129 vs 3328
python rust sha256 ⚠️ 9a183d7810aa vs 76a08aa923e6
python rust peak_rms ⚠️ Δrms=0.20627 (≤ 0.005)
python rust snr ⚠️ frame count differs: 3129 vs 7539
python wasm sha256 ⚠️ 9a183d7810aa vs 8112e9a02f2e
python wasm peak_rms ⚠️ Δrms=0.49946 (≤ 0.005)
python wasm snr ⚠️ frame count differs: 3129 vs 3328
rust wasm sha256 ⚠️ 76a08aa923e6 vs 8112e9a02f2e
rust wasm peak_rms ⚠️ Δrms=0.29319 (≤ 0.005)
rust wasm snr ⚠️ frame count differs: 7539 vs 3328

SynthesisSession が callback.start() に 22050 を固定で申告していた。
22050Hz 以外のモデルを配置すると全発話がピッチのずれた音で再生されるが、
エラーにも警告にもならないため「なんとなく声が高い」としか気付けない。

PiperPlusEngine に sampleRate を追加し、実際の合成では engine.sampleRate を
渡すようにした。空文字の経路だけは音声を 1 バイトも出さないため、モデルを
ロードせずに FALLBACK_SAMPLE_RATE を申告する従来の挙動を保つ。

監査ではこの項目を「interface を広げる production 変更が必要」として
別 issue 送りにしていたが、同種の seam 変更を既に入れているため本 PR に含める。

定数に戻す変異でテストが落ちることを確認済み。
@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Distroless trial: webui / cpp-inference

Each canonical Dockerfile is UNCHANGED; the trial Dockerfile sits beside it as Dockerfile.distroless so docker-compose and existing CI matrices keep using the proven image.

(cpp-dev distroless was deferred — chainguard/wolfi-base does not ship the OpenJTalk / mecab build toolchain the canonical image depends on. T-016 needs a fresh design and ships in a separate PR.)

cpp-inference

Trial Dockerfile: docker/cpp-inference/Dockerfile.distroless. Canonical docker/cpp-inference/Dockerfile is UNCHANGED.

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) 928MB 764MB -17.7%

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-trials matrix).
  • CVE Trivy diff lives in trivy-container-scan.yml (per-image SARIF upload).
  • For webui: full Gradio cold-start under distroless (a real docker run -p 7860:7860 from webui-test.yml) is the promotion gate.
  • For cpp-inference: end-to-end piper-plus --model ... --output_file ... with a real model fixture is the promotion gate.

@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Multi-Runtime RTF Benchmark

Policy: warn-only (thresholds: RTF +/-10%, P50 +/-10%, P95 +/-15%)

Model: test/models/multilingual-test-medium.onnx (warmup=5, runs=30)

Runtime Text RTF P50 (ms) P95 (ms) Baseline RTF RTF Δ Baseline P50 P50 Δ Baseline P95 P95 Δ
python short 3.3962 4606.1 4686.2 4.4317 -23.4% 6324.8 -27.2% 6757.1 -30.6%
python medium 1.0064 5529.3 5741.4 1.0438 -3.6% 5882.0 -6.0% 6208.1 -7.5%
python long 0.2443 8371.9 8519.7 0.2251 +8.5% 7755.6 +7.9% 8124.9 +4.9%
rust short 0.8773 1318.4 1384.0 0.8309 +5.6% 1310.9 +0.6% 1359.6 +1.8%
rust medium 0.2548 1631.6 1660.0 0.2501 +1.9% 1614.3 +1.1% 1636.8 +1.4%
rust long 0.0847 2987.1 3099.5 0.1039 -18.5% 3673.8 -18.7% 3697.7 -16.2%
go short 2.4524 3151.0 3221.1 2.3051 +6.4% 3125.1 +0.8% 3216.7 +0.1%
go medium 0.5454 3369.0 3482.3 0.5123 +6.5% 3158.6 +6.7% 3358.1 +3.7%
go long 0.1434 5252.3 5334.0 0.1464 -2.0% 5374.6 -2.3% 5467.3 -2.4%
csharp short 2.8289 1527.3 1620.0 2.8462 -0.6% 1542.0 -1.0% 1627.3 -0.4%
csharp medium 0.8891 1610.3 1683.2 0.8929 -0.4% 1608.6 +0.1% 1811.7 -7.1%
csharp long 0.1806 2292.4 2381.8 0.1413 +27.8% ⚠️ 1813.8 +26.4% ⚠️ 1913.0 +24.5% ⚠️
cpp short 0.9079 1373.1 1415.3 0.8694 +4.4% 1402.4 -2.1% 1431.6 -1.1%
cpp medium 0.2535 1675.0 1704.9 0.2529 +0.2% 1686.2 -0.7% 1724.0 -1.1%
cpp long 0.1026 3700.0 3741.0 0.0988 +3.8% 3573.3 +3.5% 3641.5 +2.7%
wasm short (missing) - - n/a - n/a - n/a -
wasm medium (missing) - - n/a - n/a - n/a -
wasm long (missing) - - n/a - n/a - n/a -

⚠️ One or more cells regressed beyond the RTF, P50, or P95 threshold. Warn-only while the baseline is being calibrated; this will become a hard gate once we have ~2-3 weeks of variance data.

ayutaz added 2 commits July 26, 2026 00:17
マージ前の確認で、ktlint と detekt が :piper-plus-g2p にしか適用されておらず、
:piper-plus と :piper-plus-tts-engine の Kotlin が一度も lint されていないことが
判明した。pre-commit の "ktlint --android (Kotlin)" が Passed と表示されていたのは
g2p モジュールだけを見た結果で、本ブランチが追加した約 700 行は素通しだった。

- 両モジュールに ktlint を適用 (g2p と同じ plugin 12.3.0 / engine 1.3.1 / android=true)
- ktlintFormat を適用。既存コードを含め 22 ファイルが整形された
- pre-commit hook を 3 モジュールすべてに拡張
- android-build.yml に blocking な ktlint step を追加
  (pre-commit 側は non-blocking なため)

あわせて、確認の過程で見つかった残りのドリフトを解消:

- 設計書 §6.2 が synthesize(text, speakerId) に @deprecated を付けると
  規定していたが実装は付けていない。単一話者モデルを既定設定で鳴らす用途では
  今も最短の書き方であり、非推奨にしても既存利用者に移行の実益がないため、
  実装側の判断を採用してドキュメントを実態に合わせた。
  あわせて options に既定値を付けられない理由 (speakerId 版と併存するため
  synthesize("text") が両方に解決してコンパイルエラーになる) を明記した
- examples/dart の FFI binding に piper_plus_synth_abort を追加

C API の消費者 (JNI / Dart / Godot / C++ テスト) を網羅的に確認し、Godot の
iterator ループは常に DONE まで drain するか synth_next 自身が解放する
エラー経路で抜けるため、abort の追加漏れがないことを確認済み。
ktlint に続き、静的解析も g2p モジュールにしか掛かっていなかった。
本ブランチが追加した Kotlin を対象に含める。

検出された 17 件はすべて対応済み (閾値の引き上げや baseline での
黙らせは行っていない):

- PcmEmitter の 0xFF / 8 を BYTE_MASK / BITS_PER_BYTE に命名
- SynthesisParams.lengthScaleOf / ModelPaths.isInstalled の長すぎる行を分割
- PiperPlusNativeBridgeTest.bodyAfter の return を 3 → 2 に整理
- PiperPlus.extractDictIfNeeded のネストと return を整理し、
  アセット複製を copyAssetsTo に切り出し (isDirectory は exists() を含意し、
  isNullOrEmpty() は null と空の両方を覆うため挙動は同一)
- EngineHolderTest の throw IllegalStateException を error() に
- LocaleResolver の language_id (MagicNumber)、PiperPlusNative の
  関数数 (TooManyFunctions)、SynthesisSession のガード節 (ReturnCount) と
  ネイティブ例外の総取り (TooGenericExceptionCaught) は理由付きで @Suppress

あわせて:

- detekt.yml の ComplexMethod を CyclomaticComplexMethod に改名
  (1.23 で改名済み。旧名は deprecation 警告を出していた)
- android-build.yml に detekt step を追加

:piper-plus-g2p は plugin を適用済みだが CI gate には含めない。dev 時点で
既に 47 件 (大半が DictionaryDownloader.kt の detekt-formatting 系) を
報告しており、本 PR とは無関係の既存負債のため。件数が本 config 変更の
前後で同一 (47 → 47) であることを確認済み。経緯と「閾値で消すな」の
方針を detekt.yml に明記した。
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.

1 participant