DNSMOSで音声評価を行う

初めに

AI声づくり技術研究会 アドカレ 12日目です。

今回は 音声合成におけるデータセット作成の一つの重要な要素である データセットの音声評価について 比較的新しめの評価手法およびライブラリを触っていきます。 今回触っていくのは、 DNSMOSという マイクロソフトが開発したものになっています。

過去にいくつかの音声データの評価についてのライブラリを触った記事を書いているので、ほかのライブラリも気になる場合はご覧ください。

ayousanz.hatenadiary.jp

ayousanz.hatenadiary.jp

ayousanz.hatenadiary.jp

以下にて今回の記事の内容をそのまま公開したリポジトリがあります。音声ファイルは各自でダウンロードして配置してください

github.com

開発環境

DNSMOSについて

DNSMOSは、ノイズ抑制アルゴリズムの性能を評価するための非侵襲的な音声品質評価指標です。人間の主観的評価(MOSスコア)と高い相関を持つよう設計されており、参照となるクリーンな音声が不要で、実際の録音環境での評価が可能です。これにより、ノイズ抑制の効果を客観的かつ効率的に測定できます。

環境構築

git clone https://github.com/ayutaz/dnsmos.git
cd dnsmos
uv sync

音声ファイルの評価

任意の音声を評価することができます

今回は、つくよみちゃんコーパス│声優統計コーパス(JVSコーパス準拠)を使って評価を行います。

tyc.rei-yumesaki.net

また 公式からモデルを取得・配置します

以下のような配置を行います (記事で使用しているリポジトリには モデルは含まれています)

dnsmos/
├── .venv/
├── DNSMOS/
│   ├── model_v8.onnx
│   └── sig_bak_ovr.onnx
├── test(音声ファイル)
├── .gitignore
├── .python-version
├── dnsmos_local.py
├── LICENSE
├── pyproject.toml
├── README.md
├── sample.csv
└── uv.lock

音声とモデルを配置してから音声評価処理を実行します

python .\dnsmos_local.py -t .\test\ -o sample.csv 

もしくは

uv run .\dnsmos_local.py -t .\test\ -o sample.csv 

引数の -t でフォルダパスの指定、-o で出力する評価データ名を指定します。

また -pをつけることで、干渉話者(周囲の話者やバックグラウンドの音声)がいる場合に、それらの影響をペナルティとして反映させることができます。音声内で話しているメインの話者以外が入っている場合にスコアが低くなります。

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

WindowsでFreeze-Omniを動かす

初めに

speech to speechのライブラリの Freeze-Omniを動かしていきます。これは従来のような speech to text(STT) → text to text(LLM) → text to speech(STT)ではなく、speech to speechを実現しているものになります

処理の流れは以下のようになっています

github.com

開発環境

セットアップ

uv環境を作成します

uv venv -p 3.10
.venv\Scripts\activate

次にライブラリをインストールします

uv pip install --upgrade pip
uv pip install -r requirements.txt
uv pip install torch==2.2.0 --index-url https://download.pytorch.org/whl/cu121 --force-reinstall
uv pip install torchaudio==2.2.0 --index-url https://download.pytorch.org/whl/cu121

次にモデルをダウンロードします

 huggingface-cli download --repo-type model VITA-MLLM/Freeze-Omni --local-dir .
 huggingface-cli download --repo-type model Qwen/Qwen2-7B-Instruct --local-dir Qwen2-7B-Instruct

音声ファイルから推論

以下で指定して音声ファイルを使って推論を行います

python bin\inference.py --model_path .\checkpoints --input_wav .\assets\question.wav --output_wav .\assets\answer.wav --llm_path .\Qwen2-7B-Instruct --top_p 0.8 --top_k 20 --temperature 0.8

リアルタイムのインタラクティブDemoを動かす

まずはサーバーを起動します

python bin\server.py --ip 127.0.0.1 --port 8009 --max_users 3 --llm_exec_nums 1 --timeout 180 --model_path .\checkpoints --llm_path .\Qwen2-7B-Instruct --top_p 0.8 --top_k 20 --temperature 0.8

サーバーが起動すると https://127.0.0.1:8009/ にアクセスすることで、以下の対話画面を表示することができます

