認識したテキストを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 で再生

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

gemini で広告解析とレシピ提案と読み上げ

https://axross-recipe.com/recipes/1741
を参考いgemini で広告解析とレシピ提案と読み上げ

pip install streamlit google-generativeai pillow pyttsx3

で必要なライブラリインストール

import streamlit as st
import google.generativeai as genai
from io import BytesIO
from PIL import Image  
import io
import pyttsx3
import queue

# Gemini API設定
genai.configure(api_key="YOUR_API_KEY_HERE")

# TTSエンジンの初期化
engine = pyttsx3.init()
is_speaking = False  # 音声合成中かどうかを示すフラグ
tts_queue = queue.Queue()  # 音声合成リクエストのキュー

def extract_text_from_image(image):
    model = genai.GenerativeModel(model_name="gemini-1.5-pro-latest")
    response = model.generate_content([
        image,
        {"text": "画像からテキストを抽出してください。"}
    ])
    return response.text if response else ""

def parse_product_data(text):
    lines = text.split("\n")
    ingredients = set()

    for line in lines:
        line = line.strip()
        ingredients.update(line.split())
    return list(ingredients)

def suggest_recipes(ingredients):
    model = genai.GenerativeModel(model_name="gemini-1.5-pro-latest")
    prompt = f"以下の食材を使って作れる料理のレシピを提案してください: {', '.join(ingredients)}"
    response = model.generate_content(prompt)
    return response.text if response else ""

def read_aloud(text):
    tts_queue.put(text)  # リクエストをキューに追加
    process_queue()  # キューを処理

def process_queue():
    global is_speaking

    if not tts_queue.empty() and not is_speaking:
        text = tts_queue.get()  # キューからテキストを取得
        is_speaking = True  # 音声合成中フラグを立てる
        engine.say(text)  # 音声合成を実行
        engine.runAndWait()  # 音声合成が完了するまで待機
        is_speaking = False  # 音声合成が完了したらフラグをリセット
        process_queue()  # 次のリクエストを処理

def start_reading(text):
    read_aloud(text)

st.title("チラシ画像からレシピ提案")

uploaded_file = st.file_uploader("チラシ画像をアップロード", type=["jpg", "png", "jpeg"])
if uploaded_file:
    st.image(uploaded_file, caption="アップロードされた画像", use_column_width=True)
    image_bytes = uploaded_file.read()
    image = Image.open(BytesIO(image_bytes))

    # 一時的に画像ファイルとして保存する例(環境に合わせて変更すること)
    img_byte_arr = io.BytesIO()
    image.save(img_byte_arr, format='PNG')
    img_byte_arr = img_byte_arr.getvalue()

    with open("uploaded_tirashi_image.png", "wb") as f:
        f.write(img_byte_arr)
    input_file = genai.upload_file(path="uploaded_tirashi_image.png", display_name="image")
    
    with st.spinner("データを解析中..."):
        extracted_text = extract_text_from_image(input_file)
    
    st.subheader("抽出されたテキスト")
    st.text_area("テキスト", extracted_text, height=200)

    ingredients = parse_product_data(extracted_text)

    if ingredients:
        st.subheader("提案されたレシピ")
        recipes = suggest_recipes(ingredients)
        st.write(recipes)
        if st.button("提案されたレシピを読み上げる"):
            # レシピが長すぎる場合は最初の部分だけ読み上げるなどの調整も可能
            start_reading("提案されたレシピは次の通りです。 " + recipes)
    
    # 停止ボタンの実装
    if st.button("読み上げ停止"):
        engine.stop()
        engine.endLoop()
        st.info("読み上げを停止しました。")

が全体コード

このコードはダウンロードできる
あとチラシ画像もダウンロードしておく

これでやることは
アップロードされたチラシ画像をGemini APIに送信し、商品情報やキャンペーン内容などのテキストを抽出

抽出されたテキストから使用可能な食材を解析し、Gemini APIにより最適なレシピを生成

Pyttsx3ライブラリを用い、提案された情報やレシピを音声で読み上げます。
ユーザーは「停止」ボタンにより、いつでも読み上げを中断

この中で欲しい機能は
Gemini のチラシ解析
Python でgemini の結果の読み上げ

 touch voice_resipi.py

でファイルを作成

