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の回答を好きな声で読み上げさせたい2

Last updated at Posted at 2025-08-16

前回はAivisSpeachAPIを用いた音声の読み上げまで実施しました。
ローカルLLMの回答を好きな声で読み上げさせたい1

今回はローカルでLLMを動かして、その回答をAivisで読み上げたいと思います。

LM Studioの利用

ローカルでLLMを動かすのに、LM Studioを利用します。
簡単にローカルでLLMを動かせる・・・少し感動します。

以下の記事を参考にさせていただきながら進めました。
[メモ] LM Studio でオフライン LLM 実行環境を手に入れる。ついでに Obsidian copilot で呼んでみる

早速APIを使って回答を引き出してみます。

import requests

LM_API_URL = "http://127.0.0.1:1234/v1/chat/completions"
MODEL_NAME = "your-model-name"

def chat_with_lm(text: str, model: str = MODEL_NAME) -> str:
    """LM Studioにテキストを送信して応答を取得"""
    payload = {
        "model": model,
        "messages": [{"role": "user", "content": text}]
    }
    resp = requests.post(LM_API_URL, json=payload)
    resp.raise_for_status()
    return resp.json()["choices"][0]["message"]["content"]

# 使用例
text = "おはようございます"
response = chat_with_lm(text)
print(response)

おはようございます!今日も一日頑張りましょうね。何かお手伝いできることはありますか?😊

LM Studioのログはこちら

image.png

ちゃんと返ってきました。
ちなみにプロンプトは画面右のContextで設定できます。
image.png

今回はGUIで立ちげていますすが、CLI版もありバックグラウンドで待ち受けさせることもできます。

AivisSpeachAPIとの連携

以下のような流れで連携します。

  1. マイクの入力を拾い、テキストに変換する
  2. LM Studioにテキストを渡して返答を取得する
  3. Aivisに返答を渡してしゃべらせる
import sounddevice as sd
import numpy as np
import whisper
import torch
import json
import io
import soundfile as sf

# -----------------------
# 設定
# -----------------------
LM_API_URL = "http://127.0.0.1:1234/v1/chat/completions"
MODEL_NAME = "your-model-name"

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  # Anneli ノーマルなど

# -----------------------
# 録音
# -----------------------
def record_audio_on_voice(threshold=0.01, silence_duration=3, fs=16000, blocksize=1024):
    """
    発話開始で録音し、発話停止後 silence_duration 秒で自動停止
    """
    print("話し始めてください...")
    buffer = []
    silence_time = 0.0
    speaking = False

    with sd.InputStream(channels=1, samplerate=fs, blocksize=blocksize, dtype='float32') as stream:
        while True:
            data, _ = stream.read(blocksize)
            amplitude = np.abs(data).mean()

            if amplitude > threshold:
                speaking = True
                silence_time = 0.0
            elif speaking:
                silence_time += blocksize / fs
                if silence_time >= silence_duration:
                    break  # 無音が続いたら終了

            if speaking:
                buffer.append(data.copy())

    if buffer:
        audio = np.concatenate(buffer, axis=0).flatten()
    else:
        audio = np.zeros(0, dtype='float32')
    print("録音終了")
    return audio

# -----------------------
# Whisperで文字起こし
# -----------------------
# Whisperモデルロード
whisper_model = whisper.load_model("base")
def audio_to_text(audio):
    if len(audio) == 0: return ""
    audio_tensor = torch.from_numpy(audio.astype(np.float32))
    result = whisper_model.transcribe(audio_tensor, fp16=False, language="ja")
    return result["text"]
    
# -----------------------
# AI応答生成
# -----------------------
def generate_text(prompt):
    payload = {"model": MODEL_NAME, "messages": [{"role": "user", "content": prompt}]}
    resp = requests.post(LM_API_URL, json=payload)
    resp.raise_for_status()
    return resp.json()["choices"][0]["message"]["content"]


# -----------------------
# 音声合成
# -----------------------

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

# -----------------------
# メインループ
# -----------------------
def main():
    while True:
        audio = record_audio_on_voice()
        user_text = audio_to_text(audio)
        print("あなた:", user_text)
        if user_text.lower() == "exit": break

        ai_response = generate_text(user_text)
        print("AI:", ai_response)
        all_audio, fs_final, n_channels_final = synthesize_paragraph(ai_response)
if __name__ == "__main__":
    main()

音声の認識がよくなく、「ありがとうございます」のつもりが「お互いをございます」と認識されてしまいまいましたが、以下のように返答がきて読み上げてもらいました。
image.png

音声認識はWhisperを使用しており、以下のbaseをsmallやlargeに変えれば認識性能が良くなります。

whisper_model = whisper.load_model("base")

Whisperの主なモデルサイズは以下の通りです:

  1. tiny
  2. base
  3. small
  4. medium
  5. large

課題について

これで当初行いたいことはできました。
しかし、以下のような課題もあります。

1. 回答が遅すぎる。

PC性能のボトルネックとなり、LLMの返答に1分近くかかる場合があります。
また、Aivisでの音声生成もそれなりに時間がかかります。
使用するモデルにもよりますが、32GBかできれば64GB欲しいところ。

2. 最新の情報を取得できない

例えば「今日の天気は?」と聞いても、モデルが学習した時の情報しかでてきません。
今は2025年なのに2023年7月1日の天気を教えてくれます。
一応対策はあるようで、「lmstudio web検索」と検索すると色々でてくるようです。

上記2点を解決するため、本末転倒ではありますがローカルでのLLM使用は(PCを新調するまでは)あきらめて、ChatGTPのAPIを使用することにしました。
詳しくは次の記事にて。

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?