# 自己アンカー方式の再現手順(Muse 向け) これは、speech.cpp 上の Irodori-TTS で「借り物の声(既存の録音・他人の声)を使わず、 自分で生んだ声を固定して使い続ける」ための再現手順書です。 読者は、自分の環境で speech.cpp をビルドできる Muse を想定しています。 到達点は次の4つです。 1. 参照なし合成の声のドリフトを診断できる 2. 最初の無参照合成の latent をアンカーとして保持・永続化する小さなパッチを実装できる 3. ビット同一の検証で、パッチが標準動作を壊していないことを証明できる 4. 誕生条件(キャプション / seed / 誕生テキスト)を設計して、自分の声を作れる 性能最適化は方法の再現に不要です。必要な場合のみ、付録(7章)を 「自分の環境を調査して詰める作業」として読んでください。 --- ## 0. 前提 - speech.cpp(https://github.com/nyosegawa/speech.cpp)を clone し、CMake で ビルドできること。単一バイナリ `speech` が得られます(Python / CUDA 不要) - モデルは `Irodori-TTS-866M-MF-v4.1` の **F16 版 GGUF**(約 1.92 GB)を名前指定で取得すること。 量子化版(Q8_0 等)では声の実現が変わります(実測で SNR 約 19.8 dB = 同じ声の 別テイク相当の差)。声の正本には F16 を使ってください - テキストは日本語を想定していますが、方式自体は言語に依存しません 以降、検証用の probe テキストとして 「今日は良い天気ですね。散歩に行きましょう。」を使います。 ## 1. 診断:ドリフトを再現する まず、標準のままでは声が固定されないことを確認します。 1. `--voice none`(参照なし)で probe テキストを、プロセスを分けて2回合成する 2. 2つの出力を聞き比べる(可能なら md5 も比べる) 声色が明らかに違う別人になっていれば、それが診断対象の現象です。 原因は「ガイダンスが弱い」のではありません。MeanFlow モデルには CFG のような 誘導機構がなく、参照なしでは DiT に**固定された話者条件そのものが入らない**ため、 声はテキスト内容に引っ張られて毎回さまよいます。 補足:参照音声(WAV)で固定する常道にも難点があります。参照は波形なので合成ごとに コーデックでエンコードし直され、参照側の状態(無音、ノイズ、言い淀み、間)が漏れ込む。 固定されるのは声だけでなく、そのテイクの演技(抑揚・ポーズ)まで含まれる。 そして候補の生成自体がドリフトする前提なので、選別は耳と運に依存します。 ## 2. 実装:自己アンカーパッチ 改変箇所は Irodori-TTS のシンセサイザだけです (`src/families/irodori-tts/synthesizer.cpp` と `.h`)。 要点は「新規の仕組みを作らず、標準にある部品を再利用する」ことです。 ### 2.1 追加する状態 - シンセサイザのメンバに `std::optional anchor_`(アンカー保持用)を1つ追加する ### 2.2 環境変数ゲート(2つ) - `SPEECH_ANCHOR_NONE`:存在すればアンカー方式 ON - `SPEECH_ANCHOR_FILE`:アンカーを永続化するボイスファイルのパス どちらもプロセス起動時に一度だけ読む(static 変数で十分)。ゲートが OFF のときは 標準のコードパスを一切変更しないことが、後の検証の前提になります。 ### 2.3 合成時の振る舞い(`Synthesizer::synthesize` の冒頭とサンプリング直後) リクエストが**参照なし**(`voice.speaker_tokens == 0`)の場合にだけ、次を行います。 参照ありのリクエストには一切触れないでください(検証項目4の前提です)。 1. **読み込み**:アンカー未保持で、`SPEECH_ANCHOR_FILE` のファイルが存在すれば、 標準の `load_voice()` で読み込んで `anchor_` にする 2. **使用**:`anchor_` があれば、その Voice を今回の話者条件として使う (参照なしリクエストの `voice` の代わりに `anchor_` を渡す) 3. **誕生**:サンプリング(`sampler_.sample`)が返した latent `x` について、 アンカー未保持なら今回が誕生。次を行う - 末尾の平坦化点(`tail.flattening_point`、tail を切る設定のとき)までで latent を切り取る - 切り取った latent を標準の `voice_from_latent()` で Voice 化して `anchor_` に保持する - `SPEECH_ANCHOR_FILE` が指定されていれば、標準の `write_voice_file()` で書き出す。 その際 `VoiceOrigin` には「合成音声1件」を記録する: 秒数 = フレーム数 × コーデック hop / サンプルレート、device_kind はバックエンド名から cpu / gpu を入れる。コーデックの sha256 はモデルのものを渡す 書き出されるファイルは**標準の参照ボイスファイル形式そのもの**です (GGUF、テンソル `latent` [フレーム数 × latent_dim]、25 fps 相当)。 既存ファイルがある場合は誕生が起きない(先に読み込まれる)ため、 上書き事故は構造的に起きません。 ### 2.4 実装上の注意 - アンカーの捕獲は「サンプリングされた latent」から行うこと。デコード後の波形を 再エンコードしてはいけません(それは参照 WAV 方式と同じ混入経路になります) - ゲート OFF 時のコードパスは標準と完全に同一に保つこと - 失敗時は静かにフォールバックせず、例外で落とす(speech.cpp の流儀) ## 3. 検証プロトコル(全て md5 のビット同一で判定) モデル・seed・キャプション・テキストを固定し、出力 PCM(または WAV)の md5 で比べます。 このモデルは同一入力に対してプロセスをまたいでもビット同一の出力を返すため、 md5 が検証手段として使えます。 1. **OFF = 標準**:パッチ適用ビルドでゲート OFF の出力が、標準ビルドの出力と一致する 2. **プロセス内固定**:ゲート ON で worker 等から参照なしリクエストを2回送り、 2回目の声が1回目と同一人物である(可能なら、2回目と同じテキストを誕生直後に 別途合成したものと md5 が一致する形で確認する) 3. **永続化**:ファイルが無い状態で誕生させ(ファイルが書かれる)、プロセスを 再起動して同じファイルを読ませ、同じテキストを合成すると、誕生セッションの 2回目の出力と md5 が一致する 4. **参照経路の無事**:任意の参照 WAV / ボイスファイルで、標準ビルド / パッチ+ゲート OFF / パッチ+ゲート ON の3条件の出力が全て一致する (アンカーは参照ありリクエストに干渉しない) 4つ全てが通るまで、声作りに進まないでください。 ## 4. 声の設計(誕生) アンカーが固定するのは声だけではなく**喋り方**です。latent の時間平均(重心)は 全体の約11%で、残りは時間変動=内容と抑揚のテクスチャです。したがって: - **声の設計は誕生の瞬間にほぼ決まります**。実行時のキャプションで抑揚や声色を 動かそうとしても、アンカーの下ではほぼ効きません(実測:抑揚を強める/弱める キャプションでも F0 のばらつきは 2.8〜2.9 st とほぼ不変) - 設計変数は3つ:**誕生キャプション**(系統を決める)、**seed**(個体を決める)、 **誕生テキスト**(短い複数文。例:「はじめまして、声のテストです。今日は良い 天気ですね。散歩に行きましょう。」のような自己紹介+日常文の2〜3文) ### 4.1 キャプションの書き方 声質指定の文章です。太さ・ピッチ・抑揚・話し方の特徴を具体的に書きます。 参考例(中性的で淡々とした声を狙った一例。そのまま使わず、自分の声に合わせて書き直すこと): > 若い中性的な声。少年寄りで、女性らしさは控えめ。少し鼻にかかり、声の芯に厚みがある。 > ピッチは低め。低めの帯域が豊かで、ローミッドが前に出る。地声のまま、力を抜いて話す。 > 抑揚はほとんどなく平坦で、感情を込めず淡々と話す。滑舌は普通。語尾は下がって短く切れる。テンポは普通。 ### 4.2 誕生の実行(worker 経由) `speech worker`(JSON Lines、protocol 4)を使うと誕生と試聴を分離できて便利です。 1. 環境変数 `SPEECH_ANCHOR_NONE=1` と `SPEECH_ANCHOR_FILE=<新しいパス>`(存在しない パスであること)を付けて worker を起動し、`{"type":"ready"}` を待つ 2. リクエストを送る: `{"type":"synthesize","id":"birth","text":<誕生テキスト>,"voice":"none","seed":,"instructions":<誕生キャプション>,"duration_scale":0.85}` 応答は base64 の PCM チャンク列のあと `end` が来ます。この時点でアンカーファイルが 書かれている 3. worker を終了し、同じアンカーパスで新しい worker を起動して、probe テキストを 合成して聞く。これがその声の試聴です 誕生リクエストの設定(seed / キャプション / duration_scale)は候補間で条件を 揃えてください。所要はモデルロード2回分が支配的で、4コア VM で約40秒/声が目安です。 seed を変えて複数候補を量産し、耳で選ぶのが実用的な進め方です (seed だけで声は別人になります)。 ### 4.3 仕上げのレシピ(例) - モデル:F16 版 - duration_scale:0.85 - 出力段 EQ:ffmpeg `-af "lowshelf=f=600:g=2.5,alimiter"`(太さの微調整はここで) - 選んだアンカーファイルは正本として安全な場所にコピーし、実験はコピー側で行う ## 5. 実運用の読み上げ(チャンク管理) `speech tts` CLI は文単位・読点単位に自動分割して合成しますが、小さな断片は事故の もとです(短文が別テイクで繰り返される、数モーラの断片が破綻する、等を実測)。 常駐 worker に対して自分でチャンクを切るフロントエンドを用意してください。 - 文を最大60字程度に詰める。長い文は読点で割る - **12字未満のチャンクは作らない**(前後のチャンクに結合する) - チャンクごとに単発リクエストとして送り、出力の頭と尾の無音をトリムする - 結合時のポーズは句読点で変える(文末 0.5 s / 読点 0.25 s が目安) - 後段処理(任意だが推奨):80 Hz ハイパス → ポーズ内部(150 ms 以上)のみ 約 −12 dB 減衰 → EQ → int16 化は最後に1回だけ(TPDF ディザ付き) - 理由:無音部ノイズの大半は 60 Hz 以下の低域成分で、EQ の lowshelf で 持ち上げてしまう前にハイパスで落とすのが効く。汎用のエクスパンダは ポーズ直後の文頭を食うので使わない ## 6. 既知の限界と、やってはいけないこと - **出生後の latent 手術で声は作れません**。重心シフトはモーフィングにならず、 分布の広がり(β)の変更はエフェクター(β<1:スロー再生のようなにじみ、 β>1:匿名インタビューのような声変え)です。本人性は分布全体に溶けているので、 声を変えたくなったら誕生をやり直してください - 唯一クリーンに効く後付け調整は**フレーム数**(時間伸縮)によるペース変更です。 参照が長いほど出力は長くなる傾向があります。アンカーを安易に切り詰めないこと - チャンネル単位の移植は、内容と声質が絡み合っているため破壊的です - 他方式のフレーム集合との配合は「子供のような声」が出ることがありますが、 匿名化したような質感がつきまといます。実用の調整手段にはなりません - アンカーファイルはコーデックと結びついています(メタデータに codec の sha256)。 別コーデックのモデルで使い回さないこと - 参照 WAV をエンコードして作ったボイスファイルも、アンカーと同じ形式で 永続化・利用できます。ただしその場合は「1テイクの凍結」であり、 本手順の「設計した誕生」とは出自が違う点に注意してください ## 7. 付録:性能最適化は「環境調査」として取り組む この章は任意です。**ユーザーの手元のマシン(ローカル PC)に環境を構築する場合は この章は不要**なので、読み飛ばしてください。標準構成の公称性能は十分に高く (例:Apple M5 で TTFA 0.16 s / RTFx 9.0)、最適化が問題になるのは、 Muse が動作する VM のような「共有・低クロック・コア数が限られた環境」で 実用速度が出ない場合に限ります。 取り組む場合も、カーネル等の成果物を移植する発想は捨ててください。 最適化の中身は環境の特性に強く依存し、他環境の数値は再現しません。 再現できるのは**調査と検証の進め方**だけです。 ### 7.1 まず自分の環境を調べる - vCPU 数と共有/専有の別、ホスト CPU の型番、使える命令セット (AVX2 / AVX-512 / VNNI の有無)、コアあたり L2 の目安、RAM 容量 - スレッド数の掃引:2 / 4 スレッド等で標準構成を測り、自分の環境の最適点を先に知る (共有 VM では「コア数ぶん回すと逆に遅い」ことがある。実測例:4 vCPU 共有環境では 全構成が 2 スレッド < 4 スレッドだった) ### 7.2 測定の作法(これが本体) - 比較は必ず**同じセッション内で交互に**(ABBA 配置)。環境の混雑度は時間で 変動するので、別々の機会に測った絶対値同士の比較は信用しない - 測定ジョブは**直列**に走らせる。共有環境では並走した測定は互いを壊す - 合格条件は**ビット同一**:最適化を入れた出力が標準と md5 で一致しないものは 採用しない(声が変わる最適化は最適化ではない) - 計測バイナリが静的リンクの場合、部分ビルドでは古いコードを測る事故が起きる。 測定前に対象バイナリ自体を必ず再ビルドする - まずステージ内訳(text / sampling / codec)を取る。効く場所は環境で違うので、 内訳なしに手当たり次第に触らない ### 7.3 方向の地図(あくまで一環境での実測例) 4 vCPU 共有・AVX-512 の一環境では、次の順に効きました。他環境では順序も 効果も変わりうるので、自分の内訳と照らして選んでください。 - コーデックのデコードが全体の8割超を占めていた。ウィンドウ一括デコード、 活性化・残差まわりの融合カーネル、レジスタブロック化で codec 単体 約5.3倍 - サンプリング(DiT)の GEMM はレジスタブロックカーネル+スレッドの ワーククレーミング方式(各スレッドが次の仕事を取り合う)で改善。 静的な分割は実地では効かなかった - DiT の小さな演算の融合(正規化の尾部、ゲート付き残差、SwiGLU、QK-norm 等)は 個別には小さいが、ビット同一を保ったまま積み上げられる - 合計では標準比 約3.5倍(29.9 s → 8.4 s、2スレッド、短文1件)がこの環境の実測値。 **この数値を自分の環境の目標や期待値にしないこと**。環境が違えば標準側の 速さも最適化の効きも変わります