import streamlit as st
import google.generativeai as genai
from io import BytesIO
from PIL import Image  
import io
import pyttsx3
import queue

で必要なライブラリのインポート

* streamlit: Webアプリ開発を簡単に行えるライブラリ。
* google.generativeai (Gemini): 画像やテキストの解析・生成を行うGoogleの生成AI。
* BytesIO, PIL.Image, io: 画像データの取り扱いに使用。
* pyttsx3: テキストを音声に変換するためのTTSライブラリ。
* queue: 複数の処理を順次実行するための仕組みを提供。

次にGemini APIとTTSエンジンの初期設定

# Gemini APIの設定
genai.configure(api_key="YOUR_API_KEY_HERE")

# TTSエンジンの初期化
engine = pyttsx3.init()
is_speaking = False  # 音声合成中かどうかのフラグ
tts_queue = queue.Queue()  # 音声合成リクエストのキュー

* APIキーを設定し、Geminiサービスの利用を可能にします。
* pyttsx3を初期化して音声読み上げの準備を行い、フラグとキューで複数のリクエストを管理

やっぱり設定は gemini.configure で行っている
以前参考にしたサイトのコードは間違っていた

https://qiita.com/shisuto3141/items/2f9349e96703c189f572
のように

self.client = genai.Client(api_key=API_KEY)

は間違い

次に画像からテキストを抽出する関数

def extract_text_from_image(image):
    model = genai.GenerativeModel(model_name="gemini-1.5-pro-latest")
    response = model.generate_content([
        image,
        {"text": "画像からテキストを抽出してください。"}
    ])
    return response.text if response else ""

* アップロードされた画像をGemini APIに送信し、画像内の文字情報を抽出します。
* エラーが発生した場合は、空文字を返す

もし動かないのならモデル名を変更する

次に
テキストから商品情報を解析する関数

def parse_product_data(text):
    lines = text.split("\n")
    ingredients = set()
    
    for line in lines:
        line = line.strip()
        ingredients.update(line.split())
    return list(ingredients)

* 各行の単語をセットに追加して重複を排除し、食材や商品名を抽出

次に
Gemini APIでレシピを提案する関数

def suggest_recipes(ingredients):
    model = genai.GenerativeModel(model_name="gemini-1.5-pro-latest")
    prompt = f"以下の食材を使って作れる料理のレシピを提案してください: {', '.join(ingredients)}"
    response = model.generate_content(prompt)
    return response.text if response else ""

* 解析された食材情報を基に、Gemini APIにレシピ生成を依頼します。
* 提案されたレシピをテキスト形式で返す

なおingreduebtsはリストで

def parse_product_data(text):

の処理結果の食材のリストになる

これを使って処理して結果はtext に入る
つまりgeminiの処理結果、つまり提案されたレシピがテキスト情報として格納される

次に音声読み上げリクエストを管理する関数群

リクエスト追加用関数を作成しておく

def read_aloud(text):
    tts_queue.put(text)  # 読み上げリクエストをキューに追加
    process_queue()      # キューを処理

これで
ユーザーが「読み上げ」ボタンを押すと、対象のテキストがキューに追加され、順次処理

次にリクエスト処理用関数

def process_queue():
    global is_speaking

    if not tts_queue.empty() and not is_speaking:
        text = tts_queue.get()  # キューからテキストを取得
        is_speaking = True      # 読み上げ中のフラグを立てる
        engine.say(text)        # テキストを音声合成
        engine.runAndWait()     # 読み上げ完了まで待機
        is_speaking = False     # フラグをリセット
        process_queue()         # 次のリクエストを処理

キューに溜まったリクエストを1件ずつ順次処理し、同時に複数の読み上げが行われないよう制御

次に ラッパー関数

def start_reading(text):
    read_aloud(text)

これは
「read_aloud」を呼び出すラッパー関数で、コード全体の可読性を向上

次にStreamlitによるWeb画面の基本設定

st.title("チラシ画像からレシピ提案")

アプリのタイトルをWeb画面に表示し、ユーザーに内容を即座に伝える

次に画像アップロード機能の実装

uploaded_file = st.file_uploader("チラシ画像をアップロード", type=["jpg", "png", "jpeg"])
if uploaded_file:
    st.image(uploaded_file, caption="アップロードされた画像", use_column_width=True)

