[LLM] Flaskで簡易 /speak APIを作成

from flask import Flask, request, send_file, jsonify
from gtts import gTTS
import os
import tempfile
from datetime import datetime

app = Flask(__name__)

@app.route('/speak', methods=['POST'])
def speak():
    """
    テキストを音声に変換するAPI
    
    リクエスト例:
    {
        "text": "こんにちは、世界",
        "lang": "ja",
        "slow": false
    }
    """
    try:
        # リクエストからJSONデータを取得
        data = request.get_json()
        
        if not data or 'text' not in data:
            return jsonify({'error': 'text パラメータが必要です'}), 400
        
        text = data['text']
        lang = data.get('lang', 'ja')  # デフォルトは日本語
        slow = data.get('slow', False)  # ゆっくり話すかどうか
        
        if not text.strip():
            return jsonify({'error': 'テキストが空です'}), 400
        
        # 一時ファイルを作成
        temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.mp3')
        temp_filename = temp_file.name
        temp_file.close()
        
        # TTSで音声ファイルを生成
        tts = gTTS(text=text, lang=lang, slow=slow)
        tts.save(temp_filename)
        
        # 音声ファイルを返す
        response = send_file(
            temp_filename,
            mimetype='audio/mpeg',
            as_attachment=True,
            download_name=f'speech_{datetime.now().strftime("%Y%m%d_%H%M%S")}.mp3'
        )
        
        # レスポンス送信後にファイルを削除
        @response.call_on_close
        def cleanup():
            try:
                os.unlink(temp_filename)
            except Exception as e:
                print(f"一時ファイルの削除に失敗: {e}")
        
        return response
        
    except Exception as e:
        return jsonify({'error': str(e)}), 500


@app.route('/speak/languages', methods=['GET'])
def get_languages():
    """サポートされている言語のリストを返す"""
    languages = {
        'ja': '日本語',
        'en': '英語',
        'zh-cn': '中国語(簡体字)',
        'zh-tw': '中国語(繁体字)',
        'ko': '韓国語',
        'es': 'スペイン語',
        'fr': 'フランス語',
        'de': 'ドイツ語',
        'it': 'イタリア語',
        'pt': 'ポルトガル語',
    }
    return jsonify(languages)


@app.route('/health', methods=['GET'])
def health_check():
    """ヘルスチェック用エンドポイント"""
    return jsonify({'status': 'ok'})


if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=5000)

$ curl -X POST http://localhost:5000/speak \ \
-H “Content-Type: application/json” \
-d ‘{“text”:”こんにちは、世界”, “lang”:”ja”}’ \
–output speech.mp3
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 18096 100 18048 100 48 34083 90 –:–:– –:–:– –:–:– 34207

なるほど。textをPOSTしていますが、gptからのレスポンスをそのまま出力しても良さそうですね。。

[TTS] MaAI

$ sudo apt install portaudio19-dev
$ pip3 install maai

import time
from maai import Maai, MaaiOutput, MaaiInput 
import numpy as np

# 1. 設定
# マイクを使用するため、ファイルパスはコメントアウト
# AUDIO_FILE_PATH = "sample.wav" 

# maai の初期化設定
# 安定動作が確認された "vap" モードを使用
maai = Maai(
    mode="vap", 
    lang="jp",  # 日本語設定
    frame_rate=10, 
    context_len_sec=5,
    
    # マイク入力を使用
    audio_ch1=MaaiInput.Mic(), 
    audio_ch2=MaaiInput.Zero(), 
    device="cpu"
)

# 2. 結果出力設定(ConsoleBarは使用せず、直接値を表示)
print("--- maai リアルタイム予測開始 (マイク入力) ---")
print("マイクに向かって何か話してください。予測結果が直接出力されます。Ctrl+Cで停止します。")
print("---------------------------------------------")
maai.start()

start_time = time.time() # 時間計測開始

