前回は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のログはこちら
ちゃんと返ってきました。
ちなみにプロンプトは画面右のContextで設定できます。
今回はGUIで立ちげていますすが、CLI版もありバックグラウンドで待ち受けさせることもできます。
AivisSpeachAPIとの連携
以下のような流れで連携します。
- マイクの入力を拾い、テキストに変換する
- LM Studioにテキストを渡して返答を取得する
- 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()
音声の認識がよくなく、「ありがとうございます」のつもりが「お互いをございます」と認識されてしまいまいましたが、以下のように返答がきて読み上げてもらいました。
音声認識はWhisperを使用しており、以下のbaseをsmallやlargeに変えれば認識性能が良くなります。
whisper_model = whisper.load_model("base")
Whisperの主なモデルサイズは以下の通りです:
- tiny
- base
- small
- medium
- large
課題について
これで当初行いたいことはできました。
しかし、以下のような課題もあります。
1. 回答が遅すぎる。
PC性能のボトルネックとなり、LLMの返答に1分近くかかる場合があります。
また、Aivisでの音声生成もそれなりに時間がかかります。
使用するモデルにもよりますが、32GBかできれば64GB欲しいところ。
2. 最新の情報を取得できない
例えば「今日の天気は?」と聞いても、モデルが学習した時の情報しかでてきません。
今は2025年なのに2023年7月1日の天気を教えてくれます。
一応対策はあるようで、「lmstudio web検索」と検索すると色々でてくるようです。
上記2点を解決するため、本末転倒ではありますがローカルでのLLM使用は(PCを新調するまでは)あきらめて、ChatGTPのAPIを使用することにしました。
詳しくは次の記事にて。