ガイド

Ollamaに読み上げを追加:ローカルモデルを完全オフラインでしゃべらせる

Ollamaは自分のパソコンで言語モデルを動かし、そのチャットAPIはテキストで答えます。回答を聞くには読み上げのステップが必要で、プライバシーのためにOllamaを選んだのなら、そのステップもローカルで動かすべきです。

このガイドでは、無料のSuperClonicaエンジンでそれを追加します。短いPythonスクリプトが質問をOllamaに送り、ストリーミングで届く回答を読み、文がそろうたびに読み上げます。そのため、モデルが書き終わるずっと前に声が始まります。データはパソコンの外に出ません。

インストール

Ollamaをインストールしてモデルを取得・起動し、Pythonのパッケージを2つインストールします:

ollama pull llama3.2

pip install requests superclonica-cpu

エンジンは初回にSupertonic 3のモデル(約400 MB)をダウンロードし、その後は普通のCPUでオフラインで動きます。NVIDIAのグラフィックボードは速くするだけです。Linuxではalsa-utilsのaplayで音を再生します。

スクリプト

ollama_voice.pyとして保存して実行します。質問を入力すると、回答が表示と同時に読み上げられ、会話の履歴も保たれます:

import json, os, re, subprocess, sys, tempfile
import requests
from superclonica import Superclonica

OLLAMA = "http://localhost:11434/api/chat"
MODEL = "llama3.2"  # any model you pulled with ollama pull
VOICE = "F1"  # M1..M5, F1..F5 or a voice file from the catalog
SENTENCE_END = re.compile(r"[.!?]\s+")
tts = Superclonica()

def play(path):
    if sys.platform == "win32":
        import winsound
        winsound.PlaySound(path, winsound.SND_FILENAME)
    elif sys.platform == "darwin":
        subprocess.run(["afplay", path], check=True)
    else:
        subprocess.run(["aplay", "-q", path], check=True)

def say(sentence):
    sentence = re.sub(r"[*#`_]", "", sentence).strip()
    if sentence:
        audio, _ = tts.synthesize(sentence, voice=VOICE, lang="en")
        path = os.path.join(tempfile.gettempdir(), "ollama-say.wav")
        tts.save(path, audio)
        play(path)

history = []
while True:
    history.append({"role": "user", "content": input("You: ")})
    answer = buffer = ""
    body = {"model": MODEL, "messages": history, "stream": True}
    with requests.post(OLLAMA, json=body, stream=True, timeout=300) as response:
        response.raise_for_status()
        for line in response.iter_lines():
            if not line:
                continue
            piece = json.loads(line).get("message", {}).get("content", "")
            print(piece, end="", flush=True)
            answer += piece
            buffer += piece
            while match := SENTENCE_END.search(buffer):
                say(buffer[:match.end()])
                buffer = buffer[match.end():]
    say(buffer)
    print()
    history.append({"role": "assistant", "content": answer})

スクリプトはストリームを文の終わりで区切り、Ollamaが続きを書いている間に読み上げます。モデルがよく使うアスタリスクなどのMarkdown記号は、読み上げると不自然なので取り除きます。

自然に聞こえるように

モデルには書くのではなく話すよう頼みましょう。履歴の最初に「リストや表、書式を使わず、短い話し言葉の文で答えて」のようなシステムメッセージを入れます。sayのlangには会話の言語(日本語なら"ja")を指定します。エンジンは31言語を話しますが、中国語にはまだ対応していません。VOICEには内蔵の10の声(M1〜M5、F1〜F5)か、SuperClonicaカタログの声のファイルのパスを指定します。映画のAIのようなアシスタントの声などです。

声で話しかける

入力の代わりに話したいなら、スクリプトの前にローカルの音声認識(たとえばfaster-whisperパッケージのWhisper)を置き、その文字起こしをスクリプトがinputを呼ぶところに渡します。これで耳も頭脳も声も、アシスタント全体が自分のマシンで動きます。