# 3. 処理ループ
try:
    while True:
        # 結果を待機して取得
        result = maai.get_result()
        
        if result is None:
            continue
            
        # 💡 リスト型対応の処理: result の値がリストの場合、最初の要素 [0] を抽出する
        processed_result = {}
        for key, value in result.items():
            if isinstance(value, list) and value:
                # リストの最初の要素を抽出
                processed_result[key] = value[0]
            else:
                # リストではないか、空のリストの場合はそのまま使用
                processed_result[key] = value

        # 頷き/相槌の値を取得 (キーが "nod" や "aizuchi" になっていることを期待)
        nod_val = processed_result.get('nod', processed_result.get('p_nod', -1)) 
        aizuchi_val = processed_result.get('aizuchi', processed_result.get('p_aizuchi', -1)) 
        
        # ターンテイキングの値を取得
        p_now_val = processed_result.get('p_now', -1)
        p_future_val = processed_result.get('p_future', -1)
        
        output_line = f"Time: {time.time() - start_time:.2f}s | "
        
        # 頷き/相槌の予測値が含まれているか確認
        if nod_val != -1 or aizuchi_val != -1:
             # 頷き/相槌の予測がある場合の出力
            output_line += f"Nod: {nod_val:.3f} | Aizuchi: {aizuchi_val:.3f} | P_Now: {p_now_val:.3f}"
        else:
            # 含まれていない場合のデフォルト出力(ターンテイキング予測のみ)
            output_line += f"P_Now: {p_now_val:.3f} | P_Future: {p_future_val:.3f}"
            
        print(output_line)
        
        time.sleep(0.1) # 100ms ごとに結果を出力

except KeyboardInterrupt:
    print("\n処理を中断しました。")

# 4. 終了処理
maai.stop()
print("\n--- maai 処理終了 ---")

画面収録 2025-10-25 午前11.38.11

[TTS] Rule-basedとNeural TTSの違い

TTS(Text-to-Speech)は大きく分けると、Rule-based(ルールベース)TTS と Neural(ニューラル)TTS の2種類に分類される

全体像:TTS技術の進化段階
世代 技術名 代表方式 特徴
第1世代 Rule-based TTS 記号変換・音声ルール合成 機械的・不自然だが制御しやすい
第2世代 Statistical Parametric TTS HMMなど 統計的だが声がややロボット的
第3世代 Neural TTS(Deep Learning TTS) Tacotron, WaveNetなど 人間のように自然な音声

① Rule-based TTS(ルールベース音声合成)
人間が手作業で定義した「発音ルール」「音声単位(音素)」をもとに、
音を組み合わせて音声を作る仕組みです。
テキスト → 発音記号(ルールで変換) → 音声単位をつなげて波形生成

方式 説明
Formant synthesis(フォルマント合成) 声帯や口の共鳴特性を数式モデル化(例:Bell Labsの技術)
Concatenative synthesis(連結合成) 実際の録音音声(単語や音素)を切り貼りして繋ぐ方式(例:初期のナビ音声)

🎙 特徴
✅ メモリ・計算コストが低い
✅ 特定の発音やイントネーションを細かく制御できる
❌ 音のつなぎ目が不自然(滑らかさがない)
❌ 抑揚や感情表現が単調・ロボット的

② Neural TTS(ニューラル音声合成)
🧠 仕組み
深層学習モデル(ディープニューラルネットワーク)が
テキスト→音声波形 の変換を「学習」

主な代表モデル
モデル 内容
Tacotron / Tacotron2 テキスト→メルスペクトログラムをSeq2Seqで生成。自然なイントネーション。
FastSpeech / FastSpeech2 Tacotronを改良し、高速かつ安定。
WaveNet / HiFi-GAN / DiffWave 高品質なボコーダ(波形生成)。人間に近い音質。

🎙 特徴
✅ 自然な抑揚・滑らかさ・感情表現
✅ 大量データを使えば「人の声をそっくり再現」できる
✅ マルチスピーカー・多言語対応が容易
❌ 計算コストが高く、学習に大規模データが必要
❌ 「声のなりすまし」などの倫理リスクがある