ユーザーがチラシ画像をアップロードできるようにし、選択した画像を画面に表示

次に画像の読み込みと変換

    image_bytes = uploaded_file.read()
    image = Image.open(BytesIO(image_bytes))

アップロードされた画像ファイルをバイナリデータとして読み込み、PILで画像オブジェクトに変換

次に画像を一時ファイルとして保存

    # 一時的に画像ファイルとして保存する例(環境に合わせて調整してください)
    img_byte_arr = io.BytesIO()
    image.save(img_byte_arr, format='PNG')
    img_byte_arr = img_byte_arr.getvalue()

画像データをPNG形式で一時ファイルに変換し、Gemini APIに渡す準備を行う

次に画像ファイルのアップロードとGemini API連携

    with open("uploaded_tirashi_image.png", "wb") as f:
        f.write(img_byte_arr)
    input_file = genai.upload_file(path="uploaded_tirashi_image.png", display_name="image")

一時ファイルとして保存した画像をGemini APIへアップロードし、テキスト抽出を実行可能にする

次にテキスト抽出と結果表示

    with st.spinner("データを解析中..."):
        extracted_text = extract_text_from_image(input_file)
    
    st.subheader("抽出されたテキスト")
    st.text_area("テキスト", extracted_text, height=200)

処理中はスピナーを表示し、抽出されたテキストをテキストエリアでユーザーに提示

次にレシピの生成

    ingredients = parse_product_data(extracted_text)

抽出テキストから食材情報を解析し、該当する場合は音声読み上げ機能でお知らせ

次にレシピ提案の表示と音声読み上げ

    if ingredients:
        st.subheader("提案されたレシピ")
        recipes = suggest_recipes(ingredients)
        st.write(recipes)
        if st.button("提案されたレシピを読み上げる"):
            start_reading("提案されたレシピは次の通りです。 " + recipes)

解析した食材情報を元に、Gemini APIで生成されたレシピを画面に表示し、音声でも確認

次に読み上げ停止機能の実装

    if st.button("読み上げ停止"):
        engine.stop()
        engine.endLoop()
        st.info("読み上げを停止しました。")

ユーザーが「読み上げ停止」ボタンを押すと、現在の音声再生を即座に中断し、停止完了のメッセージを表示

これらを保存し

streamlit run voice_resipi.py 

で起動
自動でブラウザが立ち上がるので
テスト画像をアップ
すると食材とレシピが表示される

なお読み上げには1分以上経ってからにしないとAPIリクエストが多すぎてエラーになるので注意

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

この内容の長さでも pyttxs なら macなら読み上げ可能

Ubuntuだと音源がないのでできないので
別のものを試す

チラシの解析(gemini)

チラシの解析(gemini)

【AI × チラシ解析】忙しい社会人のための“節約レシピ提案アプリ”を作ってみた

がまさに答えっぽい

touch image_analysis.py

でファイルを作成

イメージは.pngなので
これを読み込むようにする

from google import genai
from PIL import Image

class Gemini:
    def __init__(self):
        API_KEY = "生成したAPIキー"
        self.model = "gemini-2.0-flash"
        self.client = genai.Client(api_key=API_KEY)
        self.prompt = "スーパーマーケットの広告画像です。それぞれの広告に掲載されている商品と価格を”全て”抽出してリストにしてください。また掲載されている食材を使った今晩のレシピを提案してください。その際、1人前のおおよその価格も計算して教えてください。なお、調味料や米などは自由に使えるものとします。"

    def loadImage(self):
        # スクレイピングした画像データをロード
        image_paths = glob.glob("./source/*.jpg")
        images = []
        for image_path in image_paths:
            images.append(Image.open(image_path))
        self.images = images

    def run(self,prompt="",image=""):
        prompt = self.prompt
        images = self.images

        response = self.client.models.generate_content(
            model=self.model
            ,contents=[images,prompt]
        )
        self.response = response.text
        print(self.response)

が参考もとコード

これを商品リストのみに変更する

mkdir source
cp step-1.png source 

でファイルを移動

実行したら

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/gemini/image_analysis.py", line 1, in <module>
    from google import genai
ImportError: cannot import name 'genai' from 'google' (unknown location)

となった

どうやらインポートの文が間違いらしい

これを

import google.generativeai as genai

としたけど動作しない

Mainの記述がないで