ボタンを教えて話しかけると以下のような感じで動きを確認することができます

VRAMはかなり使っています

sudachipyを使って文章をひらがなに変換する

初めに

TTSの学習のひとつで、文字列をすべてひらがなにしたい場合があります。その際に簡単に使える sudachiを使って処理をしてみます

以下にサンプルリポジトリを公開しています

github.com

開発環境

セットアップ

uv pip install sudachipy sudachidict-core

ひらながに変換

今回は、つくよみちゃんコーパス│声優統計コーパス(JVSコーパス準拠)の内容で実行をしてみます

sudachiを使ったファイル読み込みおよび変換コードは以下です

from sudachipy import dictionary, tokenizer

def katakana_to_hiragana(text):
    """カタカナをひらがなに変換する関数"""
    hiragana = ''
    for char in text:
        code = ord(char)
        # カタカナの範囲内の文字をひらがなに変換
        if 0x30A1 <= code <= 0x30FA:
            hiragana += chr(code - 0x60)
        else:
            hiragana += char
    return hiragana

# Sudachiの辞書をロード
tokenizer_obj = dictionary.Dictionary().create()
mode = tokenizer.Tokenizer.SplitMode.C  # 分割モードを指定

# 結果を保存するファイルを開く
with open('sudachi-result.txt', 'w', encoding='utf-8') as f_output:
    # sample.txtを読み込んで処理
    with open('sample.txt', 'r', encoding='utf-8') as f_input:
        for line in f_input:
            line = line.strip()
            if not line:
                continue
            if ':' in line:
                identifier, script = line.split(':', 1)
                readings = []
                for token in tokenizer_obj.tokenize(script, mode):
                    yomi = token.reading_form()
                    readings.append(yomi)
                katakana_text = ''.join(readings)
                hiragana_text = katakana_to_hiragana(katakana_text)
                output_line = f"{identifier}:{hiragana_text}\n"
                f_output.write(output_line)
            else:
                f_output.write(line + '\n')

以下のようなフォーマットの sample.txt がルートパスにあることを想定しています

VOICEACTRESS100_001:また、東寺のように、五大明王と呼ばれる、主要な明王の中央に配されることも多い。
VOICEACTRESS100_002:ニューイングランド風は、牛乳をベースとした、白いクリームスープであり、ボストンクラムチャウダーとも呼ばれる。
VOICEACTRESS100_003:コンピュータゲームのメーカーや、業界団体などに関連する人物のカテゴリ。

こちらを処理をすることで以下のような結果になります

VOICEACTRESS100_001:また、とうじのように、ごだいみょうおうとよばれる、しゅようなみょうおうのちゅうおうにはいされることもおおい。
VOICEACTRESS100_002:にゅーいんぐらんどふうは、ぎゅうにゅうをべーすとした、しろいくりーむすーぷであり、ぼすとんくらむちゃうだーともよばれる。
VOICEACTRESS100_003:こんぴゅーたげーむのめーかーや、ぎょうかいだんたいなどにかんれんするじんぶつのかてごり。

辞書の変更

sudachiの辞書は3つあります。

  • core
  • small
  • full

今回は 一番語彙が多い full を使ってみます

まずはインストールをします

uv pip install sudachidict_full

モードを指定する際に辞書を指定します

# Sudachiの辞書をロードし、full辞書を使用するように指定
tokenizer_obj = dictionary.Dictionary(dict="full").create()
mode = tokenizer.Tokenizer.SplitMode.C  # 分割モードを指定

github.com

ここで core と full での精度の比較を行います。今回は、以下のようなdiffコードを作って実行しました。

import difflib