🎯 Rule-based vs Neural TTS 比較表
比較項目 Rule-based TTS Neural TTS
生成方法 手作りルール・辞書ベース ディープラーニングによる学習ベース
音声の自然さ 機械的・単調 滑らかで人間的
柔軟性(声質・感情) 制御しやすいが不自然 データ次第で柔軟・感情表現可
開発コスト 小規模でも可能 大量データとGPUが必要
リアルタイム性 軽量・高速 モデルによる(最近はリアルタイム可能)
代表的技術 Formant, Concatenative Tacotron, FastSpeech, WaveNet
応用例 初期のカーナビ、読み上げ機器 音声アシスタント、AIナレーション、音声翻訳

[TTS] Text Normalization → Acoustic Model → Vocoder

[全体像]
テキスト(文字列)

① Text Normalization(前処理)

② Acoustic Model(音響モデル)

③ Vocoder(ボコーダ)

音声波形(実際に聞こえる音)

① Text Normalization(テキスト正規化・前処理)
🔍 目的:
文字データを「読み上げ可能な形」に整えること。
人間が“読む”ときに自然になるよう、TTSに理解できる形式へ変換します。

数字の読み方変換 「2025年」 「にせんにじゅうごねん」
記号・略語展開 「Dr.」 「ドクター」
文の区切り付け 「こんにちは。元気?」 (文境界を明示)
発音表記への変換(G2P: Grapheme→Phoneme) 「東京」 /toːkjoː/

② Acoustic Model(音響モデル)
🔍 目的:
「この文をどう発音するか(音の特徴)」を予測する。
つまり、テキスト(音素列)→メルスペクトログラム を生成します。
📘 メルスペクトログラムとは?
時間 × 周波数の2次元表現で、音の「強さ」「高さ」「声質」を可視化したもの。

音声波形を直接作る代わりに、この中間表現を生成することで、
音の自然さや安定性を確保できます。
💡 Acoustic Modelの役割
要素 内容
発音タイミング 各音素の長さや間(Duration)を決定
抑揚(イントネーション) ピッチ(F0)の変化を予測
音量・エネルギー 声の強弱を再現
声質・感情 声のトーンや感情表現を生成

⚙️ 代表的なモデル構造
モデル名 特徴
Tacotron / Tacotron2 Seq2Seqでメルスペクトログラムを直接生成。自然な抑揚。
FastSpeech / FastSpeech2 Duration情報を明示的に予測し、高速かつ安定。
Glow-TTS / Grad-TTS 正規化フローや拡散モデルで高品質生成。

③ Vocoder(ボコーダ)
🔍 目的:
メルスペクトログラムなどの中間表現を、
実際の波形(音声)に変換 する。
🎧 どうやって波形を作るの?

スペクトログラムを入力として、時間ドメインの音波をサンプル単位で生成。
深層学習モデルが「周波数特性」を復元し、自然な音を再構成します。
💡 代表的なニューラルボコーダ
モデル名 特徴
WaveNet (Google) 最初の高品質ニューラルボコーダ。非常に自然だが遅い。
WaveRNN 軽量・高速でリアルタイム合成可能。
Parallel WaveGAN / HiFi-GAN GANベースで高音質&超高速。現行主流。
DiffWave / WaveGrad 拡散モデルを応用。高品質で滑らか。

補足:3ステップの関係性
ステップ 入力 出力 技術の主役
Text Normalization 文字列 発音記号 or 音素列 言語処理(NLP)
Acoustic Model 音素列 メルスペクトログラム 深層学習(Seq2Seq, Transformerなど)
Vocoder メルスペクトログラム 波形データ 深層生成モデル(WaveNet, GAN, Diffusionなど)

[TTS] TTSとは何か?

TTSとは 「Text-To-Speech(テキスト読み上げ)」 の略で、
簡単に言うと 文字を音声に変換する技術 のことです。

たとえば…
スマホの読み上げ機能で、メッセージやニュースを声で聞ける
カーナビが目的地までの案内を音声で話す
Siri や Google アシスタントが話して答える
これらはすべて TTS(音声合成) の仕組みを使っている。