if __name__ == "__main__":
    gemini = Gemini()
    gemini.loadImage()
    gemini.run()

を追加したが

python image_analysis.py
Traceback (most recent call last):
  File "/Users/snowpool/aw10s/gemini/image_analysis.py", line 34, in <module>
    gemini = Gemini()
             ^^^^^^^^
  File "/Users/snowpool/aw10s/gemini/image_analysis.py", line 9, in __init__
    self.client = genai.Client(api_key=API_KEY)
                  ^^^^^^^^^^^^
AttributeError: module 'google.generativeai' has no attribute 'Client'

となった

Google の google-generativeai ライブラリには Client というクラスは存在しません
とのこと

以下のように genai.configure() を使い、GenerativeModel を直接生成
gemini-2.0-flash → テキスト特化(画像には非対応)
gemini-pro-vision → 画像入力に対応 ✅

ということなので

import glob
from PIL import Image
import google.generativeai as genai

class Gemini:
    def __init__(self):
        API_KEY = "AIzaSyBGtutzF_xdEWcPf8343jyAt_Qq3t1cFIQ"  # セキュアな方法で管理を推奨
        genai.configure(api_key=API_KEY)

        # モデルを設定(画像対応モデルは gemini-pro-vision)
        self.model = genai.GenerativeModel("gemini-pro-vision")
        self.prompt = "スーパーマーケットの広告画像です。それぞれの広告に掲載されている商品と価格を”全て”抽出してリストにしてください。"

    def loadImage(self):
        # ./source/*.png を取得
        image_paths = glob.glob("./source/*.png")
        if not image_paths:
            print("⚠️ 画像が見つかりません")
        self.images = [Image.open(path) for path in image_paths]

    def run(self):
        for idx, image in enumerate(self.images):
            print(f"🖼 画像{idx+1} を処理中...")
            try:
                response = self.model.generate_content(
                    [self.prompt, image],
                    stream=False
                )
                print("✅ 結果:")
                print(response.text)
            except Exception as e:
                print("⚠️ エラー:", e)

# 実行用ブロック
if __name__ == "__main__":
    gemini = Gemini()
    gemini.loadImage()
    gemini.run()

としたが

🖼 画像1 を処理中...
⚠️ エラー: 404 Gemini 1.0 Pro Vision has been deprecated on July 12, 2024. Consider switching to different model, for example gemini-1.5-flash.
WARNING: All log messages before absl::InitializeLog() is called are written to STDERR
E0000 00:00:1745014048.933884 7409777 init.cc:232] grpc_wait_for_shutdown_with_timeout() timed out.

gemini-1.5-pro-vision
にしても

🖼 画像1 を処理中...
⚠️ エラー: 404 models/gemini-1.5-pro-vision is not found for API version v1beta, or is not supported for generateContent. Call ListModels to see the list of available models and their supported methods.
WARNING: All log messages before absl::InitializeLog() is called are written to STDERR
E0000 00:00:1745014171.856705 7412024 init.cc:232] grpc_wait_for_shutdown_with_timeout() timed out.

ということで使用するモデルバージョンを変更

import glob
from PIL import Image
import google.generativeai as genai

class Gemini:
    def __init__(self):
        API_KEY = "AIzaSyBGtutzF_xdEWcPf8343jyAt_Qq3t1cFIQ"  # セキュアな方法で管理を推奨
        genai.configure(api_key=API_KEY)

        # モデルを設定(画像対応モデルは gemini-pro-vision)
        self.model = genai.GenerativeModel("gemini-1.5-pro")
        self.prompt = "スーパーマーケットの広告画像です。それぞれの広告に掲載されている商品と価格を変更せずに”全て”抽出してリストにしてください。"

    def loadImage(self):
        # ./source/*.png を取得
        image_paths = glob.glob("./source/*.png")
        if not image_paths:
            print("⚠️ 画像が見つかりません")
        self.images = [Image.open(path) for path in image_paths]

    def run(self):
        for idx, image in enumerate(self.images):
            print(f"🖼 画像{idx+1} を処理中...")
            try:
                response = self.model.generate_content(
                    [self.prompt, image],
                    stream=False
                )
                print("✅ 結果:")
                print(response.text)
            except Exception as e:
                print("⚠️ エラー:", e)

