概要
Rust ランタイムは 10 文字以下のテキストで SSML マークアップを音声として読み上げる。synthesize_with_params が Strategy C のラッパ文字列を SSML としてパースせず、そのまま phonemizer に渡しているため。
根拠(コード)
src/rust/piper-core/src/voice.rs:421-424:
// Strategy C: 短テキストを SSML <break> でラップ
let effective_text = crate::short_text::wrap_short_text_ssml(text);
let text_ref = effective_text.as_str();
// 1. Phonemize: テキストをトークン列 + プロソディ情報に変換
let (tokens, prosody) = self.phonemizer.phonemize_with_prosody(text_ref)?;
wrap_short_text_ssml(src/rust/piper-core/src/short_text.rs)は
<speak><break time="300ms"/>{text}<break time="300ms"/></speak>
を返すが、phonemize_with_prosody は SSML を解釈しない(piper-plus-g2p::ssml は存在するが、この経路からは呼ばれない)。
実測
--timing json の音素列(issue #656 の対応で音素名が見えるようになって発覚):
$ piper-plus-cli --model multilingual-test-medium.onnx --text "Sol" --timing json
^ _ s _ p _ ˈ _ i _ ː _ k _ _ b _ ɹ _ ˈ _ e _ ɪ _ k _ _ t _ ˈ _ a _ ɪ _ m _ ...
└─ "speak" ─┘ └─── "break" ───┘ └── "time" ──┘
s p ˈ i ː k = speak、b ɹ ˈ e ɪ k = break、t ˈ a ɪ m = time。つまり <speak> <break time= というマークアップが英語として音素化されている。
phoneme ID 数の比較(phonemize_to_ids(text) vs 実際に推論へ渡った列):
| テキスト |
文字数 |
text 由来 ID |
実際の ID |
差 |
Sol |
3 |
11 |
111 |
+100 |
Hola |
4 |
15 |
115 |
+100 |
Hola mundo |
10 |
31 |
131 |
+100 |
Hola mundo bonito hoy |
21 |
— |
一致 |
0 |
差 +100 がラッパ文字列の音素数にあたる。閾値 SHORT_TEXT_CHARS = 10 を超えると発生しない。
長文では正しく h ˈ o ʊ l ə("Hola")から始まり "speak" は現れない。
影響
他ランタイムとの比較
C++ は Strategy C を CLI の --ssml フラグ経由でのみ適用し(src/cpp/main.cpp)、短文の自動ラップは行っていない。Python / Go / C# の該当箇所は未調査。
修正方針(要判断)
- ラップ後に SSML としてパースする —
piper_plus_g2p::ssml で segment 分解し、<break> を無音として扱う(C++ の synthesizeSsmlToBuffer と同じ形)
- ラップをやめ、無音を直接挿入する — SSML を経由せず、前後に
SILENCE_PAD_MS 相当の無音サンプルを付ける。Strategy C の目的(短文の音質改善)はこれで達成できる
- Strategy C 自体を Rust では無効化する
2 が最も単純で、SSML パーサへの依存も増えない。
受け入れ条件
関連
#656(Rust / Go の timing が音素名でなくプレースホルダ)の対応中に発見。プレースホルダを実音素名に変えた結果、初めて観測可能になった。
概要
Rust ランタイムは 10 文字以下のテキストで SSML マークアップを音声として読み上げる。
synthesize_with_paramsが Strategy C のラッパ文字列を SSML としてパースせず、そのまま phonemizer に渡しているため。根拠(コード)
src/rust/piper-core/src/voice.rs:421-424:wrap_short_text_ssml(src/rust/piper-core/src/short_text.rs)はを返すが、
phonemize_with_prosodyは SSML を解釈しない(piper-plus-g2p::ssmlは存在するが、この経路からは呼ばれない)。実測
--timing jsonの音素列(issue #656 の対応で音素名が見えるようになって発覚):s p ˈ i ː k= speak、b ɹ ˈ e ɪ k= break、t ˈ a ɪ m= time。つまり<speak><breaktime=というマークアップが英語として音素化されている。phoneme ID 数の比較(
phonemize_to_ids(text)vs 実際に推論へ渡った列):SolHolaHola mundoHola mundo bonito hoy差 +100 がラッパ文字列の音素数にあたる。閾値
SHORT_TEXT_CHARS = 10を超えると発生しない。長文では正しく
h ˈ o ʊ l ə("Hola")から始まり "speak" は現れない。影響
--timing出力も当然その音素列になるph_0,ph_1, ... という連番プレースホルダだったため、音素列を見ても何も分からなかった他ランタイムとの比較
C++ は Strategy C を CLI の
--ssmlフラグ経由でのみ適用し(src/cpp/main.cpp)、短文の自動ラップは行っていない。Python / Go / C# の該当箇所は未調査。修正方針(要判断)
piper_plus_g2p::ssmlで segment 分解し、<break>を無音として扱う(C++ のsynthesizeSsmlToBufferと同じ形)SILENCE_PAD_MS相当の無音サンプルを付ける。Strategy C の目的(短文の音質改善)はこれで達成できる2 が最も単純で、SSML パーサへの依存も増えない。
受け入れ条件
関連
#656(Rust / Go の timing が音素名でなくプレースホルダ)の対応中に発見。プレースホルダを実音素名に変えた結果、初めて観測可能になった。