Skip to content

feat: GPL-free 6言語マルチリンガル TTS — 学習パイプライン + C++ G2P - #218

Merged
ayutaz merged 71 commits into
devfrom
feat/bilingual-phonemizer
Mar 18, 2026
Merged

feat: GPL-free 6言語マルチリンガル TTS — 学習パイプライン + C++ G2P#218
ayutaz merged 71 commits into
devfrom
feat/bilingual-phonemizer

Conversation

@ayutaz

@ayutaz ayutaz commented Feb 17, 2026

Copy link
Copy Markdown
Owner

Summary

Piper TTS を日英バイリンガルから 6言語マルチリンガル (JA/EN/ZH/ES/FR/PT) に拡張。Python 学習パイプラインと C++ 推論エンジンの両方で GPL フリーの多言語対応を実現。

主な変更点

Python: 多言語 Phonemizer + 学習パイプライン

  • 6言語 Phonemizer: JA (pyopenjtalk) / EN (g2p-en) / ZH (pypinyin) / ES・FR・PT (規則ベース、依存なし)
  • MultilingualPhonemizer: Unicode 言語検出で文内コードスイッチングに対応
  • 統一 phoneme_id_map: 6言語 173 シンボル、ID 衝突なし
  • Phonemizer ABC + レジストリ: 新言語追加が1ファイルで完結
  • 言語 embedding: nn.Embedding(n_languages, gin_channels) を VITS に統合
  • ONNX export/推論: lid (language ID) テンソル対応
  • --freeze-dp: Duration Predictor 凍結でファインチューニング時の catastrophic forgetting 防止
  • --resume-from-multispeaker-checkpoint: マルチ→シングル話者転移を1フラグで自動化
  • 言語均等サンプリング: 話者数比 ≥ 3:1 で自動有効化
  • 学習高速化: Validation 頻度削減、DataLoader 最適化、DDP 修正、WandB Audio Logging