# 実行用ブロック
if __name__ == "__main__":
    gemini = Gemini()
    gemini.loadImage()
    gemini.run()

として実行すると

✅ 結果:
リストは以下の通りです。

* カップヌードル:125円
* 柔軟剤:699円
* 冷凍食品:109円
* ポカリスエット:329円
* キリンレモン:459円
* お茶漬け:179円
* 大人用オムツ:299円
* ミックスナッツ:329円
* 牛乳:299円
* ドレッシング:249円
* 救急バン:169円
* フェイスマスク:99円
* 豆乳飲料:259円
* ティッシュペーパー:399円
* ハーゲンダッツ:199円
* 海苔:199円
* 食パン:89円
* ウェットティッシュ:99円
* バナナ:139円
* 卵:128円
* ポテトチップス:159円
* ベーコン:89円
* プチトマト:299円
* キュウリ:249円
* サラダチキン:299円
* 豆腐:369円
* ヨーグルト:179円
* 冷凍うどん:848円
* 冷凍食品:880円
WARNING: All log messages before absl::InitializeLog() is called are written to STDERR
E0000 00:00:1745015805.274105 7441488 init.cc:232] grpc_wait_for_shutdown_with_timeout() timed out.

元々のプロンプトは

        self.prompt = "スーパーマーケットの広告画像です。それぞれの広告に掲載されている商品と価格を”全て”抽出してリストにしてください。"

これだと

🖼 画像1 を処理中...
✅ 結果:
画像に掲載されている商品と価格のリストです。

* カップ麺:109円
* ドリンク:30円/329円
* 食パン:125円/249円
* 殺虫剤:50円/699円
* ポカリスエット:150円/459円
* 鶏肉:179円/329円
* 牛乳:299円
* ペン:20円/249円
* 救急バン:169円/119円
* パック飲料:10円/299円
* シャンプー:259円/399円
* ハーゲンダッツ:250円/199円
* 緑茶:199円
* トイレットペーパー:89円
* 卵:99円
* バナナ:139円/128円
* 豆腐:159円
* 納豆:89円
* 油揚げ:299円
* ヨーグルト:249円
* みかん:299円/369円
* ソーセージ:179円/848円
* ハム:880円

チラシ解析を cloud vision API で行う

チラシ解析を cloud vision API で行う

Python のバージョンを変えたので

pip install google-auth google-auth-oauthlib google-auth-httplib2 google-api-python-client pillow selenium google-cloud-vision requests

で必要なライブラリを再インストール
こういう時に
Requestments.txt を作ってあると何が必要かわかる

次に

from google.cloud import vision

client = vision.ImageAnnotatorClient()

with open("test.jpg", "rb") as fb:
    content = fb.read()

image = vision.Image(content=content)

response = client.document_text_detection(image=image)
texts = response.text_annotations
print(texts[0].description)

でまずは動作するかテスト
成功したら

cp ../gemini/step-1.png .

でこのスクショでも読み込めるかテストする

from google.cloud import vision

client = vision.ImageAnnotatorClient()

# with open("test.jpg", "rb") as fb:
with open("step-1.png", "rb") as fb:
    
    content = fb.read()

image = vision.Image(content=content)

response = client.document_text_detection(image=image)
texts = response.text_annotations
print(texts[0].description)

これを

python vision_api_test.py

で実行

結果は

4月 16日
水餃
125
249
本日限定
50
699
涼食品3a
10
30
$150
179
ジャクエッセン
POCA
109 329 459
SWEAT
ちゃんぽん
299
399
介護オムツ
20
10
$299
249
169
クレラップレ
299
救急バン
259
クレラッ 399
250
199119
Häagen-Dazs
99
89-35
139 128
159
89
99 299
299
249
30
299 399 179848
179 848 880
Q
Q
?
搞大
縮小 ブラウザ表示 プリント ヘルプ

というようになる

大きな文字は読み取れるが
小さな文字は解像度の問題か読み取れていない

次はgemini を試す

Yomitokuでチラシを解析Colab

Yomitokuでチラシを解析Colab

Yomitokuで写真やレシートを解析してみる

Colab で行なってるのがあったので参考にする

出力形式によって得られる情報が異なるらしい

A100で実験する

https://github.com/kotaro-kinoshita/yomitoku
でライブラリが公開されているので
ドキュメントなどはこれを読む