仕組みのざっくりイメージ
テキスト入力:「こんにちは、今日はいい天気ですね」
音声合成エンジンが文字を読み方に変換
人工的な声で読み上げる音声を生成(自然な抑揚をつける)

最近のTTSはAI技術(特に深層学習)によって、
まるで人間のように自然な話し方ができるようになっている。

「深層学習を使ったTTS」は、文字列→音声の処理を学習ベースで行い、人間らしい抑揚・リズム・声質を自動で作れるようになった技術です。従来より滑らかで自然な発話が可能になり、少ない手作業で多様な話し方や声を生成できるようになった。

仕組み(高レベルのパイプライン)
一般的な現代TTSは大きく3つのステップに分かれる

– テキスト前処理(Frontend)
文字列を音素(発音単位)に変換(G2P = Grapheme→Phoneme)したり、句読点や数字を正しい読みへ変換したりする。

– アコースティックモデル(Acoustic / Spectrogram Generator)
テキスト(音素や言語特徴)を入力に取り、音の時間周波数表現(例:メルスペクトログラム)を生成する。
ここで抑揚(ピッチ)、長さ(音の持続)、強弱などの「話し方」が決まる。

– ボコーダ(Vocoder)
メルスペクトログラムなどの中間表現を受け取り、実際の波形(聞ける音声)を生成する。
深層学習は主に(2)と(3)で活躍

なるほど〜 かなり勉強になりますね。少しずつでも毎日勉強したほうが良さそう…

Pythonで話者分離して、片方の話者の発話を切り抜き

無料のブラウザツールだと、音楽などで、音声とインストラメントを分離することはできるようなのですが、二人が喋っていて、片方の音声に分離することができなかったので、Pythonで実行します。

前準備として、以下の手配が必要
1) ffmpeg, pyannote.audio のインストール
2) Hugging Faceでのaccess token発行(read)およびモデル利用のaccept
3) Hugging Faceでのaccess tokenをコードのHUGGINGFACE_TOKENに埋め込み
4) python3 speaker_separation.py の実行

import subprocess
from pyannote.audio import Pipeline
from pydub import AudioSegment
import os
from collections import defaultdict

# ===== 設定 =====
mp4_file = "video.mp4"
wav_file = "conversation.wav"
output_file = "main_speaker_only.wav"

HUGGINGFACE_TOKEN = "****"  # Hugging Face token

# ===== WAV変換 =====
if os.path.exists(wav_file):
    os.remove(wav_file)

subprocess.run([
    "ffmpeg", "-y", "-i", mp4_file,
    "-vn", "-acodec", "pcm_s16le", "-ar", "16000", "-ac", "1",
    wav_file
], check=True)

# ===== 話者分離 =====
pipeline = Pipeline.from_pretrained(
    "pyannote/speaker-diarization-3.1",
    use_auth_token=HUGGINGFACE_TOKEN
)

diarization = pipeline(wav_file)

# ===== 各話者の合計発話時間を計算 =====
speaker_durations = defaultdict(float)
for turn, _, speaker in diarization.itertracks(yield_label=True):
    speaker_durations[speaker] += turn.end - turn.start

# 発話時間が最も長い話者を自動選択
target_speaker = max(speaker_durations, key=speaker_durations.get)
print("選択された話者:", target_speaker)

# ===== その話者だけ抽出 =====
audio = AudioSegment.from_wav(wav_file)
speaker_segments = [
    audio[int(turn.start * 1000): int(turn.end * 1000)]
    for turn, _, speaker in diarization.itertracks(yield_label=True)
    if speaker == target_speaker
]

if speaker_segments:
    speaker_audio = sum(speaker_segments)
    speaker_audio.export(output_file, format="wav")
    print(f"✅ 保存しました: {output_file}")
else:
    print("⚠️ 対象話者の音声が見つかりませんでした")

Style-Bert-VITS2を試そう

