0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ローカルLLMの回答を好きな声で読み上げさせたい1

Last updated at Posted at 2025-08-16

はじめに

最近、ちょっと時間ができたので久しぶりにプログラミングでも…と思ったんですが、AIがここまで発達すると、プログラムを一から書く価値が薄れてきたように感じます。(モチベーション低下中)

じゃあ、AI自体を使って何か作れないかなと思い、ChatGPTじゃなできないこととして、コンプラに縛らなれないAIの回答が好きな音声で返ってきたら自由だなと考えたのが発端です。
How Toではなく、私が実際にやった作業の記録として残す形にしています。
(HowToは先人達が詳しく書いているので、そちらをご参照ください)

以下のようなステップで進めていきます。

  1. 音声モデルの作成・準備・読み上げ(TTS:Text-to-Speech)
  2. ローカルLLMの準備とAivisAPIとの連携(LM StudioAPI→AivisAPI)
  3. UIの変更(Pythonでの待ち受け方式からWeb画面での入力方式に)
  4. 音声認識とLLMのChatGPT API利用に変更
  5. 課題改善

環境

今回使ったPCのスペックはこんな感じです
3年前に25万程度で購入したノートPCのため時代遅れ感があります。
💻 CPU:AMD Ryzen 7 5800H(8コア/16スレッド)
🎮 GPU:NVIDIA GeForce RTX 3060 Laptop GPU + AMD Radeon 内蔵
🧠 メモリ:16 GB
💾 ストレージ:SSD 512GB

音声モデルの作成

まずは音声学習から。
ChatGPTに聞いたところ「ESPnet」というツールで実装できるとのこと。
ただ、実際に試してみるとLinux環境前提で、WindowsだとLinuxのエミュレータのようなもの(WSL)を入れる必要があり、少し面倒でした。
(GIT Bashで無理やり動かそうとしましたが、シェルコマンドの違いなどでソースから書き直す必要があり断念…)

他に色々調べてみたら、「Style-Bert-VITS2 JP-Extra 」というものがわかりやすそう。
https://zenn.dev/litagin/articles/034819a5256ff4

  • バッチ1つで環境構築完了
  • 操作はほぼGUIで完結
  • 日本語チュートリアル動画まである

まだまだ調べ物はAIよりマニアックな人のブログが頼りになりますね。
(AIに頼ってたら1日半無駄にしました)

実際にモデル作成では、ゲームなどから素材(音声データ)を用意し、チュートリアル通りに学習させました。
最新バージョンはチュートリアル動画より若干差がありますが、理解できる範疇です。

最小学習量で試してみたところ問題なく動作しましたが、効果はほぼ実感できず。
学習量を増やすと、1時間経っても2%進捗…さすがに断念しました。

というわけで、学習方法は理解できたので、一旦自作モデルの作成は中止し、公開されているモデルを使うことにしました。

公開されている音声モデルの入手

今は、音声読み上げツールが豊富です。
AmazonやGoogleのような大手だけでなく、VOICEVOXやAivisなどローカルでAPI利用できるものもあります。
https://aivis-project.com/
https://voicevox.hiroshiba.jp/
今回はAivisを使用しました。手順は簡単です

  1. インストールフォルダのAivisSpeech\AivisSpeech-Engine\run.exeを起動

  2. ブラウザからhttp://localhost:10101/speakersを開き、使いたいIDを確認
    image.png

  3. APIにテキストと一緒にIDを渡して読み上げ
    以下、簡単なコードです。0.5秒の余白ですが、これを追加しないと音声の最後がぶつ切り?のようになってしまい違和感ましましだったので追加しました。

import sounddevice as sd
import numpy as np
import json
import io
import soundfile as sf

AI_VOICE_URL_AUDIO_QUERY = "http://127.0.0.1:10101/audio_query"
AI_VOICE_URL_SYNTHESIS = "http://127.0.0.1:10101/synthesis"
STYLE_ID  = 888753760
text = "おはようございます"

all_audio = []

# audio_query
query = requests.post(AI_VOICE_URL_AUDIO_QUERY, params={"text": text, "speaker": STYLE_ID}).json()

# synthesis
resp = requests.post(
    AI_VOICE_URL_SYNTHESIS,
    params={"speaker": STYLE_ID},
    headers={"accept": "audio/wav", "Content-Type": "application/json"},
    data=json.dumps(query),
)

# WAV読み込み
wav_io = io.BytesIO(resp.content)
audio, fs = sf.read(wav_io, dtype='float32')

# 0.5秒の余白を追加
n_channels = audio.shape[1] if audio.ndim > 1 else 1
padding = np.zeros((int(fs * 0.5), n_channels), dtype=np.float32)
audio = np.vstack([audio, padding]) if n_channels > 1 else np.concatenate([audio, padding.flatten()])

all_audio.append(audio)
fs_final = fs
n_channels_final = n_channels

# 再生例
sd.play(audio, fs)
sd.wait()

自分で学習したモデルを使うこともできるので、今後自作モデルを作成した際も活用できそうです。

また、あまり長い文章を渡すとメモリリークが発生しました。
Q. 長い文章を一度に音声合成 API に送ると、音声が不自然になったりメモリリークが発生します。

対策として文章を区切って処理することで、メモリリーク対策だけでなく、処理速度が向上しました。
例では「。」で区切っていますが、改行や「!」などの記号で区切りなども効果的です。

import sounddevice as sd
import numpy as np
import json
import io
import soundfile as sf

AI_VOICE_URL_AUDIO_QUERY = "http://127.0.0.1:10101/audio_query"
AI_VOICE_URL_SYNTHESIS = "http://127.0.0.1:10101/synthesis"
STYLE_ID = 888753760

def synthesize_text(text: str, style_id: int = STYLE_ID, padding_sec: float = 0.5):
    """単一テキストを音声合成してnumpy配列で返す"""
    # audio_query
    query = requests.post(AI_VOICE_URL_AUDIO_QUERY, params={"text": text, "speaker": style_id}).json()

    # synthesis
    resp = requests.post(
        AI_VOICE_URL_SYNTHESIS,
        params={"speaker": style_id},
        headers={"accept": "audio/wav", "Content-Type": "application/json"},
        data=json.dumps(query),
    )

    wav_io = io.BytesIO(resp.content)
    audio, fs = sf.read(wav_io, dtype='float32')

    # チャンネル数
    n_channels = audio.shape[1] if audio.ndim > 1 else 1

    # 0.5秒パディング
    padding = np.zeros((int(fs * padding_sec), n_channels), dtype=np.float32)
    audio = np.vstack([audio, padding]) if n_channels > 1 else np.concatenate([audio, padding.flatten()])

    return audio, fs, n_channels

def synthesize_paragraph(paragraph: str):
    """文章を「。」で区切って順次音声合成し、all_audioに格納"""
    all_audio = []
    fs_final = None
    n_channels_final = None

    # 「。」で分割して空要素は除外
    chunks = [chunk.strip() for chunk in paragraph.split('。') if chunk.strip()]
    for chunk in chunks:
        audio, fs, n_channels = synthesize_text(chunk)
        all_audio.append(audio)
        fs_final = fs
        n_channels_final = n_channels

        # 再生
        sd.play(audio, fs)
        sd.wait()

    return all_audio, fs_final, n_channels_final

# 使用例
text = "おはようございます。今日も元気に行きましょう。頑張ってください。"
all_audio, fs_final, n_channels_final = synthesize_paragraph(text)

音声モデルの準備ができたので、次はローカルLLM環境の構築に進みたいと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?