LFM2.5-Audio-1.5B-JP に LoRA でずんだもんの声を追加学習をしてずんだもんの口調およびボイスのモデルを作る

初めに

LFM2.5-Audio-1.5B-JP は、音声入力に対してテキストと音声を interleaved に生成できる日本語 speech-to-speech(s2s)会話モデルです。

前回の記事では、このモデルに LoRA で ずんだもん口調を追加学習しました。ただし前回の口調のみ版は assistant の教師を text-only にしていたため、声色はほぼベースモデルのままでした。

ayousanz.hatenadiary.jp

今回はその続きとして、VOICEVOX ずんだもん音声を教師にして、口調だけでなく声もずんだもん寄りにするところまで進めます。

デモ

youtu.be

開発環境

項目 バージョン / 構成
OS Windows 11
GPU RTX 4070 Ti SUPER 16GB
パッケージ管理 uv
Python 3.12(>=3.12,<3.13 固定)
torch / torchaudio CUDA 12.8 wheel
liquid-audio 1.3.0
peft 0.19.1
ruff dev dependency として導入
VOICEVOX ENGINE ローカル API(http://127.0.0.1:50021

Windows では Triton の公式ビルドがないため、Mimi decode 周りで torch.compile を使うと落ちます。前回の口調のみ版と同じく、liquid_audio import より前に以下を設定して eager 実行に落とします。

import os

os.environ.setdefault("NO_TORCH_COMPILE", "1")
os.environ.setdefault("NO_CUDA_GRAPH", "1")
os.environ.setdefault("PYTHONUTF8", "1")
os.environ.setdefault("PYTHONIOENCODING", "utf-8")

口調のみ版から声込み版で何を変えたか

口調のみ版と声込み版の差分は、assistant の教師データです。

項目 口調のみ版 声込み版
目的 ずんだもん口調だけを学習 ずんだもん口調 + ずんだもん寄りの声を学習
assistant 教師 TextSegment(text=ztext) InterleavedSegment(text=ztext, audio=VOICEVOX音声)
audio_out 損失 0 有効
教師音声 不要 VOICEVOX ずんだもん speaker=3
LoRA target backbone 基線 backbone 基線 + 音声側 target
主な評価 口調率・応答妥当性 口調率・崩壊有無・話者類似度・音声品質

口調のみ版では、以下のように assistant を text-only にしていました。

ChatMessage(role="assistant", content=[TextSegment(text=ztext)])

この場合、audio_out は空なので、モデルは音声 codebook を教師として見ません。つまり、返答テキストはずんだもん口調になりますが、声はベースモデルの声のままです。

声込み版ではここを次の形に変えます。

ChatMessage(
    role="assistant",
    content=[InterleavedSegment(text=ztext, audio=zundamon_voicevox_wav)],
)

InterleavedSegment に同一テキストの VOICEVOX 音声を入れると、mapper が Mimi で audio_out を 8 codebook にエンコードし、assistant 側の text/audio 両方に supervision がかかります。これで、ずんだもん口調とずんだもん寄りの声を同時に学習できます。

重要なのは、InterleavedSegment.text と VOICEVOX 合成元テキストを必ず一致させることです。テキストと音声が違うと、同じ assistant 区間で「この文字列」と「別内容の音声 token」を同時に当てに行くことになり、学習が壊れます。

データ作成

声込み版のデータ作成は、前回の QA/ずんだ口調データに VOICEVOX 教師音声を追加する形です。

1) VOICEVOX ずんだもん音声の合成

data/synth_zundamon.py で、各行の zundamon_text を VOICEVOX ずんだもん speaker=3 で合成します。出力は Mimi と合わせて 24kHz mono WAV にします。

項目 内容
VOICEVOX 音声キャッシュ data/zundamon_audio
音声規模 2095 wav / 約 4.814 時間 / 平均 8.27 秒
音声教師データ 1982 件
mapper LFM2AudioChatMapper(codebooks=8, interleaved_text_tokens=6, interleaved_audio_tokens=9)
context 512

声込み版の 1 サンプルは以下の 3 メッセージ構成です。

samples.append([
    ChatMessage(role="system", content=[TextSegment(text=SYSTEM_ZUNDA)]),
    ChatMessage(role="user", content=[AudioSegment(audio=in_bytes)]),
    ChatMessage(role="assistant", content=[
        InterleavedSegment(text=ztext, audio=z_bytes)
    ]),
])

汎用会話リプレイ行は前回と同じく text-only にして、声を汚さず会話能力の忘却を抑える方針にしています。

assistant_seg = TextSegment(text=replay)

学習

学習スクリプトは口調のみ版と同じ train/train_zundamon_lora.py を使います。口調のみ版では text_only=Trueaudio_loss_multiplier=0 にしていましたが、声込み版では --allow-audio-loss を付けて audio_out 損失を有効化します。

最終採用モデルの構成は以下です。

項目
採用モデル checkpoints/zundamon_m2_v4/best
データ data/dataset_m2/arrow_train_v4 / arrow_val_v4
LoRA rank / alpha r=96 / alpha=192
target q_proj,k_proj,v_proj,out_proj,w1,w2,w3,depth_linear,to_logits
dropout 0.05
semantic codebook factor 1
best val_loss 2.3147 @ step4000
adapter size 約 287MB

実行イメージは以下です。

$env:PYTHONUTF8="1"
$env:PYTHONIOENCODING="utf-8"
$env:NO_TORCH_COMPILE="1"
$env:NO_CUDA_GRAPH="1"

uv run --no-sync python -u train/train_zundamon_lora.py `
    --train-data data/dataset_m2/arrow_train_v4 `
    --val-data data/dataset_m2/arrow_val_v4 `
    --output checkpoints/zundamon_m2_v4 `
    --allow-audio-loss `
    --semantic-codebook-factor 1 `
    --lora-r 96 `
    --lora-alpha 192 `
    --lora-targets q_proj,k_proj,v_proj,out_proj,w1,w2,w3,depth_linear,to_logits `
    --max-steps 4000

TTS 評価は以下です。

tag WavLM cos to ref delta vs base gap closed log-mel cos to ref
base 0.5798 ± 0.0985 +0.0000 +0.0% 0.9652
音声追加学習モデル 0.9054 ± 0.0189 +0.3256 +75.8% 0.9888

実行すると前回と同じく以下のような画面が起動します 話しかけることでずんだもんの口調およびボイスで返信が返ってきます