# 自己アンカー方式の再現手順(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字未満のチャンクは作らない**(前後のチャンクに結合する) - **感情の stretch の途中では切らない**:感情・状態クラスの絵文字(公式リストの 感情系。効果音系は状態を開かない)が宣言されてから、次の感情宣言までの文の 連なりは可能な限り1リクエストに収める。通常の上限を超えても、1リクエストの 安全圏(約150字/30秒)の手前、140字までは超過を許す。それより長い stretch は 上限付近の文境界で切る - 理由:同一リクエスト内では感情の直後の文がその register を引き継ぐが、 境界で切ると次チャンクは初期 register から始まる(暗い場面での実測: 継続 246 Hz に対し再起動 310 Hz)。明るい感情では目立たないが、 暗い感情では続きが急に明るく戻る - チャンクごとに単発リクエストとして送り、出力の頭と尾の無音をトリムする - 結合時のポーズは句読点で変える(文末 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テイクの凍結」であり、 本手順の「設計した誕生」とは出自が違う点に注意してください - チャンク間で「感情値」や latent の末尾を引き継ぐ改造はしないこと。 このモデルに抽出可能な感情値という内部量は存在しない(条件付けは全て トークン列で、AdaLN の条件は時刻埋め込みのみ)。latent 末尾の引き継ぎは 低エネルギーの文末が累積し、後半がウィスパー化する。頭への感情絵文字の 再宣言も、地味な文では register を戻せない。感情の持続は分割位置の ポリシー(§5)で扱うこと ## 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件)がこの環境の実測値。 **この数値を自分の環境の目標や期待値にしないこと**。環境が違えば標準側の 速さも最適化の効きも変わります ## 8. Q&A(実測ベース) 実際に寄せられた質問への回答です。数値は断りのない限り §7.3 と同じ 一環境(4 vCPU 共有・AVX-512、2 スレッド、同一セッション内の交互測定)での 実測で、他環境での再現を保証するものではありません。 **Q. Muse 環境最適化で、どれくらい速くなりますか?** A. 最終測定では短文1件が標準 32.2 s → 最適化 9.1 s で、合計約3.5倍でした (内訳:コーデック単体で約5.3倍、サンプリングは約1.25倍)。体感としては 「短文でも数十秒かかって実用にならない」が「会話の合間に回せる」水準に なります。ただしこれは一環境の値です。自分の環境の数値は §7 の作法 (交互測定・直列・ビット同一判定)で測ってください。 **Q. 結局、手元のローカル PC で回す方がいいのですか?** A. 速さと安定だけなら、はい。GPU や Apple Silicon のあるローカルは別次元です (上流の公称値では Apple M5 で RTFx 9.0、つまり実時間の9倍速)。Muse 環境 最適化の目的は「VM の中でも実用速度を出す」ことで、ローカルに勝つことでは ありません。聞き流しや準リアルタイムの用途ならローカルが無難ですし、 Muse の会話の中でその場で声を返す用途なら、VM 内で完結すること自体に 意味があります。なお自己アンカー方式(声の固定)そのものは環境に依存 しないので、ローカルでも同じ手順がそのまま使えます。 **Q. 速くすると声は変わりますか?** A. 変わりません。この手順書の最適化の合格条件は「標準とビット同一の出力」 です(§7.2)。md5 が一致しない最適化は採用していないので、最適化の前後で 声も喋り方もバイト単位で同じです。逆にモデルの量子化は声が変わります (§0:F16 と Q8_0 では SNR 約19.8 dB = 別テイク相当の差)。声の正本は F16 で運用してください。 **Q. 長い文はどうなりますか?** A. 1リクエストの設計上の安全圏は約150字/30秒で、超えると後半の破綻 リスクが上がります。長文は §5 のチャンク管理(最大60字程度に詰める、 感情の stretch の途中では切らない)で割って、常駐 worker に流し続けるのが VM 環境での実用解です。連続生成の絶対速度は環境の素の性能に引っ張られる ので、「速くする」より「細かく流す」設計が効きます。 **Q. スレッドは多いほど速いですか?** A. 共有 VM では逆でした。実測した環境では全構成で 2 スレッドより 4 スレッドが遅く、特にサンプリングとテキスト処理がスレッド数に敏感でした。 コア数ぶんを指定する前に、§7.1 の掃引で自分の環境の最適点を掴んでください。 **Q. 起動が毎回重いのですが?** A. モデルロードが支配的です(実測した環境では F16 で約18秒)。都度プロセスを 起動せず、`speech worker` を常駐させて使い回してください(§4.2・§5)。 常駐はスループットのためのもので、声の一貫性はアンカーファイルが担うので、 プロセスを再起動しても声は変わりません。