ローカルPC上で完全に動作する音声会話AIを作る方法を解説します。DeepSeek-R1(Ollama)・Silero VAD・Whisper・VOICEVOXを組み合わせて、「話しかけると自動で認識して自然な声で返答してくれるシステム」をWindows環境に構築します。


システム構成

マイク音声
  ↓
Silero VAD(発話検出)  ← 声が来たら自動で反応
  ↓
Whisper(音声→テキスト)
  ↓
DeepSeek-R1 / Ollama(AIの返答生成)
  ↓
VOICEVOX(テキスト→自然な音声)
  ↓
スピーカー

必要なもの

ツール 用途 入手先
Python(embed版 or 通常版) スクリプト実行 python.org
ollama.exe AIモデルのサーバー github.com/ollama/ollama/releases
VOICEVOX(run.exe) 日本語音声合成 voicevox.hiroshiba.jp
ffmpeg.exe Whisperの音声前処理 github.com/BtbN/FFmpeg-Builds/releases

フォルダ構成

すべてを1つのフォルダにまとめることで、フォルダごとコピーして別PCでも動かせます。

AItalker\
├── ollama.exe
├── ffmpeg.exe
├── ffprobe.exe
├── voicevox\
│   └── run.exe
├── python-3.13.13-embed-amd64\
├── models\              ← Ollamaのモデル保存先
├── cache\               ← Whisperのモデル保存先
├── voice_chat_vad.py    ← A版(基本)
├── voice_chat_vad_full.py ← B版(ウェイクワード・割り込み対応)
└── start_voice_chat.bat ← 起動用バッチ

セットアップ手順

Step 1:Python embed版の準備

Python embed版を使う場合は pip が使えるよう設定が必要です。

① python313._pth を編集

python-3.13.13-embed-amd64\python313._pth をメモ帳で開き、