C++: GPL-free マルチリンガル G2P エンジン (#247)

  • 6言語ネイティブ音素化: eSpeak-ng 不要で JA/EN/ZH/ES/FR/PT を C++ で直接処理
  • CMU Dict 統合: 134K 語の英語辞書 + G2P フォールバック
  • Pinyin 変換: 単漢字 + 多音字フレーズ辞書による中国語音素化
  • Unicode 言語検出: LanguageDetector で混合テキストを自動セグメント分割

その他

  • Docker: 多言語モデル推論対応 (lid テンソル、eSpeak フォールバック)
  • テストモデル更新: CSS10 6lang ファインチューニングモデルに置換
  • CI 修正: ruff lint/format、coverage 互換性、テスト graceful skip

変更規模

  • 162 ファイル変更 (+25,127 / -4,300)
  • Python Phonemizer: 7言語 (KO は optional)
  • C++ G2P: 6言語ネイティブ実装

Test plan

  • Python Phonemizer テスト: JA/EN/ZH/ES/FR/PT/KO 全言語 PASSED
  • C++ ビルド + ユニットテスト PASSED
  • マルチリンガル推論テスト (6言語) PASSED
  • 6lang 事前学習完了 (75 epoch, 508K 発話, 571 話者)
  • つくよみちゃん 6lang FT 完了 (500 epoch, 全6言語推論成功)
  • CI: ruff lint/format + coverage 互換性 + graceful skip

Copilot AI review requested due to automatic review settings February 17, 2026 17:02
@ayutaz ayutaz self-assigned this Feb 17, 2026

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

バイリンガル(JA+EN)TTS のコードスイッチング対応を学習〜推論(ONNX)まで貫通させ、言語埋め込みやログ/最適化も含めてパイプラインを拡張するPRです。

Changes:

  • ja-en のバイリンガル音素化(統一 phoneme_id_map・Unicodeベースのセグメンテーション)を追加
  • 学習側に language_id を伝播し、SynthesizerTrn に language embedding を追加(ONNX export / infer も lid 対応)
  • WandB音声ログ最適化、WavLM の間引き実行、DataLoader/Optimizer 最適化などを追加

Reviewed changes

Copilot reviewed 16 out of 17 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
test/test_phonemizer_registry.py get_phoneme_id_map() の monolingual/bilingual 挙動をテストで明確化
test/test_bilingual_phonemizer.py バイリンガルIDマップ、セグメンテーション、レジストリ統合のテストを追加
src/python/piper_train/vits/models.py 言語埋め込みの追加、global conditioning 統合、maximum_path 実装の切替
src/python/piper_train/vits/lightning.py language_id 伝播、固定テストセット、WandB音声ログ、最適化フラグ追加
src/python/piper_train/vits/dataset.py Dataset/Batch に language_id(s) を追加し collate でバッチ化
src/python/piper_train/preprocess.py ja-en 前処理(BILINGUAL phoneme type・ワーカー)を追加
src/python/piper_train/phonemize/registry.py ja-en phonemizer の自動登録を追加
src/python/piper_train/phonemize/bilingual_id_map.py JA+EN 統一 phoneme_id_map を新規追加
src/python/piper_train/phonemize/bilingual.py Unicodeベースの言語セグメント分割+委譲 phonemizer を新規追加
src/python/piper_train/infer_onnx.py ONNX 推論で lid 入力対応& --text モードの language_id 決定追加
src/python/piper_train/export_onnx.py ONNX export で lid 入力対応&ダミー入力構築を変更
src/python/piper_train/main.py num_languages 読み込み、WavLM無効化/間引き、--compile 追加
pyproject.toml 依存関係の追記、setuptools の packages.find 設定追加
prepare_bilingual_dataset.py 既存JA+ENデータを統合し学習用 dataset/config を生成するスクリプト追加
benchmark_optimizations.sh 最適化のベンチマーク用スクリプト追加
CLAUDE.md バイリンガル学習完了・使い方・最適化/ログ機能のドキュメント更新

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

Comment thread src/python/piper_train/export_onnx.py Outdated
Comment thread src/python/piper_train/__main__.py Outdated
Comment thread src/python/piper_train/vits/lightning.py
Comment thread src/python/piper_train/vits/lightning.py
Comment thread src/python/piper_train/vits/lightning.py Outdated
Comment thread src/python/piper_train/vits/lightning.py
Comment thread src/python/piper_train/phonemize/bilingual_id_map.py Outdated
@ayutaz
ayutaz force-pushed the feat/bilingual-phonemizer branch from 9e1e8db to dd1cdfe Compare February 18, 2026 11:56
ayutaz added a commit that referenced this pull request Feb 18, 2026
- export_onnx.py: dummy_inputとinput_namesの条件分岐を一致させる(単一話者/言語モデルでNoneが渡される問題)
- lightning.py: AdamW fused=TrueをCUDA有無で切り替え(CPU環境でTypeError)
- bilingual_id_map.py: ENGLISH_PHONEMESの重複 "'" を削除

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ayutaz ayutaz changed the title feat: バイリンガル (JA+EN) TTS — 文内コードスイッチング + 200epoch学習完了 feat: バイリンガル (JA+EN) TTS — v3学習完了・v4データセット作成完了 Feb 28, 2026
ayutaz added a commit that referenced this pull request Mar 1, 2026
- 次のステップにCI修正完了を追記
- 実装済み機能にCI修正セクション追加
- 関連PR/IssueテーブルにPR #218追加

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ayutaz ayutaz changed the title feat: バイリンガル (JA+EN) TTS — v3学習完了・v4データセット作成完了 feat: マルチリンガル (6言語) TTS — v4学習完了・6lang学習完了・つくよみちゃんFT完了 Mar 16, 2026
ayutaz added a commit that referenced this pull request Mar 16, 2026
- export_onnx.py: dummy_inputとinput_namesの条件分岐を一致させる(単一話者/言語モデルでNoneが渡される問題)
- lightning.py: AdamW fused=TrueをCUDA有無で切り替え(CPU環境でTypeError)
- bilingual_id_map.py: ENGLISH_PHONEMESの重複 "'" を削除

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ayutaz added a commit that referenced this pull request Mar 16, 2026
- 次のステップにCI修正完了を追記
- 実装済み機能にCI修正セクション追加
- 関連PR/IssueテーブルにPR #218追加

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ayutaz
ayutaz force-pushed the feat/bilingual-phonemizer branch from 9f25faf to 98f1053 Compare March 16, 2026 02:52
@ayutaz
ayutaz requested a review from Copilot March 16, 2026 03:02

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

Copilot reviewed 58 out of 59 changed files in this pull request and generated 6 comments.


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

Comment thread src/python/piper_train/export_onnx.py Outdated
Comment thread src/python/piper_train/phonemize/multilingual.py
Comment thread src/python/piper_train/phonemize/multilingual.py
Comment thread src/python/piper_train/phonemize/registry.py Outdated
Comment thread src/python/piper_train/infer_onnx.py Outdated
Comment thread docs/guides/training/multi-gpu-training.md Outdated
ayutaz added a commit that referenced this pull request Mar 16, 2026
- export_onnx: dummy_input/input_names条件を統一し入力数不一致を防止
- export_onnx: enc_pにg(conditioning)を渡しcond_layerをONNXに反映
- __main__: torch.compileをLightningModule全体→サブモジュール(model_g/d)に限定
- lightning: Utterance型をintで統一 (LongTensorではなく)
- lightning: config.json読み込みをループ外に移動しI/O削減
- registry: _detect_default_latin()のフォールバックをlanguages内に限定
- multilingual: default_latin_language未サポート時のバリデーション追加
- bilingual_id_map: ENGLISH_PHONEMESの"?"重複を削除
- infer_onnx: _cache型アノテーションをtuple[tuple[str,int],...]に修正
- multi-gpu-training.md: 実装にないstatic_graph記述を削除
ayutaz added a commit that referenced this pull request Mar 16, 2026
- export_onnx.py: dummy_inputとinput_namesの条件分岐を一致させる(単一話者/言語モデルでNoneが渡される問題)
- lightning.py: AdamW fused=TrueをCUDA有無で切り替え(CPU環境でTypeError)
- bilingual_id_map.py: ENGLISH_PHONEMESの重複 "'" を削除

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ayutaz added a commit that referenced this pull request Mar 16, 2026
- 次のステップにCI修正完了を追記
- 実装済み機能にCI修正セクション追加
- 関連PR/IssueテーブルにPR #218追加

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ayutaz added a commit that referenced this pull request Mar 16, 2026
- export_onnx: dummy_input/input_names条件を統一し入力数不一致を防止
- export_onnx: enc_pにg(conditioning)を渡しcond_layerをONNXに反映
- __main__: torch.compileをLightningModule全体→サブモジュール(model_g/d)に限定
- lightning: Utterance型をintで統一 (LongTensorではなく)
- lightning: config.json読み込みをループ外に移動しI/O削減
- registry: _detect_default_latin()のフォールバックをlanguages内に限定
- multilingual: default_latin_language未サポート時のバリデーション追加
- bilingual_id_map: ENGLISH_PHONEMESの"?"重複を削除
- infer_onnx: _cache型アノテーションをtuple[tuple[str,int],...]に修正
- multi-gpu-training.md: 実装にないstatic_graph記述を削除
@ayutaz
ayutaz force-pushed the feat/bilingual-phonemizer branch from 5a7754e to 65639b5 Compare March 16, 2026 14:10
ayutaz and others added 9 commits March 17, 2026 02:05
統一phoneme_id_map (JA+EN ~110記号) とUnicode範囲ベースの言語自動検出により、
「今日はgood morningですね」のような混合テキストを正しく音素化できる。
Phase B (学習パイプライン) / Phase C (データ準備+学習) の基盤。

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- SynthesizerTrnにnn.Embedding(n_languages, gin_channels)追加
- Dataset/Batchにlanguage_id伝播
- ONNX export/推論にlid入力対応
- preprocess.pyに--language ja-enバイリンガルモード追加
- config.jsonにnum_languages/language_id_map追加

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- validation_stepにlid (language_id)を追加し、EN utteranceの評価精度を改善
- infer_onnx.py --textモードでconfig.jsonのlanguage_id_mapからlanguage_idを決定
- dataset.py collateでlanguage_idsテンソルを.zero_()で初期化(ゴミ値防止)
- bilingual_id_mapを実際のEnglishPhonemizer出力に合わせて101→97シンボルに修正
- CLAUDE.mdをバイリンガルモデル学習状況に更新

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Bug #1: _add_inter_phoneme_padding()で既存padding(ID=0)をスキップし三重padding防止
- Bug #2: BilingualPhonemizer.post_process_ids()が最終セグメントのEOS($/?)を保持
- EN phonemization をProcessPoolExecutorで並列化(60ワーカーで~10倍高速化)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- EN phonemizationをProcessPoolExecutor並列化(60ワーカーで約10倍高速化)
- Audio caching Phase 2でキャッシュ済みファイルをスキップ(librosa/torch import不要)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- torch, pytorch-lightning, librosa, cython, pyopenjtalk-plus, wandb を
  pyproject.toml の dependencies に宣言
- [tool.setuptools.packages.find] where=["src/python"] を追加し
  piper_train パッケージを正しく認識させる
- これにより uv add で新パッケージ追加時に既存依存が削除される問題を防止

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
batch-size 28はV100-16GBでOOM発生。batch-size 20で安定稼働を確認。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- __main__.py: --no-wavlm で WavLM Discriminator を無効化、--wavlm-every-n-steps でWavLM loss計算頻度を制御
- lightning.py: wavlm_every_n_steps 対応、MEMORY_CLEANUP_FREQUENCY を500に変更
- prepare_bilingual_dataset.py: 空phoneme_ids (16件) をスキップ
- CLAUDE.md: WavLM無効化の経緯・学習コマンド・状況を更新

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ayutaz added 11 commits March 17, 2026 02:05
Run `ruff format` (v0.12.5) across all src/python/ files to match
CI's `ruff format --check` requirement.
Replace ja_JP-test-medium.onnx with a model fine-tuned from the
6-language base model using CSS10 Japanese dataset (6,841 utterances).
The new model supports 173 symbols (multilingual) instead of 65
(JA-only), matching the multilingual training pipeline.
- Rename ja_JP-test-medium.onnx → multilingual-test-medium.onnx
  (CSS10 fine-tuned from 6-lang base, 173 symbols)
- Remove test_voice.onnx (old English-only espeak model)
- Update all 55 files referencing old model names
- deploy-huggingface.yml: merge duplicate JA/EN copy blocks into single
  multilingual model block
- deploy-webassembly-demo.yml: remove duplicate CRITICAL_MODELS entry
- huggingface-space: consolidate two model entries into one multilingual
- simple-multilingual.html: use single model for all languages instead
  of conditional en_US-test-medium fallback
- multilingual.html: update EN button to use multilingual-test-medium
- prepare-english-model.sh: simplify to verify multilingual model exists
- Remove stale en_US-test-medium references from .gitignore, README, TESTING
Remove 10 stale model files (ja_JP-test-medium, en_US-test-medium,
test_voice) from huggingface-space/models/ and src/wasm/openjtalk-web/models/.
Replace with multilingual-test-medium.onnx in both directories.
Add .gitignore to huggingface-space to prevent future stale model commits.
Fix en_US-test.onnx references in test_webui.py.
- docs/cli-enhancements.md: ja_JP-test.onnx → multilingual-test-medium.onnx
- lightning.py: replace hardcoded {0:"ja",1:"en"} fallback with dynamic
  lang map derived from num_languages hparam (supports 6-lang models)
- C++ piper: add MultilingualPhonemes type using OpenJTalk phonemization
  with intersperse padding, add lid (language ID) tensor support
- Python runtime: add MULTILINGUAL to PhonemeType enum, handle in
  phonemize/phonemes_to_ids/synthesize_ids_to_raw with lid support
- ruff format: fix 3 files (app.py, download_models.py, lightning.py)
…ilable

C++ and Python runtimes now fall back to eSpeak with "en" voice when
OpenJTalk is not available for multilingual models. This fixes CI tests
that run in environments without OpenJTalk installed.

Also fixes ruff format for voice.py.
- main.cpp: initialize eSpeak when phoneme type is MultilingualPhonemes
  (needed for eSpeak fallback when OpenJTalk is unavailable)
- voice.py: provide zero-filled prosody_features tensor when the ONNX
  model requires it (multilingual model trained with --prosody-dim 16)
The multilingual model expects lid as rank-1 tensor (shape [1]),
not rank-2 (shape [1,1]). Remove np.expand_dims wrapper.
@ayutaz
ayutaz force-pushed the feat/bilingual-phonemizer branch from 0c98d20 to 2bb3849 Compare March 17, 2026 02:06
…#247)

* feat: add C++ multilingual inference support (Phase 1 — lid tensor + config)

Add language ID (lid) tensor support to C++ inference pipeline, enabling
multilingual ONNX models to run without crashing.

Changes:
- piper.hpp: Add LanguageId type, MultilingualPhonemes enum, languageId
  field in SynthesisConfig, numLanguages/languageIdMap in ModelConfig,
  hasLanguageInput in ModelSession
- piper.cpp: Parse num_languages/language_id_map from config.json,
  detect "lid" ONNX input, construct lid tensor in synthesize() with
  correct ordering (sid -> lid -> prosody_features), handle "multilingual"
  and "bilingual" phoneme types, add PUA entries for rr/y_vowel
- main.cpp: Add --language/-l CLI option with numeric ID and language
  code resolution, JSON input language_id/language support, eSpeak
  config for multilingual models, languageId save/restore in main loop
- docs: Add C++ multilingual G2P implementation spec

Backward compatible: monolingual models are unaffected (all new code
is behind conditional guards on numLanguages/hasLanguageInput).

* docs: update C++ multilingual G2P spec — Phase 1 complete

Mark Phase 1 as Done with commit hash, actual line numbers, and
usage examples. Update gap table, phase table, add implementation
details for all 18 change locations. Update Section 4 (ES/FR/PT)
with missing rules from review. Mark ARCH-3 as resolved.

* feat: add UnicodeLanguageDetector and multilingual phonemization (Phase 2)

Add automatic language detection and per-segment phonemization routing
for multilingual ONNX models.

New files:
- language_detector.hpp/cpp: UnicodeLanguageDetector with 6 Unicode
  range checks (Kana, CJK, Hangul, FullwidthLatin, JaPunct, Latin),
  text segmentation state machine, CJK disambiguation via kana context,
  and dominant language detection for lid tensor

Changes:
- piper.cpp: Add MultilingualPhonemes branch in textToAudio() that
  segments text by language, routes JA to OpenJTalk and others to
  eSpeak with correct voice mappings (en-us, cmn, ko, es-la, fr,
  pt-br), strips BOS/EOS from JA segments, tracks dynamic EOS,
  sets lid via dominant language detection. Update useProsody and
  sentenceBoundary conditions. Add streaming fallback.
- CMakeLists.txt: Add language_detector.cpp to piper and test_piper

Backward compatible: monolingual models are unaffected.

* docs: update spec — Phase 2 complete, gap table and appendix refreshed

- Mark Phase 2 as Done with commit hash and file details
- Expand gap table to show all 10 capabilities with status icons
- Add Section 3.1 listing all implemented Phase 2 features
- Update Section 3.5 with actual implementation details (file paths, line counts)
- Mark ARCH-2 (BOS/EOS double-apply) as resolved in Phase 2
- Update REG-1 scope (Phase 3+ only, no impact on Phase 2)
- Note Phase 2 architecture: language_detector in src/cpp/ (not phonemizer/ subdir)

* feat: add native C++ G2P for Spanish, French, and Portuguese (Phase 3)

Port rule-based phonemizers from Python to C++, replacing eSpeak
fallback for ES/FR/PT with Python-accurate G2P.

New files (6):
- spanish_phonemize.hpp/cpp (782 lines): seseo, yeismo, betacismo,
  allophonic b/d/g, syllabification, stress assignment
- french_phonemize.hpp/cpp (1084 lines): nasal vowels (PUA 0xE056-58),
  -er/-ille exceptions, silent finals, context-dependent e/o
- portuguese_phonemize.hpp/cpp (893 lines): nasal absorption,
  t/d palatalization (PUA 0xE054-55), coda-l vocalization, BR rules

Changes:
- piper.cpp: Route ES/FR/PT to native phonemizers (EN/ZH/KO on eSpeak)
- CMakeLists.txt: Add 3 new source files to piper and test_piper

* feat: add native C++ G2P for English, Chinese, and Korean (Phase 4)

English: CMU dictionary-based G2P (123K words, Apache-2.0)
- ARPAbet→IPA conversion with context rules (AA+R, ER1/0, AH0)
- Function word destressing (89 words)
- OOV fallback to eSpeak
- Runtime dictionary loading from cmudict_data.json

Chinese: pypinyin-based G2P (42K chars + 47K phrases, MIT)
- Pinyin→IPA tables (21 initials, 45 finals)
- Tone sandhi (T3+T3, yi, bu — 4 rules)
- Erhua handling, pinyin normalization (y/w/v)
- Runtime dictionary loading from pinyin_single/phrases.json

Korean: Hangul decomposition G2P (no external data)
- Pure arithmetic decomposition (19×21×28 jamo)
- IPA tables (68 entries) with correct neutralization
- Basic liaison rule (연음화)
- PUA: tense 0xE04B-4F, unreleased 0xE050-52

Integration:
- piper.hpp: Add dictionary fields to Voice struct
- piper.cpp: Route EN/ZH/KO to native phonemizers in multilingual
  dispatch, load dictionaries from model directory
- CMakeLists.txt: Add 3 new source files
- eSpeak fallback when dictionaries are not available

Data files (in src/cpp/, loaded at runtime from model directory):
- cmudict_data.json (3.7MB, 123K English words)
- pinyin_single.json (705KB, 42K Chinese characters)
- pinyin_phrases.json (1.9MB, 47K Chinese phrases)

* docs: update spec — all 4 phases complete

Mark Phase 3 and Phase 4 as Done with commit hashes and file details.
Update gap table: all 12 capabilities now have checkmarks.
Update file structure to reflect actual implementation (src/cpp/ flat).
Mark ARCH-1, EN-1, REG-1 as resolved or not applicable.
Add Phase 4 commit info, data file sizes, and line counts.

* fix: address critical review findings for C++ multilingual G2P

- Unify PUA codepoint mappings: add 57 fixed entries to token_mapper.py
  covering ZH (43), KO (8), ES/PT (2), FR (3) to match C++ hardcoded values
- Fix Chinese phrase dict parser for nested array format [["yí"],["gè"]]
- Fix Korean liaison IPA remapping with residualFinal for complex finals
- Add LanguageID bounds validation in piper.cpp, main.cpp (CLI/JSON/ONNX)
- Strip BOS/EOS from eSpeak fallback segments in multilingual pipeline
- Warn when multilingual model missing language_id_map
- Prevent dominant language auto-detection from overwriting explicit langId
- Add NFD→NFC combining accent collapse for ES/FR/PT phonemizers
- Fix Portuguese intervocalic x to recognize accented vowels
- Add tʃ/dʒ to Portuguese phoneme inventory, remove unused ø from Korean
- Add warnings for unknown phoneme_type and model without lid input

* fix: add non-JA prosody extraction and EN OOV eSpeak fallback

- Add computeNonJaProsody() for 5 languages (ZH/EN/ES/FR/PT)
  - ZH: tone(1-5) from PUA markers → a1, syllable position → a2
  - EN/ES/PT: stress markers (ˈ→2, ˌ→1) → a2, word phoneme count → a3
  - FR: last vowel in word → a2=2 (final-syllable stress)
- EN OOV fallback: when CMU dict returns empty, fall back to eSpeak
- Replace hardcoded {0,0,0} prosody for non-JA with actual values
- docs: add Section 11 with detailed investigation of M1/M4/M7/M11

* docs: update spec to reflect all completed fixes and investigation results

- Update header with latest commit ref (0f9429e)
- Add EN OOV fallback and non-JA prosody rows to Section 1.1 gap table
- Add Review R1/R2 phases to Section 1.2 phase table
- Add M11 to Section 7.1 summary table, update M4/M7 status
- Add R1/R2 rows to Section 7.3 phase status table
- Rewrite Section 7.4 as clean status matrix (done vs pending)
- Fix Section 8.2 PUA mapping count (31 → 89 entries)
- Update Appendix A priority phases with completion status
- Mark Sections 11.2/11.3/11.4 as resolved with commit refs
- Update Section 11.4 prosody table (all non-JA now show fixed)
- Update Section 11.5 priority matrix with implementation status

* refactor: remove piper-phonemize header dependency from public API

- piper.hpp: replace 3 piper-phonemize #includes with self-contained
  type definitions (Phoneme from phoneme_parser.hpp, PhonemeId/PhonemeIdMap
  as local typedefs) — binary-compatible with piper-phonemize for linking
- piper.cpp: move piper-phonemize includes here (implementation-only,
  not exposed in public header)
- Remove tashkeel (Arabic diacritization) dependency entirely:
  - PiperConfig: remove useTashkeel, tashkeelModelPath, tashkeelState
  - piper.cpp: remove tashkeel init and runtime code
  - main.cpp: remove --tashkeel_model CLI option and auto-enable logic

piper.hpp is now free of piper-phonemize headers. The runtime still
links to piper-phonemize for phonemize_eSpeak() and phonemes_to_ids().

* refactor: replace piper-phonemize/phoneme_ids.hpp with self-contained implementation

- Create src/cpp/phoneme_ids.hpp with self-contained phonemes_to_ids()
  function, PhonemeIdConfig struct, and PhonemeId/PhonemeIdMap typedefs
- piper.hpp: include phoneme_ids.hpp instead of phoneme_parser.hpp,
  remove duplicate PhonemeId/PhonemeIdMap typedefs
- piper.cpp: replace piper-phonemize/phoneme_ids.hpp with local header

Remaining piper-phonemize dependency: only phonemize.hpp for
phonemize_eSpeak() and phonemize_codepoints() runtime functions.

* feat: remove eSpeak-ng and piper-phonemize runtime dependencies

Complete removal of GPL-licensed eSpeak-ng and piper-phonemize from the
C++ inference pipeline. piper-plus now uses only self-contained, Apache-2.0
compatible G2P for all 6 supported languages (JA/EN/ZH/ES/FR/PT).

piper.hpp:
- Remove eSpeakConfig struct, eSpeakDataPath, useESpeak
- Remove eSpeakPhonemes and TextPhonemes from PhonemeType enum
- Default phoneme type changed to MultilingualPhonemes

piper.cpp:
- Remove #include <espeak-ng/speak_lib.h> and <piper-phonemize/phonemize.hpp>
- Remove espeak_Initialize()/espeak_Terminate() calls
- Remove findEspeakDataPath() (~110 lines)
- Remove eSpeakPhonemes and TextPhonemes dispatch branches
- Replace eSpeak fallbacks with warnings (EN OOV, unknown language)
- Clean up streaming mode to remove legacy code paths

main.cpp:
- Remove --espeak_data CLI option and help text
- Remove espeak-ng.dll/piper_phonemize.dll preloading
- Remove eSpeak path resolution logic

english_phonemize.cpp:
- Add tryMorphologicalFallback() for OOV words (replaces eSpeak fallback)
- Supports -ing, -ed, -s/-es/-ies, -er, -ly/-ily, -est suffix stripping
- Handles consonant doubling (running→run) and 'e' restoration (making→make)

CMakeLists.txt:
- Remove piper_phonemize ExternalProject (no more download/build of eSpeak)
- Remove link_libraries for piper_phonemize and espeak-ng
- Remove include_directories for piper_phonemize
- Remove install commands for espeak-ng data and libtashkeel
- Significant build time reduction

Remaining external C++ dependencies: ONNX Runtime, OpenJTalk, fmt, spdlog

* fix: remove last eSpeak fallback reference from OpenJTalk error path

* test: add 102 tests for C++ multilingual G2P changes

- test_phoneme_ids.py (10 tests): BOS/EOS insertion, inter-phoneme
  padding, missing phoneme tracking, multi-ID phonemes, edge cases
- test_morphological_fallback.py (18 tests): EN OOV suffix stripping
  for -ing/-ed/-s/-es/-ies/-er/-ly/-est with consonant dedup and
  vowel restoration
- test_non_ja_prosody.py (33 tests): prosody extraction for ZH (tone),
  EN/ES/PT (stress markers), FR (final-syllable stress), alignment
  verification for all 5 languages
- test_pua_mapping_consistency.py (43 tests): validates all 87 entries
  in FIXED_PUA_MAPPING with exact codepoints for JA/ZH/KO/ES/PT/FR

* fix: update Docker and test CMake for eSpeak/piper-phonemize removal

- docker/cpp-dev/Dockerfile: remove eSpeak-ng and piper-phonemize build
  steps, add ONNX Runtime 1.14.1 direct install
- docker/cpp-dev/test.sh: remove eSpeak-ng library and CLI checks
- src/cpp/tests/CMakeLists.txt: remove piper_phonemize and espeak-ng
  from link_libraries and add_dependencies

Verified: C++ build succeeds in Docker without eSpeak/piper-phonemize
(47/47 targets linked, warnings only, no errors)

* fix: add ONNX Runtime ExternalProject for Linux/macOS and clean test.cpp

- CMakeLists.txt: add onnxruntime_external ExternalProject for non-Windows
  platforms (downloads pre-built ONNX Runtime 1.14.1, supports x64/aarch64)
- CMakeLists.txt: add ONNX Runtime shared lib install rule for Linux/macOS
- src/cpp/test.cpp: remove all eSpeak references (eSpeakDataPath, espeak-ng
  data path detection), simplify to 2-arg CLI (model + output)

Verified: cpp-inference Docker builds and runs inference for all 6 languages
(JA/EN/ZH/ES/FR/PT) without eSpeak-ng or piper-phonemize.

* style: apply ruff format to token_mapper.py

* fix: CI build errors — macOS ONNX Runtime arch name + missing cstdint

- CMakeLists.txt: use 'arm64' (not 'aarch64') for macOS ONNX Runtime
  download URL (onnxruntime-osx-arm64-1.14.1.tgz)
- language_detector.cpp: add #include <cstdint> for uint32_t on Ubuntu

* fix: CI build errors — test sources, eSpeak enum refs, macOS arch, cstdint

- tests/CMakeLists.txt: update include/lib paths from pi/ to ort/ (ONNX
  Runtime), add missing phonemizer sources to streaming test targets
- test_streaming_raw_phonemes.cpp: replace eSpeakPhonemes with
  MultilingualPhonemes (removed enum value)
- test_streaming.cpp: remove useESpeak and TextPhonemes references
- CMakeLists.txt: fix macOS ONNX Runtime arch (aarch64→arm64)
- language_detector.cpp: add #include <cstdint> for uint32_t

* fix: CI failures — Windows ONNX Runtime paths, dict file copy, eSpeak refs

- tests/CMakeLists.txt: add ORT_INCLUDE_DIR/ORT_LIB_DIR variables that
  resolve to ONNXRUNTIME_INCLUDE_DIR (Windows) or ort/ prefix (Linux/macOS);
  fix self-referencing default values
- test-multilingual-tts.yml: add step to copy cmudict/pinyin dict files
  alongside model before running multilingual inference tests
- docker-test.yml: add step to copy dict files to test/models/ before
  C++ inference tests
- test_streaming.cpp: remove TextPhonemes/useESpeak references
- test_streaming_raw_phonemes.cpp: replace eSpeakPhonemes with
  MultilingualPhonemes

* fix: remove PARENT_SCOPE from find_onnxruntime_windows (called via include())

* fix: remaining CI failures — multilingual TTS, macOS dylib, JA TTS

- test-multilingual-tts.yml: replace eSpeak-based model downloads with
  direct 6-language tests using bundled multilingual-test-medium.onnx
- test-japanese-tts.yml: add DYLD_LIBRARY_PATH for macOS (was Linux-only)
- build-piper.yml: explicitly copy ONNX Runtime dylibs/so to dist for
  macOS and Linux distributions
- CMakeLists.txt: add BUILD_RPATH for macOS to find ONNX Runtime during
  build-time testing, add @executable_path to INSTALL_RPATH

* fix: broaden Dictionary Auto-Download grep patterns for OpenJTalk error messages

* fix: copy G2P dict files to Docker config fallback test directory

* fix: remove eSpeak-ng checks from ARM64 build verification test
@ayutaz ayutaz changed the title feat: マルチリンガル (6言語) TTS — v4学習完了・6lang学習完了・つくよみちゃんFT完了 feat: GPL-free 6言語マルチリンガル TTS — Python/C++/Web 全スタック対応 Mar 17, 2026
@ayutaz ayutaz changed the title feat: GPL-free 6言語マルチリンガル TTS — Python/C++/Web 全スタック対応 feat: GPL-free 6言語マルチリンガル TTS — 学習パイプライン + C++ G2P Mar 17, 2026
ayutaz and others added 7 commits March 17, 2026 22:04
10エージェント並列レビューで検出した40件の問題を修正し、
再発防止のための回帰テスト21件を追加。

## Critical/High 修正 (14件)
- piper.cpp: Windows非ASCIIパス破損、PUA音素表示、RTF計算逆転
- Python Runtime: PhonemeType.BILINGUAL追加、lid固定解除、MultilingualPhonemizer対応
- Training: _validate_cache_files実装、language_id assert修正、speaker_id型不一致修正
- ONNX: single-speaker multilingual JSONL sid欠落修正
- Security: model_manager シェルインジェクション対策強化
- C++ UTF-8: 全G2P公開APIに入力バリデーション追加
- pyproject.toml: torch>=2.1.0、onnxruntime>=1.17に修正

## Medium 修正 (16件)
- C++ G2P: portuguese stressIdx調整、korean NFC正規化、spanish xc音素数、
  chinese句読点マップ補完、french確認(問題なし)
- Training: validation metrics汚染防止、DataLoader shuffle追加、
  _build_trainer()ヘルパー抽出
- Phonemizer: chinese.py ImportError化、post_process_ids委譲、
  portuguese no-op関数削除
- Runtime: 0.0 falsy修正、Docker espeak残存チェック削除

## Low 修正 (10件)
- utf8_utils.hpp共通化、piper.hpp using json削除・numSpeakers初期化、
  norm_audio weights_only、export_onnx unsqueeze除去 + 全squeeze箇所整合

## 回帰テスト追加 (21件)
- C++: PUA codepoint判定、ModelConfig初期化、RTF計算式、UTF-8バリデーション
- Python: collate language_id、speaker_id型、sid default、PhonemeType enum、
  lid propagation、noise_scale 0.0、pypinyin ImportError、
  portuguese stressIdx、korean NFC、spanish xc、chinese句読点、
  post_process_ids委譲、validation metrics隔離、ONNX 3D出力、
  multilingual phonemizer import、multispeaker transfer
- __main__.py: remove blank line between `import torch` and
  `from pytorch_lightning` (I001 import block un-sorted)
- pyproject.toml: add PLR0911 (too many return statements) to ignore
  list; voice.py phonemize() has 7 returns due to multi-language
  fallback chain, consistent with existing PLR0912/PLR0915 ignores
Merge the two phonemize_japanese() return paths (with/without custom
dict) into a single conditional expression instead of suppressing
the lint rule.  Revert PLR0911 from pyproject.toml ignore list.
Characters with IDs 128-255 cannot be represented as valid single-byte
UTF-8 (those are continuation bytes, not valid standalone characters).
isSingleCodepoint() calls utf8::distance() (checked variant) which throws
utf8::invalid_utf8 when given such an invalid byte string.

Change the guard from id < 256 to id < 128 so only ASCII codepoints (which
are identical in UTF-8) take this path. IPA phonemes like ə (U+0259) and
ʊ (U+028A) are multi-byte in UTF-8 and are correctly handled via the
phonemeIdToStringMap lookup path instead.
- ruff format: portuguese.py, dataset.py, lightning.py, voice.py
- test_multilingual_g2p.cpp: remove piper.hpp (pulls in onnxruntime_cxx_api.h
  which requires OrtGetApiBase at link time). Replace with phoneme_parser.hpp
  for piper::Phoneme + inline minimal ModelConfig / isSingleCodepoint /
  getCodepoint stubs so the test links without onnxruntime.
In extractTimingsFromDurations(), the reverse map (PhonemeId -> string) was
built by assigning a char32_t key directly to std::string via
std::string::operator=(char), which silently truncates to the lowest byte.

For IPA codepoints whose lower byte falls in 0x80–0xBF (e.g. ʊ = U+028A
→ 0x8A), this stored a bare UTF-8 continuation byte as a standalone string.
The subsequent isSingleCodepoint() call passes that string to utf8::distance()
(checked variant) which throws utf8::invalid_utf8.

Fix: use utf8::append(static_cast<uint32_t>(phonemeChar), ...) to correctly
encode any Unicode codepoint as a well-formed UTF-8 string before inserting
into the map. This mirrors the pattern already used at line 1386 of piper.cpp.
* feat: add 6-language support to WebUI and HuggingFace demo

- docker/webui/app.py: extend language radio to ja/en/zh/es/fr/pt,
  add has_lid check and language_id_map → lid injection to ONNX inputs,
  pass language_id_map to text_to_phoneme_ids_and_prosody()
- huggingface-space/app.py: expand MODELS dict to all 6 languages,
  add zh/es/fr/pt fallback chain in text_to_phonemes() via
  MultilingualPhonemizer → per-language Phonemizer → eSpeak → char,
  add lid input to ONNX inference, add language_radio UI component,
  extend Examples with multilingual sample texts
- huggingface-space/download_models.py: add language_id_map and
  num_languages to create_dummy_config()
- huggingface-space/requirements.txt: add pypinyin>=0.50
- huggingface-space/app_imports.py: add HAS_PYPINYIN / HAS_G2PK checks
- huggingface-space/README.md: document 6-language support, add
  Supported Languages table and multilingual usage examples
- src/python_run/piper/config.py: add num_languages and language_id_map
  fields to PiperConfig
- src/python_run/piper/webui.py: add language_code param to
  synthesize_speech(), add language selector dropdown, wire language_id
  through to voice.synthesize()
- src/python_run/piper/http_server.py: accept language / language_id
  query params and pass language_id to voice.synthesize()
- src/python_run/piper/sample_texts.py: add zh/es/fr/pt sample texts
- pyproject.toml: add huggingface-space/*.py to ruff PLC0415 ignores

* feat: add per-language sample texts with auto-fill on language change

- docker/webui/app.py: add SAMPLE_TEXTS dict for 6 languages,
  set initial text to JA sample, wire language.change() to update
  text_input automatically
- huggingface-space/app.py: add SAMPLE_TEXTS dict, wire
  model_dropdown.change() to on_model_change() which fills
  text_input with the selected language's sample text
- Users can still freely edit the text after auto-fill

* feat: add 6-language support to GitHub Pages WebAssembly demo

- index.html: expand language buttons to ja/en/zh/es/fr/pt, add
  languageConfigs for all 6 languages with sample texts, add lid
  tensor to ONNX feeds, update updateLanguageInfo() for all languages
- multilingual.html: expand language buttons, add languageConfigs
  and langIdMap, add lid tensor, add example texts for zh/es/fr/pt
- simple-multilingual.html: expand language buttons and sample texts,
  add lid tensor to ONNX inference feeds
- simple_unified_api.js: add phonemizeChinese() and
  phonemizeLatinFallback() for zh/es/fr/pt, extend textToPhonemes()
  routing, enhance detectLanguage() for CJK vs Kana distinction
- docker/webui/Dockerfile: add multilingual extras for pypinyin

* fix: add missing prosody_features input to HuggingFace demo ONNX inference

The multilingual model requires prosody_features as a required input.
Add zero-filled prosody_features tensor (shape [1, num_phonemes, 3])
when the model declares this input, matching the existing behavior in
docker/webui/app.py and voice.py.

* feat: use piper_train phonemize pipeline in HuggingFace demo

Replace the standalone text_to_phonemes()/phonemes_to_ids() with
piper_train.infer_onnx.text_to_phoneme_ids_and_prosody() which
produces correct prosody_features (A1/A2/A3) for Japanese instead
of zero-filled tensors, improving Duration Predictor accuracy.

- huggingface-space/app.py: remove ~260 lines of duplicated
  phonemization code, use shared text_to_phoneme_ids_and_prosody()
  with proper prosody extraction for JA and zero-fill for other langs
- deploy-huggingface.yml: copy piper_train/phonemize/ module and
  infer_onnx.py to HF Space (inference-only, no torch dependency)
- requirements.txt: add g2p-en>=2.1.0, replace pyopenjtalk with
  pyopenjtalk-plus>=0.4 (Apache-2.0)

* fix: copy vits/utils.py and wavfile.py to HF Space deployment

infer_onnx.py imports from .vits.utils and .vits.wavfile which were
missing from the deployment, causing ModuleNotFoundError at startup.

* ci: add pre-deployment import validation and HF Space build test

- deploy-huggingface.yml: add "Validate Python imports" step that
  checks all piper_train modules are importable before deploying,
  aborting on any ImportError (prevents broken deploys)
- test-hf-space.yml: new CI workflow with 5 validation layers:
  Layer 1: core import check (no external deps)
  Layer 2: install HF Space requirements
  Layer 3: JA phonemization + prosody test
  Layer 4: EN phonemization test
  Layer 5: app.py full import test
  Triggered on push/PR when HF Space or phonemize files change

* fix: use heredoc for inline Python in deploy workflow to fix YAML syntax

The inline python3 -c "..." block contained colons that YAML parsed
as mapping keys, breaking workflow_dispatch. Switch to heredoc syntax
(python3 << 'PYEOF') which avoids YAML interpretation of the script.

* fix: move import validation after setup-python to have numpy available

* fix: add onnxruntime to import validation deps

* fix: download nltk averaged_perceptron_tagger_eng for English g2p

* fix: add prosody_features tensor to all WebAssembly demo HTML files

The multilingual model requires prosody_features as a required ONNX
input. Add zero-filled fallback when prosody data is not available
(non-JA languages), preventing 'input is missing' runtime errors.

* fix: add BOS/EOS/PAD intersperse to WebAssembly demo phonemesToIds

VITS model expects phoneme IDs in the format:
  [BOS, PAD, ph1, PAD, ph2, PAD, ..., EOS]
but the JS implementation was producing:
  [ph1, ph2, ...]
without BOS/EOS framing or PAD intersperse between phonemes.

This caused all languages to produce garbled audio on GitHub Pages
while HuggingFace (using Python text_to_phoneme_ids_and_prosody)
worked correctly.

* fix: CI failures — ruff lint/format, test-hf-space layer ordering, g2pk2 import

- huggingface-space/app.py: fix I001 (import sort) and add E402 noqa
  for intentional post-nltk-download imports
- pyproject.toml: add E402 to huggingface-space per-file-ignores
- test-hf-space.yml: move numpy/pyopenjtalk-dependent imports to
  Layer 2.5 (after pip install), keep Layer 1 pure-Python only
- app_imports.py: fix g2pk -> g2pk2 import probe to match actual
  dependency (Copilot review)
- http_server.py: ruff format fix

* fix: download nltk averaged_perceptron_tagger_eng in test-hf-space CI
@ayutaz
ayutaz merged commit 5f2a5a7 into dev Mar 18, 2026
57 checks passed
@ayutaz
ayutaz deleted the feat/bilingual-phonemizer branch March 18, 2026 08:40
@ayutaz ayutaz mentioned this pull request Mar 18, 2026
4 tasks
ayutaz added a commit that referenced this pull request Mar 18, 2026
- ONNX入力テンソルに lid (language_id) を追加
- config.json スキーマに num_languages, language_id_map を追加
- 7言語 (JA/EN/ZH/KO/ES/FR/PT) のG2P実装方針を追加
- Unicode言語検出とコードスイッチング設計を追加
- PUAマッピングを89エントリ (U+E000-E058) に拡張
- A1/A2/A3プロソディの言語別セマンティクスを文書化
- 推奨クレートに pinyin, hangul, cmudict-fast を追加
- ロードマップ Phase 3 を多言語G2P対応に更新
ayutaz added a commit that referenced this pull request Mar 19, 2026
- ONNX入力テンソルに lid (language_id) を追加
- config.json スキーマに num_languages, language_id_map を追加
- 7言語 (JA/EN/ZH/KO/ES/FR/PT) のG2P実装方針を追加
- Unicode言語検出とコードスイッチング設計を追加
- PUAマッピングを89エントリ (U+E000-E058) に拡張
- A1/A2/A3プロソディの言語別セマンティクスを文書化
- 推奨クレートに pinyin, hangul, cmudict-fast を追加
- ロードマップ Phase 3 を多言語G2P対応に更新
ayutaz added a commit that referenced this pull request Mar 19, 2026
* docs: Rust推論実装の調査・設計ガイドを追加

技術スタック選定、アーキテクチャ設計、日本語/英語音素化方針、
ロードマップ、リスク分析を含む包括的なドキュメント。

* docs: Rust推論ガイドを多言語対応 (PR #218) に更新

- ONNX入力テンソルに lid (language_id) を追加
- config.json スキーマに num_languages, language_id_map を追加
- 7言語 (JA/EN/ZH/KO/ES/FR/PT) のG2P実装方針を追加
- Unicode言語検出とコードスイッチング設計を追加
- PUAマッピングを89エントリ (U+E000-E058) に拡張
- A1/A2/A3プロソディの言語別セマンティクスを文書化
- 推奨クレートに pinyin, hangul, cmudict-fast を追加
- ロードマップ Phase 3 を多言語G2P対応に更新

* docs: ロードマップを調査結果と照合し精緻化

- Phase 1: phoneme_type分岐判定・--deviceオプションを追加
- Phase 2: 正規表現パターン・post_process_ids動作を明記
- Phase 3: 6言語G2Pの実装詳細・推定行数・OOVフォールバックを追加、工数を5-7週に修正
- Phase 4: phoneme timing出力・クロスフェードストリーミングを追加
- 工数サマリを実装規模に基づき修正 (合計19-26週)

* feat: Rust推論 Phase 1 MVP — ONNX推論エンジン + JSONL入力 + WAV出力 (全20テストパス)

Cargo workspace (piper-core / piper-cli) を構築し、
phoneme_ids 直接入力による ONNX 推論パイプラインを実装。

piper-core:
- error.rs: PiperError (9バリアント)
- config.rs: VoiceConfig パース (多言語 phoneme_type 対応)
- engine.rs: OnnxEngine (ort v2 rc12, sid/lid/prosody 条件付きテンソル)
- audio.rs: float32→int16 ピーク正規化 + WAV書き出し
- input.rs: JSONL パーサー (Python infer_onnx.py 互換)
- phonemize/token_map.rs: 87 PUA 固定マッピング (JA/ZH/KO/ES/FR/PT)
- phonemize/mod.rs: Phonemizer trait + PhonemizerRegistry

piper-cli:
- main.rs: clap CLI (--model, --config, --output-dir, --device等)

* chore: .gitignore に src/rust/target/ を追加

* feat: Rust推論 Phase 2 — 日本語音素化 + PiperVoice API (全259テストパス)

jpreprocess ベースの日本語 Phonemizer を実装し、テキストから直接
音声合成が可能な PiperVoice 高レベル API を追加。

新規モジュール:
- phonemize/japanese.rs: 栗原法 prosody マーク、A1/A2/A3 抽出、
  疑問詞マーカー (Issue #204)、N変異規則 (Issue #207)、PUA マッピング
- phonemize/custom_dict.rs: JSON v1.0/v2.0 カスタム辞書
- phonemize/phoneme_converter.rs: トークン→phoneme_id 変換
- voice.rs: PiperVoice (テキスト→音素化→推論→WAV)
- 統合テスト 7 ファイル (123テスト)

修正:
- token_map.rs: PUA マッピングを Python token_mapper.py と完全一致に修正
  (JA E00A-E015 順序、ZH E031-E045 compound finals)
- CLI: --text, --language, --custom-dict オプション追加

* feat: Rust推論 Phase 3 — 7言語 G2P + 多言語Phonemizer (全648テストパス)

6言語の G2P Phonemizer と多言語コードスイッチング基盤を実装し、
テキストから直接音声合成が可能な完全パイプラインを実現。

新規 Phonemizer (6言語):
- english.rs: CMU辞書 + ARPAbet→IPA + 形態論OOVフォールバック
- chinese.rs: pypinyin辞書 + ピンイン→IPA + 声調サンドヒ + 儿化
- korean.rs: Hangul算術分解 + jamo→IPA + 連音化(liaison)
- spanish.rs: ルールベース G2P (seseo, 異音, 音節化, ストレス)
- french.rs: ルールベース G2P (鼻母音, 無音末尾子音, 例外辞書)
- portuguese.rs: ルールベース G2P (鼻母音, coda-l, t/d口蓋化)

多言語基盤:
- multilingual.rs: UnicodeLanguageDetector + MultilingualPhonemizer
  + default_post_process_ids (BOS/EOS/インタースパースパディング)
- voice.rs: Bilingual/Multilingual phoneme type 対応
- CLI: --language バリデーション + 言語自動検出ログ

* docs: Rust推論ガイドを実装に合わせて全面更新

- PUAエントリ数を89→87に修正 (3箇所)
- ディレクトリ構成を実装に合致させ未実装モジュールを削除
- Phonemizer trait に detect_primary_language 追記
- PiperVoice API シグネチャを実際の引数に更新
- エラー enum を6→10バリアントに更新
- 推奨クレート表に状態列追加 (使用中/Phase 4)
- CLI オプション一覧を実装に合わせて更新
- Feature Flags / テストカバレッジ (648テスト) セクション新設
- Phase 1 に完了マーク追加

* feat: Rust推論 Phase 4 — ストリーミング・タイミング・GPU・WASM・PyO3 (全1,227テストパス)

Phase 4 の5サブフェーズを実装:

4a: ストリーミング再生
  - AudioSink trait + BufferSink + WavFileSink (streaming.rs)
  - RodioPlayer リアルタイム再生 (playback.rs, feature-gated)
  - センテンス分割 + クロスフェード (text_splitter.rs)

4b: 音素タイミング出力
  - PhonemeTimingInfo + duration→timestamp変換 (timing.rs)
  - JSON/TSV/SRT 出力フォーマット
  - engine.rs: duration テンソル抽出

4c: WASM 互換 API
  - WasmVoice: in-memory モデル読み込み (wasm.rs)
  - WAV bytes 生成 (ファイルシステム不要)

4d: GPU 推論
  - CUDA/CoreML/DirectML/TensorRT ExecutionProvider (gpu.rs)
  - デバイス列挙 + 自動選択 (device.rs)
  - engine.rs: GPU プロバイダ統合 + CPU フォールバック

4e: PyO3 Python バインディング
  - piper-python crate (Cargo.toml + pyproject.toml + lib.rs)
  - PiperVoice Python クラス + numpy 出力
  - GIL 解放による並行推論

追加機能:
  - モデルダウンロード (model_download.rs, feature-gated)
  - 音声フォーマット変換 + リサンプリング (audio_format.rs)
  - バッチ合成 (batch.rs)
  - CLI: --stream, --timing, --list-devices, --list-models, --batch

新規ソースファイル: 10 (6,570行)
新規テストファイル: 8 (3,946行)
新規 PyO3 クレート: 3ファイル (444行)
変更ファイル: 7
テスト合計: 1,227 パス (Phase 1-3: 648 + Phase 4: 579)

* chore: .gitignore に piper-python ビルド成果物を追加

* docs: Rust推論ガイドを Phase 4 実装に合わせて更新

- ディレクトリ構成: Phase 4 の10モジュール + piper-python クレート追加
- Feature flags: 9 features (playback, download, resample, cuda 等)
- エラー列挙: 22 バリアント (Phase 4 で8追加)
- 推奨クレート: rodio 0.19, reqwest 0.12, rubato 0.16, pyo3, numpy
- AudioSink trait (旧 AudioOutput → AudioSink に修正)
- SynthesisResult: durations フィールド追加
- Phase 4 ロードマップ ✅ マーク
- CLI: --stream, --timing, --list-devices 等の新フラグ
- テストカバレッジ: 648 → 1,227 テスト

* fix: Phase 4 コードレビュー指摘の critical/major 修正 (全1,232テストパス)

レビューで指摘された7件の critical/major 問題を修正:

1. AudioSink trait 重複解消: playback.rs のローカル定義を削除し
   streaming.rs の正規定義を use で参照

2. CLI --model を Optional に変更: --list-devices, --list-models が
   モデル指定なしで実行可能に

3. --stream/--batch フラグを実装に接続:
   - --batch FILE: テキストファイルから一括合成
   - --stream --text: センテンス単位の逐次合成
   - --text と --batch の排他バリデーション追加

4. GPU 無言フォールバック修正: 明示的デバイス指定("cuda:0"等)の
   パース失敗時にエラーを返すように変更 (autoは従来通りフォールバック)

5. WavFileSink に Drop 実装: finalize() 忘れ時も WAV ヘッダ更新

6. Duration テンソル抽出にログ追加: 失敗時 warn、成功時 debug

7. DeviceType/DeviceKind ブリッジ: From<DeviceSelection> for DeviceType
   変換を追加、両モジュールの役割を doc comment で明確化

* test: TDD監査に基づくテスト135件追加 (全1,367テストパス)

TDD監査で指摘された全モジュールのカバレッジギャップを修正:

エラー変数テスト (+24):
- 全22 PiperError 変数の構築・Display・パターンマッチ
- From<io::Error> と From<serde_json::Error> の変換テスト

detect_primary_language テスト (+16):
- 7言語 (JA/EN/ZH/KO/ES/FR/PT) で未テストだった trait メソッド
- KO: get_phoneme_id_map, post_process_ids も補完

wasm エッジケース (+10):
- WasmVoice::load_from_bytes エラーパス (無効モデル/config)
- i16→f32 境界値、WAV大容量データ、parse_config追加フィールド

audio_format エッジケース (+12):
- NaN/Inf→i16 変換、zero fade、single sample resample
- stereo_to_mono 奇数長、concat crossfade > chunk長

timing エッジケース (+8):
- TSV タブ文字、SRT 改行、NaN/Inf duration
- Unicode IPA、超小 duration 精度、直接構築

batch/text_splitter (+11):
- JSONL missing text field、invalid speaker_id type
- nested quotes、CJK句読点のみ、CRLF改行

streaming (+8):
- WavFileSink Drop 自動finalize、negative sample crossfade
- i16::MAX/MIN crossfade overflow、大容量 BufferSink

playback (+8):
- double finalize idempotent、sample rate mismatch
- 大容量サンプル、複数 sample rate テスト

model_download (+9):
- zero total bytes progress、empty fields、Unicode名
- cache empty model name、extra JSON fields

gpu/device/engine (+12):
- auto_detect、whitespace parse、large device_id
- SynthesisResult durations、RTF edge case
- ModelCapabilities all true/false

playback モジュールの feature gate を解除 (DummyPlayer/CollectorSink
は rodio 不要のため常時コンパイル可能に)

* perf: パフォーマンスチューニング — WAV batch write, テンソルalloc削減, 辞書キャッシュ (全1,371テストパス)

HIGH Impact 修正:
- WAV batch write: per-sample syscall → 一括write_all (audio.rs, streaming.rs, wasm.rs)
- engine.rs: audio_f32中間Vec除去 (slice直接渡し), with_capacity明示化
- CMU辞書/pypinyin辞書: OnceLock化で初回のみロード (english.rs, chinese.rs)
- text_splitter: Vec<char> → char_indices() に変更、byte_pos増分追跡
- PyO3: take_audio_int16 (clone除去) + synthesize_batch API追加
- model_download: BufWriter 256KB + builtin_registry OnceLock化

MEDIUM Impact 修正:
- resample_linear: per-sample除算 → 増分加算 (step accumulator)
- fade_in/fade_out: per-sample除算 → inv_len accumulator
- concat_audio crossfade: pre-computed slope
- multilingual: bos_eos_tokens/eos_tokens HashSet OnceLock化
- multilingual segment_text: clone → std::mem::take
- voice.rs: prosody 4段Vec変換 → build_prosody_direct 1段に統合
- gpu.rs: to_lowercase() → eq_ignore_ascii_case() (allocation除去)
- device.rs: enumerate_devices/is_device_available OnceLock キャッシュ
- input.rs: JsonlUtterance::to_request move semantics (clone除去)

推定改善:
- WAV I/O: 100-500x高速化 (syscall削減)
- 辞書ロード: 50-100ms + 15MB削減 (2回目以降)
- メモリピーク: 88-176KB/call削減 (中間audio Vec除去)
- resample/fade: 2-3x高速化 (除算→加算)

* fix: 日本語テストを辞書不在時 graceful skip に修正 (全1,384テストパス)

test_japanese_phonemize.rs の create_phonemizer() を require_phonemizer!()
マクロに変更。naist-jdic feature 無効時は辞書検索に失敗してもパニック
せず早期リターン (SKIP) する。

- naist-jdic 有効: バンドル辞書で実テスト実行 (13テストパス)
- naist-jdic 無効: 辞書不在でも全テスト ok (graceful skip)
- どちらの場合も 1,384 テスト全パス、0 失敗

* ci: Rust CI/CD ワークフロー追加

rust-tests.yml (スタンドアロン):
- cargo check --workspace --all-features
- cargo test (default + naist-jdic) on ubuntu/macos/windows
- cargo fmt --check
- cargo clippy -D warnings
- maturin build (Python バインディング)

ci.yml (メイン CI に統合):
- rust-tests ジョブを追加 (ubuntu + macos)
- fmt, clippy, test, naist-jdic test, piper-python check

* fix: Copilot レビュー指摘10件を全修正 (全1,394テストパス)

1. custom_dict.rs: pattern_cache を Mutex<HashMap> に変更し実際にキャッシュ
2. input.rs: 空行スキップの再帰をループに変更 (スタックオーバーフロー防止)
3. playback.rs: doc example の AudioSink import パスを streaming に修正
4. device.rs: cpu:N (N!=0), coreml:N (N!=0), 負ID を拒否
5. engine.rs: doc comment から未対応の "gpu" を削除、正しいデバイス名を記載
6. streaming.rs: WavFileSink sample_rate 不一致時にエラー返却
7. streaming.rs: WAV u32 オーバーフロー検出 (4GB超でエラー)
8. streaming.rs: crossfade alpha を i/(overlap-1) に修正 (1.0到達)
9. model_download.rs: reqwest に connect/read timeout 設定 (30s/600s)
10. model_download.rs: url_filename がクエリ文字列・フラグメントを除去

* fix: CI失敗を修正 (rustfmt, maturin python-source, ort-sys glibc)

- cargo fmt --all で全ソースをフォーマット統一
- piper-python pyproject.toml から python-source 設定を削除 (ディレクトリ不在エラー)
- CI の ubuntu-22.04 を ubuntu-24.04 に変更 (ort-sys が glibc 2.38+ を要求)

* fix: CI失敗を修正 (alsa-sys, PyO3 Python 3.14, unused imports)

- rust-tests.yml: libasound2-dev インストール追加 (rodio/alsa-sys ビルド用)
- rust-tests.yml/ci.yml: setup-python 3.12 追加 (PyO3 0.22 は Python 3.14 非対応)
- audio_format.rs: PiperError import を #[cfg(feature = "resample")] で条件付きに
- voice.rs: build_prosody_direct を #[cfg(test)] で条件付きに
- model_download.rs: url_filename を #[cfg(any(feature = "download", test))] で条件付きに

* fix: AudioSink から不要な Send 境界を削除 + CI から libasound2-dev 依存を除去

AudioSink トレイトの Send 境界は、コードベース全体でスレッド間転送が
一切行われないため不要。この境界が Linux CI で cpal/rodio の
!Send (ALSA バックエンド) と衝突しビルド失敗を引き起こしていた。

10エージェントによる調査で以下を確認:
- AudioSink は thread::spawn / tokio::spawn / async で一切使用されない
- Box<dyn AudioSink> のインスタンスは存在しない
- 全使用箇所は &mut dyn AudioSink のローカル参照のみ

* fix: rust-tests CIにlibasound2-devを復元

--all-features ビルドで playback feature → rodio → alsa-sys が
必要なため、check/clippy ジョブに libasound2-dev インストールを追加。

* fix: clippy警告35件を修正

- multilingual.rs: repeat().take() → repeat_n()
- phoneme_converter.rs: too_many_arguments を allow
- portuguese.rs: collapsible if, bool simplification, &mut Vec→&mut [char], enumerate
- spanish.rs: identical if/else blocks, needless range loop
- device.rs: FromStr trait を正式実装
- wasm.rs: needless late init を直接初期化に
- playback.rs: needless return を除去

* fix: clippy 15件 + rustfmt 4件を修正

- portuguese.rs: rustfmt brace位置修正 + int_plus_one
- engine.rs: needless_late_init 3件 + 不要な参照除去
- chinese.rs: identical if blocks統合 + complex type alias
- french.rs: collapsible if, bool simplification, needless range loop 2件
- japanese.rs: &mut Vec → &mut [String]
- korean.rs: RangeInclusive::contains 使用
- multilingual.rs: 不要な明示的ライフタイム除去

* fix: piper-python clippy 7件修正

- SendPtr::as_mut: mut_from_ref を allow (unsafe設計上必要)
- synthesize/synthesize_batch: too_many_arguments を allow (PyO3 API)
- to_pyresult削除: useless conversion警告の原因 → 直接 .map_err() に置換
- save_wav: 同上

* fix: piper-python useless conversion根本修正 + rustfmt

clippy useless_conversion の根本原因: .map_err().map() チェーンが
PyErr→PyErr の無意味な変換と判定されていた。
Ok(result.map_err(piper_err_to_pyerr)?.into()) パターンに統一し、
cargo fmt で正確なフォーマットを適用。

* fix: piper-python clippy useless_conversion を impl ブロックで allow

PyO3 の #[pymethods] が自動で From<PiperError> -> PyErr 変換を挿入するため、
明示的な map_err(piper_err_to_pyerr) が clippy に「同型への無意味な変換」と
判定される。カスタム例外マッピング (ValueError/IOError/RuntimeError) を
維持するため、impl ブロック全体に allow を適用。

* fix: Cargo.toml [lints.clippy] で useless_conversion を allow

#[pymethods] マクロ展開後のコードに #[allow] attribute が伝播しない問題。
Cargo.toml の [lints.clippy] セクションでクレートレベルで allow することで
確実に clippy useless_conversion を抑制。
ayutaz added a commit that referenced this pull request May 2, 2026
二次監査で「低優先度・別 PR 推奨」と棚上げした 5 項目に対応。レガシー API
(BILINGUAL/espeak) の意図と廃止予定を明示し、サンプル/パッケージのバージョン
管理状況をユーザーが誤解しないようドキュメント化する目的。

- Go (src/go/piperplus/config.go): PhonemeTypeEspeak / PhonemeTypeBilingual に
  Deprecated コメント追加 (CONTRIBUTING.md "espeak-ng Policy" / PR #218 への参照付き)
- Python (src/python_run/piper/voice.py): PhonemeType.BILINGUAL 分岐に
  Deprecation 注記 + SV/KO が学習済みモデル未対応の理由をコメント化
- C# (src/csharp/PiperPlus.Core/Config/PiperConfig.cs): Espeak フィールドに
  XML doc 追加 (legacy config.json 互換のみ、新規モデルは null とする旨)
- examples/{c-api,dart,godot}/README.md: CI で実行検証されているか否かを
  各 README 冒頭に明示 (c-api はビルドのみ検証、dart/godot は未検証)
- CONTRIBUTING.md: "Package Versioning Policy" セクション新規追加
  (8 パッケージそれぞれの独立バージョン管理ポリシーとタグ命名規則を表形式で明文化)

実装ロジックには変更なし (コメント・docstring・README のみ)。
ayutaz added a commit that referenced this pull request May 3, 2026
* docs: 監査結果に基づくドキュメント全面同期

v1.11.0 以降の実装変更 (#321 OpenAI互換API, #331 Voice Cloning/SSML, #337 短文品質, #342 HTS voice除去, #349/#367 phoneme timing/streaming, #361 FastAPI移行, #366 arm64 Docker) が
CHANGELOG・多言語 README・docstring に未反映だった部分を一括同期。新規に C# CLI/Core README を追加。

- ルート CHANGELOG: Unreleased に #360-#366 (FastAPI移行/arm64 Docker/短文崩壊修正/Wyoming HA等) を追記
- 多言語 README 9 ファイル (DE/ES/FR/HI/KO/PT/RU/SV/ZH): Voice Cloning/SSML/Phoneme Timing/Strategy A/B/C/FastAPI を反映
- C# CLI/Core README 新規作成 (src/csharp/PiperPlus.Cli/, PiperPlus.Core/)
- WASM CHANGELOG: [0.3.1] セクション追加 + [0.4.0] 日付修正 (0.3.0 と同日問題解消)
- docstring 補強: http_server.py FastAPI エンドポイント / inference.py OpenAI 互換 API
- HTS voice 残存言及を削除 (windows-setup.md)
- --language-balanced-sampling help を 6 言語対応に更新 (バイリンガル時代の遺物修正)
- Go README: Docker multi-arch (amd64/arm64) + serve サブコマンドのセクション追加
- pretrained-models.md: つくよみちゃん 6lang-v2 / 6lang ベースモデルの詳細追記
- huggingface-space/docker/README: 言語表記/Go arm64 表記の整合修正

* docs: 二次監査で発見した取り残しを追加修正

前回コミット (f212aba) では多言語 README 9 ファイルを更新したが、
ベースとなる README.md (日本語) と README_EN.md には Voice Cloning / SSML / 短文品質改善の bullets を入れ忘れていた。
また Rust piper-core/src/lib.rs のクレートドキュメントが「7 言語 (KO 含むが SV を欠く)」のまま、
8 言語 G2P + 6 言語学習済みモデルという最新の状態を反映していなかった。

- README.md / README_EN.md のインターフェースセクション末尾に Voice Cloning / SSML / 短文品質改善 (Strategy A/B/C) の 3 bullets を追加 (多言語版と同等内容)
- src/rust/piper-core/src/lib.rs:4 のクレートドキュメントを「8 言語 G2P (JA/EN/ZH/KO/ES/FR/PT/SV)、学習済みモデルは 6 言語」に修正

二次監査で誤指摘と判明した項目はスキップ:
- WASM g2p test の "all 7 non-JA languages" は JA を除いた 7 言語の意味で正しい (ALL_NON_JA = en/zh/ko/es/fr/pt/sv)
- C# CLI README の --debug/--quiet/--version はすでに L66 に記載済み

* docs: 低優先度の取り残し項目をすべて対応

二次監査で「低優先度・別 PR 推奨」と棚上げした 5 項目に対応。レガシー API
(BILINGUAL/espeak) の意図と廃止予定を明示し、サンプル/パッケージのバージョン
管理状況をユーザーが誤解しないようドキュメント化する目的。

- Go (src/go/piperplus/config.go): PhonemeTypeEspeak / PhonemeTypeBilingual に
  Deprecated コメント追加 (CONTRIBUTING.md "espeak-ng Policy" / PR #218 への参照付き)
- Python (src/python_run/piper/voice.py): PhonemeType.BILINGUAL 分岐に
  Deprecation 注記 + SV/KO が学習済みモデル未対応の理由をコメント化
- C# (src/csharp/PiperPlus.Core/Config/PiperConfig.cs): Espeak フィールドに
  XML doc 追加 (legacy config.json 互換のみ、新規モデルは null とする旨)
- examples/{c-api,dart,godot}/README.md: CI で実行検証されているか否かを
  各 README 冒頭に明示 (c-api はビルドのみ検証、dart/godot は未検証)
- CONTRIBUTING.md: "Package Versioning Policy" セクション新規追加
  (8 パッケージそれぞれの独立バージョン管理ポリシーとタグ命名規則を表形式で明文化)

実装ロジックには変更なし (コメント・docstring・README のみ)。

* docs: Copilot レビュー指摘 16 件 + 追加発見をすべて対応

Copilot pull-request reviewer が PR #368 に付けた 16 件のレビューコメントと、
それを契機にした再監査で見つかった整合性問題を一括解消。リリース直前の
一貫性確認に過ぎないので、いずれもドキュメント・コメントレベルの調整のみ。

Copilot 指摘 (16件):
- 多言語 README 11 ファイル (README.md/EN/DE/ES/FR/HI/KO/PT/RU/SV/ZH): bullets が
  「7 ランタイム」と書きながら列挙は 6 つだったのを「6 ランタイム」に統一。
  日本語版/英語版は libpiper_plus を C++ にまとめる旨を補足
- docker/python-inference/inference.py: モジュール docstring が定義されていない
  `POST /api/phoneme-timing` を載せ、`/synthesize` を POST と書いていたのを修正
  (該当エンドポイントは src/python_run/piper/http_server.py 側であることも明記)
- src/python/piper_train/__main__.py:255-260: `--language-balanced-sampling` が
  `num_speakers > 1` も必要 (single-speaker は SpeakerBalancedBatchSampler を
  バイパス) という条件を help に追記
- docker/README.md:19: Go Dockerfile 行の base image を `golang:1.22` から
  実際の `golang:1.26` に修正
- src/go/README.md:380-: `docker buildx build` の例に `--load` (single-arch) と
  `--push` (multi-arch) の両パターンを示し、ローカル実行不可だった例を解消
- src/go/README.md:298 / 406: 見出し「## HTTP API / HTTPエンドポイント」を
  「## HTTP API」に変更してアンカーを `#http-api` に統一、リンク
  `[HTTP API](#http-api--http-api)` の broken anchor を修正
- src/csharp/PiperPlus.Core/README.md:34: `new DotNetG2PEngine()` は
  PiperPlus.Cli の `internal sealed class` で外部から呼べないため、
  IJapaneseG2PEngine を持ち込む必要がある旨を明記したサンプルに置換
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.

Webデモに中国語のサポートの追加 マルチ言語モデル作成のための学習・推論コードの対応

2 participants