$ mkdir tts
$ cd tts
$ git clone https://github.com/litagin02/Style-Bert-VITS2.git
$ cd Style-Bert-VITS2
$ python3 -m venv venv
$ ls
$ source venv/bin/activate
$ pip3 install “torch<2.4" "torchaudio<2.4" --index-url https://download.pytorch.org/whl/cu118 $ cat requirements.txt [code] # onnxruntime-gpu; sys_platform != 'darwin' [/code] $ pip3 install onnxruntime $ pip3 install -r requirements.txt $ pip3 initialize.py $ pip install style-bert-vits2 soundfile simpleaudio $ sudo apt install libasound2-dev $ pip3 install simpleaudio $ pip3 install style-bert-vits2 soundfile [code] from style_bert_vits2.tts_model import TTSModel from pathlib import Path import soundfile as sf # soundfile は requirements.txt に含まれています # モデルパス model_path = Path("model_assets/jvnv-F1-jp/jvnv-F1-jp_e160_s14000.safetensors") config_path = Path("model_assets/jvnv-F1-jp/config.json") style_vec_path = Path("model_assets/jvnv-F1-jp/style_vectors.npy") model = TTSModel( model_path=model_path, config_path=config_path, style_vec_path=style_vec_path, device="cpu" ) text = "こんにちは!仮想環境でも音声ファイルに保存できます。" # 音声生成 sr, audio = model.infer(text=text) # WAV に保存 sf.write("output.wav", audio, sr) print("output.wav に保存完了") [/code] [audio wav="http://hpscript.com/blog/wp-content/uploads/2025/09/output.wav"][/audio] jvnv-F1-jp/jvnv-F1-jp_e160_s14000.safetensors はStyle-Bert-VITS2 が使う日本語向け音声合成モデル本体 style_vectors.npy:複数の話者や声質の特徴ベクトル(スタイル変換用) スタイルベクトルで 複数話者・声質を切り替えられる pitchで微調整できる。 [code] sr, audio = model.infer( text="こんにちは", style=selected_style, length_scale=1.0, # 1.0が標準、<1で早口、>1でゆっくり noise_scale=0.5, # 音声の自然さ noise_scale_w=0.5, # ピッチ揺れの調整 ) [/code]

mcd(Mel Cepstral Distortion)による音声評価

$ pip3 install librosa

import librosa
import numpy as np

def calculate_mcd(ref_wav_path, syn_wav_path, sr=16000, n_mfcc=25):
    """
    Mel Cepstral Distortion (MCD) を計算する修正版関数
    正規化+フレームごとの距離平均を行います
    """
    # 音声読み込み
    ref, _ = librosa.load(ref_wav_path, sr=sr)
    syn, _ = librosa.load(syn_wav_path, sr=sr)

    # 正規化 (-1〜1 の範囲)
    ref = ref / np.max(np.abs(ref))
    syn = syn / np.max(np.abs(syn))

    # MFCC 抽出(c0 は除外)
    ref_mfcc = librosa.feature.mfcc(y=ref, sr=sr, n_mfcc=n_mfcc)[1:]
    syn_mfcc = librosa.feature.mfcc(y=syn, sr=sr, n_mfcc=n_mfcc)[1:]

    # フレーム数を揃える(短い方に合わせる)
    min_len = min(ref_mfcc.shape[1], syn_mfcc.shape[1])
    ref_mfcc = ref_mfcc[:, :min_len]
    syn_mfcc = syn_mfcc[:, :min_len]

    # フレームごとのユークリッド距離
    distances = np.sqrt(np.sum((ref_mfcc - syn_mfcc)**2, axis=0))

    # MCD 計算式(フレームごとの平均)
    mcd = (10.0 / np.log(10)) * np.sqrt(2.0) * np.mean(distances)

    return mcd

# 使用例
if __name__ == "__main__":
    ref_file = "original.wav"  # 参照音声
    syn_file = "tts.wav"       # 合成音声
    mcd_value = calculate_mcd(ref_file, syn_file)
    print(f"MCD: {mcd_value:.2f} dB")

$ python3 app.py
MCD: 568.47 dB

明らかに大きい