# import site
↓ 変更(# を削除するだけ)
import site

② pip をインストール

cd python-3.13.13-embed-amd64
curl -O https://bootstrap.pypa.io/get-pip.py
python get-pip.py

Step 2:ライブラリをインストール

python -m pip install setuptools wheel
python -m pip install torch torchaudio --index-url https://download.pytorch.org/whl/cpu
python -m pip install numpy pyaudio openai-whisper ollama

※ NVIDIA GPU がある場合はCUDAバージョンに合わせてtorchを変更します。

REM CUDA 12.4 の場合
python -m pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu124

※ pyaudio がインストールできない場合

python -m pip install pipwin
python -m pipwin install pyaudio

Step 3:Ollamaとモデルのセットアップ

OllamaはPythonのライブラリとは別のアプリケーションです。pip install ollama でインストールされるのはPythonからOllamaを操作するクライアントライブラリのみで、実際にAIを動かすOllama本体は別途必要です。

REM モデルをダウンロード(約5GB)
ollama.exe pull deepseek-r1:8b

Step 4:起動バッチファイルを作成

start_voice_chat.bat として保存してください。

@echo off
chcp 65001
cd /d C:\Users\Taro\Desktop\AItalker

REM パス設定(ffmpeg・Whisperモデル保存先・Ollamaモデル保存先)
set PATH=%~dp0;%PATH%
set XDG_CACHE_HOME=%~dp0cache
set OLLAMA_MODELS=%~dp0models

REM VOICEVOX 起動
echo VOICEVOX を起動中...
start /min "" "%~dp0voicevox\run.exe"
timeout /t 15 /nobreak > nul

REM Ollama 起動(二重起動防止)
tasklist | find "ollama.exe" > nul
if errorlevel 1 (
    echo Ollama を起動中...
    start /min "" "%~dp0ollama.exe" serve
    timeout /t 5 /nobreak > nul
) else (
    echo Ollama は既に起動中です。
)

REM Python 実行
python-3.13.13-embed-amd64\python voice_chat_vad.py

REM 終了処理
taskkill /f /im ollama.exe > nul 2>&1
taskkill /f /im run.exe    > nul 2>&1
pause

あとは start_voice_chat.bat をダブルクリックするだけで全部起動します。


サンプルコード

常時マイクを監視して、声を検出したら自動で認識・返答します。ボタン操作は一切不要です。

"""
voice_chat_vad.py  ─  A版(基本)
Silero VAD で常時マイクを監視し自然な会話を実現
"""

import sys, time, tempfile, os, re, json, wave
import urllib.request, urllib.parse, subprocess
import numpy as np

# ── 設定 ──────────────────────────────────────────
OLLAMA_MODEL       = "deepseek-r1:8b"
WHISPER_MODEL      = "medium"   # tiny/base/small/medium から選ぶ
LANGUAGE           = "ja"

SAMPLE_RATE        = 16000
CHUNK_SAMPLES      = 512        # Silero VAD 固定値(変更不可)
CHUNK_MS           = int(CHUNK_SAMPLES / SAMPLE_RATE * 1000)  # ≒ 32ms

VAD_THRESHOLD      = 0.5        # 発話判定閾値(大きいほど厳しい)
SILENCE_TRIGGER_MS = 800        # 無音が続いたら発話終了とみなす時間(ms)
MIN_SPEECH_MS      = 300        # これより短い発話は誤検知として無視

VOICEVOX_URL       = "http://localhost:50021"
VOICEVOX_SPEAKER   = 3         # 3=ずんだもん / 1=四国めたん / 8=春日部つむぎ

SYSTEM_PROMPT = """あなたは親切な日本語アシスタントです。
音声で会話するので、回答は3文以内で簡潔にまとめてください。
思考プロセス (...) は除いて最終的な回答のみ返してください。"""

EXIT_WORDS = ["終了", "やめて", "バイバイ", "おわり"]

# ── ライブラリ読み込み ──────────────────────────────
import torch, pyaudio, whisper, ollama

# ── VAD・Whisper 初期化 ────────────────────────────
def load_vad():
    model, _ = torch.hub.load(
        "snakers4/silero-vad", "silero_vad",
        force_reload=False, trust_repo=True
    )
    return model

def load_whisper_model():
    return whisper.load_model(WHISPER_MODEL)

# ── VOICEVOX 読み上げ ──────────────────────────────
def speak(text: str):
    print(f"🔊 AI: {text}")
    try:
        url1 = f"{VOICEVOX_URL}/audio_query?text={urllib.parse.quote(text)}&speaker={VOICEVOX_SPEAKER}"
        with urllib.request.urlopen(urllib.request.Request(url1, method="POST"), timeout=10) as r:
            query = json.loads(r.read())
        query["speedScale"] = 1.1
        url2 = f"{VOICEVOX_URL}/synthesis?speaker={VOICEVOX_SPEAKER}"
        data = json.dumps(query).encode()
        req2 = urllib.request.Request(url2, data=data, method="POST",
                                      headers={"Content-Type": "application/json"})
        with urllib.request.urlopen(req2, timeout=30) as r:
            wav = r.read()
        with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
            f.write(wav); tmp = f.name
        subprocess.run(["powershell", "-c",
                        f'(New-Object Media.SoundPlayer "{tmp}").PlaySync()'],
                       check=True, creationflags=subprocess.CREATE_NO_WINDOW)
        os.unlink(tmp)
    except Exception as e:
        print(f"⚠️ TTS エラー: {e}")

# ── VOICEVOX 起動待ち ──────────────────────────────
def wait_for_voicevox(timeout=30):
    print("⏳ VOICEVOX 起動待ち", end="", flush=True)
    for _ in range(timeout):
        try:
            urllib.request.urlopen(f"{VOICEVOX_URL}/version", timeout=1)
            print(" ✅"); return True
        except:
            print(".", end="", flush=True); time.sleep(1)
    return False

# ── DeepSeek-R1 問い合わせ ─────────────────────────
def ask_deepseek(text: str, history: list) -> str:
    history.append({"role": "user", "content": text})
    res = ollama.chat(
        model=OLLAMA_MODEL,
        messages=[{"role": "system", "content": SYSTEM_PROMPT}] + history
    )
    answer = res.message.content if hasattr(res, "message") else res["message"]["content"]
    answer = re.sub(r".*?", "", answer, flags=re.DOTALL).strip()
    history.append({"role": "assistant", "content": answer})
    return answer

# ── WAV 保存 ───────────────────────────────────────
def save_wav(frames, path):
    with wave.open(path, "wb") as wf:
        wf.setnchannels(1); wf.setsampwidth(2)
        wf.setframerate(SAMPLE_RATE)
        wf.writeframes(b"".join(frames))

# ── メイン VAD ループ ──────────────────────────────
def vad_loop(vad_model, whisper_model):
    pa = pyaudio.PyAudio()
    stream = pa.open(rate=SAMPLE_RATE, channels=1, format=pyaudio.paInt16,
                     input=True, frames_per_buffer=CHUNK_SAMPLES)

    WAITING, RECORDING, PROCESSING = "WAITING", "RECORDING", "PROCESSING"
    state = WAITING
    frames, silence_n, speech_n = [], 0, 0
    silence_limit     = int(SILENCE_TRIGGER_MS / CHUNK_MS)
    min_speech_chunks = int(MIN_SPEECH_MS / CHUNK_MS)
    history = []

    print("\n👂 聞いています...\n")
    try:
        while True:
            raw = stream.read(CHUNK_SAMPLES, exception_on_overflow=False)
            audio = torch.from_numpy(
                np.frombuffer(raw, dtype=np.int16).astype(np.float32) / 32768.0)
            with torch.no_grad():
                is_speech = vad_model(audio, SAMPLE_RATE).item() > VAD_THRESHOLD

            if state == WAITING:
                if is_speech:
                    state = RECORDING; frames = [raw]; silence_n = 0; speech_n = 1
                    print("🎤 発話検知")

            elif state == RECORDING:
                frames.append(raw)
                if is_speech: silence_n = 0; speech_n += 1
                else: silence_n += 1
                if silence_n >= silence_limit:
                    state = PROCESSING if speech_n >= min_speech_chunks else WAITING
                    if state == WAITING:
                        frames = []; silence_n = speech_n = 0

            if state == PROCESSING:
                stream.stop_stream()
                with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
                    tmp = f.name
                save_wav(frames, tmp)
                result = whisper_model.transcribe(tmp, language=LANGUAGE, fp16=False)
                os.unlink(tmp)
                text = result["text"].strip()
                print(f"👤 あなた: {text}")
                if not text:
                    pass
                elif any(w in text for w in EXIT_WORDS):
                    speak("またね!"); break
                else:
                    print("🤔 考え中...")
                    speak(ask_deepseek(text, history))
                    print("\n👂 聞いています...\n")
                state = WAITING; frames = []; silence_n = speech_n = 0
                stream.start_stream()

    except KeyboardInterrupt:
        speak("終了します。")
    finally:
        stream.stop_stream(); stream.close(); pa.terminate()

# ── エントリポイント ───────────────────────────────
if __name__ == "__main__":
    if not wait_for_voicevox(): sys.exit(1)
    vad_loop(load_vad(), load_whisper_model())

ハマりやすいエラーと解決策

❌ Cannot import ‘setuptools.build_meta’

embed版Pythonのデフォルト設定では setuptools が使えません。

解決: python313._pth# import siteimport site に変更してから get-pip.py を実行。


❌ ValueError: Provided number of samples is 1536

Silero VAD はチャンクサイズが 512サンプル固定 です。

解決: 設定を以下のように修正。

# ❌ 間違い
CHUNK_MS      = 96
CHUNK_SAMPLES = int(SAMPLE_RATE * CHUNK_MS / 1000)  # = 1536

# ✅ 正しい
CHUNK_SAMPLES = 512   # 変更不可
CHUNK_MS      = int(CHUNK_SAMPLES / SAMPLE_RATE * 1000)  # ≒ 32ms

❌ URLError: [WinError 10061] 接続が拒否されました

VOICEVOXが起動する前にPythonが接続しようとしています。

解決①: バッチファイルの待機時間を延ばす(timeout /t 15

解決②: Pythonスクリプト側でVOICEVOXの起動を確認してから進む。

def wait_for_voicevox(timeout=30):
    for _ in range(timeout):
        try:
            urllib.request.urlopen("http://localhost:50021/version", timeout=1)
            return True
        except:
            time.sleep(1)
    return False

❌ VOICEVOX がバッチから起動しない

start "" voicevox\run.exe だと相対パスが解決できない場合があります。

解決: %~dp0(バッチファイルの場所)を使って絶対パスで指定。

start /min "" "%~dp0voicevox\run.exe"

❌ ffmpegが見つからない

WhisperはPythonの中だけで動くのではなく、内部でffmpegコマンドを呼び出します。システムにffmpegがインストールされていない場合はエラーになります。

解決: ffmpeg.exeAItalker\ フォルダに置き、バッチで PATH に追加。

set PATH=%~dp0;%PATH%

❌ ollama に接続できません

pip install ollama はPythonのクライアントライブラリのみです。AIを実際に動かすOllama本体(ollama.exe)は別途必要です。

解決: ollama.exe serve が起動しているか確認。バッチファイルで自動起動させる。


Whisperモデルの保存先

デフォルトのダウンロード先は C:\Users\ユーザー名\.cache\whisper\ です。

AItalkerフォルダ内にまとめたい場合はバッチファイルに追加:

set XDG_CACHE_HOME=%~dp0cache

これで AItalker\cache\whisper\medium.pt のように保存されます。

モデル サイズ 日本語精度 推奨用途
tiny 39MB とにかく速さ優先
base 74MB まず試すならここ
small 244MB バランス重視
medium 769MB ◎◎ 日本語をしっかり認識したい
large 1.5GB ◎◎◎ 最高精度・重い

 

 


まとめ

本記事で構築したシステムの特徴:

  • 完全ローカル動作:音声認識・AI・音声合成すべてオフライン
  • 無料:使用ツールはすべて無料・オープンソース
  • ポータブル:AItalkerフォルダごとコピーで別PCでも動作
  • 自然な会話:ボタン不要・話しかけるだけで自動応答

VOICEVOXのキャラクターは VOICEVOX_SPEAKER の番号を変えるだけで変更でき、Whisperのモデルサイズを medium にすると日本語認識の精度が大幅に向上します。