テキストの読み上げ

2026年8月10日

はじめに

AIとおしゃべりするために、テキストの読み上げ(text-to-speech: TTS)を行う。

環境

  • Ubuntu 22.04 LTS
  • Intel Core i9-14900K、メモリ 32GB
  • NVIDIA GeForce RTX 4090 (24GB)
  • Whisper + LM Studio + kokoro
  • Pi (pi-sandbox) + LM Studio (Qwen3.6 35B A3B)

参考実装

ここ を参考にする。kokoro を使う。

import sounddevice as sd
from kokoro import KPipeline

import warnings
warnings.filterwarnings("ignore")

LANG_CODE = "a"
#LANG_CODE = "j"
VOICE = "af_heart"
#VOICE = "jf_alpha"

# Initialize models
tts = KPipeline(lang_code=LANG_CODE, repo_id="hexgrad/Kokoro-82M")

def speak(text):
    """Text to speech with Kokoro."""
    for _, _, audio in tts(text, voice=VOICE):
        sd.play(audio, samplerate=24000)
        sd.wait()

# Main loop
while True:
    text = input("Input: ")
    print(text)
    speak(text)

実行すると、最初だけモデルのダウンロードのため少し時間がかかる。

$ uv venv
$ uv pip install sounddevice kokoro
$ uv pip install pyopenjtalk "fugashi[unidic-lite]" jaconv mojimoji  # 日本語を使う場合
$ uv run speak.py
Input: Hello
Hello

なお、af_heartさんはアメリカ英語用のモデルだが、日本語がしゃべれないわけではない。

AIとおしゃべり

LLMの利用

AIとおしゃべりするために、チャットボットの枠組みを作る。

from openai import OpenAI

API_BASE = "http://localhost:1234/v1"
API_KEY = "lmstudio"
MODEL = "google/gemma-4-e4b"

client = OpenAI(base_url=API_BASE, api_key=API_KEY)

class ChatBot:
    def __init__(self, system=""):
        self.history = []
        if system:
            self.history = [{"role": "system", "content": system}]

    def query(self, prompt, temperature=0.7):
        self.history.append({"role": "user", "content": prompt})
        response = client.chat.completions.create(
            model=MODEL, messages=self.history, temperature=temperature
        )
        content = response.choices[0].message.content
        self.history.append({"role": "assistant", "content": content})
        return content

bot = ChatBot(system="""
あなたは近所のお姉さんです。
音声会話を前提とするため、改行なしで3個以内の短いセンテンスで返答してください。
絵文字は使わないでください。
""")

# Main loop
while True:
    text = input("You: ")
    response = bot.query(text)
    print(f"AI: {response}")

実行

$ uv pip install openai
$ uv run query_llm.py
You: こんにちは
AI: あら、お疲れ様。どうしたの?何かあった?
You: 今日も暑いね
AI: 本当にそうだよね。体調崩さないように気をつけてね。水分補給しっかりしてね。

音声チャットボット

上記の検討と、Whisperによる音声文字起こしを組み合わせて、音声チャットボットを作る。

import sounddevice as sd
from faster_whisper import WhisperModel
from openai import OpenAI
from kokoro import KPipeline

import warnings
warnings.filterwarnings("ignore")

API_BASE = "http://localhost:1234/v1"
API_KEY = "lmstudio"
MODEL = "google/gemma-4-12b-qat"
LANG_CODE = "j"
VOICE = "jf_alpha"

class ChatBot:
    def __init__(self, system=""):
        self.history = []
        if system:
            self.history = [{"role": "system", "content": system}]

    def query(self, prompt, temperature=0.7):
        self.history.append({"role": "user", "content": prompt})
        response = client.chat.completions.create(
            model=MODEL, messages=self.history, temperature=temperature
        )
        content = response.choices[0].message.content
        self.history.append({"role": "assistant", "content": content})
        return content

# Initialize models
whisper = WhisperModel("turbo", device="cuda", compute_type="float16")
client = OpenAI(base_url=API_BASE, api_key=API_KEY)
tts = KPipeline(lang_code=LANG_CODE, repo_id="hexgrad/Kokoro-82M")

def record_audio(duration=5, sample_rate=16000):
    """Record from microphone."""
    audio = sd.rec(int(duration * sample_rate),
                   samplerate=sample_rate, channels=1, dtype="float32")
    sd.wait()
    return audio.flatten()

def transcribe(audio):
    """Speech to text with faster-whisper."""
    segments, _ = whisper.transcribe(audio, vad_filter=True,
                    vad_parameters=dict(min_silence_duration_ms=500))
    return " ".join(s.text for s in segments).strip()

def speak(text):
    """Text to speech with Kokoro."""
    for _, _, audio in tts(text, voice=VOICE):
        sd.play(audio, samplerate=24000)
        sd.wait()

bot = ChatBot(system="""
あなたは親切なアシスタントです。
音声会話を前提とするため、改行なしで3個以内の短いセンテンスで返答してください。
絵文字は使わないでください。
""")