def compare_results(core_file, full_file, diff_output_file):
    # ファイルを読み込みます
    with open(core_file, 'r', encoding='utf-8') as f_core:
        core_lines = f_core.readlines()
    with open(full_file, 'r', encoding='utf-8') as f_full:
        full_lines = f_full.readlines()
    
    # 結果を保存するファイルを開く
    with open(diff_output_file, 'w', encoding='utf-8') as f_output:
        # 行数のチェック
        max_lines = max(len(core_lines), len(full_lines))
        
        for i in range(max_lines):
            core_line = core_lines[i].strip() if i < len(core_lines) else ''
            full_line = full_lines[i].strip() if i < len(full_lines) else ''
            
            # 識別子と台本部分を分離
            core_identifier, core_script = (core_line.split(':', 1) + [''])[:2]
            full_identifier, full_script = (full_line.split(':', 1) + [''])[:2]
            
            # 台本部分を比較
            if core_script != full_script:
                f_output.write(f"--- 差分が見つかりました(行 {i+1}) ---\n")
                f_output.write(f"識別子: {core_identifier}\n")
                f_output.write(f"【Core辞書の結果】\n")
                f_output.write(core_script + '\n')
                f_output.write(f"【Full辞書の結果】\n")
                f_output.write(full_script + '\n')
                f_output.write(f"【差分】\n")
                # 差分を表示(文字単位での比較)
                diff = difflib.ndiff(core_script, full_script)
                f_output.write(''.join(diff) + '\n\n')
            else:
                pass
                # f_output.write(f"行 {i+1}({core_identifier}): 差分はありません。\n")

# 比較するファイルのパス
core_file = 'sudachi-result.txt'
full_file = 'sudachi-full-result.txt'
diff_output_file = 'diff_results.txt'

compare_results(core_file, full_file, diff_output_file)

差分は以下です

--- 差分が見つかりました(行 9) ---
識別子: VOICEACTRESS100_009
【Core辞書の結果】
またねじめしは、なかやまおうのおさめる、りゅうきゅうおうこくとのこうえきにもさんかした。
【Full辞書の結果】
またねじめしは、ちゅうざんおうのおさめる、りゅうきゅうおうこくとのこうえきにもさんかした。
【差分】
  ま  た  ね  じ  め  し  は  、- な- か- や- ま+ ち+ ゅ+ う+ ざ+ ん  お  う  の  お  さ  め  る  、  り  ゅ  う  き  ゅ  う  お  う  こ  く  と  の  こ  う  え  き  に  も  さ  ん  か  し  た  。

--- 差分が見つかりました(行 27) ---
識別子: VOICEACTRESS100_027
【Core辞書の結果】
ちょういきにあった、みねやまはんは、ながおかはんに、べいひゃくひょうをおくったことでゆうめい。
【Full辞書の結果】
ちょういきにあった、みねやまはんは、ながおかはんに、こめひゃっぴょうをおくったことでゆうめい。
【差分】
  ち  ょ  う  い  き  に  あ  っ  た  、  み  ね  や  ま  は  ん  は  、  な  が  お  か  は  ん  に  、- べ- い+ こ+ め  ひ  ゃ- く- ひ+ っ+ ぴ  ょ  う  を  お  く  っ  た  こ  と  で  ゆ  う  め  い  。

WeSpeakerで音声データ内の話者分離および話者数の特定を行う

初めに

最近公開された WeSpeakerを使って 音声データでいろいろ行っていきます。今回は、音声データ内の話者分離および話者数の特定を行います

github.com

以下にてサンプルリポジトリを公開しています

https://github.com/ayutaz/wespeakergithub.com

開発環境

セットアップ

以下で必要なライブラリをインストールします

uv pip install git+https://github.com/wenet-e2e/wespeaker.git

また依存ライブラリを入れていきます( 公式のRepositoryを参照)

fire==0.4.0
kaldiio==2.17.2
numpy==1.22.4
PyYAML==6.0
scipy==1.10.0
tableprint==0.9.1
torchnet==0.0.4
tqdm==4.66.3
scikit-learn
matplotlib==3.5.1
flake8==3.8.2
flake8-bugbear
flake8-comprehensions
flake8-executable
flake8-pyi==20.5.0
mccabe
h5py
pycodestyle==2.6.0
pyflakes==2.2.0
lmdb==1.3.0
onnxruntime
soundfile
pypeln==0.4.9
silero-vad
pre-commit==3.5.0
s3prl
hdbscan==0.8.37
umap-learn==0.5.6

話者分離の実行

以下のスクリプトを実行することで、音声データ内の話者分離および話者数の特定を行うことができます

実行コマンドは以下です

python .\diarization.py .\test.wav

コードは以下になります

import argparse
import os
import sys
import wespeaker

