指南

为 Ollama 加上文字转语音:让本地模型完全离线地开口说话

请注意:Supertonic 3 目前不支持中文,本文中的语音可朗读其支持的 31 种语言,因此请让模型用这些语言回答。Ollama 在你自己的电脑上运行语言模型,它的聊天 API 返回文字。要听到回答,就需要加上文字转语音这一步;如果你选择 Ollama 是为了隐私,这一步也应当在本地运行。

本指南用免费的 SuperClonica 引擎实现这一步:一段简短的 Python 脚本把问题发给 Ollama,边接收流式回答边读取,每凑满一句就立即朗读,所以模型还没写完,声音就已经开始了。任何数据都不会离开你的电脑。

安装

安装 Ollama,拉取并启动一个模型,然后安装两个 Python 包:

ollama pull llama3.2

pip install requests superclonica-cpu

引擎首次运行时会下载约 400 MB 的 Supertonic 3 模型;之后在普通 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 设为对话语言,例如英语 "en";引擎支持 31 种语言,但暂不支持中文。VOICE 可以是十个内置声音(M1–M5、F1–F5)之一,也可以是 SuperClonica 目录中声音文件的路径,例如电影里 AI 风格的助手声音。

用语音对话

如果想说话而不是打字,可以在脚本前面加上本地语音识别,例如通过 faster-whisper 包使用 Whisper,并把识别出的文字传到脚本调用 input 的地方。这样整个助手——耳朵、大脑和声音——都在你自己的机器上运行。