ウェイクワードエンジンモジュールテスト(失敗)

ウェイクワードエンジンモジュールテスト

import pyaudio
import numpy as np
from openwakeword.model import Model

class SimpleWakeWordDetector:
    def __init__(self, model_path="alexa_v0.1.onnx", threshold=0.5):
        self.model_path = model_path
        self.threshold = threshold
        self.format = pyaudio.paInt16
        self.channels = 1
        self.rate = 16000
        self.chunk = 1024

        self.model_name = model_path  # モデル名 = ファイル名
        self.model = Model(
            wakeword_models=[self.model_path],
            inference_framework="onnx"
        )

        self.audio = pyaudio.PyAudio()
        self.stream = self.audio.open(
            format=self.format,
            channels=self.channels,
            rate=self.rate,
            input=True,
            frames_per_buffer=self.chunk
        )

    def listen_for_wakeword(self):
        print(f"ウェイクワード待機中...({self.model_path})")
        prev_detect = False

        while True:
            data = self.stream.read(self.chunk, exception_on_overflow=False)
            audio = np.frombuffer(data, dtype=np.int16).astype(np.float32) / 32768.0

            self.model.predict(audio)
            score = self.model.get_last_prediction(self.model_name)

            print(f"score: {score:.3f}", end='\r')

            detect = score > self.threshold
            if detect and not prev_detect:
                print(f"\nWakeword Detected! (score: {score:.3f})")
                return True

            prev_detect = detect

 touch simple_wakeword.py

で作成

touch wordtest.py

from simple_wakeword import SimpleWakeWordDetector

# モジュール初期化(Alexa用モデルとしきい値指定)
wakeword_detector = SimpleWakeWordDetector(
    model_path="alexa_v0.1.onnx",
    threshold=0.5
)

# 検知まで待機
detected = wakeword_detector.listen_for_wakeword()

if detected:
    print("こんにちは")

しかし反応がない

とりあえずウェイクワードは保留とし
先にRAGと llama index を行う

ウェイクワードエンジンと ollama gemma3 4b の組み合わせ(失敗)

ウェイクワードエンジンと ollama gemma3 4b の組み合わせ

とりあえずopenwakeword で alexa なら動作するので
次に音声入力が起動するように組み合わせる

これならAlexa と言ったら
音声入力を開始
という

常にマイクを監視
「ねえラマ」などのウェイクワードが話されたら録音開始
認識→質問→読み上げを実行
終了後、またウェイクワード待ちに戻る

ができるはず

import pyaudio
import numpy as np
from openwakeword.model import Model
import sys

FORMAT = pyaudio.paInt16
CHANNELS = 1
RATE = 16000
CHUNK = 1024
audio = pyaudio.PyAudio()
mic_stream = audio.open(format=FORMAT, channels=CHANNELS, rate=RATE, input=True, frames_per_buffer=CHUNK)

model_name = "alexa_v0.1.onnx"

model = Model(
  wakeword_models=[model_name],
  inference_framework="onnx"
)

print("Listening for wakeword \"Alexa\"...")
print()

prev_detect=False

while True:
    audio = np.frombuffer(mic_stream.read(CHUNK), dtype=np.int16)

    prediction = model.predict(audio)

    scores = model.prediction_buffer[model_name]
    curr_score = format(scores[-1], '.20f')
    detect = True if float(curr_score) > 0.5 else False

    if detect:
        if detect != prev_detect:
            print(f"Detected!({curr_score[:5]})")
            prev_detect=True
    else:
        prev_detect=False
を元にGPTの提案したコードを書き換える

touch module/module_wakeword.py
で

import pyaudio
import numpy as np
from openwakeword.model import Model

class WakeWordDetector:
    def __init__(self, model_path="alexa_v0.1.onnx", threshold=0.5):
        self.model_name = model_path
        self.threshold = threshold

        # モデル初期化
        self.model = Model(
            wakeword_models=[self.model_name],
            inference_framework="onnx"
        )

        # PyAudio設定
        self.format = pyaudio.paInt16
        self.channels = 1
        self.rate = 16000
        self.chunk = 1024

        self.audio = pyaudio.PyAudio()
        self.stream = self.audio.open(
            format=self.format,
            channels=self.channels,
            rate=self.rate,
            input=True,
            frames_per_buffer=self.chunk
        )

    def listen_for_wakeword(self):
        print(f"ウェイクワード待機中...(モデル: {self.model_name})")

        prev_detect = False

        while True:
            audio_chunk = self.stream.read(self.chunk, exception_on_overflow=False)
            audio_np = np.frombuffer(audio_chunk, dtype=np.int16)

            _ = self.model.predict(audio_np)

            score = self.model.prediction_buffer[self.model_name][-1]
            detect = score > self.threshold

            if detect and not prev_detect:
                print(f"ウェイクワード検出!(スコア: {score:.3f})")
                return True  # 検出されたら終了

            prev_detect = detect

✅ 特徴と使い方
* alexa_v0.1.onnx をモデルとして使う(変更可能)
* listen_for_wakeword() を呼び出すと、検出されるまでループし、検出されたら return True

✅ モデルファイルが models/ にある場合の使い方

wakeword_detector = WakeWordDetector(model_path="models/alexa_v0.1.onnx", threshold=0.5)

✅ 使い方例(main側)

from module.module_wakeword import WakeWordDetector

wakeword_detector = WakeWordDetector("models/alexa_v0.1.onnx", threshold=0.5)
wakeword_detector.listen_for_wakeword()

これらを元に

touch main6.py

を作成し

from module.module_audio_to_text import AudioToTextCorrector
from module.module_speaker import Speaker
from module.module_wakeword import WakeWordDetector
from ollama import chat, ChatResponse

# モデル名(Ollama用)
OLLAMA_MODEL = 'gemma3:4b'

# 各モジュールの初期化
speaker = Speaker()
audio_to_text = AudioToTextCorrector("config.json")
wakeword_detector = WakeWordDetector(model_path="models/alexa_v0.1.onnx", threshold=0.5)

def ask_ollama(prompt: str) -> str:
    try:
        response: ChatResponse = chat(model=OLLAMA_MODEL, messages=[
            {'role': 'user', 'content': prompt}
        ])
        return response.message.content.strip()
    except Exception as e:
        print(f"Ollamaエラー: {e}")
        return "エラーが発生しました。"

def main():
    while True:
        # ① ウェイクワードを検出するまで待機
        wakeword_detector.listen_for_wakeword()

        # ② 音声を認識してテキスト化(+日本語補正)
        corrected_text = audio_to_text.record_and_correct(timeout_seconds=10)

        if corrected_text is None:
            print("無音またはタイムアウトで中断。再びウェイクワード待ちに戻ります。")
            continue

        print("\n【認識・補正したテキスト】")
        print(corrected_text)

        # ③ Ollama(gemma3:4b)へ質問
        ollama_reply = ask_ollama(corrected_text)

        print("\n【gemma3:4bの返答】")
        print(ollama_reply)

        # ④ 読み上げ
        speaker.speak(ollama_reply)

if __name__ == "__main__":
    main()

でウェイクワード対応させる

実行すると