def main():
    # コマンドライン引数の解析
    parser = argparse.ArgumentParser(description='WeSpeakerによる話者ダイアライゼーション')
    parser.add_argument('audio_file', type=str, help='解析する音声ファイルのパス')
    parser.add_argument('--language', type=str, default='english', choices=['english', 'chinese'], help='モデルの言語を指定')
    parser.add_argument('--device', type=str, default='cpu', help='使用するデバイスを指定 (例: cpu, cuda:0, mps)')

    args = parser.parse_args()

    audio_file = args.audio_file
    language = args.language
    device = args.device

    # 音声ファイルの存在確認
    if not os.path.isfile(audio_file):
        print(f"エラー: 指定されたファイルが見つかりません: {audio_file}")
        sys.exit(1)

    # モデルのロード
    model = wespeaker.load_model(language)

    # デバイスの指定
    model.set_device(device)

    # 話者ダイアライゼーションの実行
    diarization_result = model.diarize(audio_file)

    # ダイアライゼーション結果の確認
    print("ダイアライゼーション結果の内容:")
    print(diarization_result)

    # 結果の表示
    for segment in diarization_result:
        # タプルから要素をアンパック(4つの要素に対応)
        spk_id, stime, etime, cluster_id = segment
        print(f"話者ID: {spk_id}, 開始: {stime}秒, 終了: {etime}秒, クラスターID: {cluster_id}")

    # 話者数の特定
    speaker_ids = set(cluster_id for _, _, _, cluster_id in diarization_result)
    print(f"\n推定される話者数: {len(speaker_ids)}")

