OuteTTSをDockerを使ってWindowsで動かす

初めに

以下のOuteTTSをローカル環境で動かしていきます。

github.com

以下のリポジトリに記事の内容を公開しています。

github.com

開発環境

Docker環境の作成

以下のようなDockerfileを作ります

# ベースイメージ: CUDA 12.4 Runtime + Ubuntu 22.04
FROM nvidia/cuda:12.4.0-runtime-ubuntu22.04

# 環境変数の設定
ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=Asia/Tokyo

# 必要なシステムパッケージのインストール(Cコンパイラ等)
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        software-properties-common \
        wget \
        libsndfile1 \
        tzdata \
        build-essential && \
    ln -fs /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \
    dpkg-reconfigure -f noninteractive tzdata && \
    apt-get clean && rm -rf /var/lib/apt/lists/*

# Python3.11、pip、およびPython開発パッケージのインストール
RUN add-apt-repository ppa:deadsnakes/ppa -y && \
    apt-get update && \
    apt-get install -y --no-install-recommends \
        python3.11 \
        python3.11-distutils \
        python3.11-dev && \
    wget -qO /tmp/get-pip.py https://bootstrap.pypa.io/get-pip.py && \
    python3.11 /tmp/get-pip.py && \
    rm /tmp/get-pip.py

# python3 コマンドを Python3.11 にリンク
RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1

# python コマンドも Python3.11 を指すようにシンボリックリンクを作成
RUN ln -sf /usr/bin/python3.11 /usr/bin/python

# 作業ディレクトリの設定
WORKDIR /app

# ローカルのrequirements.txtをコンテナにコピー
COPY requirements.txt /app/

# Pythonパッケージのインストール
RUN pip install --no-cache-dir -r requirements.txt

# ソースコードを全てコピー
COPY . /app/

これを以下のコマンドでビルドして、実行します。

docker build . -t oute-tts

実行

コンテナの立ち上げは以下で行います。

docker run -it --gpus all --rm -v "${PWD}:/work" oute-tts:latest bash

推論は以下のようなコードを作って実行します。初回実行時はモデルのダウンロードがされます。

import outetts

# Configure the model
model_config = outetts.HFModelConfig_v2(
    model_path="OuteAI/OuteTTS-0.3-1B",
    tokenizer_path="OuteAI/OuteTTS-0.3-1B"
)
# Initialize the interface
interface = outetts.InterfaceHF(model_version="0.3", cfg=model_config)

# You can create a speaker profile for voice cloning, which is compatible across all backends.
# speaker = interface.create_speaker(audio_path="path/to/audio/file.wav")
# interface.save_speaker(speaker, "speaker.json")
# speaker = interface.load_speaker("speaker.json")

# Print available default speakers
interface.print_default_speakers()
# Load a default speaker
speaker = interface.load_default_speaker(name="en_male_1")

# Generate speech
gen_cfg = outetts.GenerationConfig(
    text="Speech synthesis is the artificial production of human speech.",
    temperature=0.1,
    repetition_penalty=1.1,
    max_length=4096,
    speaker=speaker,
    # voice_characteristics="upbeat enthusiasm, friendliness, clarity, professionalism, and trustworthiness"
)
output = interface.generate(config=gen_cfg)

# Save the generated speech to a file
output.save("output.wav")

Music2EmotionをWindowsで動かしつつyoutubeのURLを指定で動くようにしてみる

初めに

音楽の感情ラベルのライブラリおよびモデルが公開されたので触っていきます

github.com

公式からspaceは出ています

huggingface.co

デモ

以下のように youtubeのURLを指定して実行すると音声のダウンロード + 分析をやってくれるようにします。

python sample_youtube.py https://youtu.be/Ljr2wMSBHqU

結果

🎵 **Music Emotion Recognition Results [【Offical Music Video】みむかゥわナイストライ / Mimukauwa Nice Try]** 🎵
--------------------------------------------------
🎭 **Predicted Mood Tags:** christmas, energetic, fast, fun, funny, game, groovy, happy, holiday, party, positive, retro, sexy, sport, summer, upbeat
💖 **Valence:** 6.30 (Scale: 1-9)
âš¡ **Arousal:** 7.49 (Scale: 1-9)
--------------------------------------------------

(みむかゥわナイストライ なのはちょうどハマっているからです)

開発環境

  • Windows11

環境構築

まずは環境を作ります。公式が Python3.10がいいと言っているので3.10で作っていきます。

uv venv -p 3.10
.venv\Scripts\activate 

次にライブラリを入れていきます。

uv pip install -r .\requirements.txt 

torchはcuda対応を入れます

uv pip install torch==2.3.1 torchvision==0.18.1 torchaudio==2.3.1 --index-url https://download.pytorch.org/whl/cu121 

ここでサンプルのコードは動くようになります。

次にyoutubeからダウンロードしてffmgpeでmp3に変換できるようにしていきます。

youtubeの動画ダウンロード用のライブラリを入れます

uv pip install yt-dlp

ffmpeg周りは以下からダウンロードしてパスを追加すれば動きます(長いので省略します)

ffmpeg.org

引数にURLを指定して実行する

サンプルコードを修正していきます。

import os
import sys
import yt_dlp
from music2emo import Music2emo

def download_audio_from_youtube(url, output_dir="inference/input"):
    # 出力ディレクトリがなければ作成
    os.makedirs(output_dir, exist_ok=True)
    
    # yt_dlpのオプション設定(出力ファイル名は固定: tmp.mp3)
    ydl_opts = {
        'format': 'bestaudio/best',
        'outtmpl': os.path.join(output_dir, 'tmp.%(ext)s'),
        'postprocessors': [{
            'key': 'FFmpegExtractAudio',
            'preferredcodec': 'mp3',
            'preferredquality': '192',
        }],
        'noplaylist': True,
        'quiet': True,
    }
    
    # yt_dlpで情報抽出とダウンロードを実施
    with yt_dlp.YoutubeDL(ydl_opts) as ydl:
        info = ydl.extract_info(url, download=True)
        title = info.get('title', 'Unknown Title')
    
    # 固定ファイル名tmp.mp3を指定
    output_file = os.path.join(output_dir, "tmp.mp3")
    return output_file, title

def main():
    # コマンドライン引数からURLを取得
    if len(sys.argv) < 2:
        print("使い方: python sample_youtube.py <YouTube URL>")
        sys.exit(1)
    
    input_audio = sys.argv[1]
    
    # URLの場合はダウンロードしてmp3に変換
    if input_audio.startswith("http"):
        input_audio, video_title = download_audio_from_youtube(input_audio)
    else:
        # URLでなければローカルファイルとみなす
        video_title = os.path.basename(input_audio)
    
    # Music2emoで音楽感情認識を実行
    music2emo = Music2emo()
    output_dic = music2emo.predict(input_audio)
    
    valence = output_dic["valence"]
    arousal = output_dic["arousal"]
    predicted_moods = output_dic["predicted_moods"]
    
    # 結果表示(動画タイトルを含む)
    print(f"\n🎵 **Music Emotion Recognition Results [{video_title}]** 🎵")
    print("-" * 50)
    print(f"🎭 **Predicted Mood Tags:** {', '.join(predicted_moods) if predicted_moods else 'None'}")
    print(f"💖 **Valence:** {valence:.2f} (Scale: 1-9)")
    print(f"âš¡ **Arousal:** {arousal:.2f} (Scale: 1-9)")
    print("-" * 50)

if __name__ == "__main__":
    main()

上記を以下のコマンドで実行します

python sample_youtube.py url

以下が実行結果です

🎵 **Music Emotion Recognition Results** 🎵
--------------------------------------------------
🎭 **Predicted Mood Tags:** christmas, energetic, fast, fun, funny, game, groovy, happy, holiday, party, positive, retro, sexy, sport, summer, upbeat
💖 **Valence:** 6.30 (Scale: 1-9)
âš¡ **Arousal:** 7.49 (Scale: 1-9)
--------------------------------------------------

toioをクラシック音楽のMIDIデータから音楽を鳴らしながら躍らせる

初めに

年末ごろに toioを買っていろいろ遊んでいたので、その一つの成果に対する内容になります!

以下のように Unity Editorでシミュレーションしながら開発をした クラシック音楽に合わせて踊るtoioを実際に動かしてみました

Unity Editorでのシミュレーションの動画は以下です

youtu.be

この記事は xRギルド Advent Calendar 2024 12日目の記事です。

toioとは

toioは公式サイトには以下のような説明があります。

いろいろなものを取り付けて自由にあそべるtoio。はじめの一歩はガイドにそって。少しずつ変えながら、想像を形に。つくる楽しさ、つくったものであそぶ喜び、そこで出会う偶然の発見がひらめきにつながっていく。手を動かして夢中になるうちに、小さなひらめきが積み重なって、創意工夫が自然に生まれる。そんな体験がtoioには詰まっています。

(https://toio.io/ より)

しかし、本格的に本格ロボットプログラミングができるようにコア キューブ技術仕様の公開やjavascript・python・unityで開発を行うことができます

(https://toio.io/programming/advanced/ より)

以下の コアキューブ技術仕様(v2.4)では、以下の要素部分を制御することができます

  • 読み取りセンサー
  • モーション検出
  • 姿勢角検出
  • 磁気センサー
  • ボタン
  • バッテリー
  • モーター
  • ランプ
  • サウンド
  • シリアライズ情報

開発環境

midiファイルからtoio-jsonに変換する

toio-jsonとは

以下のようなtoioのSDKで扱いやすくしたフォーマットと定義します

[
  {
    "track_name": "ALBENIZ: Aragon Op 47/6",
    "priority": 1,
    "notes": [
      {
        "note_number": 77,
        "start_time_ms": 0,
        "duration_units": 26
      },
      {

      },
  },
    {
    "track_name": "[email protected]",
    "priority": 2,
    "notes": [
        {
        "note_number": 53,
        "start_time_ms": 0,
        "duration_units": 58
        },
    ]
    }
]

まずはtoioで鳴らすサウンドのデータを作る必要があります。そこで、今回はライセンス上使いやすい クラシック音楽を使用していきます。

以下にクラシック音楽のmidiファイルのデータがあるので、こちらを使ってjsonに変換していきます

huggingface.co

環境構築

変換するためのpythonを使って変換処理をおこなっていきます。

今回はuvを使ってpython3.11を作っていきます。

uv venv -p 3.11
.venv\Scripts\activate

次に必要なライブラリを入れていきます

uv pip install mido==1.3.3 packaging==24.2 python-rtmidi==1.5.8

midiファイルからjsonに変換

次にmidiファイルからjson形式に変換していきます。以下のコードを使って変換を行います。

import mido
import json
import os
from multiprocessing import Pool, cpu_count
from functools import partial

def midi_to_toio_notes(midi_file_path):
    # MIDIファイルの読み込み
    try:
        midi_file = mido.MidiFile(midi_file_path)
        print(f"Loaded MIDI file: {midi_file_path}")
    except Exception as e:
        print(f"Failed to load MIDI file: {midi_file_path}, Error: {e}")
        return

    # テンポの取得(デフォルトのテンポを設定)
    tempo = 500000  # デフォルトのテンポ(500,000マイクロ秒/拍 = 120BPM)
    for track in midi_file.tracks:
        for msg in track:
            if msg.type == 'set_tempo':
                tempo = msg.tempo
                break
        else:
            continue
        break

    ticks_per_beat = midi_file.ticks_per_beat

    # 時間の変換用の係数
    tick_time = tempo / ticks_per_beat  # 1tickあたりの時間(マイクロ秒)
    # print(f"Tempo: {tempo} microseconds per beat")
    # print(f"Ticks per beat: {ticks_per_beat}")
    # print(f"Tick time: {tick_time} microseconds per tick")

    # 全トラックのデータを格納するリスト
    tracks_data = []
    priority_counter = 1  # 優先度のカウンターを初期化

    # トラックごとに処理
    for i, track in enumerate(midi_file.tracks):
        current_time = 0  # 累積時間(ticks)
        note_on_events = {}

        # print(f"Processing Track {i}: {track.name}")

        track_notes = []

        # トラック名を取得(なければ番号)
        track_name = track.name if track.name else f"Track {i}"

        for msg in track:
            current_time += msg.time  # 時間を累積

            if msg.type == 'set_tempo':
                # 曲中でテンポが変更された場合に対応
                tempo = msg.tempo
                tick_time = tempo / ticks_per_beat  # 1tickあたりの時間(マイクロ秒)
                # print(f"Tempo change detected at {current_time} ticks: {tempo} microseconds per beat")
                continue

            if msg.type == 'note_on' and msg.velocity > 0:
                # Note On イベント
                note_on_events.setdefault(msg.note, []).append(current_time)
            elif (msg.type == 'note_off') or (msg.type == 'note_on' and msg.velocity == 0):
                # Note Off イベント
                if msg.note in note_on_events and note_on_events[msg.note]:
                    start_time = note_on_events[msg.note].pop(0)
                    duration = current_time - start_time

                    # 時間をミリ秒に変換
                    start_time_ms = (start_time * tick_time) / 1000  # ミリ秒
                    duration_ms = (duration * tick_time) / 1000  # ミリ秒

                    note_number = msg.note

                    # toioの音程範囲(45~81)に合わせて音程を調整
                    original_note_number = note_number  # デバッグ用
                    while note_number < 45:
                        note_number += 12
                    while note_number > 81:
                        note_number -= 12

                    # 再生時間を10ms単位に変換(1~255の範囲)
                    play_time_units = int(duration_ms / 10)
                    if play_time_units < 1:
                        play_time_units = 1
                    elif play_time_units > 255:
                        play_time_units = 255

                    # 音符情報を保存
                    note_info = {
                        'note_number': note_number,
                        'start_time_ms': int(start_time_ms),
                        'duration_units': play_time_units
                    }
                    track_notes.append(note_info)

                    # デバッグ用の出力をコメントアウトまたは削除可能
                    # print(f"{track_name}, Note {original_note_number} ({start_time_ms:.2f} ms): Duration {duration_ms:.2f} ms, Adjusted Note {note_number}, Play Time Units {play_time_units}")

        if track_notes:
            # トラック情報を保存
            track_data = {
                'track_name': track_name,
                'priority': priority_counter,
                'notes': track_notes
            }
            tracks_data.append(track_data)
            priority_counter += 1  # 音符情報があるトラックに対してのみ優先度を増加

    if not tracks_data:
        print(f"No note data found in MIDI file: {midi_file_path}")
        return

    # 各トラックの音符を開始時間でソート
    for track_data in tracks_data:
        track_data['notes'].sort(key=lambda x: x['start_time_ms'])

    # MIDIファイル名からJSONファイル名を生成
    midi_filename = os.path.basename(midi_file_path)
    midi_name, _ = os.path.splitext(midi_filename)
    output_json_filename = f'{midi_name}_processed.json'
    output_json_path = os.path.join(os.path.dirname(midi_file_path), output_json_filename)

    # データをJSONファイルに保存
    try:
        with open(output_json_path, 'w') as f:
            json.dump(tracks_data, f, indent=2)
            print(f"Notes have been saved to {output_json_path}")
    except Exception as e:
        print(f"Failed to save JSON file: {output_json_path}, Error: {e}")

def collect_midi_files(root_dir):
    midi_files = []
    for dirpath, dirnames, filenames in os.walk(root_dir):
        for filename in filenames:
            if filename.lower().endswith(('.mid', '.midi')):
                midi_file_path = os.path.join(dirpath, filename)
                midi_files.append(midi_file_path)
    return midi_files

def process_all_midis(root_dir):
    midi_files = collect_midi_files(root_dir)
    total_files = len(midi_files)
    print(f"Total MIDI files to process: {total_files}")

    cpu_cores = cpu_count()
    print(f"Using {cpu_cores} CPU cores for parallel processing")

    with Pool(processes=cpu_cores) as pool:
        pool.map(midi_to_toio_notes, midi_files)

if __name__ == '__main__':
    import sys

    # dataディレクトリのパスを指定
    data_dir = 'data'  # スクリプトの実行ディレクトリに対する相対パス

    # コマンドライン引数でデータディレクトリを指定可能
    if len(sys.argv) > 1:
        data_dir = sys.argv[1]

    if not os.path.exists(data_dir):
        print(f"The specified directory does not exist: {data_dir}")
        sys.exit(1)

    process_all_midis(data_dir)

このコードのように実行します

python midi_to_toio.py midi_file_path

先ほどのデータセットからjsonに変換したものは以下にて公開しています。自分で変換するのが大変という方はこちらからダウンロードしてお使いください。

huggingface.co

また変換するための処理は以下のリポジトリにまとめています。

github.com

Unityでmidi-jsonからtoioを動かす

セットアップ

まずは Unityでtoioが動く環境を作成します。Unityのインストールは終わっているものとします。

toio SDK for Unity v1.6.0から Unity向けのSDKをダウンロードして、importを行います。

untiyからキューブに接続する

toio sdk for unityでは以下の流れでunityからキューブに対して、接続をします。

  1. CubeScanner.NearestScan()で近くのキューブを探す
  2. CubeConnecter().Connect()で接続

toioで特定の音を再生する

toioでは Midi note numberとnote nameの対応表があります。

toio.github.io

これに従うと 特定の周波数の音が出すことができます。

C#で特定の音を再生する場合は、以下のように実装します。

Cube.SoundOperation soundOp = new Cube.SoundOperation(duration_ms, volume, note.note_number);
cube.PlaySound(1, new Cube.SoundOperation[] { soundOp });

midi-jsonをロードする

cube側に note_number や duration_ms の情報を渡すため、先ほどjsonからデータをロードする処理を作ります。

まずはデータ用のクラスを定義します

    [Serializable]
    public class NoteData
    {
        public byte note_number;
        public int start_time_ms;
        public int duration_units;
    }

    [Serializable]
    public class TrackData
    {
        public string track_name;
        public int priority;
        public List<NoteData> notes;
    }

次にローカルにあるjsonデートをロードして、上記のクラスに格納していく処理を作ります。

今回はmidiファイルをロードしてクラスに入れるクラスを Song とします。

public class Song
    {
        public List<TrackData> Tracks { get; private set; }

        public Song()
        {
            Tracks = new List<TrackData>();
        }

        // JSONファイルからデータを読み込むメソッドを追加
        public async UniTask LoadFromJsonAsync(string jsonFilePath)
        {
            Tracks.Clear();

            // ファイルが存在するかチェック
            if (!File.Exists(jsonFilePath))
            {
                Debug.LogError($"JSON file not found: {jsonFilePath}");
                return;
            }

            try
            {
                // ファイルからJSON文字列を非同期的に読み込む
                string jsonText = await ReadFileAsync(jsonFilePath);
                

                // JSONをパースしてTrackDataのリストを取得
                Tracks = JsonConvert.DeserializeObject<List<TrackData>>(jsonText);

                if (Tracks == null || Tracks.Count == 0)
                {
                    Debug.LogError("No track data found in JSON.");
                    return;
                }

                // トラックを優先度でソート(昇順)
                Tracks.Sort((a, b) => a.priority.CompareTo(b.priority));
                Debug.Log($"Loaded {Tracks.Count} tracks from JSON.");
            }
            catch (Exception e)
            {
                Debug.LogError($"Failed to load or parse JSON file: {e.Message}");
            }
        }

        // ファイルを非同期で読み込むヘルパーメソッド
        private async UniTask<string> ReadFileAsync(string filePath)
        {
            using (var reader = new StreamReader(filePath))
            {
                return await reader.ReadToEndAsync();
            }
        }
    }

これによりローカルにあるmidi-jsonファイルからmidiデータをロードすることができるようになりました。

midi-jsonからtoioでクラシック音楽を鳴らす

ここまででローカルのmidiファイルから作成したjsonデータをロードして、toioで音を鳴らす準備ができました。最後に再生時間ごとにどのnoteを鳴らす計算して jsonのデータで一つのリスト文を鳴らすようにします。

以下は 先ほど作成した NoteDataのリストデータの TrackDataを用いて音を再生する処理になります。

private async UniTask PlayTrackOnCubeAsync(Cube cube, TrackData track)
        {
            if (cube == null || track == null || track.notes == null || track.notes.Count == 0)
            {
                return;
            }

            Debug.Log($"Starting playback on cube {cube.id} for track '{track.track_name}'");

            float startTime = Time.time;

            foreach (var note in track.notes)
            {
                // 現在時刻から経過時間を計算
                float elapsedTime = (Time.time - startTime) * 1000f; // ミリ秒に変換
                float waitTime = (note.start_time_ms - elapsedTime) / 1000f; // 秒に変換

                if (waitTime > 0)
                {
                    // 次の音符まで待機
                    await UniTask.Delay(TimeSpan.FromSeconds(waitTime));
                }

                // 音符を再生
                ushort duration_ms = (ushort)(note.duration_units * 10); // duration_unitsをミリ秒に変換
                byte volume = 15; // 音量を設定

                Cube.SoundOperation soundOp = new Cube.SoundOperation(duration_ms, volume, note.note_number);
                cube.PlaySound(1, new Cube.SoundOperation[] { soundOp });
            }

            Debug.Log($"Finished playback on cube {cube.id} for track '{track.track_name}'");
        }

cubeに移動の命令を送る

cubeに対して移動の命令は以下で実行することができます。

// キューブを動かす(命令の優先度を強く設定)
cube.Move(action.leftSpeed, action.rightSpeed, action.durationMs,Cube.ORDER_TYPE.Strong);

公式のドキュメントは以下になります。

toio.github.io

midi情報から動きを決める

noteの情報からcubeの右・左のモーターの速度および移動時間を計算します。このときにcubeが移動できる範囲(シミュレーション上)が決まっているので、落ちないように調節をしました。

private List<MovementAction> GenerateMovementPlan(TrackData track, bool mirror)
    {
        List<MovementAction> movementPlan = new List<MovementAction>();

        foreach (var note in track.notes)
        {
            float startTime = note.start_time_ms / 1000f; // 開始時間(秒)
            int durationMs = note.duration_units * 10; // 持続時間(ミリ秒)

            // ノート番号をモーター速度にマッピング
            (int leftSpeed, int rightSpeed) = MapNoteNumberToSpeeds(note.note_number, mirror);

            // MovementActionの作成
            MovementAction action = new MovementAction
            {
                startTime = startTime,
                durationMs = durationMs,
                leftSpeed = leftSpeed,
                rightSpeed = rightSpeed
            };

            movementPlan.Add(action);
        }

        return movementPlan;
    }

    // ノート番号を左右のモーター速度にマッピングするメソッド
    private (int leftSpeed, int rightSpeed) MapNoteNumberToSpeeds(int noteNumber, bool mirror)
    {
        // ノート番号を0~1に正規化
        float normalized = (noteNumber - _minNote) / (float)(_maxNote - _minNote);

        // スピードを決定(速度の範囲を60~100に設定)
        int baseSpeed = (int)(normalized * 40) + 60; // 60~100に変換

        int leftSpeed, rightSpeed;

        if (mirror)
        {
            // キューブ2(ミラーリング)では、左に曲がる
            leftSpeed = baseSpeed - 20; // スピードを減少
            rightSpeed = baseSpeed;
        }
        else
        {
            // キューブ1では、右に曲がる
            leftSpeed = baseSpeed;
            rightSpeed = baseSpeed - 20; // スピードを減少
        }

        // スピードの範囲を調整(-100から100)
        leftSpeed = Mathf.Clamp(leftSpeed, -100, 100);
        rightSpeed = Mathf.Clamp(rightSpeed, -100, 100);

        return (leftSpeed, rightSpeed);
    }

これで クラシック音楽のmidi情報からtoioのcubeを使って音楽を再生しつつ音楽に合わせて動かすことができました。

esnya/japanese_speecht5_ttsを動かして音声合成を行う

初めに

前に英語版を動かしてみました。今回は日本語の追加学習モデルのesnya/japanese_speecht5_ttsが出ていたので,こちらを動かしていきます

ayousanz.hatenadiary.jp

以下にて記事の内容をリポジトリで公開しています。

github.com

開発環境

環境作成

まずは仮想環境を作成します

uv venv -p 3.11
source venv/bin/activate

注意 この記事では,OpenJTalkのPythonのラッパーライブラリとして pyopenjtalk-plusを使用しています。そのため,python は3.11以降でしか動きません

次に関連するライブラリを入れます cudaは入っていないので,cpu版を入れていきます

uv pip install transformers==4.29.2 sentencepiece torch soundfile accelerate pyopenjtalk-plus

注意

  1. transformersのversionは,モデルカードのサンプルカードで最新版では削除されているPRETRAINED_POSITIONAL_EMBEDDINGS_SIZESの定数を使っているため,4.29.2 をインストールします

  2. 上記に記載した通りOpenJTalkのPythonのラッパーライブラリは,pyopenjtalk-plusを使用しています。

推論の準備

日本語の推論をするため,OpenJTalkのtokenizerのコードを公式モデルの付属しているものをダウンロードします

curl -O https://huggingface.co/esnya/japanese_speecht5_tts/resolve/main/speecht5_openjtalk_tokenizer.py

推論

macのM系チップでは,畳み込み層の出力チャネルが 65536 を超える場合に発生するバグがあるため CPUにて推論を行います

以下が該当のissueです

github.com

以下が推論コードです

import numpy as np
from transformers import (
    SpeechT5ForTextToSpeech,
    SpeechT5HifiGan,
    SpeechT5FeatureExtractor,
    SpeechT5Processor,
)
from speecht5_openjtalk_tokenizer import SpeechT5OpenjtalkTokenizer
import soundfile
import torch

# MPS が利用可能なら "mps"、なければ "cpu" を使用(今回はモデル本体は MPS/CPU で動作)
device = torch.device("mps" if torch.backends.mps.is_available() else "cpu")

# vocoder を CPU で実行するためのラッパークラス
class VocoderCPUWrapper(torch.nn.Module):
    def __init__(self, vocoder):
        super().__init__()
        self.vocoder = vocoder  # この vocoder は全パラメータが CPU 上にある必要があります
    def forward(self, x):
        # 入力を CPU に移動してから vocoder を実行
        x_cpu = x.cpu()
        return self.vocoder(x_cpu)

model_name = "esnya/japanese_speecht5_tts"
with torch.no_grad():
    # SpeechT5ForTextToSpeech を float32 でロードし、device に移動
    model = SpeechT5ForTextToSpeech.from_pretrained(
        model_name, torch_dtype=torch.float32
    )
    model.to(device)

    tokenizer = SpeechT5OpenjtalkTokenizer.from_pretrained(model_name)
    feature_extractor = SpeechT5FeatureExtractor.from_pretrained(model_name)
    processor = SpeechT5Processor(feature_extractor, tokenizer)
    
    # SpeechT5HifiGan (vocoder) は MPS の制限があるため、明示的に CPU にロード
    vocoder = SpeechT5HifiGan.from_pretrained(
        "microsoft/speecht5_hifigan", torch_dtype=torch.float32
    )
    vocoder.to("cpu")  # ここで CPU に移動
    vocoder_wrapper = VocoderCPUWrapper(vocoder)

    input_text = "吾輩は猫である。名前はまだ無い。どこで生れたかとんと見当がつかぬ。"
    # processor により input_ids を生成し、device に転送
    input_ids = processor(text=input_text, return_tensors="pt").input_ids.to(device)

    speaker_embeddings = np.random.uniform(-1, 1, (1, 16))
    speaker_embeddings = torch.FloatTensor(speaker_embeddings).to(device=device, dtype=model.dtype)

    # generate_speech 呼び出し時に vocoder_wrapper を指定
    waveform = model.generate_speech(
        input_ids,
        speaker_embeddings,
        vocoder=vocoder_wrapper,
    )

    waveform = waveform / waveform.abs().max()  # 正規化
    waveform = waveform.reshape(-1).cpu().float().numpy()

    soundfile.write(
        "output.wav",
        waveform,
        vocoder.config.sampling_rate,
    )

microsoft/speecht5_ttsを動かして音声合成を行う

初めに

以下のTTSモデルを動かしていきます。音声とテキストの両方のデータを用いた新しい事前学習モデルです

huggingface.co

以下にて記事の内容をリポジトリで公開しています。

github.com

開発環境

環境作成

まずは仮想環境を作成します

uv venv -p 3.11
source venv/bin/activate

次に関連するライブラリを入れます cudaは入っていないので,cpu版を入れていきます

uv pip install transformers sentencepiece datasets torch soundfile

実行

推論方法は,「(TTS) pipeline」を使う方法と「transformers modelling code」を使う方法があります。

TTS pipelineを使う方法

以下のコードで実行できます

from transformers import pipeline
from datasets import load_dataset
import soundfile as sf
import torch

synthesiser = pipeline("text-to-speech", "microsoft/speecht5_tts")

embeddings_dataset = load_dataset("Matthijs/cmu-arctic-xvectors", split="validation")
speaker_embedding = torch.tensor(embeddings_dataset[7306]["xvector"]).unsqueeze(0)
# You can replace this embedding with your own as well.

speech = synthesiser("Hello, my dog is cooler than you!", forward_params={"speaker_embeddings": speaker_embedding})

sf.write("speech_pipeline.wav", speech["audio"], samplerate=speech["sampling_rate"])

transformers modelling codeを使う方法

以下のコードで実行できます

from transformers import SpeechT5Processor, SpeechT5ForTextToSpeech, SpeechT5HifiGan
from datasets import load_dataset
import torch
import soundfile as sf
from datasets import load_dataset

processor = SpeechT5Processor.from_pretrained("microsoft/speecht5_tts")
model = SpeechT5ForTextToSpeech.from_pretrained("microsoft/speecht5_tts")
vocoder = SpeechT5HifiGan.from_pretrained("microsoft/speecht5_hifigan")

inputs = processor(text="Hello, my dog is cute.", return_tensors="pt")

# load xvector containing speaker's voice characteristics from a dataset
embeddings_dataset = load_dataset("Matthijs/cmu-arctic-xvectors", split="validation")
speaker_embeddings = torch.tensor(embeddings_dataset[7306]["xvector"]).unsqueeze(0)

speech = model.generate_speech(inputs["input_ids"], speaker_embeddings, vocoder=vocoder)

sf.write("speech_modelling.wav", speech.numpy(), samplerate=16000)

sbintuitions/modernbert-ja-130mに追加学習をして文章からVTuberかどうかを判断する教師あり学習による2値分類モデルを作成する

初めに

1週間ほど前に sbintuitions/modernbert-ja-130mが公開されたので遊んでいきます。

今回は bertモデルなのでテキストクラスタリングをやってみます.文章を入れたら、その文章がVTuberっぽいのかどうかを判定するモデルを作ってみます。 (25/02/17時点では、データセットがyoutube apiの制限上 あまり集められていないので精度は悪いです)

この記事の学習Colobおよび推論のリポジトリは以下で公開しています

学習Colob

colab.research.google.com

推論

github.com

データセット

huggingface.co

学習済みモデル

huggingface.co

開発環境

  • 学習環境 : Google Colob (T4)
  • 推論環境 : Windows 11

Google Colobの準備

以下のAPIキーを使うため シークレットキーを登録します

  • huggingface
  • youtube api(データセットを用意されているものを使う場合は必要ない)
  • wandb(使用しない場合は不要)

データセットの作成

まずは学習をするためのyoutubeからVTuberと非VTuberのチャンネルの情報を取得します。 (youtubeAPIの制限により多くは取得できないです)

# 必要なライブラリのインストール(初回のみ)
!pip install -U google-api-python-client huggingface_hub

# APIキーはGoogle Colabのシークレット値から取得
import os
from google.colab import userdata
API_KEY = userdata.get('YOUTUBE_API_KEY')
if API_KEY is None:
    raise ValueError("YOUTUBE_API_KEYが環境変数に設定されていません。ColabのシークレットにAPIキーを登録してください。")

from googleapiclient.discovery import build

# YouTube Data APIのクライアントを作成
youtube = build('youtube', 'v3', developerKey=API_KEY)

def fetch_channels(query, max_results=50, page_limit=3):
    """指定した検索クエリでチャンネル情報を取得する関数"""
    channels = []
    next_page_token = None
    for _ in range(page_limit):
        request = youtube.search().list(
            q=query,
            type="channel",
            part="id,snippet",
            maxResults=max_results,
            pageToken=next_page_token
        )
        response = request.execute()
        for item in response.get("items", []):
            channel_id = item["id"]["channelId"]
            title = item["snippet"]["title"]
            description = item["snippet"]["description"]
            channels.append({"channel_id": channel_id, "title": title, "description": description})
        next_page_token = response.get("nextPageToken")
        if not next_page_token:
            break
    return channels

# VTuber候補のチャンネル情報を取得(例:"VTuber"で検索)
vtuber_channels = fetch_channels(query="VTuber", max_results=500, page_limit=10)
print("VTuber候補のチャンネル数:", len(vtuber_channels))

# 非VTuber候補のチャンネル情報を取得(例:"料理"で検索)
non_vtuber_channels = fetch_channels(query="料理", max_results=10, page_limit=10)
print("非VTuber候補のチャンネル数:", len(non_vtuber_channels))

データをhuggingfaceにアップロード

作成したデータを整理して,huggingfaceにアップロードします

def add_label_and_text(item, label):
    # "title"と"description"を結合して"text"を作成し、ラベルを追加
    item["text"] = item["title"] + " " + item["description"]
    item["label"] = label
    return item

# VTuber候補にはラベル1を付与
vtuber_channels_labeled = [add_label_and_text(item, 1) for item in vtuber_channels]
# 非VTuber候補にはラベル0を付与
non_vtuber_channels_labeled = [add_label_and_text(item, 0) for item in non_vtuber_channels]

# 両方のリストを連結して1つのリストにする
all_channels = vtuber_channels_labeled + non_vtuber_channels_labeled

# JSONL形式で保存する
import json
def save_to_jsonl(data, filename):
    with open(filename, "w", encoding="utf-8") as f:
        for item in data:
            f.write(json.dumps(item, ensure_ascii=False) + "\n")

save_to_jsonl(all_channels, "vtuber_youtube_list.jsonl")


# Hugging Face CLIのインストール(必要な場合)
!pip install huggingface_hub

# Hugging Faceにログイン(アクセストークンを入力するプロンプトが表示されます)
!huggingface-cli login

# 必要なライブラリのインストール(初回のみ)
!pip install huggingface_hub

from huggingface_hub import HfApi, upload_file
from google.colab import userdata
import time

# Colabのシークレットからアクセストークンを取得("HF_TOKEN"として登録済み)
hf_token = userdata.get('HF_TOKEN')
if hf_token is None:
    raise ValueError("HF_TOKENがシークレットに登録されていません。")

# アップロード先のリポジトリID("your_username" をあなたのHugging Faceユーザー名に置き換えてください)
repo_id = "ayousanz/vtuber-youtube-list-dataset"

# HfApiを利用してリポジトリを作成(既に存在する場合はスキップ)
api = HfApi()
try:
    api.create_repo(repo_id=repo_id, repo_type="dataset", exist_ok=True, token=hf_token)
    print(f"リポジトリ '{repo_id}' が作成済み、または既に存在します。")
except Exception as e:
    print("リポジトリ作成時のエラー:", e)

# Hub上に反映されるまで待機(例:10秒)
print("Hub上に反映されるまで10秒待ちます...")
time.sleep(10)

# JSONLファイル(例:vtuber_channels.jsonl, non_vtuber_channels.jsonl)のアップロード
for filename in ["vtuber_youtube_list.jsonl"]:
    try:
        upload_file(
            path_or_fileobj=filename,
            path_in_repo=filename,
            repo_id=repo_id,
            repo_type="dataset",
            token=hf_token
        )
        print(f"{filename} のアップロードが完了しました。")
    except Exception as e:
        print(f"{filename} のアップロード時にエラーが発生しました:", e)

print("すべてのファイルのアップロードが完了しました。")

wandbの準備(必要なければスキップ可)

!pip install -U transformers>=4.48.0 datasets evaluate wandb

# wandbのログイン(初回のみ実行)
import wandb
from google.colab import userdata
wandb_api_key = userdata.get('WANDB_API_KEY')
!wandb login $wandb_api_key

学習・評価

以下で学習および評価(テスト推論)を行います

# -------------------------------
# 0. Flash Attention の無効化(GPUがAmpere未満の場合)
# -------------------------------
import os
os.environ["FLASH_ATTN_DISABLE"] = "1"

# -------------------------------
# 1. 必要なライブラリのインストール(初回のみ)
# -------------------------------
!pip install -U transformers datasets evaluate wandb huggingface_hub

# -------------------------------
# 2. 必要なモジュールのインポート
# -------------------------------
import torch
import numpy as np
from transformers import AutoTokenizer, AutoModelForSequenceClassification, TrainingArguments, Trainer
from datasets import load_dataset, DatasetDict, Features, Value
import evaluate
import wandb
from wandb import Settings
import time

# -------------------------------
# 3. wandb の初期化(project と entity を適切に設定)
# -------------------------------
wandb.init(
    project="modernbert-vtuber",
    entity="yousan",  # ご自身の wandb ユーザー名に変更してください
    config={
        "model_name": "sbintuitions/modernbert-ja-130m",
        "epochs": 3,
        "batch_size": 4,
        "learning_rate": 2e-5
    },
    settings=Settings(init_timeout=210)
)

# -------------------------------
# 4. Hugging Face Hub からデータセットの読み込み
# -------------------------------
# JSONL ファイルは、各レコードが "channel_id", "title", "description", "text", "label" を持つ前提
features = Features({
    "channel_id": Value("string"),
    "title": Value("string"),
    "description": Value("string"),
    "text": Value("string"),
    "label": Value("int64")
})

# ここでは、アップロード済みのデータセット名(例:"ayousanz/vtuber-youtube-list-dataset")を利用します
dataset = load_dataset("ayousanz/vtuber-youtube-list-dataset", features=features)

# もしデータセットに "train" と "validation" の split が存在しない場合は、単一の split から分割
if not ("train" in dataset and "validation" in dataset):
    single_split = list(dataset.keys())[0]
    split_dataset = dataset[single_split].train_test_split(test_size=0.2, seed=42)
    dataset = DatasetDict({
        "train": split_dataset["train"],
        "validation": split_dataset["test"]
    })

# JSONL に "text" フィールドが既に存在する前提ですが、念のため title と description を連結する処理を追加
def add_text_field(example):
    if not example.get("text"):
        example["text"] = example["title"] + " " + example["description"]
    return example

dataset = dataset.map(add_text_field)

# ★ 念のため、ラベルが None でないレコードのみ残す
dataset = dataset.filter(lambda x: x["label"] is not None)

print("サンプルデータ(train):")
print(dataset["train"][0])
print("サンプルデータ(validation):")
print(dataset["validation"][0])

# -------------------------------
# 5. モデルとトークナイザーの読み込み・前処理
# -------------------------------
model_name = "sbintuitions/modernbert-ja-130m"
tokenizer = AutoTokenizer.from_pretrained(model_name)
# モデルはデフォルト(FP32)でロードする(fp16はTrainerで管理)
model = AutoModelForSequenceClassification.from_pretrained(
    model_name, num_labels=2
)

# GPUが利用可能な場合、モデルをGPUに移動
device = "cuda" if torch.cuda.is_available() else "cpu"
model.to(device)

# 前処理:各レコードの "text" をトークナイズ(最大長128、padding)
def preprocess_function(examples):
    return tokenizer(examples["text"], truncation=True, max_length=128, padding="max_length")

tokenized_datasets = dataset.map(preprocess_function, batched=True)

# -------------------------------
# 6. 評価指標およびトレーニング設定(wandb連携)
# -------------------------------
accuracy_metric = evaluate.load("accuracy")
def compute_metrics(eval_pred):
    logits, labels = eval_pred
    predictions = np.argmax(logits, axis=-1)
    return accuracy_metric.compute(predictions=predictions, references=labels)

training_args = TrainingArguments(
    output_dir="./modernbert_vtuber_model",
    evaluation_strategy="epoch",   # 将来的には eval_strategy に変更
    learning_rate=2e-5,
    per_device_train_batch_size=4,
    per_device_eval_batch_size=4,
    num_train_epochs=3,
    weight_decay=0.01,
    save_strategy="epoch",
    logging_dir="./logs",
    report_to=["wandb"],
    run_name="modernbert_vtuber_finetuning",
    fp16=True  # Trainer による混合精度トレーニングを有効化
)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_datasets["train"],
    eval_dataset=tokenized_datasets["validation"],
    compute_metrics=compute_metrics,
)

# -------------------------------
# 7. ファインチューニング実行
# -------------------------------
trainer.train()

# 学習済みモデルとトークナイザーの保存
model.save_pretrained("./modernbert_vtuber_model")
tokenizer.save_pretrained("./modernbert_vtuber_model")

wandb.finish()

# -------------------------------
# 8. 推論関数の定義と使用例
# -------------------------------
def classify_vtuber(text, threshold=50.0):
    """
    入力文章に対して VTuber 判定を行い、VTuber である確信度 (rate) を算出します。
    threshold 以上なら VTuber と判定します。
    """
    inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=128, padding="max_length")
    inputs = {k: v.to(device) for k, v in inputs.items()}
    outputs = model(**inputs)
    logits = outputs.logits
    probabilities = torch.softmax(logits, dim=-1).squeeze().tolist()  # [非VTuber確率, VTuber確率]
    vtuber_rate = probabilities[1] * 100
    is_vtuber = vtuber_rate >= threshold
    return {"isVTuber": is_vtuber, "rate": round(vtuber_rate, 3)}

# 使用例
input_text = "この動画では、バーチャルなキャラクターがリアルタイムに動く様子を配信しています。"
result = classify_vtuber(input_text)
print("入力文章の判定結果:")
print(result)

推論の結果は以下のようになります

入力文章の判定結果:
{'isVTuber': True, 'rate': 100.0}

学習したモデルをhuggingfaceにアップロード

以下で学習したモデルをhuggingfaceにアップロードします

# huggingface_hub ライブラリからリポジトリ作成用の関数をインポート
from huggingface_hub import create_repo

# アップロード先のリポジトリ名を指定(既に作成済みならこのステップはスキップ可能)
repo_id = "ayousanz/modernbert-vtuber-finetuned-1"  # ご自身のユーザー名とリポジトリ名に変更してください
create_repo(repo_id, exist_ok=True)

# 学習済みモデルとトークナイザーをアップロード
model.push_to_hub(repo_id)
tokenizer.push_to_hub(repo_id)

モデルをWindowsで推論する

(ColobのT4を使い切ってしまったので) Windowsで推論を行なっていきます。推論だけ試したい方はこちらのみで良さそうです

環境作成

uv venv -p 3.11
source venv/bin/activate
uv pip install -r requirements.txt

推論

以下のコードを実行します

import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification, pipeline

# GPU が利用可能か確認
device = "cuda" if torch.cuda.is_available() else "cpu"
print("Using device:", device)

# Hugging Face Hub 上のリポジトリからモデルとトークナイザーをロード
model_name = "ayousanz/modernbert-vtuber-finetuned"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSequenceClassification.from_pretrained(model_name)
model.to(device)  # GPU が利用可能なら GPU に移動

# 推論用パイプラインの作成
vtuber_classifier = pipeline("text-classification", model=model, tokenizer=tokenizer, device=0 if device=="cuda" else -1)

# 5つのサンプルテキストで推論例を実行
sample_texts = [
    "この動画では、バーチャルなキャラクターがリアルタイムに動く様子を配信しています。",
    "このチャンネルは、料理レシピの動画を投稿しています。",
    "最新のVTuberがライブ配信を行っており、視聴者との交流が盛んです。",
    "旅行動画を中心に、世界各地の観光地を紹介しています。",
    "ここでは、3Dモデルを使ったアニメーション動画を配信しています。"
]

for text in sample_texts:
    result = vtuber_classifier(text)
    print("入力:", text)
    print("推論結果:", result)
    print("-" * 50)

実行結果は以下のようになります

Using device: cuda
Device set to use cuda:0
入力: この動画では、バーチャルなキャラクターがリアルタイムに動く様子を配信しています。
推論結果: [{'label': 'LABEL_1', 'score': 1.0}]
--------------------------------------------------
入力: このチャンネルは、料理レシピの動画を投稿しています。
推論結果: [{'label': 'LABEL_0', 'score': 1.0}]
--------------------------------------------------
入力: 最新のVTuberがライブ配信を行っており、視聴者との交流が盛んです。
推論結果: [{'label': 'LABEL_1', 'score': 1.0}]
--------------------------------------------------
入力: 旅行動画を中心に、世界各地の観光地を紹介しています。
推論結果: [{'label': 'LABEL_0', 'score': 1.0}]
--------------------------------------------------
入力: ここでは、3Dモデルを使ったアニメーション動画を配信しています。
推論結果: [{'label': 'LABEL_1', 'score': 0.8205193877220154}]
--------------------------------------------------

Docker環境でJETSモデルの音声合成および学習を行う

初めに

TTSのモデルの中にjetsがあります。2年ほど前のモデルなので環境構築がかなり大変だったのでDockerを使って動かせる環境を作ります

今回の記事に関する内容は以下のリポジトリにまとめています。

github.com

開発環境

  • Windows11

必要なリポジトリをclone

jetsを動かすためには、以下のライブラリが必要になります。 * espnet * kaldi

そのため以下のような構造になるようにリポジトリをcloneしていきます。 それぞれのリポジトリは、今回のために整理したリポジトリを作成しました。

project/
├── espnet/
   ├── tools
      ├── kaldi

Docker環境を作る

jetsの当時の環境が安定していたた、以下の条件でdocker環境を構築します

  • cuda 11.x
  • python 3.8
  • torch 1.10

以下が実際に作成したDockerfileです

FROM nvidia/cuda:11.3.1-cudnn8-runtime-ubuntu20.04

# 非対話モードとタイムゾーンの設定
ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=Asia/Tokyo

# tzdata を先にインストールしてタイムゾーンの設定、その後必要なパッケージをインストール
RUN apt-get update && \
    apt-get install -y tzdata && \
    ln -fs /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \
    dpkg-reconfigure --frontend noninteractive tzdata && \
    apt-get install -y software-properties-common && \
    add-apt-repository universe && \
    apt-get update && \
    apt-get install -y git \
                       python3.8 python3.8-dev python3.8-venv python3-pip wget \
                       libfreetype6-dev libpng-dev pkg-config && \
    python3.8 -m pip install --upgrade pip

# PyTorch 1.10.1 (CUDA 11.3 対応版) のインストール
RUN python3.8 -m pip install torch==1.10.1+cu113 torchvision==0.11.2+cu113 torchaudio==0.10.1+cu113 \
    -f https://download.pytorch.org/whl/torch_stable.html

# python コマンドで python3.8 を利用できるようにシンボリックリンクを作成
RUN ln -sf /usr/bin/python3.8 /usr/bin/python

WORKDIR /work/espnet

ENV PYTHONPATH=/work/espnet:$PYTHONPATH

# ここで ESPnet の依存ライブラリをインストール
RUN python -m pip install \
    kaldiio==2.18.0 \
    humanfriendly==10.0 \
    numpy==1.24.4 \
    resampy==0.4.3 \
    soundfile==0.13.1 \
    nltk==3.9.1 \
    tqdm==4.67.1 \
    matplotlib==3.7.5 \
    typeguard==2.7.1 \
    inflect==5.0.3 \
    espnet_model_zoo==0.1.7

# 必要な NLTK リソースのダウンロード(同じ Python 環境内で実行)
RUN python -c "import nltk; nltk.download('averaged_perceptron_tagger_eng')"

CMD ["/bin/bash"]

この時点でのフォルダ構造は以下になります

project/
├── espnet/
│  ├── tools
│     ├── kaldi
├──Dockerfile

このDockerfileを以下でビルドして、コンテナ内に入ります。このときにボリュームはマウントして実行します

docker build -t espnet-jets .
docker run -it --rm -v "${PWD}:/work" espnet-jets bash

前処理

コンテナ内に入った後にespnetが正しくインストールされるように以下を実行してインストールを行います

pip install -e .

音声合成の実行

まずは実行するためのフォルダを移動します

cd egs2/ljspeech/tts1

次に音声合成を行うために以下を実行します。 データの前処理、モデルのダウンロードを一括で行います (少し時間がかかるので放置してください)

./run.sh --skip_data_prep false --skip_train true --download_model imdanboy/jets

音声合成の結果

以下のパスに処理が終わった後にwavファイルが保存されています。

exp/imdanboy/jets/decode_train.loss.ave/dev/wav/
root@a895d360c83b:/work/espnet/egs2/ljspeech/tts1# ls -l exp/imdanboy/jets/decode_train.loss.ave/dev/wav/
total 69404
-rw-r--r-- 1 root root 235052 Feb 17 15:21 LJ049-0008.wav
-rw-r--r-- 1 root root 421420 Feb 17 15:22 LJ049-0009.wav
-rw-r--r-- 1 root root 138284 Feb 17 15:22 LJ049-0010.wav
-rw-r--r-- 1 root root 394284 Feb 17 15:22 LJ049-0011.wav

以下は推論実行時のログです

root@a895d360c83b:/work/espnet/egs2/ljspeech/tts1# ./run.sh --skip_data_prep false --skip_train true --download_model imdanboy/jets
2025-02-17T15:11:01 (tts.sh:211:main) ./tts.sh --lang en --feats_type raw --fs 22050 --n_fft 1024 --n_shift 256 --token_type phn --cleaner tacotron --g2p g2p_en_no_space --train_config conf/train.yaml --inference_config conf/decode.yaml --train_set tr_no_dev --valid_set dev --test_sets dev eval1 --srctexts data/tr_no_dev/text --audio_format wav --skip_data_prep false --skip_train true --download_model imdanboy/jets
2025-02-17T15:11:02 (tts.sh:307:main) Stage 1: Data preparation for data/tr_no_dev, data/dev, etc.
2025-02-17T15:11:02 (data.sh:16:main) local/data.sh 
2025-02-17T15:11:02 (data.sh:39:main) stage -1: Data Download
already exists. skipped.
2025-02-17T15:11:03 (data.sh:44:main) stage 0: Data Preparation
utils/validate_data_dir.sh: WARNING: you have only one speaker.  This probably a bad idea.
   Search for the word 'bold' in http://kaldi-asr.org/doc/data_prep.html
   for more information.
utils/validate_data_dir.sh: Successfully validated data-directory data/train
2025-02-17T15:20:07 (data.sh:77:main) stage 2: utils/subset_data_dir.sg
utils/subset_data_dir.sh: reducing #utt from 13100 to 500
utils/subset_data_dir.sh: reducing #utt from 500 to 250
utils/subset_data_dir.sh: reducing #utt from 500 to 250
utils/subset_data_dir.sh: reducing #utt from 13100 to 12600
2025-02-17T15:20:10 (data.sh:86:main) Successfully finished. [elapsed=548s]
2025-02-17T15:20:10 (tts.sh:323:main) Stage 2: Format wav.scp: data/ -> dump/raw/
utils/copy_data_dir.sh: copied data from data/tr_no_dev to dump/raw/org/tr_no_dev
utils/validate_data_dir.sh: WARNING: you have only one speaker.  This probably a bad idea.
   Search for the word 'bold' in http://kaldi-asr.org/doc/data_prep.html
   for more information.
utils/validate_data_dir.sh: Successfully validated data-directory dump/raw/org/tr_no_dev
2025-02-17T15:20:12 (format_wav_scp.sh:42:main) scripts/audio/format_wav_scp.sh --nj 8 --cmd run.pl --audio-format wav --fs 22050 data/tr_no_dev/wav.scp dump/raw/org/tr_no_dev
2025-02-17T15:20:13 (format_wav_scp.sh:110:main) [info]: without segments
2025-02-17T15:20:34 (format_wav_scp.sh:142:main) Successfully finished. [elapsed=22s]
utils/copy_data_dir.sh: copied data from data/dev to dump/raw/org/dev
utils/validate_data_dir.sh: WARNING: you have only one speaker.  This probably a bad idea.
   Search for the word 'bold' in http://kaldi-asr.org/doc/data_prep.html
   for more information.
utils/validate_data_dir.sh: Successfully validated data-directory dump/raw/org/dev
2025-02-17T15:20:35 (format_wav_scp.sh:42:main) scripts/audio/format_wav_scp.sh --nj 8 --cmd run.pl --audio-format wav --fs 22050 data/dev/wav.scp dump/raw/org/dev
2025-02-17T15:20:36 (format_wav_scp.sh:110:main) [info]: without segments
2025-02-17T15:20:39 (format_wav_scp.sh:142:main) Successfully finished. [elapsed=4s]
utils/copy_data_dir.sh: copied data from data/dev to dump/raw/org/dev
utils/validate_data_dir.sh: WARNING: you have only one speaker.  This probably a bad idea.
   Search for the word 'bold' in http://kaldi-asr.org/doc/data_prep.html
   for more information.
utils/validate_data_dir.sh: Successfully validated data-directory dump/raw/org/dev
2025-02-17T15:20:40 (format_wav_scp.sh:42:main) scripts/audio/format_wav_scp.sh --nj 8 --cmd run.pl --audio-format wav --fs 22050 data/dev/wav.scp dump/raw/org/dev
2025-02-17T15:20:41 (format_wav_scp.sh:110:main) [info]: without segments
2025-02-17T15:20:44 (format_wav_scp.sh:142:main) Successfully finished. [elapsed=4s]
utils/copy_data_dir.sh: copied data from data/eval1 to dump/raw/eval1
utils/validate_data_dir.sh: WARNING: you have only one speaker.  This probably a bad idea.
   Search for the word 'bold' in http://kaldi-asr.org/doc/data_prep.html
   for more information.
utils/validate_data_dir.sh: Successfully validated data-directory dump/raw/eval1
2025-02-17T15:20:45 (format_wav_scp.sh:42:main) scripts/audio/format_wav_scp.sh --nj 8 --cmd run.pl --audio-format wav --fs 22050 data/eval1/wav.scp dump/raw/eval1
2025-02-17T15:20:45 (format_wav_scp.sh:110:main) [info]: without segments
2025-02-17T15:20:49 (format_wav_scp.sh:142:main) Successfully finished. [elapsed=4s]
2025-02-17T15:20:49 (tts.sh:468:main) Stage 3: Remove long/short data: dump/raw/org -> dump/raw
utils/copy_data_dir.sh: copied data from dump/raw/org/tr_no_dev to dump/raw/tr_no_dev
utils/validate_data_dir.sh: WARNING: you have only one speaker.  This probably a bad idea.
   Search for the word 'bold' in http://kaldi-asr.org/doc/data_prep.html
   for more information.
utils/validate_data_dir.sh: Successfully validated data-directory dump/raw/tr_no_dev
fix_data_dir.sh: kept all 12600 utterances.
fix_data_dir.sh: old files are kept in dump/raw/tr_no_dev/.backup
utils/copy_data_dir.sh: copied data from dump/raw/org/dev to dump/raw/dev
utils/validate_data_dir.sh: WARNING: you have only one speaker.  This probably a bad idea.
   Search for the word 'bold' in http://kaldi-asr.org/doc/data_prep.html
   for more information.
utils/validate_data_dir.sh: Successfully validated data-directory dump/raw/dev
fix_data_dir.sh: kept all 250 utterances.
fix_data_dir.sh: old files are kept in dump/raw/dev/.backup
2025-02-17T15:20:55 (tts.sh:523:main) Stage 4: Generate token_list from data/tr_no_dev/text
[nltk_data] Downloading package averaged_perceptron_tagger to
[nltk_data]     /root/nltk_data...
[nltk_data]   Unzipping taggers/averaged_perceptron_tagger.zip.
[nltk_data] Downloading package cmudict to /root/nltk_data...
[nltk_data]   Unzipping corpora/cmudict.zip.
/usr/bin/python3 /work/espnet/espnet2/bin/tokenize_text.py --token_type phn -f 2- --input dump/raw/srctexts --output dump/token_list/phn_tacotron_g2p_en_no_space/tokens.txt --non_linguistic_symbols none --cleaner tacotron --g2p g2p_en_no_space --write_vocabulary true --add_symbol '<blank>:0' --add_symbol '<unk>:1' --add_symbol '<sos/eos>:-1'
2025-02-17 15:21:10,513 (tokenize_text:174) INFO: OOV rate = 0.0 %
2025-02-17T15:21:10 (tts.sh:907:main) Skip training stages
2025-02-17T15:21:10 (tts.sh:912:main) Use imdanboy/jets for decoding and evaluation
(…)2p_en_no_space%2Ftrain%2Fpitch_stats.npz: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 770/770 [00:00<00:00, 266kB/s]
(…)e%2Fimages%2Fdiscriminator_fake_loss.png: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 75.2k/75.2k [00:00<00:00, 4.79MB/s]
(…)p_en_no_space%2Ftrain%2Fenergy_stats.npz: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 770/770 [00:00<00:00, 238kB/s]
(…)images%2Fdiscriminator_backward_time.png: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 73.7k/73.7k [00:00<00:00, 5.66MB/s]
README.md: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 11.6k/11.6k [00:00<00:00, 4.72MB/s]
(…)n_tacotron_g2p_en_no_space%2Fconfig.yaml: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 9.54k/9.54k [00:00<00:00, 3.15MB/s]
(…)2p_en_no_space%2Ftrain%2Ffeats_stats.npz: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1.40k/1.40k [00:00<00:00, 481kB/s]
.gitattributes: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1.17k/1.17k [00:00<00:00, 426kB/s]
(…)ages%2Fdiscriminator_optim_step_time.png: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 36.0k/36.0k [00:00<00:00, 11.1MB/s]
(…)_space%2Fimages%2Fdiscriminator_loss.png: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 75.1k/75.1k [00:00<00:00, 16.8MB/s]
(…)%2Fimages%2Fdiscriminator_train_time.png: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 74.1k/74.1k [00:00<00:00, 19.7MB/s]
(…)es%2Fgenerator_align_forwardsum_loss.png: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 32.7k/32.7k [00:00<00:00, 26.1MB/s]
(…)%2Fimages%2Fgenerator_align_bin_loss.png: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 32.8k/32.8k [00:00<00:00, 23.1MB/s]
(…)pace%2Fimages%2Fgenerator_align_loss.png: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 31.7k/31.7k [00:00<00:00, 23.5MB/s]
(…)e%2Fimages%2Fgenerator_backward_time.png: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 76.1k/76.1k [00:00<00:00, 29.8MB/s]
(…)ce%2Fimages%2Fgenerator_forward_time.png: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 27.8k/27.8k [00:00<00:00, 25.9MB/s]
(…)Fimages%2Fdiscriminator_forward_time.png: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 76.3k/76.3k [00:00<00:00, 533kB/s]
(…)e%2Fimages%2Fdiscriminator_real_loss.png: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 75.7k/75.7k [00:00<00:00, 505kB/s]
(…)pace%2Fimages%2Fgenerator_g_adv_loss.png: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 65.5k/65.5k [00:00<00:00, 45.3MB/s]
(…)no_space%2Fimages%2Fgenerator_g_loss.png: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 36.8k/36.8k [00:00<00:00, 40.3MB/s]
(…)pace%2Fimages%2Fgenerator_g_mel_loss.png: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 33.9k/33.9k [00:00<00:00, 28.1MB/s]
(…)images%2Fgenerator_g_feat_match_loss.png: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 44.5k/44.5k [00:00<00:00, 21.5MB/s]
(…)n_no_space%2Fimages%2Fgenerator_loss.png: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 33.2k/33.2k [00:00<00:00, 28.5MB/s]
(…)2Fimages%2Fgenerator_optim_step_time.png: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 39.0k/39.0k [00:00<00:00, 35.3MB/s]
(…)2Fimages%2Fgenerator_var_energy_loss.png: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 31.1k/31.1k [00:00<00:00, 27.1MB/s]
(…)ce%2Fimages%2Fgenerator_var_dur_loss.png: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 39.5k/39.5k [00:00<00:00, 30.8MB/s]
(…)ace%2Fimages%2Fgpu_max_cached_mem_GB.png: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 32.8k/32.8k [00:00<00:00, 22.4MB/s]
(…)_space%2Fimages%2Fgenerator_var_loss.png: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 30.9k/30.9k [00:00<00:00, 10.4MB/s]
(…)%2Fimages%2Fgenerator_var_pitch_loss.png: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 35.4k/35.4k [00:00<00:00, 14.0MB/s]
(…)pace%2Fimages%2Fgenerator_train_time.png: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 30.3k/30.3k [00:00<00:00, 33.5MB/s]
(…)2p_en_no_space%2Fimages%2Foptim0_lr0.png: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 25.2k/25.2k [00:00<00:00, 22.6MB/s]
(…)2p_en_no_space%2Fimages%2Foptim1_lr0.png: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 25.0k/25.0k [00:00<00:00, 19.9MB/s]
(…)2p_en_no_space%2Fimages%2Ftrain_time.png: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 36.2k/36.2k [00:00<00:00, 27.2MB/s]
(…)g2p_en_no_space%2Fimages%2Fiter_time.png: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 85.4k/85.4k [00:00<00:00, 592kB/s]
meta.yaml: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 326/326 [00:00<00:00, 319kB/s]
train.total_count.ave_5best.pth: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 334M/334M [00:09<00:00, 33.6MB/s]
Fetching 36 files: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 36/36 [00:12<00:00,  2.90it/s]
2025-02-17T15:21:24 (tts.sh:933:main) Stage 7: Decoding: training_dir=exp/imdanboy/jets
2025-02-17T15:21:24 (tts.sh:956:main) Generate 'exp/imdanboy/jets/decode_train.loss.ave/run.sh'. You can resume the process from stage 7 using this script
2025-02-17T15:21:25 (tts.sh:1010:main) Decoding started... log: 'exp/imdanboy/jets/decode_train.loss.ave/dev/log/tts_inference.*.log'

2025-02-17T15:30:33 (tts.sh:1010:main) Decoding started... log: 'exp/imdanboy/jets/decode_train.loss.ave/eval1/log/tts_inference.*.log'

2025-02-17T15:40:02 (tts.sh:1180:main) Skip the uploading stage
2025-02-17T15:40:02 (tts.sh:1232:main) Skip the uploading to HuggingFace stage
2025-02-17T15:40:02 (tts.sh:1235:main) Successfully finished. [elapsed=1741s]

JETSの学習

学習をする場合は、以下のコマンドにて一から学習を開始できます

./run.sh --train_config conf/tuning/train_jets.yaml --tts_task gan_tts --stage 1 --stop_stage 7 --ngpu 1

トレーニングのパラメータを引数から変更したい場合は、以下を付けます

--train_args "--max_epoch 1 --num_iters_per_epoch 30"