[2025-05-14 06:23:54.568] [ctranslate2] [thread 311016] [warning] The compute type inferred from the saved model is float16, but the target device or backend do not support efficient float16 computation. The model weights have been automatically converted to use the float32 compute type instead.
Traceback (most recent call last):
  File "/Users/snowpool/aw10s/gemma/main6.py", line 12, in <module>
    wakeword_detector = WakeWordDetector(model_path="models/alexa_v0.1.onnx", threshold=0.5)
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/snowpool/aw10s/gemma/module/module_wakeword.py", line 11, in __init__
    self.model = Model(
                 ^^^^^^
  File "/Users/snowpool/.pyenv/versions/3.11.0/lib/python3.11/site-packages/openwakeword/utils.py", line 686, in wrapped
    return func(*args, **new_kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/snowpool/.pyenv/versions/3.11.0/lib/python3.11/site-packages/openwakeword/model.py", line 97, in __init__
    raise ValueError("Could not find pretrained model for model name '{}'".format(i))
ValueError: Could not find pretrained model for model name 'models/alexa_v0.1.onnx'

これは
openWakeWord の Model(…) に渡されたモデルパスが正しく認識されていない

openwakeword.Model(…) に渡す wakeword_models は、**ファイル名ではなく「モデル名 or 辞書形式」**で渡す必要がある

なので

self.model = Model(
    wakeword_models=[self.model_name],  # これはNG
    inference_framework="onnx"
)

self.model = Model(
    wakeword_models={ "alexa": self.model_name },
    inference_framework="onnx"
)

にする

import pyaudio
import numpy as np
from openwakeword.model import Model

class WakeWordDetector:
    def __init__(self, model_path="models/alexa_v0.1.onnx", threshold=0.5):
        self.model_name = "alexa"
        self.threshold = threshold

        # モデル名とファイルパスを辞書で渡す
        self.model = Model(
            wakeword_models={self.model_name: model_path},
            inference_framework="onnx"
        )

        # PyAudio設定
        self.format = pyaudio.paInt16
        self.channels = 1
        self.rate = 16000
        self.chunk = 1024

        self.audio = pyaudio.PyAudio()
        self.stream = self.audio.open(
            format=self.format,
            channels=self.channels,
            rate=self.rate,
            input=True,
            frames_per_buffer=self.chunk
        )

    def listen_for_wakeword(self):
        print(f"ウェイクワード待機中...(アレクサ)")

        prev_detect = False

        while True:
            audio_chunk = self.stream.read(self.chunk, exception_on_overflow=False)
            audio_np = np.frombuffer(audio_chunk, dtype=np.int16)

            _ = self.model.predict(audio_np)

            score = self.model.prediction_buffer[self.model_name][-1]
            detect = score > self.threshold

            if detect and not prev_detect:
                print(f"「アレクサ」検出!(スコア: {score:.3f})")
                return True

            prev_detect = detect

が全体コード

今度は

[2025-05-14 06:28:28.142] [ctranslate2] [thread 314783] [warning] The compute type inferred from the saved model is float16, but the target device or backend do not support efficient float16 computation. The model weights have been automatically converted to use the float32 compute type instead.
Traceback (most recent call last):
  File "/Users/snowpool/aw10s/gemma/main6.py", line 12, in <module>
    wakeword_detector = WakeWordDetector(model_path="models/alexa_v0.1.onnx", threshold=0.5)
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/snowpool/aw10s/gemma/module/module_wakeword.py", line 11, in __init__
    self.model = Model(
                 ^^^^^^
  File "/Users/snowpool/.pyenv/versions/3.11.0/lib/python3.11/site-packages/openwakeword/utils.py", line 686, in wrapped
    return func(*args, **new_kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/snowpool/.pyenv/versions/3.11.0/lib/python3.11/site-packages/openwakeword/model.py", line 90, in __init__
    for ndx, i in enumerate(wakeword_models):
RuntimeError: dictionary changed size during iteration

コード生成がうまくいかないため

 test_openwakeword.py 

のコードをモジュール化する

module/module_wakeword_simple.py

内容は

import pyaudio
import numpy as np
from openwakeword.model import Model

class SimpleWakeWordDetector:
    def __init__(self, model_path="alexa_v0.1.onnx", threshold=0.5):
        self.model_path = model_path
        self.threshold = threshold
        self.format = pyaudio.paInt16
        self.channels = 1
        self.rate = 16000
        self.chunk = 1024

        # モデル名(ファイル名)でアクセスするためのキー
        self.model_name = model_path

        # OpenWakeWord モデルの読み込み
        self.model = Model(
            wakeword_models=[self.model_path],
            inference_framework="onnx"
        )

        # マイク初期化
        self.audio = pyaudio.PyAudio()
        self.stream = self.audio.open(
            format=self.format,
            channels=self.channels,
            rate=self.rate,
            input=True,
            frames_per_buffer=self.chunk
        )

    def listen_for_wakeword(self):
        print(f"Listening for wakeword \"{self.model_name}\"...")

        prev_detect = False

        while True:
            data = self.stream.read(self.chunk, exception_on_overflow=False)
            audio = np.frombuffer(data, dtype=np.int16)

            self.model.predict(audio)
            scores = self.model.prediction_buffer[self.model_name]
            curr_score = scores[-1]
            detect = curr_score > self.threshold

            if detect and not prev_detect:
                print(f"Wakeword Detected! (score: {curr_score:.3f})")
                return True

            prev_detect = detect

使い方は

from module.module_wakeword_simple import SimpleWakeWordDetector

wake_detector = SimpleWakeWordDetector(model_path="models/alexa_v0.1.onnx", threshold=0.5)

while True:
    wake_detector.listen_for_wakeword()
    print("処理を実行します...")
    # → 音声認識など次の処理へ

しかし、そもそもの前提として
Modelディレクトリにはモデルが存在していない

touch download.py  

内容は

import openwakeword

openwakeword.utils.download_models()


実行すると

python download.py 
embedding_model.tflite: 100%|█████████████████| 1.33M/1.33M [00:00<00:00, 9.92MiB/s]
embedding_model.onnx: 100%|███████████████████| 1.33M/1.33M [00:00<00:00, 7.13MiB/s]
melspectrogram.tflite: 100%|██████████████████| 1.09M/1.09M [00:00<00:00, 6.30MiB/s]
melspectrogram.onnx: 100%|████████████████████| 1.09M/1.09M [00:00<00:00, 6.54MiB/s]
silero_vad.onnx: 100%|████████████████████████| 1.81M/1.81M [00:00<00:00, 7.82MiB/s]
alexa_v0.1.tflite: 100%|████████████████████████| 855k/855k [00:00<00:00, 5.67MiB/s]
alexa_v0.1.onnx: 100%|██████████████████████████| 854k/854k [00:00<00:00, 5.36MiB/s]
hey_mycroft_v0.1.tflite: 100%|██████████████████| 860k/860k [00:00<00:00, 6.84MiB/s]
hey_mycroft_v0.1.onnx: 100%|████████████████████| 858k/858k [00:00<00:00, 6.52MiB/s]
hey_jarvis_v0.1.tflite: 100%|█████████████████| 1.28M/1.28M [00:00<00:00, 6.96MiB/s]
hey_jarvis_v0.1.onnx: 100%|███████████████████| 1.27M/1.27M [00:00<00:00, 6.26MiB/s]
hey_rhasspy_v0.1.tflite: 100%|██████████████████| 416k/416k [00:00<00:00, 4.76MiB/s]
hey_rhasspy_v0.1.onnx: 100%|████████████████████| 204k/204k [00:00<00:00, 3.06MiB/s]
timer_v0.1.tflite: 100%|██████████████████████| 1.74M/1.74M [00:00<00:00, 7.98MiB/s]
timer_v0.1.onnx: 100%|████████████████████████| 1.74M/1.74M [00:00<00:00, 8.96MiB/s]
weather_v0.1.tflite: 100%|████████████████████| 1.15M/1.15M [00:00<00:00, 6.99MiB/s]
weather_v0.1.onnx: 100%|██████████████████████| 1.15M/1.15M [00:00<00:00, 6.44MiB/s]

となってダウンロードされているがパスが不明

GPTで

ls ~/.cache/openwakeword/models/

で存在するというが

ls: /Users/snowpool/.cache/openwakeword/models/: No such file or directory

となるのでこれではない

 touch show_model_path.py

from openwakeword.utils import default_cache_dir
import os

print("モデル保存先:")
print(os.path.join(default_cache_dir(), "models"))

で実行する

しかし

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/gemma/show_model_path.py", line 1, in <module>
    from openwakeword.utils import default_cache_dir
ImportError: cannot import name 'default_cache_dir' from 'openwakeword.utils' (/Users/snowpool/.pyenv/versions/3.11.0/lib/python3.11/site-packages/openwakeword/utils.py)

となる

参考サイトとして
https://zenn.dev/kun432/scraps/1a987de4943c65
によれば

 pip show openwakeword | grep Location

で調べることができる

Location: /Users/snowpool/.pyenv/versions/3.11.0/lib/python3.11/site-packages

モデルはsite-packagesの下にダウンロードされる

Treeコマンドはインストールされていないので

 brew install tree

でインストール

しかしエラーになるので

arch -arm64 brew install tree

でインストールする

tree /Users/snowpool/.pyenv/versions/3.11.0/lib/python3.11/site-packages/openwakeword
/Users/snowpool/.pyenv/versions/3.11.0/lib/python3.11/site-packages/openwakeword
├── __init__.py
├── __pycache__
│   ├── __init__.cpython-311.pyc
│   ├── custom_verifier_model.cpython-311.pyc
│   ├── data.cpython-311.pyc
│   ├── metrics.cpython-311.pyc
│   ├── model.cpython-311.pyc
│   ├── train.cpython-311.pyc
│   ├── utils.cpython-311.pyc
│   └── vad.cpython-311.pyc
├── custom_verifier_model.py
├── data.py
├── metrics.py
├── model.py
├── resources
│   └── models
│       ├── alexa_v0.1.onnx
│       ├── alexa_v0.1.tflite
│       ├── embedding_model.onnx
│       ├── embedding_model.tflite
│       ├── hey_jarvis_v0.1.onnx
│       ├── hey_jarvis_v0.1.tflite
│       ├── hey_mycroft_v0.1.onnx
│       ├── hey_mycroft_v0.1.tflite
│       ├── hey_rhasspy_v0.1.onnx
│       ├── hey_rhasspy_v0.1.tflite
│       ├── melspectrogram.onnx
│       ├── melspectrogram.tflite
│       ├── silero_vad.onnx
│       ├── timer_v0.1.onnx
│       ├── timer_v0.1.tflite
│       ├── weather_v0.1.onnx
│       └── weather_v0.1.tflite
├── train.py
├── utils.py
└── vad.py

これで場所が把握できたのでモデルをコピーする

mkdir -p models
cp /Users/snowpool/.pyenv/versions/3.11.0/lib/python3.11/site-packages/openwakeword/resources/models/alexa_v0.1.onnx models/

これで実行したけど
アレクサ
と言っても検知しない

試しに

python test_openwakeword.py

を実行してもダメだった

openwakeWordと EffecientWord-Net について

ウェイクワードエンジンの導入

openwakeWordと EffecientWord-Net について調べる

1. OpenWakeWord
開発元: Picovoice(またはオープンソースコミュニティ)
特徴
* オープンソースで利用可能
* Pythonで簡単に組み込める
* 自作ウェイクワードの訓練が比較的簡単(マイク音声から自作ワードを学習可能)
* 低スペックなデバイス(例:Raspberry Pi)でも動作可能
* 英語中心だが、日本語でも利用できるようカスタムワードを収録可
メリット
* 手軽に組み込みやすい
* 小型音声アシスタントやプロトタイプ向けに最適
* 小型モデルなのでリアルタイム処理に強い
利用例
* 「ねえラマ!」など任意のフレーズで起動する家庭用AIスピーカー
* 子供の声でも誤検知しにくいように調整可能

2. EfficientWord-Net
開発元: ByteDance Research(論文あり)
特徴
* 学習済みモデルを使用した高精度ウェイクワード検出モデル
* EfficientNetアーキテクチャベースで構築されており、精度と軽量性を両立
* 公開されている論文ベースでは、LibriSpeechやGoogle Speech Commandsなどのベンチマークで優秀な精度を記録
メリット
* 音声ノイズ環境でも精度が高い
* 高速かつ高精度な推論が可能
* 多数のウェイクワードを同時に検出するシナリオに強い
注意点
* 実装がやや高度で、デバイスへの組み込みにはある程度の知識が必要
* ソースコードが論文公開のみで、実装例が少ないこともある

| 比較項目 | OpenWakeWord | EfficientWord-Net |
| ——— | ——————- | ——————– |
| オープンソース | はい | 一部のみ(論文あり) |
| カスタムワード対応 | あり(簡易に追加可能) | 理論上可能だが要学習 |
| リアルタイム性 | 高い(軽量) | 高いがややGPU依存 |
| 対応言語 | 任意(自作可能、日本語対応も工夫で可) | 主に英語想定 |
| 実装のしやすさ | 非常に簡単(Pythonで完結) | やや難(自前でモデル導入や音響処理必要) |
| 小型デバイス対応 | Raspberry Pi等でOK | 要スペック(モバイルでも可能) |

EfficientWord-Netはリアルタイム性
高いがややGPU依存だと
OpenWakeWordの方が今後ラズパイとかで動く方が良いかも

OpenWakeWordは自分の声でトリガーワードを録音し、モデルを訓練して追加可能
なお自分の声でなくても voicevox などで生成した音声でもいけるらしい

ただし要求するマシンスペックがメモリ8GBのため
Google Colab を使うことにする

ウェイクワード検出ライブラリ openWakeWord を使ってみた
での動作環境は ubuntu

オープンソースのウェイクワード検出ライブラリ「openWakeword」を試す
だと
Apple Silicon
だと問題があるらしい
自力でビルドすればいけるらしいが

GPTだと
Zennのkun432氏による記事「オープンソースのウェイクワード検出ライブラリ『openWakeword』を試す」で報告されていた問題は、現在では解決されています。
当初、Apple Silicon(M1/M2)搭載のMacでOpenWakeWordを使用する際、TensorFlow Liteランタイム(tflite-runtime)が提供されておらず、エラーが発生していました。
しかし、OpenWakeWordはONNX形式のモデルもサポートしており、onnxruntimeを使用することで、Apple Silicon環境でも問題なく動作します。

ってあるけど本当かは試さないと不明

pip install openwakeword onnxruntime sounddevice

でインストール

M1環境では tflite-runtime は使わず、onnxruntime を使うらしい

次に起動テスト

touch test_openwakeword.py

内容を

import sounddevice as sd
from openwakeword.model import Model

# ONNXモードを明示
oww_model = Model(
    wakeword_models=["hey_jarvis"],
    inference_framework="onnx"
)

sample_rate = 16000
duration = 0.5  # 秒

print("ウェイクワード検出開始(Ctrl+Cで終了)")

try:
    while True:
        audio = sd.rec(int(sample_rate * duration), samplerate=sample_rate, channels=1, dtype='float32')
        sd.wait()
        prediction = oww_model.predict(audio.flatten())

        score = prediction.get("hey_jarvis", 0)
        if score > 0.5:
            print(f"検出!スコア: {score:.2f}")
except KeyboardInterrupt:
    print("終了しました")

これを実行すると

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/gemma/test_openwakeword.py", line 5, in <module>
    oww_model = Model(
                ^^^^^^
  File "/Users/snowpool/.pyenv/versions/3.11.0/lib/python3.11/site-packages/openwakeword/utils.py", line 686, in wrapped
    return func(*args, **new_kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/snowpool/.pyenv/versions/3.11.0/lib/python3.11/site-packages/openwakeword/model.py", line 153, in __init__
    self.models[mdl_name] = ort.InferenceSession(mdl_path, sess_options=sessionOptions,
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/snowpool/.pyenv/versions/3.11.0/lib/python3.11/site-packages/onnxruntime/capi/onnxruntime_inference_collection.py", line 469, in __init__
    self._create_inference_session(providers, provider_options, disabled_optimizers)
  File "/Users/snowpool/.pyenv/versions/3.11.0/lib/python3.11/site-packages/onnxruntime/capi/onnxruntime_inference_collection.py", line 530, in _create_inference_session
    sess = C.InferenceSession(session_options, self._model_path, True, self._read_config_from_model)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
onnxruntime.capi.onnxruntime_pybind11_state.NoSuchFile: [ONNXRuntimeError] : 3 : NO_SUCHFILE : Load model from /Users/snowpool/.pyenv/versions/3.11.0/lib/python3.11/site-packages/openwakeword/resources/models/hey_jarvis_v0.1.onnx failed:Load model /Users/snowpool/.pyenv/versions/3.11.0/lib/python3.11/site-packages/openwakeword/resources/models/hey_jarvis_v0.1.onnx failed. File doesn't exist

となる

OpenWakeWord が使用しようとしているモデルファイル:

/Users/snowpool/.pyenv/versions/3.11.0/lib/python3.11/site-packages/openwakeword/resources/models/hey_jarvis_v0.1.onnx

が存在せず、onnx モデルの自動ダウンロードに失敗していることが原因

https://zenn.dev/kun432/scraps/1a987de4943c65
にも書いてあるけど
どうやらモデルファイルをダウンロードする必要がある

GPTでモデルダウンロードURLを表示されるが
存在しないURLだったので解決策を調べる

https://zenn.dev/kun432/scraps/1a987de4943c65
を参考に

touch download.py  

内容は

import openwakeword

openwakeword.utils.download_models()


実行すると

python download.py 
embedding_model.tflite: 100%|█████████████████| 1.33M/1.33M [00:00<00:00, 9.92MiB/s]
embedding_model.onnx: 100%|███████████████████| 1.33M/1.33M [00:00<00:00, 7.13MiB/s]
melspectrogram.tflite: 100%|██████████████████| 1.09M/1.09M [00:00<00:00, 6.30MiB/s]
melspectrogram.onnx: 100%|████████████████████| 1.09M/1.09M [00:00<00:00, 6.54MiB/s]
silero_vad.onnx: 100%|████████████████████████| 1.81M/1.81M [00:00<00:00, 7.82MiB/s]
alexa_v0.1.tflite: 100%|████████████████████████| 855k/855k [00:00<00:00, 5.67MiB/s]
alexa_v0.1.onnx: 100%|██████████████████████████| 854k/854k [00:00<00:00, 5.36MiB/s]
hey_mycroft_v0.1.tflite: 100%|██████████████████| 860k/860k [00:00<00:00, 6.84MiB/s]
hey_mycroft_v0.1.onnx: 100%|████████████████████| 858k/858k [00:00<00:00, 6.52MiB/s]
hey_jarvis_v0.1.tflite: 100%|█████████████████| 1.28M/1.28M [00:00<00:00, 6.96MiB/s]
hey_jarvis_v0.1.onnx: 100%|███████████████████| 1.27M/1.27M [00:00<00:00, 6.26MiB/s]
hey_rhasspy_v0.1.tflite: 100%|██████████████████| 416k/416k [00:00<00:00, 4.76MiB/s]
hey_rhasspy_v0.1.onnx: 100%|████████████████████| 204k/204k [00:00<00:00, 3.06MiB/s]
timer_v0.1.tflite: 100%|██████████████████████| 1.74M/1.74M [00:00<00:00, 7.98MiB/s]
timer_v0.1.onnx: 100%|████████████████████████| 1.74M/1.74M [00:00<00:00, 8.96MiB/s]
weather_v0.1.tflite: 100%|████████████████████| 1.15M/1.15M [00:00<00:00, 6.99MiB/s]
weather_v0.1.onnx: 100%|██████████████████████| 1.15M/1.15M [00:00<00:00, 6.44MiB/s]

となってダウンロードされる

再度

python test_openwakeword.py
ウェイクワード検出開始(Ctrl+Cで終了)
^C終了しました

と起動はするけど
ジャービス
ヘイ ジャービス
でも反応がない

https://zenn.dev/kun432/scraps/1a987de4943c65
のコードを参考に実験する

import pyaudio
import numpy as np
from openwakeword.model import Model
import sys

FORMAT = pyaudio.paInt16
CHANNELS = 1
RATE = 16000
CHUNK = 1024
audio = pyaudio.PyAudio()
mic_stream = audio.open(format=FORMAT, channels=CHANNELS, rate=RATE, input=True, frames_per_buffer=CHUNK)

model_name = "alexa_v0.1.onnx"

model = Model(
  wakeword_models=[model_name],
  inference_framework="onnx"
)

print("Listening for wakeword \"Alexa\"...")
print()

prev_detect=False

while True:
    audio = np.frombuffer(mic_stream.read(CHUNK), dtype=np.int16)

    prediction = model.predict(audio)

    scores = model.prediction_buffer[model_name]
    curr_score = format(scores[-1], '.20f')
    detect = True if float(curr_score) > 0.5 else False

    if detect:
        if detect != prev_detect:
            print(f"Detected!({curr_score[:5]})")
            prev_detect=True
    else:
        prev_detect=False


Alexaから変更する
これは家の中にあるアレクサと被るため
hey jarvisで動作確認する

とりあえず GPTでコード解説

マイク設定と音声入力の初期化

import pyaudio
import numpy as np
from openwakeword.model import Model
import sys

FORMAT = pyaudio.paInt16       # 16bit PCM
CHANNELS = 1                   # モノラル
RATE = 16000                   # サンプリング周波数 16kHz
CHUNK = 1024                   # 一度に読み取るサンプル数

audio = pyaudio.PyAudio()
mic_stream = audio.open(format=FORMAT, channels=CHANNELS, rate=RATE, input=True, frames_per_buffer=CHUNK)

これで
マイクから音声をリアルタイムで取得するためのストリームを開始

次にOpenWakeWord モデルの初期化
model_name = "alexa_v0.1.onnx"
model = Model(
  wakeword_models=[model_name],
  inference_framework="onnx"
)

今回は”alexa_v0.1.onnx” モデルをONNXランタイムで使用
macOS (M1など) では onnx が推奨

そして
メインループで音声解析

print("Listening for wakeword \"Alexa\"...")

while True:
    audio = np.frombuffer(mic_stream.read(CHUNK), dtype=np.int16)

この部分で
マイクから CHUNK = 1024 サンプル(= 約64ms)分を読み込み
int16 に変換してNumPy配列にします(OpenWakeWordの入力形式)

そしてウェイクワードの検出

    prediction = model.predict(audio)

現在のフレームに対してウェイクワードスコアを計算

    scores = model.prediction_buffer[model_name]
    curr_score = format(scores[-1], '.20f')
    detect = True if float(curr_score) > 0.5 else False

過去のスコアをバッファから取得し、最新のスコア([-1])を確認
0.5 を超えていたら検出と判定

一度だけ表示する仕組みとして

    if detect:
        if detect != prev_detect:
            print(f"Detected!({curr_score[:5]})")
            prev_detect=True
    else:
        prev_detect=False

前回検出と今回の結果が異なるときだけ表示することで、連続検出を防ぐ
例:「アレクサ」と言う → スコアが上がって「Detected!」が一度表示

これを元に音声モデルを変更

import pyaudio
import numpy as np
from openwakeword.model import Model
import sys

FORMAT = pyaudio.paInt16
CHANNELS = 1
RATE = 16000
CHUNK = 1024
audio = pyaudio.PyAudio()
mic_stream = audio.open(format=FORMAT, channels=CHANNELS, rate=RATE, input=True, frames_per_buffer=CHUNK)

model_name = "hey_jarvis_v0.1.onnx"

model = Model(
  wakeword_models=[model_name],
  inference_framework="onnx"
)

print("Listening for wakeword \"hey_jarvis\"...")
print()

prev_detect=False

while True:
    audio = np.frombuffer(mic_stream.read(CHUNK), dtype=np.int16)

    prediction = model.predict(audio)

    scores = model.prediction_buffer[model_name]
    curr_score = format(scores[-1], '.20f')
    detect = True if float(curr_score) > 0.5 else False

    if detect:
        if detect != prev_detect:
            print(f"Detected!({curr_score[:5]})")
            prev_detect=True
    else:
        prev_detect=False

実行したら

python test_openwakeword.py
Traceback (most recent call last):
  File "/Users/snowpool/aw10s/gemma/test_openwakeword.py", line 1, in <module>
    import pyaudio
ModuleNotFoundError: No module named 'pyaudio'

となったので

pip install pyaudio
Collecting pyaudio
  Downloading PyAudio-0.2.14.tar.gz (47 kB)
  Installing build dependencies ... done
  Getting requirements to build wheel ... done
  Preparing metadata (pyproject.toml) ... done
Building wheels for collected packages: pyaudio
  Building wheel for pyaudio (pyproject.toml) ... done
  Created wheel for pyaudio: filename=pyaudio-0.2.14-cp311-cp311-macosx_15_0_arm64.whl size=25773 sha256=85950d898df05ebcce820f2f6d30b030db44951656c7decef10274215dca529b
  Stored in directory: /Users/snowpool/Library/Caches/pip/wheels/80/b1/c1/67e4ef443de2665d86031d4760508094eab5de37d5d64d9c27
Successfully built pyaudio
Installing collected packages: pyaudio
Successfully installed pyaudio-0.2.14

[notice] A new release of pip is available: 24.3.1 -> 25.1.1
[notice] To update, run: pip install --upgrade pip

で結局反応しないのでアレクサで試したら動いた…

単純に私の発音が悪いらしい

GPTによれば発音がダメとのこと

なお最初にGPTで提案されたコードは動作しないので
今回のコードを使うことにする

pyttsx3 音声読み上げ機能をモジュール化

pyttsx3 音声読み上げ機能をモジュール化

module/module_speaker.py

import pyttsx3
import emoji
import re

class Speaker:
    def __init__(self):
        self.engine = pyttsx3.init()
        # 音量やスピード、声の設定があればここで行える
        self.engine.setProperty("rate", 170)  # 話すスピード(デフォルト=200)
        self.engine.setProperty("volume", 1.0)  # 音量(0.0~1.0)

    def remove_emoji(self, text: str) -> str:
        return emoji.replace_emoji(text, replace='')

    def speak(self, text: str):
        clean_text = self.remove_emoji(text)
        clean_text = re.sub(r"[*_`~^]", "", clean_text)  # マークアップ記号を除去
        print("\n【読み上げるテキスト】")
        print(clean_text)

        # 1文ずつ話すことで安定性を向上
        sentences = re.split(r"(?<=[。!?\n])", clean_text)
        for sentence in sentences:
            if sentence.strip():
                self.engine.say(sentence.strip())
        self.engine.runAndWait()

として保存

pyttsx3 関連の処理をモジュール化した module_speaker.py を使うように

touch main5.py

from module.module_audio_to_text import AudioToTextCorrector
from module.module_speaker import Speaker
from ollama import chat, ChatResponse

# モデル名
OLLAMA_MODEL = 'gemma3:4b'

# 音声読み上げクラスを初期化
speaker = Speaker()

def ask_ollama(prompt: str) -> str:
    try:
        response: ChatResponse = chat(model=OLLAMA_MODEL, messages=[
            {
                'role': 'user',
                'content': prompt,
            }
        ])
        return response.message.content.strip()
    except Exception as e:
        print(f"Ollamaエラー: {e}")
        return "エラーが発生しました。"

def main():
    audio_to_text = AudioToTextCorrector("config.json")

    while True:
        corrected_text = audio_to_text.record_and_correct(timeout_seconds=10)

        if corrected_text is None:
            print("終了条件に達したため、ループを抜けます。")
            break

        print("\n【認識・補正したテキスト】")
        print(corrected_text)

        # Ollamaへ質問
        ollama_reply = ask_ollama(corrected_text)

        print("\n【gemma3:4bの返答】")
        print(ollama_reply)

        # 読み上げ
        speaker.speak(ollama_reply)

if __name__ == "__main__":
    main()

として検証

[2025-05-10 06:23:46.009] [ctranslate2] [thread 64614] [warning] The compute type inferred from the saved model is float16, but the target device or backend do not support efficient float16 computation. The model weights have been automatically converted to use the float32 compute type instead.
stand by ready OK
recording...
finished

【認識・補正したテキスト】
こんにちは。

【gemma3:4bの返答】
こんにちは!何かお手伝いできることはありますか?😊

【読み上げるテキスト】
こんにちは!何かお手伝いできることはありますか?
stand by ready OK
recording...
finished
10秒間音声が入力されなかったため、処理を終了します。
終了条件に達したため、ループを抜けます。

となるので無事に動作しているのが確認できる

次はウェイクワードエンジンの導入

gemma3:4bの返答を pyttsx3 を使って 自然にリアルタイム読み上げる

gemma3:4bの返答を
pyttsx3 を使って
自然にリアルタイム読み上げる

読み上げは

touch main4.py

でファイルを作成し問題が起きた時にわかりやすくする

from module.module_audio_to_text import AudioToTextCorrector
from ollama import chat, ChatResponse
import pyttsx3
import re
import emoji

# モデル名
OLLAMA_MODEL = 'gemma3:4b'

# pyttsx3初期化
engine = pyttsx3.init()

def ask_ollama(prompt: str) -> str:
    try:
        response: ChatResponse = chat(model=OLLAMA_MODEL, messages=[
            {
                'role': 'user',
                'content': prompt,
            }
        ])
        return response.message.content.strip()
    except Exception as e:
        print(f"Ollamaエラー: {e}")
        return "エラーが発生しました。"

def remove_emoji(text: str) -> str:
    return emoji.replace_emoji(text, replace='')

def speak(text: str):
    clean_text = remove_emoji(text)
    print("\n【読み上げるテキスト】")
    print(clean_text)
    engine.say(clean_text)
    engine.runAndWait()

def main():
    audio_to_text = AudioToTextCorrector("config.json")

    while True:
        corrected_text = audio_to_text.record_and_correct(timeout_seconds=10)

        if corrected_text is None:
            print("終了条件に達したため、ループを抜けます。")
            break

        print("\n【認識・補正したテキスト】")
        print(corrected_text)

        # Ollamaへ質問
        ollama_reply = ask_ollama(corrected_text)

        print("\n【gemma3:4bの返答】")
        print(ollama_reply)

        # gemma3:4bの返答を音声で読み上げ
        speak(ollama_reply)

if __name__ == "__main__":
    main()

実行したけど

[2025-05-09 19:01:12.451] [ctranslate2] [thread 8719969] [warning] The compute type inferred from the saved model is float16, but the target device or backend do not support efficient float16 computation. The model weights have been automatically converted to use the float32 compute type instead. stand by ready OK recording... finished Ollamaエラー: llama runner process has terminated: exit status 2 終了条件に達したため、ループを抜けます。

となる

エラー内容を調べると

Ollamaエラー: llama runner process has terminated: exit status 2
は、Ollamaの内部プロセス(llama-runner)がクラッシュして終了

このため chrome を一度終了してから再度実行

しかし音声が出ない

engine = pyttsx3.init('nsss')  # macOSならこれで確実に動く

と明示的にしてもだめ

touch tts_test.py

でファイルを作成

import pyttsx3

engine = pyttsx3.init('nsss')
text = "こんにちは。今日はどんな気分ですか?映画でも見ませんか?"
engine.say(text)
engine.runAndWait()

として実行

しかし音声が再生されない
色々試した結果、再起動したら音声が再生された

どうやら再起動することで解決したらしい
メモリが足りなくてエラーだったので
もう少しマシンスペックを上げないと厳しいかもしれない

とりあえず動くので
次は音声再生部分をモジュールにする
その後ウェイクワードエンジンをやってみる

認識したテキストをgemma3:4bへ質問して 返答を表示する

認識したテキストをgemma3:4bへ質問して 返答を表示する

🎯 目標
* マイクで音声入力
* faster-whisperでテキスト化
* Ollama(gemma3:4b)に質問として送信
* 返答を画面に表示するだけ(まだ読み上げはしない)

AudioToTextCorrector でテキストを取得
そのテキストを gemma3:4b に渡す
gemma3:4bの返答をprintする

これに伴い

from module.module_audio_to_text import AudioToTextCorrector

def main():
    audio_to_text = AudioToTextCorrector("config.json")

    recognized_texts = []

    while True:
        corrected_text = audio_to_text.record_and_correct(timeout_seconds=10)

        if corrected_text is None:
            print("終了条件に達したため、ループを抜けます。")
            break

        recognized_texts.append(corrected_text)
        print(corrected_text)

    if recognized_texts:
        message = "\n".join(recognized_texts)
        print("\n入力された音声テキスト一覧:")
        print(message)
    else:
        print("入力メッセージはありませんでした")

if __name__ == "__main__":
    main()

のコードを変更

これで実行

今日の天気
と音声入力すると

python main3.py
[2025-05-08 04:08:59.862] [ctranslate2] [thread 8630251] [warning] The compute type inferred from the saved model is float16, but the target device or backend do not support efficient float16 computation. The model weights have been automatically converted to use the float32 compute type instead.
stand by ready OK
recording...
finished

【認識・補正したテキスト】
今日の気候はどうでしょうか。

【gemma3:4bの返答】
はい、今日の気候についてお調べします。

現在(2024年5月16日16時30分)の天気は以下の通りです。

*   **場所:** 東京都
*   **天気:** 晴れ
*   **気温:** 18℃
*   **降水確率:** 0%
*   **風:** 北西風 3~5km/h

より詳細な情報が必要な場合は、場所を具体的に指定してください。例えば、「大阪府の今日の天気は?」のように聞いていただければ、より正確な情報をお伝えできます。

また、以下のサイトでも最新の天気予報を確認できます。

*   **Yahoo!天気・災害:** [https://weather.yahoo.co.jp/](https://weather.yahoo.co.jp/)
*   **日本気象協会 tenki.jp:** [https://tenki.jp/](https://tenki.jp/)
stand by ready OK
recording...
finished
10秒間音声が入力されなかったため、処理を終了します。
終了条件に達したため、ループを抜けます。

となる

とりあえず音声で入力し
これを認識補正することで簡単な質問でも適切に回答可能できそう

次は
gemma3:4bの返答を
pyttsx3 を使って
自然にリアルタイム読み上げる

faster-whisperで音声の取得

faster-whisperで音声の取得

import pyaudio
import numpy as np
from faster_whisper import WhisperModel

# Faster Whisperのモデルをロードします
model = WhisperModel("large-v3", device="cpu", compute_type="int8")

# 音声設定
FORMAT = pyaudio.paInt16
CHANNELS = 1
RATE = 16000
CHUNK = 2048  # バッファサイズを大きく変更

# PyAudioのインスタンスを作成
audio = pyaudio.PyAudio()

# マイクから音声を取得するストリームを開きます
stream = audio.open(format=FORMAT, channels=CHANNELS, rate=RATE, input=True, frames_per_buffer=CHUNK)

print("Listening...")

# 音声データをバッファとして取得してリアルタイムに処理します
try:
    while True:
        # 音声データを取得、オーバーフロー時に例外を発生させない
        data = stream.read(CHUNK, exception_on_overflow=False)
        # NumPy配列に変換
        audio_data = np.frombuffer(data, dtype=np.int16)
        # Faster Whisperに音声データを渡してテキストを取得
        segments, _ = model.transcribe(audio_data, language="ja")  # 言語を日本語に指定
        # 取得したセグメントを出力
        for segment in segments:
            print(f"Text: {segment.text}")
except OSError as e:
    print(f"Error: {e}")
except KeyboardInterrupt:
    print("Terminating...")
finally:
    stream.stop_stream()
    stream.close()
    audio.terminate()

をGPTでコードレビュー

これは以前linebot へ音声を文字にして送信するモジュール
これを改良できるか試す

はい、これ非常に良い線いってます!
あなたが示したコードは、**リアルタイムっぽく音声入力してテキストに変換する**試みになっています。

ただし、**このままだと少し問題が起きる可能性**が高いです。
具体的に説明しますね。

### あなたのコードのいい点

* `PyAudio` で**リアルタイムでマイク入力**している
* `faster-whisper` で**その場で文字起こし**しようとしている
* `large-v3` モデルで**かなり高精度**を狙っている
* 言語指定も `language=”ja”` で正しい

→ ここまでは完璧です。🎯

### 問題点・注意すべきポイント

① `faster-whisper` の `.transcribe()` に与える音声は、
本来**数秒〜十数秒程度の「まとまった」音声**を期待しています。

* あなたの今のコードだと、**たったCHUNK(0.1秒程度)ずつ**で呼び出してしまってます。
* これは音声認識の**最小単位に全然足りてない**ので、
→ 無反応だったり、
→ 文字化け、
→ 無駄にCPU食うだけになるリスクが高いです。

### どうすればいいか?

✅ ある程度\*\*バッファリング(音声をためる)\*\*してからtranscribeするべきです!

### 【改良案】まとまった音声を一定時間ごとに送る

例えば:

* 1秒間 or 2秒間分ぐらい音をバッファにためる
* それを `model.transcribe()` に渡す
* テキストを出す
* また次のバッファをためる
* …をループ

にするとうまくいきます。

### 具体的な改良コード案

import pyaudio
import numpy as np
from faster_whisper import WhisperModel
import time

# Faster Whisperのモデルをロード
model = WhisperModel("large-v3", device="cpu", compute_type="int8")

# 音声設定
FORMAT = pyaudio.paInt16
CHANNELS = 1
RATE = 16000
CHUNK = 2048  # バッファ単位
BUFFER_DURATION = 2  # バッファリングする秒数(例:2秒ごとに処理)

# PyAudioのインスタンス作成
audio = pyaudio.PyAudio()

# ストリーム開始
stream = audio.open(format=FORMAT, channels=CHANNELS, rate=RATE, input=True, frames_per_buffer=CHUNK)

print("Listening...")

buffer = np.array([], dtype=np.int16)
start_time = time.time()

try:
    while True:
        # マイクからデータ取得
        data = stream.read(CHUNK, exception_on_overflow=False)
        audio_data = np.frombuffer(data, dtype=np.int16)
        buffer = np.concatenate((buffer, audio_data))

        # 一定時間分たまったら処理
        if time.time() - start_time > BUFFER_DURATION:
            if len(buffer) > 0:
                segments, _ = model.transcribe(buffer, language="ja", sampling_rate=RATE)
                for segment in segments:
                    print(f"認識結果: {segment.text}")
            # バッファリセット
            buffer = np.array([], dtype=np.int16)
            start_time = time.time()

except KeyboardInterrupt:
    print("Terminating...")

finally:
    stream.stop_stream()
    stream.close()
    audio.terminate()

### これでどうなる?

* 2秒間分録音する
* 2秒ごとに自動でテキスト化
* また録音スタート
* これを繰り返す

だから、**リアルタイム感を持ちつつちゃんと認識できる**感じになります!

### あなたがやりたいことにこの改良版はぴったりです!

このあと、

* 取得したテキストをそのままollamaに渡して
* 返答を音声で読み上げ
を追加すれば、**完全な音声会話ボット**になります。

✅ ここまで整理できたので、
次はこの改良版をベースに、

* 「録音→認識→ollamaへ送信→pyttsx3で読み上げ」
* ずっと繰り返す(連続対話モード)

を一緒に組み上げましょうか?

やりますか?🚀
(次、完成版一気に出します)

しかし、以前に linebot で作成したモジュールがあるので
それを流用する
なおLINEbot 送信機能は不要なのでオミットする

Main2.pyで

import sounddevice as sd
from module.module_whisper import FasterWhisperModel
from module.module_recorder import Recorder
import time
from line_notify import LineNotify  # 作成したLineNotifyモジュールをインポート
from ollama_text_correction import OllamaTextCorrector  # Ollamaによる修正モジュールをインポート
from line_bot_sender import LineBotSender  # LINE bot送信用のモジュールをインポート

def main():
    recorder = Recorder()
    fasterWhispermodel = FasterWhisperModel()

    # 入力された音声テキストを格納するリスト
    recognized_texts = []

    # LINE Notifyのモジュールを初期化(config.jsonからトークンを読み込む)
    line_notify = LineNotify("config.json")
    
    # Ollamaのテキスト修正モジュールを初期化
    text_corrector = OllamaTextCorrector("config.json")
    
    # LINE bot送信用のモジュールを初期化
    line_bot_sender = LineBotSender("config.json")

    while True:
        start_time = time.time()  # 処理開始時刻を記録
        audio_data = recorder.speech2audio()

        # 処理が10秒間行われなかった場合はループを抜ける
        if time.time() - start_time >= 10:
            print("10秒間音声が入力されなかったため、ループを終了します。")
            break

        if audio_data is None:
            print("無音状態が続いたため、ループを終了します。")
            break  # 無音でループを抜ける
        
        # 音声をテキストに変換
        text = fasterWhispermodel.audio2text(audio_data)
        
        # Ollamaでテキストを構成
        corrected_text = text_corrector.correct_text(text)
        
        if corrected_text:  # Noneが返された場合はスキップ
            recognized_texts.append(corrected_text)
            print(corrected_text)

    # ループ終了後に、入力した音声テキストを改行付きで一覧表示
    if recognized_texts:
        message = "\n".join(recognized_texts)
        print("\n入力された音声テキスト一覧:")
        print(message)

        # LINE Notifyでメッセージを送信
        line_notify.send(f"入力された音声テキスト一覧:\n{message}")
        
        # LINE botで修正されたテキストを送信
        line_bot_sender.send_message(f"修正された音声テキスト:\n{message}")
    else:
        print("入力メッセージはありませんでした")

if __name__ == "__main__":
    main()

そしてこれで読み込んでいる

そしてこの時に自作モジュールを読み込んでいるが
これはrequest なので公式ライブラリへ変換する

以下変更後のコード

from ollama import chat
import json

class OllamaTextCorrector:
    def __init__(self, config_file_path):
        self.config = self._load_config(config_file_path)
        self.model = self.config.get("ollama_model", "elyza:jp8b")

    def _load_config(self, config_file_path):
        with open(config_file_path, 'r') as file:
            config = json.load(file)
        return config

    def correct_text(self, text):
        try:
            response = chat(model=self.model, messages=[
                {
                    'role': 'user',
                    'content': (
                        f"以下の文を、固有名詞や商品名を変更せずに、"
                        f"文脈や意味を考慮しながら、より自然な日本語に修正してください。"
                        f"単語の置き換えや意味を変えず、解説は不要です。修正後の文章だけを返してください:\n{text}"
                    ),
                }
            ])
            return response.message.content.strip()
        except Exception as e:
            print(f"Ollamaエラー: {e}")
            return None

あとは main2.py をコピーする
なお linebot関連は不要なので削除

import sounddevice as sd
from module.module_whisper import FasterWhisperModel
from module.module_recorder import Recorder
import time
from ollama_text_correction import OllamaTextCorrector  # Ollamaによる修正モジュールをインポート

def main():
    recorder = Recorder()
    fasterWhispermodel = FasterWhisperModel()

    # 入力された音声テキストを格納するリスト
    recognized_texts = []

    # Ollamaのテキスト修正モジュールを初期化
    text_corrector = OllamaTextCorrector("config.json")
    
    while True:
        start_time = time.time()  # 処理開始時刻を記録
        audio_data = recorder.speech2audio()

        # 処理が10秒間行われなかった場合はループを抜ける
        if time.time() - start_time >= 10:
            print("10秒間音声が入力されなかったため、ループを終了します。")
            break

        if audio_data is None:
            print("無音状態が続いたため、ループを終了します。")
            break  # 無音でループを抜ける
        
        # 音声をテキストに変換
        text = fasterWhispermodel.audio2text(audio_data)
        
        # Ollamaでテキストを構成
        corrected_text = text_corrector.correct_text(text)
        
        if corrected_text:  # Noneが返された場合はスキップ
            recognized_texts.append(corrected_text)
            print(corrected_text)

    # ループ終了後に、入力した音声テキストを改行付きで一覧表示
    if recognized_texts:
        message = "\n".join(recognized_texts)
        print("\n入力された音声テキスト一覧:")
        print(message)

    else:
        print("入力メッセージはありませんでした")

if __name__ == "__main__":
    main()

また
読み込んでいるモジュールの
ollama_text_correction.py
のコードも
requestからchatへ変更する

from ollama import chat
import json

class OllamaTextCorrector:
    def __init__(self, config_file_path):
        self.config = self._load_config(config_file_path)
        self.model = self.config.get("ollama_model", "elyza:jp8b")

    def _load_config(self, config_file_path):
        with open(config_file_path, 'r') as file:
            config = json.load(file)
        return config

    def correct_text(self, text):
        try:
            response = chat(model=self.model, messages=[
                {
                    'role': 'user',
                    'content': (
                        f"以下の文を、固有名詞や商品名を変更せずに、"
                        f"文脈や意味を考慮しながら、より自然な日本語に修正してください。"
                        f"単語の置き換えや意味を変えず、解説は不要です。修正後の文章だけを返してください:\n{text}"
                    ),
                }
            ])
            return response.message.content.strip()
        except Exception as e:
            print(f"Ollamaエラー: {e}")
            return None

これで実行したら

python main2.py 
Traceback (most recent call last):
  File "/Users/snowpool/aw10s/gemma/main2.py", line 2, in <module>
    from module.module_whisper import FasterWhisperModel
ModuleNotFoundError: No module named 'module'

となった

cp -rp ../linebot/module .

でモジュール関連をコピーする

しかし

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/gemma/main2.py", line 2, in <module>
    from module.module_whisper import FasterWhisperModel
  File "/Users/snowpool/aw10s/gemma/module/module_whisper.py", line 1, in <module>
    from faster_whisper import WhisperModel
ModuleNotFoundError: No module named 'faster_whisper'

これは python 環境を変えたため

pip install faster-whisper

設定ファイル関連がないので

cp -rp ../linebot/configs .

これで再度実行

[2025-05-07 00:28:39.222] [ctranslate2] [thread 8562645] [warning] The compute type inferred from the saved model is float16, but the target device or backend do not support efficient float16 computation. The model weights have been automatically converted to use the float32 compute type instead.
Traceback (most recent call last):
  File "/Users/snowpool/aw10s/gemma/main2.py", line 50, in <module>
    main()
  File "/Users/snowpool/aw10s/gemma/main2.py", line 15, in main
    text_corrector = OllamaTextCorrector("config.json")
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/snowpool/aw10s/gemma/ollama_text_correction.py", line 6, in __init__
    self.config = self._load_config(config_file_path)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/snowpool/aw10s/gemma/ollama_text_correction.py", line 10, in _load_config
    with open(config_file_path, 'r') as file:
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'config.json'

今度はconfig.jsonが足りないので

cp ../linebot/config.json .

これで動作する

次に faster-whisper で行っている録音と文字起こしをモジュール化

AudioToTextCorrector クラスを作る
record_and_correct() を呼ぶと
 → 録音して
 → 文字起こして
 → ollamaで自然な日本語にして
 → テキストだけ返す!
タイムアウト時間(例:10秒)もパラメータで変更可能
無音や時間切れなら None を返すから呼び出し側で判断しやすい

touch module/module_audio_to_text.py

内容は

import time
from module.module_whisper import FasterWhisperModel
from module.module_recorder import Recorder
from ollama_text_correction import OllamaTextCorrector

class AudioToTextCorrector:
    def __init__(self, config_file_path="config.json"):
        self.recorder = Recorder()
        self.faster_whisper_model = FasterWhisperModel()
        self.text_corrector = OllamaTextCorrector(config_file_path)
    
    def record_and_correct(self, timeout_seconds=10):
        """
        音声を録音して、文字起こしして、自然な日本語に補正したテキストを返す。
        無音やtimeoutになった場合はNoneを返す。
        """

        start_time = time.time()
        audio_data = self.recorder.speech2audio()

        if time.time() - start_time >= timeout_seconds:
            print(f"{timeout_seconds}秒間音声が入力されなかったため、処理を終了します。")
            return None

        if audio_data is None:
            print("無音状態が続いたため、処理を終了します。")
            return None

        text = self.faster_whisper_model.audio2text(audio_data)
        corrected_text = self.text_corrector.correct_text(text)

        return corrected_text

これにより main2.pyを簡素化する
とは言ってもバグ対策でmain3.py
としておく

実行して問題ないので
次は
まずは音声の読み上げ前に 認識したテキストをgemma3:4bへ質問して 返答を表示する
それから読み上げを行うことにする

Ollama gemma3 4b の読み上げ

Ollama gemma3 4b の読み上げ

import requests
import pyttsx3

# Ollama設定
OLLAMA_URL = "http://localhost:11434/api/generate"
MODEL_NAME = "gemma3:4b"  # モデル名(必要に応じてgemma3:4bに)

# 音声読み上げエンジン初期化
engine = pyttsx3.init()

def ask_ollama(prompt):
    payload = {
        "model": MODEL_NAME,
        "prompt": prompt,
        "stream": False
    }
    response = requests.post(OLLAMA_URL, json=payload)
    if response.status_code == 200:
        result = response.json()
        return result.get("response", "")
    else:
        print(f"Error: {response.status_code}")
        return ""

def speak(text):
    engine.say(text)
    engine.runAndWait()

def main():
    # 質問文
    prompt = "おはよう"
    
    # Ollamaに送信
    reply = ask_ollama(prompt)
    print("Ollamaの回答:", reply)
    
    # 読み上げ
    if reply:
        speak(reply)
    else:
        speak("エラーが発生しました。")

if __name__ == "__main__":
    main()

で実行したけど絵文字が入ると読み上げが変になるので削除

またrequest を使わないようにするので
Ollama gemma3 python で検索

OllamaとGemma3でローカルLLMをPythonで実行する
を参考に変更

読み上げの時に回答に絵文字などがあるとわかりにくいため、絵文字などは削除するので
Re を使う

from ollama import chat
from ollama import ChatResponse
import pyttsx3
import re

# モデル名
MODEL_NAME = 'gemma3:4b'

# pyttsx3 初期化
engine = pyttsx3.init()

def remove_emoji(text: str) -> str:
    # Unicode範囲を指定して絵文字を除去する正規表現
    emoji_pattern = re.compile(
        "["
        "\U0001F600-\U0001F64F"  # 顔文字
        "\U0001F300-\U0001F5FF"  # 天気や地図記号
        "\U0001F680-\U0001F6FF"  # 乗り物
        "\U0001F1E0-\U0001F1FF"  # 国旗
        "\U00002700-\U000027BF"  # その他の記号
        "\U000024C2-\U0001F251"
        "]+",
        flags=re.UNICODE
    )
    cleaned_text = emoji_pattern.sub('', text)
    return cleaned_text

def ask_ollama(prompt: str) -> str:
    try:
        response: ChatResponse = chat(model=MODEL_NAME, messages=[
            {
                'role': 'user',
                'content': prompt,
            }
        ])
        return response.message.content
    except Exception as e:
        print(f"Ollamaエラー: {e}")
        return "エラーが発生しました。"

def speak(text: str):
    # ここで絵文字を除去してから読み上げ
    clean_text = remove_emoji(text)
    engine.say(clean_text)
    engine.runAndWait()

def main():
    prompt = "おはよう。日本語で答えてください。"

    reply = ask_ollama(prompt)
    print("Ollamaの回答(絵文字付き):", reply)

    speak(reply)

if __name__ == "__main__":
    main()

これだと機能しない

pip install emoji

で絵文字を削除できるのでインストールしておく

import emoji

def remove_emoji(text: str) -> str:
    # emojiパッケージを使う方法(こちらの方が確実)
    return emoji.replace_emoji(text, replace='')

を追加

全体は

from ollama import chat
from ollama import ChatResponse
import pyttsx3
import re

# モデル名
MODEL_NAME = 'gemma3:4b'

# pyttsx3 初期化
engine = pyttsx3.init()

def ask_ollama(prompt: str) -> str:
    try:
        response: ChatResponse = chat(model=MODEL_NAME, messages=[
            {
                'role': 'user',
                'content': prompt,
            }
        ])
        return response.message.content
    except Exception as e:
        print(f"Ollamaエラー: {e}")
        return "エラーが発生しました。"

def speak(text: str):
    # 絵文字を除去
    clean_text = remove_emoji(text)
    print("読み上げ用のテキスト:", clean_text)  # ←追加してデバッグ
    engine.say(clean_text)
    engine.runAndWait()

import emoji

def remove_emoji(text: str) -> str:
    # emojiパッケージを使う方法(こちらの方が確実)
    return emoji.replace_emoji(text, replace='')


def main():
    prompt = "おはよう。日本語で答えてください。"

    reply = ask_ollama(prompt)
    print("Ollamaの回答(絵文字付き):", reply)

    speak(reply)

if __name__ == "__main__":
    main()

で実行すると

python voice_test.py
Ollamaの回答(絵文字付き): おはようございます!何かお手伝いできることはありますか? 😊

読み上げ用のテキスト: おはようございます!何かお手伝いできることはありますか? 

となって結果から絵文字を削除し読み上げが可能になる

次は
Faster-whisper で音声の取得

Ollama + gemma3 4B の結果を音声にする

Ollama + gemma3 4B の結果を音声にする

Gemma 3 で RAG 機能付きの安全なローカル AI チャット環境を構築する

を参考に行う

MacBook Pro 14 インチ 2021
* M1 Pro
* 16GB RAM

で動いているなら
M1MacbookAir 16GBでもいけそう

 mkdir gemma
 cd gemma 

ollama pull gemma3:4b

を実行したら

pulling manifest 
Error: pull model manifest: 412: 

The model you are attempting to pull requires a newer version of Ollama.

Please download the latest version at:

	https://ollama.com/download

となる


ollama --version
ollama version is 0.5.11

The model you are attempting to pull requires a newer version of Ollama.

→ 「このモデル(gemma3:4b)をダウンロードするには、Ollama本体を最新版にアップデートする必要がある」 という意味

最近(2025年4月末〜5月頭)にGemma 3系モデル(4B, 9B, 27B)が追加されたばかりで、
それに合わせてOllama側も内部仕様が更新され
古いOllamaではGemma3対応できない

m1macbookAir での ollamaのインストール方法は2つ

Homebrewで管理している
公式サイトから再インストール

となる

状況 更新方法
普通に公式サイトからインストールしていた場合 公式サイトから最新のdmgをダウンロード&上書き
Homebrewで管理していた場合 brew upgrade ollama で更新

どちらかわからないのなら

which ollama

を実行する

/usr/local/bin/ollama

が今回の結果

/usr/local/bin/ollama ならbrewインストール版
/Applications/Ollama.app/Contents/MacOS/ollama ならアプリ版

ということなので
Brewで更新可能

しかし

brew update
brew upgrade ollama

を実行すると

brew upgrade ollama
Warning: Treating ollama as a formula. For the cask, use homebrew/cask/ollama or specify the `--cask` flag. To silence this message, use the `--formula` flag.
Error: ollama not installed
==> `brew cleanup` has not been run in the last 30 days, running now...
Disable this behaviour by setting HOMEBREW_NO_INSTALL_CLEANUP.
Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`).
Removing: /opt/homebrew/Cellar/dtc/1.7.0... (18 files, 619.2KB)
Removing: /opt/homebrew/Cellar/freetype/2.13.2... (67 files, 2.4MB)
Removing: /opt/homebrew/Cellar/fribidi/1.0.15... (68 files, 740KB)
Removing: /opt/homebrew/Cellar/imath/3.1.11... (49 files, 940.5KB)
Removing: /opt/homebrew/Cellar/isl/0.26... (73 files, 7.6MB)
Removing: /opt/homebrew/Cellar/jpeg-turbo/3.0.3... (45 files, 3.4MB)
Removing: /opt/homebrew/Cellar/libssh/0.10.6... (23 files, 1.3MB)
Removing: /opt/homebrew/Cellar/libssh2/1.11.0_1... (197 files, 1.2MB)
Removing: /opt/homebrew/Cellar/libunistring/1.2... (59 files, 5MB)
Removing: /opt/homebrew/Cellar/libxau/1.0.11... (21 files, 123.5KB)
Removing: /opt/homebrew/Cellar/libxrender/0.9.11... (12 files, 214.1KB)
Removing: /opt/homebrew/Cellar/lz4/1.9.4... (22 files, 681.5KB)
Removing: /opt/homebrew/Cellar/mpg123/1.32.6... (34 files, 2.0MB)
Removing: /opt/homebrew/Cellar/npth/1.7... (12 files, 162.1KB)
Removing: /opt/homebrew/Cellar/nspr/4.35... (86 files, 1.2MB)
Removing: /opt/homebrew/Cellar/protobuf/21.12... (285 files, 10.2MB)
Removing: /opt/homebrew/Cellar/rubberband/3.3.0... (13 files, 1.7MB)
Removing: /opt/homebrew/Cellar/srt/1.5.3... (20 files, 4.4MB)
Removing: /opt/homebrew/Cellar/x265/3.6... (12 files, 12MB)
==> Autoremoving 1 unneeded formula:
libgit2@1.7
Removing: /Users/snowpool/Library/Caches/Homebrew/portable-ruby-3.3.5.arm64_big_sur.bottle.tar.gz... (11.2MB)
Removing: /Users/snowpool/Library/Logs/Homebrew/python@3.13... (2 files, 2KB)
Removing: /Users/snowpool/Library/Logs/Homebrew/python@3.12... (2 files, 2KB)
Removing: /Users/snowpool/Library/Logs/Homebrew/glib... (64B)
Removing: /Users/snowpool/Library/Logs/Homebrew/rust... (64B)
Removing: /Users/snowpool/Library/Logs/Homebrew/llvm... (64B)
Removing: /Users/snowpool/Library/Logs/Homebrew/gcc... (64B)
Removing: /Users/snowpool/Library/Logs/Homebrew/openssl@3... (64B)
Removing: /Users/snowpool/Library/Logs/Homebrew/ca-certificates... (64B)
Removing: /Users/snowpool/Library/Logs/Homebrew/python@3.10... (2 files, 2.5KB)
Removing: /Users/snowpool/Library/Logs/Homebrew/unbound... (64B)
Removing: /Users/snowpool/Library/Logs/Homebrew/python@3.11... (2 files, 2.5KB)
Removing: /Users/snowpool/Library/Logs/Homebrew/guile... (64B)
Removing: /Users/snowpool/Library/Logs/Homebrew/gnupg... (64B)
Removing: /Users/snowpool/Library/Logs/Homebrew/ruby... (64B)
Pruned 0 symbolic links and 8 directories from /opt/homebrew

これで再度パスから調べたら

/usr/local/bin/ollama

の場合
これを見ると、あなたの環境のOllamaは
* /Applications/Ollama.app みたいなアプリ版じゃなくて
* コマンドラインインストール(たぶん過去に手動インストール)されたものです。
つまり:
Homebrew管理でもないし、アプリ版でもない。
→ /usr/local/bin に直接インストールされてる独立版。

とのこと

このため過去ログを探す

 ollama --version

だと

ollama version is 0.3.6

2024年8月の時点のものなので最新のものにする

Apple Silicon(M3) Macで Ollama を動かしてみ
を参考に

https://ollama.com/download
からダウンロードして
Ollama-darwin.zip
をダブルクリックで解凍

Ollama.app
ができているので
これをダブルクリックで進めればOK

再度

ollama --version          

で最新のものになるのを確認

ollama version is 0.5.11

となっていたので

https://zenn.dev/cloud_ace/articles/gemma3-ollama-openwebui-rag#6.-セットアップ手順
を参考に

https://ollama.com
から最新版をダウンロードする

インストール後

ollama --version   
ollama version is 0.6.7

でバージョン更新されたのを確認

ollama pull gemma3:4b

pulling manifest 
pulling aeda25e63ebd: 100% ▕██████████████████████▏ 3.3 GB                         
pulling e0a42594d802: 100% ▕██████████████████████▏  358 B                         
pulling dd084c7d92a3: 100% ▕██████████████████████▏ 8.4 KB                         
pulling 3116c5225075: 100% ▕██████████████████████▏   77 B                         
pulling b6ae5839783f: 100% ▕██████████████████████▏  489 B                         
verifying sha256 digest 
writing manifest 
success 

でインストール成功

次にモデルの実行

ollama run gemma3:4b

プロンプトを抜けるには

/bye

とするか
Ctrl +d を実行

Open WebUI を使うなら
デフォルトでは http://localhost:11434/api に接続

試しに
Google Cloudとは?
と聞いてみると

Google Cloud(グーグル クラウド)は、Googleが提供するクラウドコンピューティング
サービスです。簡単に言うと、インターネットを通じてコンピューターの計算資源(サー
バー、ストレージ、データベースなど)を借りて利用するサービスです。

**Google Cloudの主な特徴とサービス内容**

* **幅広いサービス:**
* **コンピューティング:**
* **Compute Engine:** 仮想サーバーを構築・管理できます。
* **Kubernetes Engine (GKE):** コンテナ化されたアプリケーションを簡単に
デプロイ・管理できます。
* **Cloud Functions:** サーバーを管理することなく、イベントやトリガーに
応じてコードを実行できます。
* **ストレージ:**
* **Cloud Storage:** 画像、動画、バックアップデータなどのオブジェクトを
保存できます。
* **Cloud SQL:** MySQL、PostgreSQL、SQL Serverなどのデータベースを管理で
きます。
* **Cloud Firestore:** NoSQLデータベースです。
* **分析:**
* **BigQuery:** 大量のデータを高速に分析できるデータウェアハウスです。
* **Cloud Dataflow:** リアルタイムデータ処理パイプラインを構築・実行でき
ます。
* **機械学習:**
* **Vertex AI:** 機械学習モデルの構築、トレーニング、デプロイを支援する
プラットフォームです。
* **その他:**
* **App Engine:** Webアプリケーションを簡単にデプロイ・実行できます。
* **Cloud Identity:** ユーザー管理サービスです。
* **Cloud Operations:** アプリケーションの監視、ログ収集、エラー分析など
を支援するプラットフォームです。

* **Googleの技術との連携:**
* Google検索、YouTube、Google MapsなどのGoogleの既存のサービスとシームレスに
連携できます。
* 機械学習、データ分析、セキュリティなどの分野で、Googleが持つ高度な技術を活
用できます。

* **柔軟性とスケーラビリティ:**
* 必要な時に必要な分だけリソースを拡張したり縮小したりできるため、コストを最
適化できます。
* どのデバイスからでもアクセスできます。

**Google Cloudが活用されている事例**

* **Webアプリケーション開発:** WebサイトやWebアプリケーションのホスティング
* **データ分析:** 顧客データの分析、マーケティング分析、ビジネスインテリジェンス

* **機械学習:** レコメンデーションシステム、画像認識、自然言語処理
* **ゲーム開発:** ゲームサーバーの構築、ゲームデータの分析
* **IoT:** IoTデバイスからのデータ収集・分析、デバイス管理

**Google Cloudの料金体系**

* **従量課金制:** 実際に使用したリソースに対してのみ料金を支払います。
* **無料枠:** 一部のサービスには無料枠が用意されています。

**詳細な情報:**

* **Google Cloud公式ウェブサイト:**
[https://cloud.google.com/](https://cloud.google.com/)

Google Cloudは、規模の大小、業種の異なる様々な企業で利用されています。ご自身のビ
ジネスニーズに合わせて、最適なサービスを選択し、活用することで、ビジネスの成長を
加速させることができます。

より具体的な質問があれば、お気軽にお尋ねください。例えば、「WebサイトをGoogle
Cloudでホスティングしたいのですが、どのようなサービスを選べば良いですか?」のよ
うに、具体的な状況を教えていただければ、より詳細なアドバイスができます。

という回答が数秒で出る

なお docker で OpeenWebUI を使ってるのが多いけど
スペック的に docker をさらに動かすよりも
Enchanted アプリを使うことで同じようなことができる

使い方は

Mac用 Enchanted アプリで文章を自由自在に活用してみる
を参考に

Enchanted:iOSとmacOSのネイティブプライベートモデルをサポートしたマルチモーダルチャットアプリ
をみた感じではマルチモーダル対応っぽい

なお OpenWebUI を使うことで
RAG(検索拡張生成)も可能らしい

とりあえず文字の入力と回答だけなら快適に動く

Voicevox の読み上げ(メモリ不足のため失敗)

Voicevox の読み上げ

YouTubeで人気「ずんだもん」の音声を利用、LLMの返答をPythonで読み上げる
を参考にする

Pyttxsで読み上げの長文が可能だが
Windows Macでしかできない
ラズパイと ubuntu では音声エンジンが使えないので
Voicevox を使っているがdocker だと100文字以上だとエラーになるので
docker以外を使う

ollama run gemma2:2b

で使っているが欲しいのはvoicevox の導入と音声の生成部分

https://github.com/VOICEVOX/voicevox_core/releases
の中から
m1MacbookAir なので
macOS arm64

download-osx-arm64
を選択する

なお whlファイルも必要らしいので調べる

download-osx-arm64 と voicevox_core-0.16.0-cp310-abi3-macosx_11_0_arm64.whl について知りたい

項目 | 意味
voicevox_core | VOICEVOX(音声合成ソフト)の「コア機能ライブラリ」
0.16.0 | バージョン番号(この場合は 0.16.0)
cp310 | CPython 3.10向け(Python 3.10に対応)
abi3 | 複数のPythonバージョンに対応できるABI (バイナリ互換性)
macosx_11_0_arm64 | macOS 11.0 (Big Sur)以降、Apple Silicon (ARM64) 向け

つまりこのファイルは、
* Python 3.10 上で動かすために作られた
* Mac (Apple Silicon版) 用の
* VOICEVOXのコア部分のバイナリパッケージ(wheelファイル、.whl) ということです!
この .whl ファイルをインストールすると、Pythonプログラムから VOICEVOX のエンジン(音声合成)を直接呼び出して使えるようになります。

pip install voicevox_core-0.16.0-cp310-abi3-macosx_11_0_arm64.whl

でインストール

項目 | 内容
download-osx-arm64 | Apple Silicon Mac用の何かをダウンロードするもの(具体的な中身は文脈次第)
voicevox_core-0.16.0-…whl | Python 3.10対応のVOICEVOXエンジンをApple Silicon Mac用にビルドしたインストールファイル

とのこと

そのままdownload-osx-arm64 をクリックしても動かないので
https://github.com/VOICEVOX/voicevox_core/blob/main/docs/guide/user/usage.md
を参考に

mv ~/Downloads/download-osx-arm64 .

DirectML 版や CUDA 版があるらしいけど
m1macbookAir だとGPUは使えないらしい

https://github.com/VOICEVOX/voicevox_core/blob/main/docs/guide/user/usage.md
によれば

# CPU版を利用する場合

./download --exclude c-api # C APIを使う場合は`--exclude c-api`は無し

thread 'main' panicked at /Users/runner/.cargo/registry/src/index.crates.io-6f17d22bba15001f/minus-5.6.1/src/state.rs:322:24:
byte index 84 is not a char boundary; it is inside 'く' (bytes 82..85) of `上下キーとスペースでスクロールし、読み終えたらqを押してください`
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
ERROR something went wrong with the pager
ダウンロードには以下の利用規約への同意が必要です。
(矢印キーで移動、q で終了)
─┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 │ # VOICEVOX 音声モデル 利用規約
 │ 
 │ ## 許諾内容
 │ 
 │ 1. 商用・非商用問わず利用することができます
 │ 2. アプリケーションに組み込んで再配布することができます
 │ 3. 作成された音声を利用する際は、各音声ライブラリの規約に従ってください
 │ 4. 作成された音声の利用を他者に許諾する際は、当該他者に対し本許諾内容の 3 及び 4 の遵守を義務付けてください
 │ 
 │ ## 禁止事項
 │ 
 │ - 逆コンパイル・リバースエンジニアリング及びこれらの方法の公開すること
 │ - 製作者または第三者に不利益をもたらす行為
 │ - 公序良俗に反する行為
 │ 
 │ ## 免責事項
 │ 
 │ 本ソフトウェアにより生じた損害・不利益について、製作者は一切の責任を負いません。
 │ 
 │ ## その他
 │ 
 │ ご利用の際は VOICEVOX を利用したことがわかるクレジット表記が必要です。
 │ 
 │ ---
 │ 
 │ # 音声ライブラリ利用規約
 │ 
 │ ## 四国めたん
 │ 
 │ 四国めたんの音声ライブラリを用いて生成した音声は、
 │ 「VOICEVOX:四国めたん」とクレジットを記載すれば、商用・非商用で利用可能です。
 │ 
 │ 利用規約の詳細は以下をご確認ください。
 │ https://zunko.jp/con_ongen_kiyaku.html
 │ 
 │ ## ずんだもん
 │ 
 │ ずんだもんの音声ライブラリを用いて生成した音声は、
 │ 「VOICEVOX:ずんだもん」とクレジットを記載すれば、商用・非商用で利用可能です。
 │ 
 │ 利用規約の詳細は以下をご確認ください。
 │ https://zunko.jp/con_ongen_kiyaku.html
 │ 
 │ ## 春日部つむぎ
 │ 
 │ 春日部つむぎの音声ライブラリを用いて生成した音声は、
 │ 「VOICEVOX:春日部つむぎ」とクレジットを記載すれば、商用・非商用で利用可能です。
 │ 
 │ 利用規約の詳細は以下をご確認ください。
 │ https://tsumugi-official.studio.site/rule
 │ 
 │ ## 波音リツ
 │ 
 │ 波音リツの音声ライブラリを用いて生成した音声は、
 │ 「VOICEVOX:波音リツ」とクレジットを記載すれば、商用・非商用で利用可能です。
 │ 
 │ 利用規約の詳細は以下をご確認ください。
 │ http://canon-voice.com/kiyaku.html
 │ 
 │ ## 玄野武宏
 │ 
 │ 玄野武宏の音声ライブラリを用いて生成した音声は、
 │ 「VOICEVOX:玄野武宏」とクレジットを記載すれば、商用・非商用で利用可能です。
 │ 
 │ 利用規約の詳細は以下をご確認ください。
 │ https://virvoxproject.wixsite.com/official/voicevoxの利用規約
 │ 
 │ ## 白上虎太郎
 │ 
 │ 白上虎太郎の音声ライブラリを用いて生成した音声は、
 │ 「VOICEVOX:白上虎太郎」とクレジットを記載すれば、商用・非商用で利用可能です。
 │ 
 │ 利用規約の詳細は以下をご確認ください。
 │ https://virvoxproject.wixsite.com/official/voicevoxの利用規約
 │ 
 │ ## 青山龍星
 │ 
 │ 個人が青山龍星の音声ライブラリを用いて生成した音声は、
 │ 「VOICEVOX:青山龍星」とクレジットを記載すれば、商用・非商用で利用可能です。
 │ ただし企業が携わる形で利用する場合は、「ななはぴ(https://v.seventhh.com/contact/)」に対し事前確認を取る必要があります。
 │ 
 │ 利用規約の詳細は以下をご確認ください。
 │ https://virvoxproject.wixsite.com/official/voicevoxの利用規約
 │ 
 │ ## 冥鳴ひまり
 │ 
 │ 冥鳴ひまりの音声ライブラリを用いて生成した音声は、
 │ 「VOICEVOX:冥鳴ひまり」とクレジットを記載すれば、商用・非商用で利用可能です。
 │ 
 │ 利用規約の詳細は以下をご確認ください。
 │ https://meimeihimari.wixsite.com/himari/terms-of-use
 │ 
 │ ## 九州そら
 │ 
 │ 九州そらの音声ライブラリを用いて生成した音声は、
 │ 「VOICEVOX:九州そら」とクレジットを記載すれば、商用・非商用で利用可能です。
 │ 
 │ 利用規約の詳細は以下をご確認ください。
 │ https://zunko.jp/con_ongen_kiyaku.html
 │ 
 │ ## もち子さん
 │ 
 │ 個人がもち子さんの音声ライブラリを用いて生成した音声は、
 │ 「VOICEVOX:もち子(cv 明日葉よもぎ)」とクレジットを記載すれば、
 │ 音声作品・音声素材・ゲーム作品等を除いて商用・非商用で利用可能です。
 │ ただし企業が携わる形で利用する場合は、「もちぞら模型店(https://vtubermochio.wixsite.com/mochizora/contact)」に対し事前確認を取る必要があります。
 │ 
 │ 利用規約の詳細は以下をご確認ください。
 │ https://vtubermochio.wixsite.com/mochizora/利用規約
 │ 
 │ ## 剣崎雌雄
 │ 
 │ 剣崎雌雄の音声ライブラリを用いて生成した音声は、
 │ 「VOICEVOX:剣崎雌雄」とクレジットを記載すれば、商用・非商用で利用可能です。
 │ 
 │ 利用規約の詳細は以下をご確認ください。
 │ https://frontier.creatia.cc/fanclubs/413/posts/4507
 │ 
 │ ## WhiteCUL
 │ 
 │ WhiteCUL の音声ライブラリを用いて生成した音声は、
 │ 「VOICEVOX:WhiteCUL」とそれぞれクレジットを記載すれば、商用・非商用で利用可能です。
 │ 
 │ 利用規約の詳細は以下をご確認ください。
 │ https://www.whitecul.com/guideline
 │ 
 │ ## 後鬼
 │ 
 │ 個人が後鬼の音声ライブラリを用いて生成した音声は、
 │ 「VOICEVOX:後鬼」とクレジットを記載すれば、商用・非商用で利用可能です。
 │ ただし企業が携わる形で利用する場合は、「【鬼っ子ハンターついなちゃん】プロジェクト(https://ついなちゃん.com/mail/)」に対し事前確認を取る必要があります。
 │ 
 │ 利用規約の詳細は以下をご確認ください。
 │ https://ついなちゃん.com/voicevox_terms/
 │ 
 │ ## No.7
 │ 
 │ 個人が No.7 の音声ライブラリを用いて生成した音声は、
 │ 「VOICEVOX:No.7」とクレジットを記載すれば、非商用(同人利用や配信による収入は OK)で利用可能です。
 │ その他商用利用の場合は、「No.7 製作委員会(https://voiceseven.com/)」に対し事前確認を取る必要があります。
 │ 
 │ 利用規約の詳細は以下をご確認ください。
 │ https://voiceseven.com/#j0200
 │ 
 │ ## ちび式じい
 │ 
 │ ちび式じいの音声ライブラリを用いて生成した音声は、
 │ 「VOICEVOX:ちび式じい」とクレジットを記載すれば、商用・非商用で利用可能です。
 │ 
 │ 利用規約の詳細は以下をご確認ください。
 │ https://docs.google.com/presentation/d/1AcD8zXkfzKFf2ertHwWRwJuQXjNnijMxhz7AJzEkaI4
 │ 
 │ ## 櫻歌ミコ
 │ 
 │ 櫻歌ミコの音声ライブラリを用いて生成した音声は、
 │ 「VOICEVOX:櫻歌ミコ」とクレジットを記載すれば、商用・非商用で利用可能です。
 │ 
 │ 利用規約の詳細は以下をご確認ください。
 │ https://voicevox35miko.studio.site/rule
 │ 
 │ ## 小夜/SAYO
 │ 
 │ 小夜/SAYO の音声ライブラリを用いて生成した音声は、
 │ 「VOICEVOX:小夜/SAYO」とクレジットを記載すれば、商用・非商用で利用可能です。
 │ 
 │ 利用規約の詳細は以下をご確認ください。
 │ https://316soramegu.wixsite.com/sayo-official/guideline
 │ 
 │ ## ナースロボ_タイプT
 │ 
 │ ナースロボ_タイプTの音声ライブラリを用いて生成した音声は、
 │ 「VOICEVOX:ナースロボ_タイプT」とクレジットを記載すれば、商用・非商用で利用可能です。
 │ 
 │ 利用規約の詳細は以下をご確認ください。
 │ https://www.krnr.top/rules
 │ 
 │ ## †聖騎士 紅桜†
 │ 
 │ †聖騎士 紅桜†の音声ライブラリを用いて生成した音声は、
 │ 「VOICEVOX:†聖騎士 紅桜†」とクレジットを記載すれば、商用・非商用で利用可能です。
 │ 
 │ 利用規約の詳細は以下をご確認ください。
 │ https://commons.nicovideo.jp/material/nc296132
 │ 
 │ ## 雀松朱司
 │ 
 │ 雀松朱司の音声ライブラリを用いて生成した音声は、
 │ 「VOICEVOX:雀松朱司」とクレジットを記載すれば、商用・非商用で利用可能です。
 │ 
 │ 利用規約の詳細は以下をご確認ください。
 │ https://virvoxproject.wixsite.com/official/voicevoxの利用規約
 │ 
 │ ## 麒ヶ島宗麟
 │ 
 │ 麒ヶ島宗麟の音声ライブラリを用いて生成した音声は、
 │ 「VOICEVOX:麒ヶ島宗麟」とクレジットを記載すれば、商用・非商用で利用可能です。
 │ 
 │ 利用規約の詳細は以下をご確認ください。
 │ https://virvoxproject.wixsite.com/official/voicevoxの利用規約
 │ 
 │ ## 春歌ナナ
 │ 
 │ 春歌ナナの音声ライブラリを用いて生成した音声は、
 │ 「VOICEVOX:春歌ナナ」とクレジットを記載すれば、商用・非商用で利用可能です。
 │ 
 │ 利用規約の詳細は以下をご確認ください。
 │ https://nanahira.jp/haruka_nana/guideline.html
 │ 
 │ ## 猫使アル
 │ 
 │ 猫使アルの音声ライブラリを用いて生成した音声は、
 │ 「VOICEVOX:猫使アル」とクレジットを記載すれば、商用・非商用で利用可能です。
 │ 
 │ 利用規約の詳細は以下をご確認ください。
 │ https://nekotukarb.wixsite.com/nekonohako/利用規約
 │ 
 │ ## 猫使ビィ
 │ 
 │ 猫使ビィの音声ライブラリを用いて生成した音声は、
 │ 「VOICEVOX:猫使ビィ」とクレジットを記載すれば、商用・非商用で利用可能です。
 │ 
 │ 利用規約の詳細は以下をご確認ください。
 │ https://nekotukarb.wixsite.com/nekonohako/利用規約
 │ 
 │ ## 中国うさぎ
 │ 
 │ 中国うさぎの音声ライブラリを用いて生成した音声は、
 │ 「VOICEVOX:中国うさぎ」とクレジットを記載すれば、商用・非商用で利用可能です。
 │ 
 │ 利用規約の詳細は以下をご確認ください。
 │ https://zunko.jp/con_ongen_kiyaku.html
 │ 
 │ ## 栗田まろん
 │ 
 │ 栗田まろんの音声ライブラリを用いて生成した音声は、
 │ 「VOICEVOX:栗田まろん」とクレジットを記載すれば、商用・非商用で利用可能です。
 │ 
 │ 利用規約の詳細は以下をご確認ください。
 │ https://aivoice.jp/character/maron/
 │ 
 │ ## あいえるたん
 │ 
 │ あいえるたんの音声ライブラリを用いて生成した音声は、
 │ 「VOICEVOX:あいえるたん」とクレジットを記載すれば、商用・非商用で利用可能です。
 │ 
 │ 利用規約の詳細は以下をご確認ください。
 │ https://www.infiniteloop.co.jp/special/iltan/terms/
 │ 
 │ ## 満別花丸
 │ 
 │ 満別花丸の音声ライブラリを用いて生成した音声は、
 │ 「VOICEVOX:満別花丸」とクレジットを記載すれば、商用・非商用で利用可能です。
 │ 
 │ 利用規約の詳細は以下をご確認ください。
 │ https://100hanamaru.wixsite.com/manbetsu-hanamaru/rule
 │ 
 │ ## 琴詠ニア
 │ 
 │ 琴詠ニアの音声ライブラリを用いて生成した音声は、
 │ 「VOICEVOX:琴詠ニア」とクレジットを記載すれば、商用・非商用で利用可能です。
 │ 
 │ 利用規約の詳細は以下をご確認ください。
 │ https://commons.nicovideo.jp/works/nc315435
 │ 
 │ ## Voidoll
 │ 
 │ 個人が Voidoll の音声ライブラリを用いて生成した音声は、
 │ 「VOICEVOX:Voidoll(CV:丹下桜)」とクレジットを記載すれば、商用・非商用で利用可能です。
 │ ただし法人による利用の場合は、個別に問い合わせ(https://qa.nicovideo.jp/)が必要です。
 │ 
 │ 利用規約の詳細は以下をご確認ください。
 │ https://blog.nicovideo.jp/niconews/224589.html
 │ 
 │ ## ぞん子
 │ 
 │ ぞん子の音声ライブラリを用いて生成した音声は、
 │ 「VOICEVOX:ぞん子」とクレジットを記載すれば、商用・非商用で利用可能です。
 │ ただし商用利用の場合は、個別に問い合わせ(https://zonko.zone-energy.jp/contact)が必要です。
 │ 
 │ 利用規約の詳細は以下をご確認ください。
 │ https://zonko.zone-energy.jp/guideline
 │ 
 │ ## 中部つるぎ
 │ 
 │ 中部つるぎの音声ライブラリを用いて生成した音声は、
 │ 「VOICEVOX:中部つるぎ」とクレジットを記載すれば、商用・非商用で利用可能です。
 │ 
 │ 利用規約の詳細は以下をご確認ください。
 │ https://zunko.jp/con_ongen_kiyaku.html
─┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 │ # VOICEVOX ONNX Runtime 利用規約
 │ 
 │ ## 許諾内容
 │ 
 │ 1. 商用・非商用問わず利用することができます
 │ 2. アプリケーションに組み込んで再配布することができます
 │ 3. 作成された音声を利用する際は、各音声ライブラリの規約に従ってください
 │ 4. 作成された音声の利用を他者に許諾する際は、当該他者に対し本許諾内容の 3 及び 4 の遵守を義務付けてください
 │ 
 │ ## 禁止事項
 │ 
 │ - 逆コンパイル・リバースエンジニアリング及びこれらの方法の公開すること
 │ - 製作者または第三者に不利益をもたらす行為
 │ - 公序良俗に反する行為
 │ 
 │ ## 免責事項
 │ 
 │ 本ソフトウェアにより生じた損害・不利益について、製作者は一切の責任を負いません。
 │ 
 │ ## その他
 │ 
 │ ご利用の際は VOICEVOX を利用したことがわかるクレジット表記が必要です。
─┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
[Agreement Required]
「VOICEVOX 音声モデル 利用規約」と「VOICEVOX ONNX Runtime 利用規約」に同意しますか?
同意する場合は y を、同意しない場合は n を、再確認する場合は r を入力し、エンターキーを押してください。
[y,n,r] : 

となるので
Y

これでインストールが始まる

次にライブラリインストール

pip install voicevox_core-0.16.0-cp310-abi3-macosx_11_0_arm64.whl

音声ファイルの再生で必要になる、「soundfile」と「sounddevice」という2つのライブラリをインストール

pip install soundfile sounddevice

次に使用モデルを変更する
elyza:jp8bを使用するようにコード変更

python ollama_voicevox_test.py

として保存する

保存先はvoicevox_coreフォルダの中にする

cd voicevox_core 
touch ollama_voicevox_test.py

内容は

import ollama
from pathlib import Path
from voicevox_core import VoicevoxCore
import soundfile as sf
import sounddevice as sd

# Ollamaでelyza:jp8bにチャットする
response = ollama.chat(model="elyza:jp8b", messages=[
    {
        "role": "system",
        "content": "あなたは何でも丁寧に、短く答えてくれる日本語アシスタントです"
    },
    {
        "role": "user",
        "content": "生成AIの未来はどうなりますか?"
    }
])

# Ollamaからのレスポンス本文を取り出し
response_text = response["message"]["content"]
print(response_text)
print("クレジット表記 VOICEVOX:ずんだもん")

# VoicevoxCoreの初期化
vv_core = VoicevoxCore(open_jtalk_dict_dir=Path("open_jtalk_dic_utf_8-1.11"))
speaker_id = 1  # ずんだもん
vv_core.load_model(speaker_id)

# テキストを音声に変換
voice_data = vv_core.tts(response_text, speaker_id)

# 音声ファイルとして保存
with open("output.wav", "wb") as f:
    f.write(voice_data)

# 保存した音声ファイルを再生
data, samplerate = sf.read("output.wav")
sd.play(data, samplerate)
sd.wait()

としたけど

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/gemini/voicevox_core/ollama_voicevox_test.py", line 3, in <module>
    from voicevox_core import VoicevoxCore
ImportError: cannot import name 'VoicevoxCore' from 'voicevox_core' (/Users/snowpool/.pyenv/versions/3.11.0/lib/python3.11/site-packages/voicevox_core/__init__.py)

となる

これは
いまの voicevox_core では
→ VoicevoxCore というクラス名では提供されていません!
とのこと

VoicevoxCore ではなく、
VoicevoxCoreClient を使う

インポートは

from voicevox_core import VoicevoxCoreClient

に変わり
初期化も

vv_core = VoicevoxCoreClient(open_jtalk_dict_dir=Path("open_jtalk_dic_utf_8-1.11"))

となる

しかしコードを

import ollama
from pathlib import Path
from voicevox_core import VoicevoxCoreClient

import soundfile as sf
import sounddevice as sd
import asyncio

async def main():
    # Ollamaで elyza:jp8b にチャットする
    response = ollama.chat(model="elyza:jp8b", messages=[
        {
            "role": "system",
            "content": "あなたは何でも丁寧に、短く答えてくれる日本語アシスタントです"
        },
        {
            "role": "user",
            "content": "生成AIの未来はどうなりますか?"
        }
    ])

    response_text = response["message"]["content"]
    print(response_text)
    print("クレジット表記 VOICEVOX:ずんだもん")

    # VoicevoxCoreClientの初期化
    vv_core = VoicevoxCoreClient(open_jtalk_dict_dir=Path("open_jtalk_dic_utf_8-1.11"))

    # クライアントの起動
    async with vv_core:
        speaker_id = 1  # ずんだもん
        await vv_core.load_model(speaker_id)

        # テキストを音声に変換(非同期)
        voice_data = await vv_core.tts(response_text, speaker_id)

    # 音声ファイルとして保存
    with open("output.wav", "wb") as f:
        f.write(voice_data)

    # 保存した音声ファイルを再生
    data, samplerate = sf.read("output.wav")
    sd.play(data, samplerate)
    sd.wait()

# 非同期実行
asyncio.run(main())

と変更しても変わらない

調べた結果、ver0.16以降は色々変わっているらしい

こうなった場合ChatGPTはほぼ機能しない
Ollama Index の時のように間違ったコードのループになるだけなので
別のサイトを参考にする

PythonからVOICEVOX Coreを使ってみる

これでやりたいのは
おまけ(大量の文章の読み上げサンプル)

ファイルの書き込み用に、aiofilesを使用

pip install aiofiles

でインストール

import asyncio
import aiofiles
from voicevox_core.asyncio import Onnxruntime, OpenJtalk, Synthesizer, VoiceModelFile

texts = [
    ...  # 文章一覧をここに
]

async def tts(synthesizer, text, style_id, index):
    # 音声合成してファイルに保存するコルーチン
    wav = await synthesizer.tts(text, style_id)
    async with aiofiles.open(f"output_{index}.wav", mode="wb") as f:
        await f.write(wav)

async def main(texts):
    # synthesizerの作成
    voicevox_onnxruntime_path = "onnxruntime/lib/" + Onnxruntime.LIB_VERSIONED_FILENAME
    open_jtalk_dict_dir = "dict/open_jtalk_dic_utf_8-1.11"
    ort = await Onnxruntime.load_once(filename=voicevox_onnxruntime_path)
    ojt = await OpenJtalk.new(open_jtalk_dict_dir)
    synthesizer = Synthesizer(ort, ojt)
    # VVMファイルの読み込み
    async with await VoiceModelFile.open("models/vvms/0.vvm") as model:
        await synthesizer.load_voice_model(model)
    # 音声合成の一括実行
    # ttsのコルーチンのリストを作成し、asyncio.gatherで実行する
    style_id = 5  # ずんだもんノーマル(スタイルID: 5)
    await asyncio.gather(*[
        tts(synthesizer, t, style_id, i)
        for i, t in enumerate(texts)
    ])

asyncio.run(main(texts))

がコード

まずはこれで動くかテストする

touch voice016test.py

でファイルを作成し

import asyncio
import aiofiles
from voicevox_core.asyncio import Onnxruntime, OpenJtalk, Synthesizer, VoiceModelFile

texts = [
    "これはテストです"
]

async def tts(synthesizer, text, style_id, index):
    # 音声合成してファイルに保存するコルーチン
    wav = await synthesizer.tts(text, style_id)
    async with aiofiles.open(f"output_{index}.wav", mode="wb") as f:
        await f.write(wav)

async def main(texts):
    # synthesizerの作成
    voicevox_onnxruntime_path = "onnxruntime/lib/" + Onnxruntime.LIB_VERSIONED_FILENAME
    open_jtalk_dict_dir = "dict/open_jtalk_dic_utf_8-1.11"
    ort = await Onnxruntime.load_once(filename=voicevox_onnxruntime_path)
    ojt = await OpenJtalk.new(open_jtalk_dict_dir)
    synthesizer = Synthesizer(ort, ojt)
    # VVMファイルの読み込み
    async with await VoiceModelFile.open("models/vvms/0.vvm") as model:
        await synthesizer.load_voice_model(model)
    # 音声合成の一括実行
    # ttsのコルーチンのリストを作成し、asyncio.gatherで実行する
    style_id = 5  # ずんだもんノーマル(スタイルID: 5)
    await asyncio.gather(*[
        tts(synthesizer, t, style_id, i)
        for i, t in enumerate(texts)
    ])

asyncio.run(main(texts))

を実行

なお voicevox のファイルパスを変えてなかったので

 mv voice016test.py voicevox_core 
cd voicevox_core 
python voice016test.py  

で実行

すると
output_0.wav
ができるので
これを再生すると音声ができているのが確認できる

短文はできるので100文字以上の音声を作成する

しかし文面を変更しても
これはテストです
が繰り返されるだけで他の音声にならない

なので別の方法を使う

Cd ../
touch voice016.py

で別のディレクトリで試す

from voicevox_core.blocking import Onnxruntime, OpenJtalk, Synthesizer, VoiceModelFile

voicevox_onnxruntime_path = "voicevox_core/onnxruntime/lib/" + Onnxruntime.LIB_VERSIONED_FILENAME
open_jtalk_dict_dir = "voicevox_core/dict/open_jtalk_dic_utf_8-1.11"
synthesizer = Synthesizer(Onnxruntime.load_once(filename=voicevox_onnxruntime_path), OpenJtalk(open_jtalk_dict_dir))

with VoiceModelFile.open("voicevox_core/models/vvms/0.vvm") as model:
    synthesizer.load_voice_model(model)

style_id = 5

text = "こんにちは、これはテストです。"
wav = synthesizer.tts(text, style_id)

# wavをファイルに書き出す
with open("output.wav", "wb") as f:
    f.write(wav)

これで実行すると音声が生成される

しかし
これを長文の

不要な情報(価格、電話番号、住所、営業時間など)を取り除き、食材に焦点を当ててレシピを提案します。
鶏むね肉と夏野菜の炒め物
材料:
鶏むね肉 (100g):一口大に切る
トウモロコシ:実を外す
ピーマン (1パック):種を取り、細切りにする
トマト (1パック):くし切りにする
キュウリ (1パック):薄切りにする
旬彩盛 (1パック):内容に応じて適当な大きさに切る (例:もやしならそのまま、ニラなら3cm程度に切る)
サラダ油:大さじ1
塩コショウ:少々
醤油:小さじ1
酒:小さじ1
鶏ガラスープの素:小さじ1/2
作り方:
フライパンにサラダ油を熱し、鶏むね肉を炒める。
鶏肉の色が変わったら、ピーマン、キュウリ、旬彩盛を加えて炒める。
野菜がしんなりしてきたら、トウモロコシ、トマトを加えて軽く炒める。
塩コショウ、醤油、酒、鶏ガラスープの素で調味する。
全体に味がなじんだら、火を止めて完成。
うなぎと野菜の丼
材料:
うなぎ (1串):温める
ピーマン (1パック):千切りにする
キュウリ (1パック):千切りにする
トマト (1パック):薄切りにする
温かいご飯:適量
刻み海苔:適量
作り方:
温かいご飯を丼によそう。
ピーマン、キュウリ、トマトを丼に盛り付ける。
うなぎを乗せる。
刻み海苔を散らして完成。
国産牛ロースのミニステーキ
材料:
国産牛ロース (100g):軽く塩コショウを振る
サラダ油:小さじ1
醤油:小さじ1/2
わさび:お好みで
作り方:
フライパンにサラダ油を熱し、牛ロースを焼く。
片面1~2分ずつ焼き、焼き加減を調整する。
醤油を回し入れて香りを出す。
皿に盛り付け、お好みでわさびを添えて完成。
これらのレシピは、提示された少ない食材で簡単に作れるものです。旬彩盛の内容がわからないため、具体的な調理法は示していませんが、他の野菜と同様に炒めたり、丼に添えたりできます。 必要に応じて、他の調味料や食材を追加してアレンジしてみてください。

とするとメモリが足りずにエラーとなる

このためpyttsx で音声再生をすることにする

とりあえずコンセプトとしては音声でのみのやり取り
まずは ollama + gemma3 4B の結果の音声で再生から実験

ウェイクワードエンジンで起動
faster-whisperで音声を取得し ollama + gemma3 に渡す
処理結果を pyttsx で再生

これを機能分解して作成してく