基本的にGPUが必要

! pip install yomitoku

でライブラリインストール

最後にsession restartしろといわれたので、ダイアログボックスに促されるままrestartする

次に写真の用意

google colabなので、/content以下にimageフォルダを作成し、その中にJPGをいれ
フォルダをまるっと指定すると、その中の画像ファイルをすべて解析してくれます

!mkdir image

でフォルダ作成
ここに写真をアップロードする

/Users/snowpool/aw10s/gemini/step-1.png

をアップロード

! yomitoku /content/image/ -f md -o results -v --figure

で解析

結果は自動作成される results の中にある

ファイル名_p1.md
という感じで出力される

Linebot の画像の管理

Linebot の画像の管理

line へ投稿した画像が保持される期間を調べる

✅ LINE Bot API による画像メッセージの保持期間
項目 内容
originalContentUrl / previewImageUrl 外部URLのため、LINEサーバーには保存されない
LINEサーバー側での画像保存 ❌ なし(開発者が画像URLを提供するだけ)
ユーザー端末での表示保持 一時的にキャッシュされるが、LINE側での保証なし
画像が消える条件 URL先のファイルが削除・非公開になると表示不能になる

originalContentUrl に指定された画像は LINEアプリで直接URLにアクセスして表示しているだけ。
そのため、Google Drive 上の画像を削除すると即時表示できなくなります。
LINE自体には画像が保存されていないため、画像の「保持期間」という概念は存在しません。

既読前に削除されると見えない:「タップしても開けない」状態に。
表示後に削除されると再表示できないこともある:一部キャッシュは残るが再読み込み時にエラーになる可能性。

✅ 安全な運用方針(おすすめ)
* 送信後に数分~数時間だけDriveに画像を保持
→ ユーザーが確実に見られる時間を確保
* ログに送信済みの画像IDと時刻を記録し、後でバッチ削除

とのこと

つまり削除してしまうと閲覧不可能になる

ただ現状の用途は
在庫管理の動作の画像確認
チラシの商品の画像の確認
なので
2日、つまり48時間後には不要になる

このため48時間経過後に削除するようにモジュールを作成する
これでストレージ圧迫を回避できるはず

なお設定時間は今後変更する可能性が高いため
Config.jsonで設定値を変更可能にする

"image_retention_hours": 48  // ← 追加(削除までの保持時間)

の項目を追加

そして削除モジュール

vim delete_old_images.py

import json
import datetime
from googleapiclient.discovery import build
from google.oauth2 import service_account

# 設定ファイルの読み込み
with open("config.json", "r") as f:
    config = json.load(f)

FOLDER_ID = config["google_drive_folder_id"]
RETENTION_HOURS = config.get("image_retention_hours", 48)  # 設定がなければ48時間

# Google Drive API 認証
SCOPES = ["https://www.googleapis.com/auth/drive"]
SERVICE_ACCOUNT_FILE = "service_account.json"
creds = service_account.Credentials.from_service_account_file(SERVICE_ACCOUNT_FILE, scopes=SCOPES)
drive_service = build("drive", "v3", credentials=creds)

def get_drive_images(folder_id=FOLDER_ID):
    query = f"'{folder_id}' in parents and mimeType contains 'image/' and trashed=false"
    results = drive_service.files().list(q=query, fields="files(id, name, createdTime)").execute()
    return results.get("files", [])

def delete_old_images(files, hours_threshold):
    now = datetime.datetime.utcnow()
    for file in files:
        created_time_str = file.get("createdTime")
        if created_time_str:
            created_time = datetime.datetime.strptime(created_time_str, "%Y-%m-%dT%H:%M:%S.%fZ")
            delta = now - created_time
            if delta.total_seconds() > hours_threshold * 3600:
                try:
                    drive_service.files().delete(fileId=file["id"]).execute()
                    print(f"✅ Deleted: {file['name']} (created: {created_time})")
                except Exception as e:
                    print(f"⚠️ Error deleting file {file['id']}: {e}")

if __name__ == "__main__":
    files = get_drive_images()
    if not files:
        print("No images found.")
    else:
        delete_old_images(files, hours_threshold=RETENTION_HOURS)

しかし実行すると