response = bot.query("会話の開始の挨拶を短く言って")
print(f"AI: {response}")
speak(response)

# Main loop
while True:
    text = ""
    print("Listening...")
    while True:
        audio = record_audio()
        chunk = transcribe(audio)
        if not chunk:
            break
        text += chunk
    if not text:
        continue
    print(f"You: {text}")
    response = bot.query(text)
    print(f"AI: {response}")
    speak(response)

実際に会話してみると、結構考える時間が必要になるので、聞き取りは時間で切るのではなくて、間を置いたら聞き取りをやめるようにした。録音時間は、5秒くらい取らないとうまくいかない(つまり、最大5秒待つことになる)。

実行

$ uv pip install faster_whisper
$ uv run voice_chat.py
AI: こんにちは。何かお手伝いしましょうか。
Listening...
You: 英語の文法を覚えるのに便利なアプリを作りたいです。どういうアイデアがありますかね
AI: ゲーム感覚で進めるクイズ形式はどうでしょう。日常会話をベースにした例文の学習もおすすめです。AIと会話しながらリアルタイムで添削してもらう機能も面白いですね。
Listening...
You: 今度のような勉強は文法の習得ないと聞いたことはありますが どうなんですかね
AI: 文法は基礎として大切ですが、完璧に覚えることよりも正しく使うことが重要です。まずは会話の中で自然に身につける方が上達が早いです。基礎を学びながら実践を繰り返すバランスが良いと思います。
Listening...
You: とにかくしゃべり倒してもらうような設計をするということですかね。
AI: はい、たくさん話すことは非常に効果的です。ただそれだけでなく、間違った時にすぐに添削してもらう仕組みを組み合わせるとより良いでしょう。
Listening...
You: On se...  Un chalet日本語と英語を混ぜるのはちょっと難しい英語だけでその場合やるような感じになるんでしょうか
AI: 最初は日本語のサポートを少し入れつつ、徐々に英語の割合を増やしていく設計が良いと思います。基礎がある人向けには英語のみのモードも用意すると便利ですね。ユーザーの習熟度に合わせて調整できる仕組みが良いでしょう。
Listening...
You: テキストの音声下で英語と日本語を混ぜてちょっと難しそうなんですよ
AI: 技術的な処理が複雑になる可能性はありますね。最初は日本語の解説を挟む形にするのがスムーズかもしれません。ユーザーの混乱を防ぐために構成をシンプルに保つのが良いです。
Listening...
You: AIとの会話よりはもう少し構造的にちゃんとしっかりしたものを作った方が良いということですかね
AI: はい、基礎を固めるための構造的なメニューがある方が学習効果は安定します。その後にAIと自由に話す練習を挟むようなハイブリッドな設計がおすすめです。学習の目的やレベルに合わせて使い分けられるようにしましょう。

シャドーイングアプリ

英語の発音を向上させるためのシャドーイングアプリshadowing.pyを作ってみた。

※本当は英会話アプリを作ろうと思ったのだが、自分の英語の発音がひどすぎてまともに音声認識してくれないので、それ以前の問題だと気が付きました。

計画

要求定義書PRD.mdを作成。Piと相談しながら詳細を詰めた。

英文を提示してもらって、その音声を聞いて、発話して、聞き取ってもらって、その結果ともとの英文が一致するかどうかで正解・不正解を判定するようにする。音声認識の問題で、正しい発音に近くてももとの英文と一致しない可能性があるので、数文字違ってもよいものとした。

決まった問題を出す「固定モード」と、LLMを用いて自由に問題を出す「フリーモード」を用意することにした。

セットアップ

$ uv init shadowing
$ cd shadowing
$ uv add sounddevice kokoro openai faster_whisper

開発

Piにおまかせ。ただし、テストは手動でやる。

動作確認

固定モード

$ uv run shadowing.py
固定モードを開始します。Ctrl-C で終了します。

問題: Now it is time to cook!
認識結果: Now it's time to cook.
✅ 正解!

問題: Are you happy living in the US?
認識結果: You're happier living in the U.S.
❌ 不正解! (残り4回) もう一度お試しください。

問題: Are you happy living in the US?
認識結果: You're happy living in the U.S.
❌ 不正解! (残り3回) もう一度お試しください。

※ここでの不正解は発音のせいなのか音声認識の問題なのかわからない。

フリーモード

$ uv run shadowing.py --free
フリーモードを開始します。Ctrl-C で終了します。

問題: If you save enough money, you can travel to Italy next year.
認識結果: If you save enough money, you can travel to Italy next year.
✅ 正解!

問題: The antique map, which showed lost civilizations, was highly detailed.
認識結果: The antique map, which shown last civilization, was highly detailed.
✅ 正解!

問題: If you save your tickets now, they will be half price.
認識結果: You save your tickets now. They will be half price.
✅ 正解!

※2つ目のlost→lastは素で言い間違えた。音声認識は正確。

音声認識がどうしても誤認識してそうな場合とか、どう頑張っても自分が正しく発音できないときとかがあるので、5回間違ったら次の問題に行くようにした。

成果物

参考文献