if __name__ == '__main__':
    main()

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

  warn(
ダイアライゼーション結果の内容:
[('unk', 0.2, 0.7, 0), ('unk', 1.1, 2.3, 0), ('unk', 2.9, 4.5, 0)]
話者ID: unk, 開始: 0.2秒, 終了: 0.7秒, クラスターID: 0
話者ID: unk, 開始: 1.1秒, 終了: 2.3秒, クラスターID: 0
話者ID: unk, 開始: 2.9秒, 終了: 4.5秒, クラスターID: 0

推定される話者数: 1

UnityでDrawLineを使って擬似的にスフィアを描画する

初めに

Unityでrayの当たり判定でデバッグをする際に Gizmoや Drawlineを使って行うことが多々あります。今回は Gizmoが使えない(MonoBehaviourを継承していない or 更新関数を OnDrawGizmosまで伝播できない)場合の方法として Drawlineを使ったスフィア実装を行なってみます

Demo

以下でリポジトリを公開しています

開発環境

  • Unity 2022.3.x

実装

Cos,Sinを使って円形を擬似的に作成して描画をしています

    private void DrawDebugSphere(Vector3 position, float radius, Color color, int segments = 12)
    {
        // 経度方向の円(水平)
        for(int i = 0; i < segments; i++)
        {
            float theta1 = (i * Mathf.PI * 2) / segments;
            float theta2 = ((i + 1) * Mathf.PI * 2) / segments;

            Vector3 pos1 = new Vector3(radius * Mathf.Cos(theta1), 0, radius * Mathf.Sin(theta1)) + position;
            Vector3 pos2 = new Vector3(radius * Mathf.Cos(theta2), 0, radius * Mathf.Sin(theta2)) + position;

            Debug.DrawLine(pos1, pos2, color);
        }

        // 緯度方向の円(垂直)
        for(int i = 0; i < segments; i++)
        {
            float theta1 = (i * Mathf.PI * 2) / segments;
            float theta2 = ((i + 1) * Mathf.PI * 2) / segments;

            Vector3 pos1 = new Vector3(0, radius * Mathf.Cos(theta1), radius * Mathf.Sin(theta1)) + position;
            Vector3 pos2 = new Vector3(0, radius * Mathf.Cos(theta2), radius * Mathf.Sin(theta2)) + position;

            Debug.DrawLine(pos1, pos2, color);
        }

        // 斜め方向の円(経線)
        for(int i = 0; i < segments; i++)
        {
            float theta1 = (i * Mathf.PI * 2) / segments;
            float theta2 = ((i + 1) * Mathf.PI * 2) / segments;

            Vector3 pos1 = new Vector3(radius * Mathf.Cos(theta1) * Mathf.Sin(Mathf.PI / 4), radius * Mathf.Cos(theta1) * Mathf.Cos(Mathf.PI / 4), radius * Mathf.Sin(theta1)) + position;
            Vector3 pos2 = new Vector3(radius * Mathf.Cos(theta2) * Mathf.Sin(Mathf.PI / 4), radius * Mathf.Cos(theta2) * Mathf.Cos(Mathf.PI / 4), radius * Mathf.Sin(theta2)) + position;

            Debug.DrawLine(pos1, pos2, color);
        }
    }

ローカルでsmollmで動画の内容に関する推論を行う

初めに

昨日以下のLLMを動かしました。

ayousanz.hatenadiary.jp

昨日のPRで動画の推論コードがマージされたので、触ってみます

github.com

開発環境

  • Windows
  • uv
  • python 3.11
  • smollm[85a4eb2dd5dd0eb4e116264f1853ae259846a957]

セットアップ

ライブラリをインストールします

uv pip install transformers
uv pip install torch --index-url https://download.pytorch.org/whl/cu121
uv pip install pillow opencv-python

動画推論

動画は以下を使います

結果は以下になります

Question: Describe the video
Response: User: Answer briefly.<image>Describe the video
Assistant: A green and white airplane is parked on a runway. A group of people are walking towards the airplane. A person is walking away from the airplane. The airplane is parked on a runway. The airplane is parked on a runway. The airplane is parked on a runway. The airplane is parked on a runway. The airplane is parked on a runway. The airplane is parked on a runway. The airplane is parked on a runway. The airplane is parked on a runway. The airplane is parked on a

ローカルでsmollmを動かす

初めに

小さくて精度が高いモデルが出たので、触っていきます。TRL CLIを使ってチャットもできるみたいなのでこちらも触ってみます

github.com

開発環境

  • smollm[1e43f0dcd76261d4317cc2ee7258fe67d151d082]
  • Window
  • uv

セットアップ

まずはuvで環境を作ります

uv venv -p 3.11
.venv\Scripts\activate 

ライブラリをインストールします

uv pip install transformers
uv pip install torch --index-url https://download.pytorch.org/whl/cu121

デフォルトの質問

以下の公式のコードで動かしてみます

from transformers import AutoModelForCausalLM, AutoTokenizer
checkpoint = "HuggingFaceTB/SmolLM2-1.7B-Instruct"

device = "cuda" # for GPU usage or "cpu" for CPU usage
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
# for multiple GPUs install accelerate and do `model = AutoModelForCausalLM.from_pretrained(checkpoint, device_map="auto")`
model = AutoModelForCausalLM.from_pretrained(checkpoint).to(device)

messages = [{"role": "user", "content": "Write a 100-word article on 'Benefits of Open-Source in AI research"}]
input_text=tokenizer.apply_chat_template(messages, tokenize=False)
inputs = tokenizer.encode(input_text, return_tensors="pt").to(device)
outputs = model.generate(inputs, max_new_tokens=50, temperature=0.2, top_p=0.9, do_sample=True)
print(tokenizer.decode(outputs[0]))

結果は以下です

` to obtain reliable results.
<|im_start|>system
You are a helpful AI assistant named SmolLM, trained by Hugging Face<|im_end|>
<|im_start|>user
Write a 100-word article on 'Benefits of Open-Source in AI research<|im_end|>
<|im_start|>assistant
Open-source in AI research offers numerous benefits. Firstly, it fosters collaboration and community engagement, accelerating the development of AI technologies. Secondly, it promotes transparency and accountability, as open-source projects are subject to peer review and scrutiny

日本語での質問および回答は以下です

<|im_start|>system
You are a helpful AI assistant named SmolLM, trained by Hugging Face<|im_end|>
<|im_start|>user
まどマギで一番可愛いキャラクターは誰ですか?<|im_end|>
<|im_start|>assistant
私は日本語ではありませんが、この問題を解くためには、マギで一番可愛いキャラクターは誰ですか?という問題を解くためには、英語で書かれた文章が必要です。これは、「What is the most beautiful character in Magi?」という問題です。これは、アニメ、アニメゲーム、アニメアニメなどのゲームにおけるキャラクターという役割を担っている人物が、最も可愛いキャラクターとして見てもらえる

日本語はだめそうです