⚠️ Error deleting file : <HttpError 403 when requesting https://www.googleapis.com/drive/v3/files/? returned "The user does not have sufficient permissions for this file.". Details: "[{'message': 'The user does not have sufficient permissions for this file.', 'domain': 'global', 'reason': 'insufficientFilePermissions'}]">

となる

これはファイルの所有者が自分のアカウントで
サービスアカウントでアップしたファイルではないため

このためサービスアカウント権限に編集権限を与えるか
サービスアカウントでgoogle drive へアップする必要がある

現在 yolov8 で画像の出力はできているので
サービスアカウントで出力された画像をGoogle Drive へアップロードするモジュールが欲しい

ということで作成

drive_uploader.py

内容を

import os
import json
from googleapiclient.discovery import build
from googleapiclient.http import MediaFileUpload
from google.oauth2 import service_account

# 設定ファイルの読み込み
with open("config.json", "r") as f:
    config = json.load(f)

FOLDER_ID = config["google_drive_folder_id"]
SERVICE_ACCOUNT_FILE = "service_account.json"
SCOPES = ["https://www.googleapis.com/auth/drive"]

# 認証と Drive API の初期化
creds = service_account.Credentials.from_service_account_file(
    SERVICE_ACCOUNT_FILE, scopes=SCOPES
)
drive_service = build("drive", "v3", credentials=creds)

def upload_image_to_drive(filepath, folder_id=FOLDER_ID):
    """
    指定した画像ファイルを Google Drive にアップロードする
    :param filepath: 画像ファイルのパス
    :param folder_id: Drive フォルダ ID
    :return: アップロードされたファイルの ID(失敗時は None)
    """
    if not os.path.exists(filepath):
        print(f"❌ File not found: {filepath}")
        return None

    filename = os.path.basename(filepath)
    file_metadata = {
        "name": filename,
        "parents": [folder_id],
        "mimeType": "image/jpeg"
    }

    media = MediaFileUpload(filepath, mimetype="image/jpeg")

    try:
        file = drive_service.files().create(
            body=file_metadata,
            media_body=media,
            fields="id"
        ).execute()
        print(f"✅ Uploaded to Drive: {filename} (ID: {file['id']})")
        return file["id"]
    except Exception as e:
        print(f"⚠️ Upload failed: {e}")
        return None

def get_public_url(file_id):
    """
    指定されたファイルIDを誰でも見られるように公開設定し、表示用URLを返す
    :param file_id: アップロード済みファイルの ID
    :return: 公開URL(失敗時は None)
    """
    try:
        drive_service.permissions().create(
            fileId=file_id,
            body={"role": "reader", "type": "anyone"},
        ).execute()
        return f"https://drive.google.com/thumbnail?id={file_id}&sz=w1000"
    except Exception as e:
        print(f"⚠️ Failed to make public: {e}")
        return None

として保存

次にアップロードテスト
Yolov8の結果をアップしたいので

from drive_uploader import upload_image_to_drive, get_public_url

file_id = upload_image_to_drive("runs/detect/predict/image1.jpg")
if file_id:
    url = get_public_url(file_id)
    print("✅ 公開URL:", url)

でパスを

../inventory/runs/detect/predict26/image0.jpg 

に変更して実行

なお、コピペするときに改行してしまうと

  File "/Users/snowpool/aw10s/gas_iamge_bot/upload_test.py", line 3
    file_id = upload_image_to_drive("../inventory/runs/detect/predict26/image0.jpg 
                                    ^
SyntaxError: unterminated string literal (detected at line 3)

というようにエラーになるので注意

修正後再度実行すると

python upload_test.py

✅ Uploaded to Drive: image0.jpg (ID: )
✅ 公開URL: https://drive.google.com/thumbnail?id=&sz=w1000

というように成功する

GoogleDrive の tmpフォルダにファイルがあるのも確認

次にこのファイルを削除できるかテストする

ただし48時間経っていないと削除対象にならないためとりあえず保留

48時間経過したので実験

 python delete_old_images.py
✅ Deleted: image0.jpg (created: 2025-03-27 21:04:02.459000)
⚠️ Error deleting file : <HttpError 403 when requesting https://www.googleapis.com/drive/v3/files/? returned "The user does not have sufficient permissions for this file.". Details: "[{'message': 'The user does not have sufficient permissions for this file.', 'domain': 'global', 'reason': 'insufficientFilePermissions'}]">

となって削除完了