停止処理を5秒たったら録音を終了させる

停止処理を5秒たったら終了させる

import numpy as np
import sounddevice as sd
import os
import configparser
import errno

class Recorderconfig:
    def __init__(self, config_ini_path='./configs/config.ini'):
        # iniファイルの読み込み
        self.config_ini = configparser.ConfigParser()
        
        # 指定したiniファイルが存在しない場合、エラー発生
        if not os.path.exists(config_ini_path):
            raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), config_ini_path)
        
        self.config_ini.read(config_ini_path, encoding='utf-8')
        Recorder_items = self.config_ini.items('Recorder')
        self.Recorder_config_dict = dict(Recorder_items)

class Recorder:
    def __init__(self, config_ini_path='./configs/config.ini'):
        Recorder_config = Recorderconfig(config_ini_path=config_ini_path)
        config_dict = Recorder_config.Recorder_config_dict
        
        self.fs = int(config_dict["fs"])
        self.silence_threshold = float(config_dict["silence_threshold"])
        self.min_duration = float(config_dict["min_duration"])
        self.amplitude_threshold = float(config_dict["amplitude_threshold"])
        self.start_threshold = float(config_dict["start_threshold"])

    def speech2audio(self):
        record_Flag = False
        non_recorded_data = []
        recorded_audio = []
        silent_time = 0
        input_time = 0
        start_threshold = 0.3
        all_time = 0
        
        with sd.InputStream(samplerate=self.fs, channels=1) as stream:
            while True:
                data, overflowed = stream.read(int(self.fs * self.min_duration))
                all_time += 1
                if all_time == 10:
                    print("stand by ready OK")
                elif all_time >= 10:
                    if np.max(np.abs(data)) > self.amplitude_threshold and not record_Flag:
                        input_time += self.min_duration
                        if input_time >= start_threshold:
                            record_Flag = True
                            print("recording...")
                            recorded_audio = non_recorded_data[int(-1*start_threshold*10)-2:]

                    else:
                        input_time = 0

                    if overflowed:
                        print("Overflow occurred. Some samples might have been lost.")
                    if record_Flag:
                        recorded_audio.append(data)

                    else:
                        non_recorded_data.append(data)

                    # 無音が10秒以上続いたらループを抜ける
                    if np.all(np.abs(data) < self.amplitude_threshold):
                        silent_time += self.min_duration
                        if silent_time >= 10:  # 無音が10秒続いたらNoneを返す
                            print("finished")
                            record_Flag = False
                            return None
                    else:
                        silent_time = 0

        # 録音データが存在しない場合はNoneを返す
        if len(recorded_audio) == 0:
            return None

        audio_data = np.concatenate(recorded_audio, axis=0)
        return audio_data

として

from module.module_whisper import FasterWhisperModel
from module.module_recorder import Recorder

def main():
    recorder = Recorder()  # Recorderのインスタンス作成
    fasterWhispermodel = FasterWhisperModel()  # FasterWhisperModelのインスタンス作成

    while True:
        audio_data = recorder.speech2audio()  # 音声の取得
        if audio_data is None:
            print("5秒以上の無音状態が続いたため、ループを終了します。")
            break  # 無音5秒以上でループを抜ける

        text = fasterWhispermodel.audio2text(audio_data)  # 音声をテキストに変換
        print(text)  # テキストを表示

if __name__ == "__main__":
    main()

としたが

[2024-09-05 06:19:29.705] [ctranslate2] [thread 17518403] [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
5秒以上の無音状態が続いたため、ループを終了します。

となるが今度は入力ができなくなった

単純に

import sounddevice as sd
from module.module_whisper import FasterWhisperModel
from module.module_recorder import Recorder
import time

def main():

    recorder = Recorder()
    fasterWhispermodel = FasterWhisperModel()
    while True:
        start_time = time.time()  # 処理開始時刻を記録
        audio_data = recorder.speech2audio()
                # 処理が10秒間行われなかった場合はループを抜ける
        if time.time() - start_time >= 5:
            print("10秒間音声が入力されなかったため、ループを終了します。")
            break

        if audio_data is None:
            print("5秒以上の無音状態が続いたため、ループを終了します。")
            break  # 無音5秒以上でループを抜ける
        text = fasterWhispermodel.audio2text(audio_data)
        print(text)

if __name__ == "__main__":
    main()

としたら停止するが実際には10秒以上かかっている

これを五秒程度にしたらうまく動く

次は line notify でこの文章を送信してみる

Faster-whisperマイクのコード解説

Faster-whisperマイクのコード解説

コードを読むのにChatGPTを使用
これによりコードの解説が得られることで理解しやすくなる

git clone https://github.com/personabb/colab_AI_sample.git

でリポジトリクローン

cd colab_AI_sample 
cd colab_fasterwhisper_sample

そして
main.pyの中の

    while True:
        audio_data = recorder.speech2audio()
        text = fasterWhispermodel.audio2text(audio_data)
        print(text)

これで
recorder.speech2audio()メソッドで音声を録音して、録音データをaudio_dataに格納し、fasterWhispermodel.audio2text()メソッドにて、音声ファイルを文字起こしして、textとして表示

つまり
printではなく
文字を送信するメソッドを作成すれば処理は完成するはず

module_recoder.py
発話の録音用モジュール

import numpy as np
import sounddevice as sd

import os
import configparser
# ファイルの存在チェック用モジュール
import errno

class Recorderconfig:
    def __init__(self, config_ini_path = './configs/config.ini'):
        # iniファイルの読み込み
        self.config_ini = configparser.ConfigParser()
        
        # 指定したiniファイルが存在しない場合、エラー発生
        if not os.path.exists(config_ini_path):
            raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), config_ini_path)
        
        self.config_ini.read(config_ini_path, encoding='utf-8')
        Recorder_items = self.config_ini.items('Recorder')
        self.Recorder_config_dict = dict(Recorder_items)

class Recorder:
    def __init__(self, config_ini_path = './configs/config.ini'):
            
            Recorder_config = Recorderconfig(config_ini_path = config_ini_path)
            config_dict = Recorder_config.Recorder_config_dict
            
            self.fs = int(config_dict["fs"])
            self.silence_threshold = float(config_dict["silence_threshold"])
            self.min_duration = float(config_dict["min_duration"])
            self.amplitude_threshold = float(config_dict["amplitude_threshold"])
            self.start_threshold = float(config_dict["start_threshold"])

    def speech2audio(self):
        record_Flag = False

        non_recorded_data = []
        recorded_audio = []
        silent_time = 0
        input_time = 0
        start_threshold = 0.3
        all_time = 0
        
        with sd.InputStream(samplerate=self.fs, channels=1) as stream:
            while True:
                data, overflowed = stream.read(int(self.fs * self.min_duration))
                all_time += 1
                if all_time == 10:
                    print("stand by ready OK")
                elif all_time >=10:
                    if np.max(np.abs(data) > self.amplitude_threshold) and not record_Flag:
                        input_time += self.min_duration
                        if input_time >= start_threshold:
                            record_Flag = True
                            print("recording...")
                            recorded_audio=non_recorded_data[int(-1*start_threshold*10)-2:]  

                    else:
                        input_time = 0

                    if overflowed:
                        print("Overflow occurred. Some samples might have been lost.")
                    if record_Flag:
                        recorded_audio.append(data)

                    else:
                        non_recorded_data.append(data)

                    if np.all(np.abs(data) < self.amplitude_threshold):
                        silent_time += self.min_duration
                        if (silent_time >= self.silence_threshold) and record_Flag:
                            print("finished")
                            record_Flag = False
                            break
                    else:
                        silent_time = 0

        audio_data = np.concatenate(recorded_audio, axis=0)

        return audio_data

が全文

class Recorderconfig:
    def __init__(self, config_ini_path = './configs/config.ini'):
        # iniファイルの読み込み
        self.config_ini = configparser.ConfigParser()
        
        # 指定したiniファイルが存在しない場合、エラー発生
        if not os.path.exists(config_ini_path):
            raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), config_ini_path)
        
        self.config_ini.read(config_ini_path, encoding='utf-8')
        Recorder_items = self.config_ini.items('Recorder')
        self.Recorder_config_dict = dict(Recorder_items)

この中で

config_ini_path = './configs/config.ini'

で指定されている設定ファイルをRecorder_config_dictとして読み込む

辞書型で読み込んでいるため、設定ファイルの中身をpythonの辞書として読み込むことが可能

self.Recorder_config_dict = dict(Recorder_items)

読み込んでいるのは

[Recorder]

fs=16000
silence_threshold=0.5
min_duration=0.1
amplitude_threshold=0.05
start_threshold = 0.3

の部分
それが

Recorder_items = self.config_ini.items('Recorder')

次に
Recorderクラスのinitメソッド

class Recorder:
    def __init__(self, config_ini_path = './configs/config.ini'):
            
            Recorder_config = Recorderconfig(config_ini_path = config_ini_path)
            config_dict = Recorder_config.Recorder_config_dict
            
            self.fs = int(config_dict["fs"])
            self.silence_threshold = float(config_dict["silence_threshold"])
            self.min_duration = float(config_dict["min_duration"])
            self.amplitude_threshold = float(config_dict["amplitude_threshold"])
            self.start_threshold = float(config_dict["start_threshold"])

まず、設定ファイルの内容をconfig_dictに格納
これは辞書型のため、config_dict[“device”]のような形で設定ファイルの内容を文字列として取得
すべての文字を文字列として取得するため、int型やbool型にしたい場合は、適宜型変更をする必要があることに注意

self.fs = int(config_dict["fs"]):

ではサンプリングレート fsの設定

fs=16000

がその対象
この時文字列として読み込んでいるので
Int でキャストしている

つまり

            self.silence_threshold = float(config_dict["silence_threshold"])
            self.min_duration = float(config_dict["min_duration"])
            self.amplitude_threshold = float(config_dict["amplitude_threshold"])
            self.start_threshold = float(config_dict["start_threshold"])

の部分で

config_dict[]

で設定値を読み込んで
それぞれfloat int などにキャストして値を設定している

続いて、設定ファイルから各種値をインスタンス変数に格納する

Recorderクラスのspeech2audioメソッド

class Recorder:
   ・・・
    def speech2audio(self):
        record_Flag = False

        non_recorded_data = []
        recorded_audio = []
        silent_time = 0
        input_time = 0
        start_threshold = 0.3
        all_time = 0
        
        with sd.InputStream(samplerate=self.fs, channels=1) as stream:
            while True:
                data, overflowed = stream.read(int(self.fs * self.min_duration))
                all_time += 1
                if all_time == 10:
                    print("stand by ready OK")
                elif all_time >=10:
                    if np.max(np.abs(data) > self.amplitude_threshold) and not record_Flag:
                        input_time += self.min_duration
                        if input_time >= start_threshold:
                            record_Flag = True
                            print("recording...")
                            recorded_audio=non_recorded_data[int(-1*start_threshold*10)-2:]  

                    else:
                        input_time = 0

                    if overflowed:
                        print("Overflow occurred. Some samples might have been lost.")
                    if record_Flag:
                        recorded_audio.append(data)

                    else:
                        non_recorded_data.append(data)

                    if np.all(np.abs(data) < self.amplitude_threshold):
                        silent_time += self.min_duration
                        if (silent_time >= self.silence_threshold) and record_Flag:
                            print("finished")
                            record_Flag = False
                            break
                    else:
                        silent_time = 0

        audio_data = np.concatenate(recorded_audio, axis=0)

        return audio_data

上記の部分では,ユーザの発話をマイクを通して取得

「stand by ready OK」と表示されてから,マイクに入力された音声の大きさが閾値(self.amplitude_threshold)以上だった場合に,録音が開始
録音が開始されたら、「recording…」と表示

その後、マイクに入力された音声の大きさが閾値以下になって0.5秒(self.silence_threshold)経過したら録音が停止する

録音が停止したら、「finished」と表示

初期設定:
* record_Flag : 録音中かどうかを示すフラグ。初期値は False(録音していない)。
* non_recorded_data : 録音を開始する前に収集したデータを保持するリスト。
* recorded_audio : 録音中の音声データを保持するリスト。
* silent_time : 無音が続いた時間をカウントする変数。
* input_time : 入力が続いた時間をカウントする変数。
* start_threshold : 録音を開始するためのしきい値。
* all_time : 全体の時間をカウントする変数

with sd.InputStream(samplerate=self.fs, channels=1) as stream:: 

サンプリングレート self.fs とチャンネル数 1(モノラル)で音声入力ストリームを開始

            while True:

でループで音声を取得する

データの読み取り: data, overflowed = stream.read(int(self.fs * self.min_duration))
* self.min_duration に基づいて、一定時間分の音声データを読み取ります。
* overflowed はバッファがオーバーフローしたかどうかを示します。
スタンバイメッセージの表示:
* 最初の10回のループで “stand by ready OK” を表示します(初期化期間を意味するようです)。

                data, overflowed = stream.read(int(self.fs * self.min_duration))
                all_time += 1
                if all_time == 10:
                    print("stand by ready OK")

の部分

if np.max(np.abs(data) > self.amplitude_threshold) and not record_Flag:
    input_time += self.min_duration
    if input_time >= start_threshold:
        record_Flag = True
        print("recording...")
        recorded_audio = non_recorded_data[int(-1 * start_threshold * 10) - 2:]
else:
    input_time = 0

では

np.max(np.abs(data) > self.amplitude_threshold):

* これは、現在の音声データの振幅が self.amplitude_threshold(振幅のしきい値)を超えているかどうかをチェックします。np.abs(data) は音声データの絶対値(振幅)を計算し、np.max() はその最大値を取得します。
not record_Flag:
* record_Flag が False の場合、つまりまだ録音が開始されていない場合にこの条件が真になります。

次に入力時間の追加

input_time += self.min_duration

上記の条件が真の場合、input_time に self.min_duration を加算します。これにより、音声がしきい値を超えた時間がカウントされます。

if input_time >= start_threshold:

input_time が start_threshold を超えた場合、録音を開始する条件が満たされたとみなされます。start_threshold は録音開始のためのしきい値です。

録音の開始

record_Flag = True
print("recording...")
recorded_audio = non_recorded_data[int(-1 * start_threshold * 10) - 2:]

record_Flag = True に設定することで、録音が開始されます。

recorded_audio = non_recorded_data[int(-1 * start_threshold * 10) - 2:]:

* 録音の開始時に、直前の音声データを recorded_audio に追加します。これは、しきい値を超える前の短い期間の音声も記録するためです。int(-1 * start_threshold * 10) – 2 は、start_threshold 秒間のデータを遡って取得するインデックスを計算しています。-2 の部分は余裕を持たせるために追加されている可能性があります

module_whisper.py
音声の文字起こし用のモジュール

from faster_whisper import WhisperModel
import numpy as np
import torch

import os
import configparser
# ファイルの存在チェック用モジュール
import errno

class FasterWhisperconfig:
    def __init__(self, config_ini_path = './configs/config.ini'):
        # iniファイルの読み込み
        self.config_ini = configparser.ConfigParser()
        
        # 指定したiniファイルが存在しない場合、エラー発生
        if not os.path.exists(config_ini_path):
            raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), config_ini_path)
        
        self.config_ini.read(config_ini_path, encoding='utf-8')
        FasterWhisper_items = self.config_ini.items('FasterWhisper')
        self.FasterWhisper_config_dict = dict(FasterWhisper_items)

class FasterWhisperModel:
    def __init__(self,device = None, config_ini_path = './configs/config.ini'):
        FasterWhisper_config = FasterWhisperconfig(config_ini_path = config_ini_path)
        config_dict = FasterWhisper_config.FasterWhisper_config_dict

        if device is not None:
            self.DEVICE = device
        else:
            device = config_dict["device"]

            self.DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
            if device != "auto":
                self.DEVICE = device
            
        self.BEAM_SIZE = int(config_dict["gpu_beam_size"]) if self.DEVICE == "cuda" else int(config_dict["cpu_beam_size"])
        self.language = config_dict["language"]
        self.COMPUTE_TYPE = config_dict["gpu_compute_type"] if self.DEVICE == "cuda" else config_dict["cpu_compute_type"]
        self.MODEL_TYPE = config_dict["gpu_model_type"] if self.DEVICE == "cuda" else config_dict["cpu_model_type"]
        self.kotoba_chunk_length = int(config_dict["chunk_length"])
        self.kotoba_condition_on_previous_text = config_dict["condition_on_previous_text"]
        if self.kotoba_condition_on_previous_text == "True":
            self.kotoba_condition_on_previous_text = True
        else:
            self.kotoba_condition_on_previous_text = False

        if config_dict["use_kotoba"] == "True":
            self.use_kotoba = True
        else:
            self.use_kotoba = False

        if not self.use_kotoba:
            self.model = WhisperModel(self.MODEL_TYPE, device=self.DEVICE, compute_type=self.COMPUTE_TYPE)
        else:
            self.MODEL_TYPE = config_dict["kotoba_model_type"]
            #self.model = WhisperModel(self.MODEL_TYPE, device=self.DEVICE, compute_type=self.cotoba_compute_type)
            self.model = WhisperModel(self.MODEL_TYPE)


    def audio2text(self, data):
        result = ""
        data = data.flatten().astype(np.float32)
        if not self.use_kotoba:
            segments, _ = self.model.transcribe(data, beam_size=self.BEAM_SIZE,language=self.language)
        else:
            segments, _ = self.model.transcribe(data, beam_size=self.BEAM_SIZE,language=self.language, chunk_length=self.kotoba_chunk_length, condition_on_previous_text=self.kotoba_condition_on_previous_text)
        
        for segment in segments:
            result += segment.text
        
        return result
            
    def audioFile2text(self, file_path):
        result = ""
        if not self.use_kotoba:
            segments, _ = self.model.transcribe(file_path, beam_size=self.BEAM_SIZE,language=self.language)
        else:
            segments, _ = self.model.transcribe(file_path, beam_size=self.BEAM_SIZE,language=self.language, chunk_length=self.kotoba_chunk_length, condition_on_previous_text=self.kotoba_condition_on_previous_text)
        
        for segment in segments:
            result += segment.text

        return result
FasterWhisperconfigクラス

class FasterWhisperconfig:
    def __init__(self, config_ini_path = './configs/config.ini'):
        # iniファイルの読み込み
        self.config_ini = configparser.ConfigParser()
        
        # 指定したiniファイルが存在しない場合、エラー発生
        if not os.path.exists(config_ini_path):
            raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), config_ini_path)
        
        self.config_ini.read(config_ini_path, encoding='utf-8')
        FasterWhisper_items = self.config_ini.items('FasterWhisper')
        self.FasterWhisper_config_dict = dict(FasterWhisper_items)

ここではconfig_ini_path = ‘./configs/config.ini’で指定されている設定ファイルをFasterWhisper_config_dictとして読み込んでいます。
辞書型で読み込んでいるため、設定ファイルの中身をpythonの辞書として読み込むことが可能

これにより

[FasterWhisper]
device = auto
language = ja

gpu_model_type = large-v3
gpu_beam_size = 1
gpu_compute_type = float16

cpu_model_type = small
cpu_beam_size = 1
cpu_compute_type = int8

use_kotoba = True
kotoba_model_type = kotoba-tech/kotoba-whisper-v1.0-faster
chunk_length = 15
condition_on_previous_text = False

の部分を読み込んで辞書にしている

FasterWhisperModelクラスのinitメソッド

class FasterWhisperModel:
    def __init__(self,device = None, config_ini_path = './configs/config.ini'):
        FasterWhisper_config = FasterWhisperconfig(config_ini_path = config_ini_path)
        config_dict = FasterWhisper_config.FasterWhisper_config_dict

        if device is not None:
            self.DEVICE = device
        else:
            device = config_dict["device"]

            self.DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
            if device != "auto":
                self.DEVICE = device
            
        self.BEAM_SIZE = int(config_dict["gpu_beam_size"]) if self.DEVICE == "cuda" else int(config_dict["cpu_beam_size"])
        self.language = config_dict["language"]
        self.COMPUTE_TYPE = config_dict["gpu_compute_type"] if self.DEVICE == "cuda" else config_dict["cpu_compute_type"]
        self.MODEL_TYPE = config_dict["gpu_model_type"] if self.DEVICE == "cuda" else config_dict["cpu_model_type"]
        self.kotoba_chunk_length = int(config_dict["chunk_length"])
        self.kotoba_condition_on_previous_text = config_dict["condition_on_previous_text"]
        if self.kotoba_condition_on_previous_text == "True":
            self.kotoba_condition_on_previous_text = True
        else:
            self.kotoba_condition_on_previous_text = False

        if config_dict["use_kotoba"] == "True":
            self.use_kotoba = True
        else:
            self.use_kotoba = False

        if not self.use_kotoba:
            self.model = WhisperModel(self.MODEL_TYPE, device=self.DEVICE, compute_type=self.COMPUTE_TYPE)
        else:
            self.MODEL_TYPE = config_dict["kotoba_model_type"]
            #self.model = WhisperModel(self.MODEL_TYPE, device=self.DEVICE, compute_type=self.cotoba_compute_type)
            self.model = WhisperModel(self.MODEL_TYPE)

これは

        FasterWhisper_config = FasterWhisperconfig(config_ini_path = config_ini_path)
        config_dict = FasterWhisper_config.FasterWhisper_config_dict


まず、設定ファイルの内容をconfig_dictに格納しています。これは辞書型のため、config_dict[“device”]のような形で設定ファイルの内容を文字列として取得することができます。
あくまで、すべての文字を文字列として取得するため、int型やbool型にしたい場合は、適宜型変更をする必要があることに注意

続いて下記の順番で処理を行います。
* モデルを動作させるdeviceを指定する
* 設定ファイルの各種設定を取得する
* モデルを定義する。
* 設定ファイルに合わせて、適切なモデルを定義する

FasterWhisperModelクラスのaudioFile2textメソッド

class FasterWhisperModel:
    ・・・
    def audio2text(self, data):
        result = ""
        data = data.flatten().astype(np.float32)
        if not self.use_kotoba:
            segments, _ = self.model.transcribe(data, beam_size=self.BEAM_SIZE,language=self.language)
        else:
            segments, _ = self.model.transcribe(data, beam_size=self.BEAM_SIZE,language=self.language, chunk_length=self.kotoba_chunk_length, condition_on_previous_text=self.kotoba_condition_on_previous_text)
        
        for segment in segments:
            result += segment.text
        
        return result

faster-whisperモデルのtranscribeメソッドを呼び出して、音声認識をしています。
faster-whisperモデルのtranscribeメソッドは、ファイル名を引数にした場合は、そのファイルを読み込んで処理を行い。numpyデータやbinaryデータを引数にした場合は、そのデータをそのまま利用してくれます

設定ファイルで指定したモデルに合わせて、適切な引数を使っています。
faster-whisperは30秒以上の音声に関しては、音声を分割して処理をするため、分割されて生成されたテキストをresult変数に格納して、return

data = data.flatten().astype(np.float32)

音声データをfaster-whisperモデルに入力するために、データを整形

roboflow Workspaceでアノテーション

roboflow Workspaceでアノテーション

https://axross-recipe.com/recipes/1469
を参考に行う

https://blog.roboflow.com/train-yolov8-obb-model/
のチュートリアルも参考にする

https://blog.roboflow.com
へアクセスし
GoogleIDなどで、Sign InすればOK

その場合
Continue with Google をクリック

ユーザ名を入力すると
プランが2つ出る
Free の無料か
Starter traialの月額249$
のどっちかになるので

とりあえずはFree にする

これでCreate workspace をクリック

次に
Invite teammates.
Add collaborators to help with labeling, upload data, train models, and more.
チームメイトを招待します。
ラベル付け、データのアップロード、モデルのトレーニングなどを支援するコラボレーターを追加します。

とあるけど
とりあえずSkipでOK

これでプロジェクトの作成画面になるが
デフォルトだとライセンスが
CC BY 4.0
になっている

Project Name は
anotation_test
とした

Annotation Group
注釈グループ
が識別のため必要らしい

とりあえずバスクリンなので
Baskulin

Project Typeには
Object Detection
を選択

これで
Create Project をクリック

次に画像ファイル
もしくは画像フォルダを選択する

ここでバスクリンを撮影したフォルダを指定するが
せっかくなので
画像の水増しをする

まず
Pixcel 8 で撮影したバスクリンの画像を
Google Photo からダウンロード

4枚の写真になっているので
これを水増しする

なお複数の写真をダウンロードすると
圧縮ファイルになっているのでこれを解凍する

vim generate_images.py

で内容を

import sys
import os
from PIL import Image

# コマンドライン引数から画像ファイル名を取得
if len(sys.argv) != 2:
    print("Usage: python generate_images.py imagefile.png")
    sys.exit(1)

image_file = sys.argv[1]

# 画像を読み込む
try:
    image = Image.open(image_file)
except IOError:
    print(f"Could not open the image file {image_file}")
    sys.exit(1)

# ファイル名と拡張子を分離し、ディレクトリ名を決定
file_name, file_extension = os.path.splitext(os.path.basename(image_file))
directory_name = file_name

# ディレクトリが存在しない場合は作成
if not os.path.exists(directory_name):
    os.makedirs(directory_name)

# 画像の変形と保存を行う関数
def save_images(image, prefix, transform, count=100):
    for i in range(count):
        filename = f'{prefix}{i+1:03}{file_extension}'
        filepath = os.path.join(directory_name, filename)
        transformed_image = image.transpose(transform)
        transformed_image.save(filepath)

# 各変換を適用して画像を保存
save_images(image, f'{file_name}_', Image.FLIP_TOP_BOTTOM, 100)
save_images(image, f'{file_name}_rot90_', Image.ROTATE_90, 100)
save_images(image, f'{file_name}_rot270_', Image.ROTATE_270, 100)

という画像を回転させてコピーするスクリプトを作成

次に写真をコピーしておく

cp ~/Downloads/Photos-001\ \(1\)/PXL_20240504_1732* .

ファイル名を変えた方が楽なので
ターミナルで

i=1
for file in PXL_20240504_173232354.jpg PXL_20240504_173242547.jpg PXL_20240504_173237123.jpg PXL_20240504_173253414.jpg; do
  mv "$file" "basclin$i.jpg"
  i=$((i + 1))
done

を実行

すると

basclin1.jpg
basclin2.jpg
basclin3.jpg
basclin4.jpg
というようにファイル名が変わる

よくみたらスペルミスなので

i=1
for file in basclin*.jpg; do
  mv "$file" "Baskulin$i.jpg"
  i=$((i + 1))
done

で修正

これでファイル名が
Baskulin1.jpg
Baskulin2.jpg
Baskulin3.jpg
Baskulin4.jpg
となったので

python generate_images.py Baskulin1.jpg
python generate_images.py Baskulin2.jpg
python generate_images.py Baskulin3.jpg
python generate_images.py Baskulin4.jpg

でファイルを量産

これで各ファイルごとのフォルダができたので
これを1つのフォルダにまとめる

ls -d */ | grep Bas


Basと書かれたディレクトリのみ表示できるので

move_files.sh

というスクリプトを作成

#!/bin/bash

# 移動先のディレクトリを作成(存在しない場合)
mkdir -p baskulin

# Baskulinで始まる全ディレクトリのファイルを baskulin ディレクトリに移動
for dir in Baskulin*/; do
    # ディレクトリ内のファイルを baskulin に移動
    mv "$dir"* baskulin/
done

echo "All files have been moved to the 'baskulin' directory."

として保存

chmod +x move_files.sh

で実行権限付与

./move_files.sh

で実行すれば全てのファイルが
1つのフォルダに移動される

そしてこのフォルダをアップロード

これでsave and continueをクリック

なお100枚以上に増やしたけど
同じ画像の場合は全て1つのものとしてみなすため
意味がなかった

回転させたりした場合は
異なる画像としてカウントされる

これでデータがアップできたので
次はアノテーション

kotoba-whisper-v1.0

kotoba-whisper-v1.0

large3 より速いし日本語特化らしい
https://zenn.dev/asap/articles/ba8fcb1880165e

これと
https://zenn.dev/asap/articles/2c0d421e68ef16

生成AIをローカルで簡単に 【Part5.5 faster-whisper+マイク録音編】
と組み合わせることにする

https://github.com/personabb/colab_AI_sample/tree/main/colab_fasterwhisper_sample
のソースコード
を参考に作り上げる

とりあえず設定ファイルの解説

https://zenn.dev/asap/articles/2c0d421e68ef16
の解説を見ながら行う

git clone https://github.com/personabb/colab_AI_sample.git

でリポジトリクローン

cd colab_AI_sample 
cd colab_fasterwhisper_sample


Faster-whisperのところへ移動

python main.py

でとりあえず稼働テスト

config.json: 100%|█████████████████████████| 2.39k/2.39k [00:00<00:00, 13.2MB/s]
preprocessor_config.json: 100%|████████████████| 340/340 [00:00<00:00, 1.59MB/s]
tokenizer.json: 100%|███████████████████████| 2.48M/2.48M [00:02<00:00, 962kB/s]
vocabulary.json: 100%|██████████████████████| 1.07M/1.07M [00:02<00:00, 419kB/s]
model.bin: 100%|███████████████████████████| 1.51G/1.51G [02:37<00:00, 9.58MB/s]
[2024-09-03 05:52:04.173] [ctranslate2] [thread 17310990] [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
ごめん
stand by ready OK
recording...
finished
ごめん
stand by ready OK
recording...
finished
あれ?
stand by ready OK
recording...
finished
今日の天気
stand by ready OK
recording...
finished
明日の天気。
stand by ready OK
recording...
finished
今日はご飯を食べたいハンバーグも欲しい
stand by ready OK
recording...
finished
終わり
stand by ready OK
recording...
finished
一応はこれで使い物にはなるかな
stand by ready OK
recording...
finished
文字を少しまで何秒
stand by ready OK
recording...
finished
あらら
stand by ready OK
recording...
finished
あわわわ
stand by ready OK
recording...

となってほぼ聞き取りができている
ただしアレクサの音声は聞き取りがうまくできなかった
時間は数秒のタイムラグで実行され文字起こしされる

Ctrl+Cをおしてプログラムを停止するまで、永遠に録音と文字起こしが繰り返される

そこまでこだわらなくても
以前、顔の個人認識のときみたいに
ソースをもらって使えばいい

とりあえず修正にあたりソースの理解は必要なので

設定ファイルのソースからみる

[Recorder]

fs=16000
silence_threshold=0.5
min_duration=0.1
amplitude_threshold=0.05
start_threshold = 0.3



[FasterWhisper]
device = auto
language = ja

gpu_model_type = large-v3
gpu_beam_size = 1
gpu_compute_type = float16

cpu_model_type = small
cpu_beam_size = 1
cpu_compute_type = int8

use_kotoba = True
kotoba_model_type = kotoba-tech/kotoba-whisper-v1.0-faster
chunk_length = 15
condition_on_previous_text = False

以下解説
[Recorder]部分は録音モジュールに関係する設定

fs=16000
サンプリングレート16,000Hzで録音
これは
Whisperが16,000Hzの音声を前提としているため

silence_threshold=0.5
無音になってから0.5秒後に録音を停止するパラメータ

min_duration=0.1
streamで録音する際の、chunkの大きさ
変更の必要はない

amplitude_threshold=0.05
無音判定の閾値です。音声の音量が0.05よりも小さい音は無音と判定
これが0だと、自然なノイズも全て音と判定するため、永遠に録音が終わりません

start_threshold = 0.3
閾値(amplitude_threshold)以上の音量が、0.3秒以上継続して入ってきたら、発話開始と判断して録音を開始します。
突発的なノイズによる閾値越えを防ぐための処理

[FasterWhisper]部分は文字起こしモジュールの設定

device = auto
マシンのGPUやCPUのどちらを利用するかの設定
autoの場合は、マシンがGPUを利用できるならGPUを利用する。
その他ではcudaやcpuを指定できる

gpu_model_type = large-v3
gpu_beam_size = 1
gpu_compute_type = float16

cpu_model_type = small
cpu_beam_size = 1
cpu_compute_type = int8


モデルの設定をしている
GPUを利用する場合とCPUを利用する場合でモデルを変更している
GPU利用の場合はlarge-v3モデルという高性能モデルを利用している
CPU利用の場合はsmallモデルという軽量モデルを利用している
加えてCPUの場合は、int8という小さなデータ型を利用して、
計算量を減らしている

use_kotoba = True
kotoba_model_type = kotoba-tech/kotoba-whisper-v1.0-faster
chunk_length = 15
condition_on_previous_text = False

これは
日本語特化モデルであるkotoba-whisper-v1.0を利用する設定

kotoba_model_type = kotoba-tech/kotoba-whisper-v1.0-faster


利用するfaster-whisperモデルを指定

次に

import sounddevice as sd
from module.module_whisper import FasterWhisperModel
from module.module_recorder import Recorder

def main():

    recorder = Recorder()
    fasterWhispermodel = FasterWhisperModel()
    while True:
        audio_data = recorder.speech2audio()
        text = fasterWhispermodel.audio2text(audio_data)
        print(text)

if __name__ == "__main__":
    main()

のソース

これがmain.py

from module.module_whisper import FasterWhisperModel
from module.module_recorder import Recorder


moduleフォルダ内の録音用のモジュールと文字起こし用のモジュールを呼び出し

recorder = Recorder()
fasterWhispermodel = FasterWhisperModel()


呼び出したモジュールのクラスのインスタンスを作成

    while True:
        audio_data = recorder.speech2audio()
        text = fasterWhispermodel.audio2text(audio_data)
        print(text)

これで
recorder.speech2audio()メソッドで音声を録音して、録音データをaudio_dataに格納し、fasterWhispermodel.audio2text()メソッドにて、音声ファイルを文字起こしして、textとして表示

つまり
printではなく
文字を送信するメソッドを作成すれば処理は完成するはず

Faster WhisperとPyAudioを使用して、マイクから音声をリアルタイムで取得し、音声をテキストに変換する

Faster WhisperとPyAudioを使用して、マイクから音声をリアルタイムで取得し、音声をテキストに変換する

pip install faster-whisper pyaudio

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

録音ファイルは
mic_rec.py
とする

import pyaudio
import numpy as np
import faster_whisper

# Faster Whisperのモデルをロードします(モデルパスは適宜変更してください)
model = faster_whisper.WhisperModel("large-v2", device="cpu")  # または "cuda" でGPUを使用

# 音声設定
FORMAT = pyaudio.paInt16
CHANNELS = 1
RATE = 16000
CHUNK = 1024

# 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)
        # NumPy配列に変換
        audio_data = np.frombuffer(data, dtype=np.int16)
        # Faster Whisperに音声データを渡してテキストを取得
        segments, _ = model.transcribe(audio_data)
        # 取得したセグメントを出力
        for segment in segments:
            print(f"Text: {segment.text}")
except KeyboardInterrupt:
    # 終了処理
    print("Terminating...")
finally:
    stream.stop_stream()
    stream.close()
    audio.terminate()


生成されたコードだが
モデルは
large-v3
を使い

CPUを使う

なお以前
/aw10s/whisper/test.py
で試した時には

model = WhisperModel("large-v3", device="cpu", compute_type="int8")

としたので
この設定を使う

import pyaudio
import numpy as np
from faster_whisper import WhisperModel


# Faster Whisperのモデルをロードします(モデルパスは適宜変更してください)
# model = faster_whisper.WhisperModel("large-v2", device="cpu")  # または "cuda" でGPUを使用
model = WhisperModel("large-v3", device="cpu", compute_type="int8")
# 音声設定
FORMAT = pyaudio.paInt16
CHANNELS = 1
RATE = 16000
CHUNK = 1024

# 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)
        # NumPy配列に変換
        audio_data = np.frombuffer(data, dtype=np.int16)
        # Faster Whisperに音声データを渡してテキストを取得
        segments, _ = model.transcribe(audio_data)
        # 取得したセグメントを出力
        for segment in segments:
            print(f"Text: {segment.text}")
except KeyboardInterrupt:
    # 終了処理
    print("Terminating...")
finally:
    stream.stop_stream()
    stream.close()
    audio.terminate()

で実行

しかし

Listening...
Text:  Takk for att du så på!
Traceback (most recent call last):
  File "/Users/snowpool/aw10s/linebot/mic_rec.py", line 27, in <module>
    data = stream.read(CHUNK)
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/pyaudio/__init__.py", line 570, in read
    return pa.read_stream(self._stream, num_frames,
OSError: [Errno -9981] Input overflowed

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/linebot/mic_rec.py", line 39, in <module>
    stream.stop_stream()
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/pyaudio/__init__.py", line 500, in stop_stream
    pa.stop_stream(self._stream)
OSError: Stream not open

となる

OSError: [Errno -9981] Input overflowed は、
PyAudio がバッファから音声データを適切に読み取れなかったことを示しています。
この問題は、バッファサイズが小さいか、処理が遅いために発生することがあります。
とのこと

バッファサイズの調整:
CHUNK のサイズを大きくすることで、入力オーバーフローを防ぐことができます。
例えば、1024 を 2048 や 4096 に変更してみてください
ということで

CHUNK = 2048  # もしくは 4096

で試す

また
エラーハンドリングの追加: オーバーフローエラーが発生した場合に備え、
例外処理を追加してストリームを正しく閉じるようにします。

try:
    while True:
        data = stream.read(CHUNK, exception_on_overflow=False)
        audio_data = np.frombuffer(data, dtype=np.int16)
        segments, _ = model.transcribe(audio_data)
        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()

これらを追加して

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)
        # 取得したセグメントを出力
        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()

としたが
実行すると

Listening... Text: Takk for att du så med. Text: Teksting av Nicolai Winther Text: Teksting av Nicolai Winther Text: Продолжение следует... Text: Teksting av Nicolai Winther ^CTerminating... 

となる

Faster Whisperモデルが認識する言語が様々であるため、
期待される言語での認識がうまくいっていない可能性があります。
これを改善するために、
特定の言語を指定してモデルがその言語に焦点を当てて認識するように設定することが有効です。

Faster Whisperモデルで特定の言語を指定するためには、
transcribe メソッドに language パラメータを追加して使用します。例えば、
日本語を指定したい場合は、language=”ja” とします。

ということで

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()

実行すると

Text: ご視聴ありがとうございました
Text: ご視聴ありがとうございました
Text: ご視聴ありがとうございました

となる

多分設定が足りていない

kotoba-whisper-v1.0

large3 より速いし日本語特化らしい
https://zenn.dev/asap/articles/ba8fcb1880165e

これと
https://zenn.dev/asap/articles/2c0d421e68ef16

生成AIをローカルで簡単に 【Part5.5 faster-whisper+マイク録音編】
と組み合わせることにする

現在の天気が雨でない場合に次の1時間の天気予報をチェックするようにする

現在の天気が雨でない場合に次の1時間の天気予報をチェックするようにする

# weather_check.py
import location_utils2
import requests

def get_weather_data(api_key, latitude, longitude):
    # OpenWeather One Call API の URL
    url = f"https://api.openweathermap.org/data/3.0/onecall?lat={latitude}&lon={longitude}&exclude=minutely,daily&appid={api_key}&units=metric"
    response = requests.get(url)
    return response.json()

def check_for_rain(weather_data):
    hourly_forecast = weather_data.get('hourly', [])[:1]  # 次の1時間の予報だけをチェック
    for hour in hourly_forecast:
        for weather in hour['weather']:
            if weather['main'] == 'Rain':
                return True
    return False

# APIキーを設定
api_key = ''


# location_utils2から緯度と経度と天気をを取得
latitude, longitude, _ , weather_description = location_utils2.get_current_location_and_address()

# 天気データを取得
weather_data = get_weather_data(api_key, latitude, longitude)

# 雨が予報されているかどうかをチェック
if check_for_rain(weather_data):
    print("Alert: Rain is expected within the next hour!")
else:
    print("No rain expected in the next hour.")

のコードだと
雨が降っていても一時間後に雨なのかを調べてしまう

なので現在地の天気が雨でないのなら実行するようにする

なおこの判定は

# location_utils2.py
import requests
from geopy.geocoders import Nominatim

def get_current_location_and_address():
    # APIキーとZIPコードは予め設定しておく必要があります。
    API_key = ""
    zip_place = "郵便番号,JP"
    
    # OpenWeatherMap API URL
    url = f"https://api.openweathermap.org/data/2.5/weather?zip={zip_place}&units=metric&lang=ja&appid={API_key}"
    
    # データの取得
    response = requests.get(url)
    jsondata = response.json()
    
    # 緯度と経度の取得
    latitude = jsondata['coord']['lat']
    longitude = jsondata['coord']['lon']
    
    #天気の取得
    # weather_description = jsondata['weather'][0]['description']
    weather_description = jsondata['weather'][0]['main']

    
    # 住所の取得(オプショナル)
    geolocator = Nominatim(user_agent="geoapiExercises")
    location = geolocator.reverse((latitude, longitude), language='ja')
    address = location.address if location else None
    
    return latitude, longitude, address, weather_description

で郵便番号をもとに
現在地の緯度経度と天気を取得している

この時に
Mainだと英語で取得になるが
Descriptionだと日本語で詳細の天気になる

ただ詳細な天気だと小雨とか判定が面倒なので
とにかく雨であれば
main==rainの判定にできるので
Mainを判定基準にした

変更後のコードは

import location_utils2
import requests

def get_weather_data(api_key, latitude, longitude):
    url = f"https://api.openweathermap.org/data/3.0/onecall?lat={latitude}&lon={longitude}&exclude=minutely,daily&appid={api_key}&units=metric"
    response = requests.get(url)
    return response.json()

def check_for_rain(weather_data):
    hourly_forecast = weather_data.get('hourly', [])[:1]
    for hour in hourly_forecast:
        for weather in hour['weather']:
            if weather['main'] == 'Rain':
                return True
    return False

api_key = ''

latitude, longitude, _, weather_description = location_utils2.get_current_location_and_address()

# 現在の天気が雨でない場合にのみ次の1時間の雨の予報をチェック
if weather_description != 'Rain':
    weather_data = get_weather_data(api_key, latitude, longitude)
    if check_for_rain(weather_data):
        print("Alert: Rain is expected within the next hour!")
    else:
        print("No rain expected in the next hour.")
else:
    print("It is currently raining.")

次に音声を流れるように変更

今回も音声再生はpygameを使う

pygameライブラリを使って音声ファイルを再生しています。
まず、pygame.mixerを初期化し、音声ファイルを読み込んで再生します。
pygame.mixer.music.play()関数で音声を再生し、
while pygame.mixer.music.get_busy()ループを使って
音声が再生されている間は待機します。
このスクリプトは、雨が予報されていない場合にのみ、次の1時間の雨の予報をチェックし、
予報されていた場合に指定された音声ファイルを再生します

コードは

import location_utils2
import requests
import pygame  # Import pygame for audio playback

def get_weather_data(api_key, latitude, longitude):
    url = f"https://api.openweathermap.org/data/3.0/onecall?lat={latitude}&lon={longitude}&exclude=minutely,daily&appid={api_key}&units=metric"
    response = requests.get(url)
    return response.json()

def check_for_rain(weather_data):
    hourly_forecast = weather_data.get('hourly', [])[:1]
    for hour in hourly_forecast:
        for weather in hour['weather']:
            if weather['main'] == 'Rain':
                return True
    return False

api_key = ''

latitude, longitude, _, weather_description = location_utils2.get_current_location_and_address()

# 現在の天気が雨でない場合にのみ次の1時間の雨の予報をチェック
if weather_description != 'Rain':
    weather_data = get_weather_data(api_key, latitude, longitude)
    if check_for_rain(weather_data):
        print("Alert: Rain is expected within the next hour!")
        pygame.mixer.init()  # Initialize the mixer module
        pygame.mixer.music.load('voice.wav')  # Load your sound file
        pygame.mixer.music.play()  # Play the sound
        while pygame.mixer.music.get_busy():  # Wait for music to finish playing
            pygame.time.Clock().tick(10)
    else:
        print("No rain expected in the next hour.")
else:
    print("It is currently raining.")

でOK

音声の部分は

python create_voice.py voice.txt 192.168.1.69:50021

で作成した音声を使用

次は顔認識したら実行するようにする

以前の
Weatheer や mail_voice などで使ったkao.pyとxmlファイルを使えばできるはず

ということでモジュール化する

weather_alert.py

として

# weather_alert.py

import location_utils2
import requests
import pygame

def get_weather_data(api_key, latitude, longitude):
    url = f"https://api.openweathermap.org/data/3.0/onecall?lat={latitude}&lon={longitude}&exclude=minutely,daily&appid={api_key}&units=metric"
    response = requests.get(url)
    return response.json()

def check_for_rain(weather_data):
    hourly_forecast = weather_data.get('hourly', [])[:1]
    for hour in hourly_forecast:
        for weather in hour['weather']:
            if weather['main'] == 'Rain':
                return True
    return False

def check_and_alert(api_key):
    latitude, longitude, _, weather_description = location_utils2.get_current_location_and_address()

    if weather_description != 'Rain':
        weather_data = get_weather_data(api_key, latitude, longitude)
        if check_for_rain(weather_data):
            print("Alert: Rain is expected within the next hour!")
            pygame.mixer.init()
            pygame.mixer.music.load('voice.wav')
            pygame.mixer.music.play()
            while pygame.mixer.music.get_busy():
                pygame.time.Clock().tick(10)
        else:
            print("No rain expected in the next hour.")
    else:
        print("It is currently raining.")

として保存

他で呼び出す時には

    api_key = ''
    check_and_alert(api_key)

というように加えればいい

なので
以前作成した kao.pyの中での処理を変更すればOK

その前に
config.iniを作成し
待機時間とAPIキーを記述しておく

とりあえず mail_voiceのものを使うので

cp ../mail_voice/*.xml .
cp ../mail_voice/config.ini .

でコピー

config.iniを編集する

[Settings]
detection_interval = 1800 #30分

[API_KEYS]
OPENWEATHER_API_KEY = 

としておく

次に kao.pyの編集

 cp ../mail_voice/kao.py .

の後に編集する

import weather_alert

でインポート

# 設定を変数に格納
api_key = config['API_KEYS']['OPENWEATHER_API_KEY']
detection_interval = int(config['Settings']['detection_interval'])

そして天気の取得処理を追加

        if lastTime is None or time.perf_counter() - lastTime > detection_interval:
            # 検出時刻更新
            lastTime = time.perf_counter()
            weather_alert.check_and_alert(api_key)

とする

これで実行したら

  File "/Users/snowpool/aw10s/rain_alert/kao.py", line 12, in <module>
    detection_interval = int(config['Settings']['detection_interval'])
ValueError: invalid literal for int() with base 10: '1800 #30分'

となった

原因はコメント
これが原因で
config[‘Settings’][‘detection_interval’] の値が 1800 #30分 という文字列であり、
これを整数に変換しようとしてエラーが発生していることです。

int() 関数は整数に変換できない文字列を受け取るとエラーを発生させます。
問題の原因は、config[‘Settings’][‘detection_interval’] の値が正しい整数値ではなく、
その後にコメントが付いているためです。
Pythonでは、int() 関数は文字列中の数字のみを整数として解釈します。
とのこと

interval_string = config['Settings']['detection_interval']
interval_without_comment = interval_string.split('#')[0].strip()
detection_interval = int(interval_without_comment)

でコメントを消すこともできるらしいが
とりあえず余計なコメントは削除した

これで実行したら

Hello from the pygame community. https://www.pygame.org/contribute.html
Traceback (most recent call last):
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/geopy/geocoders/base.py", line 368, in _call_geocoder
    result = self.adapter.get_json(url, timeout=timeout, headers=req_headers)
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/geopy/adapters.py", line 472, in get_json
    resp = self._request(url, timeout=timeout, headers=headers)
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/geopy/adapters.py", line 500, in _request
    raise AdapterHTTPError(
geopy.adapters.AdapterHTTPError: Non-successful status code 403

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/rain_alert/kao.py", line 41, in <module>
    weather_alert.check_and_alert(api_key)
  File "/Users/snowpool/aw10s/rain_alert/weather_alert.py", line 21, in check_and_alert
    latitude, longitude, _, weather_description = location_utils2.get_current_location_and_address()
  File "/Users/snowpool/aw10s/rain_alert/location_utils2.py", line 28, in get_current_location_and_address
    location = geolocator.reverse((latitude, longitude), language='ja')
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/geopy/geocoders/nominatim.py", line 372, in reverse
    return self._call_geocoder(url, callback, timeout=timeout)
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/geopy/geocoders/base.py", line 388, in _call_geocoder
    res = self._adapter_error_handler(error)
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/geopy/geocoders/base.py", line 411, in _adapter_error_handler
    raise exc_cls(str(error)) from error
geopy.exc.GeocoderInsufficientPrivileges: Non-successful status code 403

となる

geopy ライブラリがジオコーディングサービスへのアクセスに失敗
が原因とのこと

住所の表示は不要で
緯度経度があれば問題ないため
住所表示機能を削除したら無事に動いた

とりあえず今回もコードを公開するのと同時に
Voicevoxのスクリプトも一緒にリポジトリに入れて公開する

Firebase の設定

Firebase の設定

実行環境
M1 MacbookAir 16GB

ゼロから始めるFirebase入門
を参考に
Firebaseの設定をしていく

まず
https://console.firebase.google.com/
にアクセスし

新規プロジェクトを作成する

_とかはプロジェクト名に使えない

自身の取引、ビジネス、仕事、または職業のみを目的として Firebase を利用することを正式に認めます。
にチェックを入れる

Google アナリティクス
はとりあえず有効にしておく

Google アナリスティクスアカウントの選択は
Default Account For Firebase
でよいらしい

Vue.js/vue-cli を Google Firebase にデプロイする
を参考にした

これで準備ができました
とでたら
続行をクリック

これでFirebaseプロジェクトのダッシュボードが憑依される

次にFirebase CLI のインストール

npm install -g firebase-tools

を実行したが

zsh: command not found: npm

となる

【初心者向け】zsh: command not found: npm がでた時の対処法
を参考に

brew -v

でバージョン確認

Homebrew 4.3.15

次に
nodebrewのインストール

実行すると

==> Auto-updating Homebrew...
Adjust how often this is run with HOMEBREW_AUTO_UPDATE_SECS or disable with
HOMEBREW_NO_AUTO_UPDATE. Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`).
==> Auto-updated Homebrew!
Updated 2 taps (homebrew/core and homebrew/cask).
==> New Formulae
asm6809                    go@1.22                    packcc
boost@1.85                 gql                        pcaudiolib
crow                       http-server-rs             pgcopydb
dwarfs                     kea                        tevent
fast_float                 kubernetes-cli@1.30        tmpmail
flexiblas                  ldb                        truetree
gabo                       mariadb@11.4               ufbt
==> New Casks
kindle-create              localcan                   nrf-connect

You have 27 outdated formulae installed.

Error: Cannot install under Rosetta 2 in ARM default prefix (/opt/homebrew)!
To rerun under ARM use:
    arch -arm64 brew install ...
To install under x86_64, install Homebrew into /usr/local.

となるので

arch -arm64 brew install nodebrew

で再度実行

==> Downloading https://ghcr.io/v2/homebrew/core/nodebrew/manifests/1.2.0-1
######################################################################### 100.0%
==> Fetching nodebrew
==> Downloading https://ghcr.io/v2/homebrew/core/nodebrew/blobs/sha256:bd835b02e
######################################################################### 100.0%
==> Pouring nodebrew--1.2.0.all.bottle.1.tar.gz
==> Caveats
You need to manually run setup_dirs to create directories required by nodebrew:
  /opt/homebrew/opt/nodebrew/bin/nodebrew setup_dirs

Add path:
  export PATH=$HOME/.nodebrew/current/bin:$PATH

To use Homebrew's directories rather than ~/.nodebrew add to your profile:
  export NODEBREW_ROOT=/opt/homebrew/var/nodebrew

zsh completions have been installed to:
  /opt/homebrew/share/zsh/site-functions
==> Summary
🍺  /opt/homebrew/Cellar/nodebrew/1.2.0: 9 files, 43.2KB
==> Running `brew cleanup nodebrew`...
Disable this behaviour by setting HOMEBREW_NO_INSTALL_CLEANUP.
Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`).

と表示される
パスを通す必要があるので

echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.zshrc
source ~/.zshrc

次に

nodebrew ls-remote

でインストールできるnode.jsのバージョンを確認

v0.0.1    v0.0.2    v0.0.3    v0.0.4    v0.0.5    v0.0.6    

v0.1.0    v0.1.1    v0.1.2    v0.1.3    v0.1.4    v0.1.5    v0.1.6    v0.1.7
v0.1.8    v0.1.9    v0.1.10   v0.1.11   v0.1.12   v0.1.13   v0.1.14   v0.1.15
v0.1.16   v0.1.17   v0.1.18   v0.1.19   v0.1.20   v0.1.21   v0.1.22   v0.1.23
v0.1.24   v0.1.25   v0.1.26   v0.1.27   v0.1.28   v0.1.29   v0.1.30   v0.1.31
v0.1.32   v0.1.33   v0.1.90   v0.1.91   v0.1.92   v0.1.93   v0.1.94   v0.1.95
v0.1.96   v0.1.97   v0.1.98   v0.1.99   v0.1.100  v0.1.101  v0.1.102  v0.1.103
v0.1.104  

v0.2.0    v0.2.1    v0.2.2    v0.2.3    v0.2.4    v0.2.5    v0.2.6    

v0.3.0    v0.3.1    v0.3.2    v0.3.3    v0.3.4    v0.3.5    v0.3.6    v0.3.7
v0.3.8    

v0.4.0    v0.4.1    v0.4.2    v0.4.3    v0.4.4    v0.4.5    v0.4.6    v0.4.7
v0.4.8    v0.4.9    v0.4.10   v0.4.11   v0.4.12   

v0.5.0    v0.5.1    v0.5.2    v0.5.3    v0.5.4    v0.5.5    v0.5.6    v0.5.7
v0.5.8    v0.5.9    v0.5.10   

v0.6.0    v0.6.1    v0.6.2    v0.6.3    v0.6.4    v0.6.5    v0.6.6    v0.6.7
v0.6.8    v0.6.9    v0.6.10   v0.6.11   v0.6.12   v0.6.13   v0.6.14   v0.6.15
v0.6.16   v0.6.17   v0.6.18   v0.6.19   v0.6.20   v0.6.21   

v0.7.0    v0.7.1    v0.7.2    v0.7.3    v0.7.4    v0.7.5    v0.7.6    v0.7.7
v0.7.8    v0.7.9    v0.7.10   v0.7.11   v0.7.12   

v0.8.0    v0.8.1    v0.8.2    v0.8.3    v0.8.4    v0.8.5    v0.8.6    v0.8.7
v0.8.8    v0.8.9    v0.8.10   v0.8.11   v0.8.12   v0.8.13   v0.8.14   v0.8.15
v0.8.16   v0.8.17   v0.8.18   v0.8.19   v0.8.20   v0.8.21   v0.8.22   v0.8.23
v0.8.24   v0.8.25   v0.8.26   v0.8.27   v0.8.28   

v0.9.0    v0.9.1    v0.9.2    v0.9.3    v0.9.4    v0.9.5    v0.9.6    v0.9.7
v0.9.8    v0.9.9    v0.9.10   v0.9.11   v0.9.12   

v0.10.0   v0.10.1   v0.10.2   v0.10.3   v0.10.4   v0.10.5   v0.10.6   v0.10.7
v0.10.8   v0.10.9   v0.10.10  v0.10.11  v0.10.12  v0.10.13  v0.10.14  v0.10.15
v0.10.16  v0.10.17  v0.10.18  v0.10.19  v0.10.20  v0.10.21  v0.10.22  v0.10.23
v0.10.24  v0.10.25  v0.10.26  v0.10.27  v0.10.28  v0.10.29  v0.10.30  v0.10.31
v0.10.32  v0.10.33  v0.10.34  v0.10.35  v0.10.36  v0.10.37  v0.10.38  v0.10.39
v0.10.40  v0.10.41  v0.10.42  v0.10.43  v0.10.44  v0.10.45  v0.10.46  v0.10.47
v0.10.48  

v0.11.0   v0.11.1   v0.11.2   v0.11.3   v0.11.4   v0.11.5   v0.11.6   v0.11.7
v0.11.8   v0.11.9   v0.11.10  v0.11.11  v0.11.12  v0.11.13  v0.11.14  v0.11.15
v0.11.16  

v0.12.0   v0.12.1   v0.12.2   v0.12.3   v0.12.4   v0.12.5   v0.12.6   v0.12.7
v0.12.8   v0.12.9   v0.12.10  v0.12.11  v0.12.12  v0.12.13  v0.12.14  v0.12.15
v0.12.16  v0.12.17  v0.12.18  

v4.0.0    v4.1.0    v4.1.1    v4.1.2    v4.2.0    v4.2.1    v4.2.2    v4.2.3
v4.2.4    v4.2.5    v4.2.6    v4.3.0    v4.3.1    v4.3.2    v4.4.0    v4.4.1
v4.4.2    v4.4.3    v4.4.4    v4.4.5    v4.4.6    v4.4.7    v4.5.0    v4.6.0
v4.6.1    v4.6.2    v4.7.0    v4.7.1    v4.7.2    v4.7.3    v4.8.0    v4.8.1
v4.8.2    v4.8.3    v4.8.4    v4.8.5    v4.8.6    v4.8.7    v4.9.0    v4.9.1


v5.0.0    v5.1.0    v5.1.1    v5.2.0    v5.3.0    v5.4.0    v5.4.1    v5.5.0
v5.6.0    v5.7.0    v5.7.1    v5.8.0    v5.9.0    v5.9.1    v5.10.0   v5.10.1
v5.11.0   v5.11.1   v5.12.0   

v6.0.0    v6.1.0    v6.2.0    v6.2.1    v6.2.2    v6.3.0    v6.3.1    v6.4.0
v6.5.0    v6.6.0    v6.7.0    v6.8.0    v6.8.1    v6.9.0    v6.9.1    v6.9.2
v6.9.3    v6.9.4    v6.9.5    v6.10.0   v6.10.1   v6.10.2   v6.10.3   v6.11.0
v6.11.1   v6.11.2   v6.11.3   v6.11.4   v6.11.5   v6.12.0   v6.12.1   v6.12.2
v6.12.3   v6.13.0   v6.13.1   v6.14.0   v6.14.1   v6.14.2   v6.14.3   v6.14.4
v6.15.0   v6.15.1   v6.16.0   v6.17.0   v6.17.1   

v7.0.0    v7.1.0    v7.2.0    v7.2.1    v7.3.0    v7.4.0    v7.5.0    v7.6.0
v7.7.0    v7.7.1    v7.7.2    v7.7.3    v7.7.4    v7.8.0    v7.9.0    v7.10.0
v7.10.1   

v8.0.0    v8.1.0    v8.1.1    v8.1.2    v8.1.3    v8.1.4    v8.2.0    v8.2.1
v8.3.0    v8.4.0    v8.5.0    v8.6.0    v8.7.0    v8.8.0    v8.8.1    v8.9.0
v8.9.1    v8.9.2    v8.9.3    v8.9.4    v8.10.0   v8.11.0   v8.11.1   v8.11.2
v8.11.3   v8.11.4   v8.12.0   v8.13.0   v8.14.0   v8.14.1   v8.15.0   v8.15.1
v8.16.0   v8.16.1   v8.16.2   v8.17.0   

v9.0.0    v9.1.0    v9.2.0    v9.2.1    v9.3.0    v9.4.0    v9.5.0    v9.6.0
v9.6.1    v9.7.0    v9.7.1    v9.8.0    v9.9.0    v9.10.0   v9.10.1   v9.11.0
v9.11.1   v9.11.2   

v10.0.0   v10.1.0   v10.2.0   v10.2.1   v10.3.0   v10.4.0   v10.4.1   v10.5.0
v10.6.0   v10.7.0   v10.8.0   v10.9.0   v10.10.0  v10.11.0  v10.12.0  v10.13.0
v10.14.0  v10.14.1  v10.14.2  v10.15.0  v10.15.1  v10.15.2  v10.15.3  v10.16.0
v10.16.1  v10.16.2  v10.16.3  v10.17.0  v10.18.0  v10.18.1  v10.19.0  v10.20.0
v10.20.1  v10.21.0  v10.22.0  v10.22.1  v10.23.0  v10.23.1  v10.23.2  v10.23.3
v10.24.0  v10.24.1  

v11.0.0   v11.1.0   v11.2.0   v11.3.0   v11.4.0   v11.5.0   v11.6.0   v11.7.0
v11.8.0   v11.9.0   v11.10.0  v11.10.1  v11.11.0  v11.12.0  v11.13.0  v11.14.0
v11.15.0  

v12.0.0   v12.1.0   v12.2.0   v12.3.0   v12.3.1   v12.4.0   v12.5.0   v12.6.0
v12.7.0   v12.8.0   v12.8.1   v12.9.0   v12.9.1   v12.10.0  v12.11.0  v12.11.1
v12.12.0  v12.13.0  v12.13.1  v12.14.0  v12.14.1  v12.15.0  v12.16.0  v12.16.1
v12.16.2  v12.16.3  v12.17.0  v12.18.0  v12.18.1  v12.18.2  v12.18.3  v12.18.4
v12.19.0  v12.19.1  v12.20.0  v12.20.1  v12.20.2  v12.21.0  v12.22.0  v12.22.1
v12.22.2  v12.22.3  v12.22.4  v12.22.5  v12.22.6  v12.22.7  v12.22.8  v12.22.9
v12.22.10 v12.22.11 v12.22.12 

v13.0.0   v13.0.1   v13.1.0   v13.2.0   v13.3.0   v13.4.0   v13.5.0   v13.6.0
v13.7.0   v13.8.0   v13.9.0   v13.10.0  v13.10.1  v13.11.0  v13.12.0  v13.13.0
v13.14.0  

v14.0.0   v14.1.0   v14.2.0   v14.3.0   v14.4.0   v14.5.0   v14.6.0   v14.7.0
v14.8.0   v14.9.0   v14.10.0  v14.10.1  v14.11.0  v14.12.0  v14.13.0  v14.13.1
v14.14.0  v14.15.0  v14.15.1  v14.15.2  v14.15.3  v14.15.4  v14.15.5  v14.16.0
v14.16.1  v14.17.0  v14.17.1  v14.17.2  v14.17.3  v14.17.4  v14.17.5  v14.17.6
v14.18.0  v14.18.1  v14.18.2  v14.18.3  v14.19.0  v14.19.1  v14.19.2  v14.19.3
v14.20.0  v14.20.1  v14.21.0  v14.21.1  v14.21.2  v14.21.3  

v15.0.0   v15.0.1   v15.1.0   v15.2.0   v15.2.1   v15.3.0   v15.4.0   v15.5.0
v15.5.1   v15.6.0   v15.7.0   v15.8.0   v15.9.0   v15.10.0  v15.11.0  v15.12.0
v15.13.0  v15.14.0  

v16.0.0   v16.1.0   v16.2.0   v16.3.0   v16.4.0   v16.4.1   v16.4.2   v16.5.0
v16.6.0   v16.6.1   v16.6.2   v16.7.0   v16.8.0   v16.9.0   v16.9.1   v16.10.0
v16.11.0  v16.11.1  v16.12.0  v16.13.0  v16.13.1  v16.13.2  v16.14.0  v16.14.1
v16.14.2  v16.15.0  v16.15.1  v16.16.0  v16.17.0  v16.17.1  v16.18.0  v16.18.1
v16.19.0  v16.19.1  v16.20.0  v16.20.1  v16.20.2  

v17.0.0   v17.0.1   v17.1.0   v17.2.0   v17.3.0   v17.3.1   v17.4.0   v17.5.0
v17.6.0   v17.7.0   v17.7.1   v17.7.2   v17.8.0   v17.9.0   v17.9.1   

v18.0.0   v18.1.0   v18.2.0   v18.3.0   v18.4.0   v18.5.0   v18.6.0   v18.7.0
v18.8.0   v18.9.0   v18.9.1   v18.10.0  v18.11.0  v18.12.0  v18.12.1  v18.13.0
v18.14.0  v18.14.1  v18.14.2  v18.15.0  v18.16.0  v18.16.1  v18.17.0  v18.17.1
v18.18.0  v18.18.1  v18.18.2  v18.19.0  v18.19.1  v18.20.0  v18.20.1  v18.20.2
v18.20.3  v18.20.4  

v19.0.0   v19.0.1   v19.1.0   v19.2.0   v19.3.0   v19.4.0   v19.5.0   v19.6.0
v19.6.1   v19.7.0   v19.8.0   v19.8.1   v19.9.0   

v20.0.0   v20.1.0   v20.2.0   v20.3.0   v20.3.1   v20.4.0   v20.5.0   v20.5.1
v20.6.0   v20.6.1   v20.7.0   v20.8.0   v20.8.1   v20.9.0   v20.10.0  v20.11.0
v20.11.1  v20.12.0  v20.12.1  v20.12.2  v20.13.0  v20.13.1  v20.14.0  v20.15.0
v20.15.1  v20.16.0  v20.17.0  

v21.0.0   v21.1.0   v21.2.0   v21.3.0   v21.4.0   v21.5.0   v21.6.0   v21.6.1
v21.6.2   v21.7.0   v21.7.1   v21.7.2   v21.7.3   

v22.0.0   v22.1.0   v22.2.0   v22.3.0   v22.4.0   v22.4.1   v22.5.0   v22.5.1
v22.6.0   v22.7.0   

io@v1.0.0 io@v1.0.1 io@v1.0.2 io@v1.0.3 io@v1.0.4 io@v1.1.0 io@v1.2.0 io@v1.3.0
io@v1.4.1 io@v1.4.2 io@v1.4.3 io@v1.5.0 io@v1.5.1 io@v1.6.0 io@v1.6.1 io@v1.6.2
io@v1.6.3 io@v1.6.4 io@v1.7.1 io@v1.8.1 io@v1.8.2 io@v1.8.3 io@v1.8.4 

io@v2.0.0 io@v2.0.1 io@v2.0.2 io@v2.1.0 io@v2.2.0 io@v2.2.1 io@v2.3.0 io@v2.3.1
io@v2.3.2 io@v2.3.3 io@v2.3.4 io@v2.4.0 io@v2.5.0 

io@v3.0.0 io@v3.1.0 io@v3.2.0 io@v3.3.0 io@v3.3.1 

io@vX.X.X形式のバージョン

2014年にNode.jsの開発に関して意見の相違が生じた結果、io.jsが誕生し
その後、Node.jsとio.jsは再統合され、現在のNode.jsのプロジェクトに吸収された
とのこと
なので最新版を使えば問題なさそう

問題はfirebaseで使うので
どのバージョンをつけば良いかということ

GPTによれば最新のものであればOK
とのこと

【初心者向け】zsh: command not found: npm がでた時の対処法

を参考に

nodebrew install v22.7.0 

としたがエラー

Fetching: https://nodejs.org/dist/v22.7.0/node-v22.7.0-darwin-arm64.tar.gz
Warning: Failed to open the file 
Warning: /Users/snowpool/.nodebrew/src/v22.7.0/node-v22.7.0-darwin-arm64.tar.gz
Warning: : No such file or directory
curl: (23) Failure writing output to destination

download failed: https://nodejs.org/dist/v22.7.0/node-v22.7.0-darwin-arm64.tar.gz

GPTで調べたらインストールコマンドが

nodebrew install-binary

となっていたので
これで検索

nodebrewによるNode.jsのインストール手順まとめ
を参考に

安定版をインストールしたいので

nodebrew install-binary stable

を実行

Fetching: https://nodejs.org/dist/v22.7.0/node-v22.7.0-darwin-arm64.tar.gz
Warning: Failed to open the file 
Warning: /Users/snowpool/.nodebrew/src/v22.7.0/node-v22.7.0-darwin-arm64.tar.gz
Warning: : No such file or directory
                                                                            0.0%curl: (23) Failure writing output to destination

download failed: https://nodejs.org/dist/v22.7.0/node-v22.7.0-darwin-arm64.tar.gz

となる

原因は

nodebrew setup

が行われていないため

既にパスは通しているけど
念の為

source ~/.zshrc    

を実行しておく

再度

nodebrew install-binary stable

を実行すると

Fetching: https://nodejs.org/dist/v22.7.0/node-v22.7.0-darwin-arm64.tar.gz
######################################################################### 100.0%
Installed successfully

でインストールできた

ログから
v22.7.0
がインストールされているのがわかるので

nodebrew use v22.7.0

の後に

node -v

v22.7.0

と表示されればOK

npm -v
10.8.2

というように
Npmも一緒にインストールされる

とりあえずこれで準備できたので

npm install -g firebase-tools    

を実行すると

added 630 packages in 33s

69 packages are looking for funding
  run `npm fund` for details

となる

firebase --version
13.15.4

が前回インストールしたときのバージョンだった

irebase --version           
13.16.0

とバージョンが表示されていればOK

firebase login

を実行すると

i  Firebase optionally collects CLI and Emulator Suite usage and error reporting information to help improve our products. Data is collected in accordance with Google's privacy policy (https://policies.google.com/privacy) and is not used to identify you.

? Allow Firebase to collect CLI and Emulator Suite usage and error reporting 
information? (Y/n) 
i Firebase は、製品の改善に役立てるために、CLI およびエミュレータ スイートの使用状況およびエラー報告情報をオプションで収集します。データは Google のプライバシー ポリシー (https://policies.google.com/privacy) に従って収集され、お客様を特定するために使用されることはありません。

? Firebase が CLI とエミュレータ スイートの使用状況とエラー レポートを収集できるようにする 
情報? (はい/いいえ)

となるので
Y

これでブラウザが立ち上がり
認証画面になるのでそのまま進めていく

i  Firebase optionally collects CLI and Emulator Suite usage and error reporting information to help improve our products. Data is collected in accordance with Google's privacy policy (https://policies.google.com/privacy) and is not used to identify you.

? Allow Firebase to collect CLI and Emulator Suite usage and error reporting 
information? Yes
i  To change your data collection preference at any time, run `firebase logout` and log in again.

Visit this URL on this device to log in:ct_uri=http%3A%2F%2Flocalhost%3A9005

Waiting for authentication...

✔  Success! Logged in as snowpoollovely@gmail.com

となり認証が成功

次にプロジェクトの初期化
プロジェクトディレクトリは

/Users/snowpool/aw10s/linebot

ここで

firebase init

を実行

     ######## #### ########  ######## ########     ###     ######  ########
     ##        ##  ##     ## ##       ##     ##  ##   ##  ##       ##
     ######    ##  ########  ######   ########  #########  ######  ######
     ##        ##  ##    ##  ##       ##     ## ##     ##       ## ##
     ##       #### ##     ## ######## ########  ##     ##  ######  ########

You're about to initialize a Firebase project in this directory:

  /Users/snowpool/aw10s/linebot

? Which Firebase features do you want to set up for this directory? Press Space 
to select features, then Enter to confirm your choices. (Press <space> to 
select, <a> to toggle all, <i> to invert selection, and <enter> to proceed)
❯◯ Realtime Database: Configure a security rules file for Realtime Database and 
(optionally) provision default instance
 ◯ Firestore: Configure security rules and indexes files for Firestore
 ◯ Functions: Configure a Cloud Functions directory and its files
 ◯ Hosting: Configure files for Firebase Hosting and (optionally) set up GitHub 
Action deploys
 ◯ Storage: Configure a security rules file for Cloud Storage
(Move up and down to reveal more choices)

となるので

chatGPTで調べる

このプロンプトは、Firebaseプロジェクトを初期化する際に、どのFirebase機能を設定するかを選択する画面です。それぞれの選択肢がどのような役割を果たすのかについて説明し、今回のプロジェクトに適した選択について提案します。

### 選択肢の説明

1. **Realtime Database**:
– セキュリティルールと、オプションでデフォルトのインスタンスを設定します。リアルタイムでデータを読み書きできるNoSQLデータベースです。

2. **Firestore**:
– セキュリティルールとインデックスファイルを設定します。リアルタイムの更新やクエリが可能なドキュメントベースのNoSQLデータベースです。

3. **Functions**:
– Cloud Functionsのディレクトリと必要なファイルを設定します。サーバーレスなバックエンドロジックを実装するための選択肢です。Webhookサーバーを構築する際には、必須となります。

4. **Hosting**:
– Firebase Hosting用のファイルを設定し、オプションでGitHub Actionsを使用してデプロイを自動化する設定を行います。静的ウェブサイトやSPAをホスティングする場合に使用します。

5. **Storage**:
– Cloud Storage用のセキュリティルールファイルを設定します。画像やファイルのアップロードとストレージ管理に使用します。

### 今回のプロジェクトでの選択

あなたが取り組んでいるのは、LINE Botで音声の読み上げと入力、そしてOllamaでの文章修正を行い、FirebaseでWebhookサーバーとして動作させることです。したがって、以下の選択肢が関連性があります:

– **Functions**: これを選択してください。これは、Webhookサーバーを構築するためにCloud Functionsを使用するために必要です。Cloud FunctionsでLINE Botの処理や、Ollamaとの連携を行います。

### 選択の方法

1. **Functions**を選択するには、`Space`キーを押してチェックマークを付けます。
2. 他の選択肢は必要なければそのままにしておきます。
3. `Enter`キーを押して選択を確定します。

### 他のオプションについて

– **Firestore**: もし、修正されたテキストやログを保存するためにデータベースを使用したい場合は、Firestoreを選択するのも有効です。ただし、基本的なWebhookの処理のみであれば、必須ではありません。
– **Hosting**: 今回はWebコンテンツのホスティングが目的ではないため、選択する必要はありません。
– **Storage**: 音声ファイルのアップロードや保存が必要でない限り、これも選択する必要はありません。

このガイドラインに従って、Firebaseプロジェクトを初期化し、必要な機能を設定してください。`Functions`を選択することで、Webhookサーバーを構築する準備が整います。

とのことなので

❯◉ Functions: Configure a Cloud Functions directory and its files

にのみ spaceキーでチェックをいれEnter

? Which Firebase features do you want to set up for this directory? Press Space 
to select features, then Enter to confirm your choices. Functions: Configure a 
Cloud Functions directory and its files

=== Project Setup

First, let's associate this project directory with a Firebase project.
You can create multiple project aliases by running firebase use --add, 
but for now we'll just set up a default project.

? Please select an option: 
❯ Use an existing project 
  Create a new project 
  Add Firebase to an existing Google Cloud Platform project 
  Don't set up a default project 

となる

意味は
このプロンプトでは、Firebaseプロジェクトをディレクトリに関連付ける方法を選択するオプションを提供しています。各オプションの意味を以下に説明し、適切な選択についても提案します。

### オプションの説明

1. **Use an existing project**:
– 既にFirebaseコンソールで作成したプロジェクトがある場合、このオプションを選択します。これにより、現在のディレクトリをその既存のFirebaseプロジェクトに関連付けることができます。
– これが最も一般的で推奨されるオプションです。

2. **Create a new project**:
– 新しいFirebaseプロジェクトを作成する場合にこのオプションを選択します。Firebaseコンソールに新しいプロジェクトが作成され、そのプロジェクトが現在のディレクトリに関連付けられます。
– まだFirebaseプロジェクトを作成していない場合や、新規のプロジェクトが必要な場合に有効です。

3. **Add Firebase to an existing Google Cloud Platform project**:
– 既存のGoogle Cloud Platform(GCP)プロジェクトがあり、それにFirebaseの機能を追加したい場合にこのオプションを選択します。
– GCPプロジェクトのリソースを有効に活用したい場合に便利です。

4. **Don’t set up a default project**:
– 既存のFirebaseプロジェクトや新しいプロジェクトを今すぐ設定したくない場合、このオプションを選択します。後でプロジェクトを設定することができますが、このディレクトリに対してFirebase機能をすぐに使用することはできません。
– 特定のプロジェクトにすぐに関連付けたくない場合にのみ選択します。

### おすすめの選択肢

– **Use an existing project**: 既にFirebaseプロジェクトがある場合、または特定のプロジェクトをこのディレクトリに関連付けたい場合はこのオプションを選びます。
– **Create a new project**: 新しいプロジェクトを作成したい場合、またはまだプロジェクトがない場合はこちらを選びます。

### 今回の状況に応じた選択

もし既にLINE BotやOllamaで使うために設定済みのFirebaseプロジェクトがある場合、**Use an existing project**を選択して、そのプロジェクトを選択するのが良いでしょう。まだプロジェクトを作成していない場合、または新規のプロジェクトが必要であれば、**Create a new project**を選択して新しいプロジェクトを作成します。

選択肢を決定したら、`Enter`キーを押して選択を確定してください。

既に firebase で
voicelinebot
というプロジェクトを作っているのでこれを使う

既に「voicelinebot」というFirebaseプロジェクトを作成しているので、そのプロジェクトを使用するのが最適です。そのために、以下の手順を実行します:

### 手順

1. **「Use an existing project」を選択**:
– `Use an existing project`オプションを選び、`Enter`キーを押します。

2. **「voicelinebot」を選択**:
– Firebase CLIがプロジェクトのリストを表示します。その中から「voicelinebot」を見つけて選択します。
– 矢印キーを使って「voicelinebot」を選択し、`Enter`キーを押します。

これにより、現在のディレクトリが「voicelinebot」プロジェクトに関連付けられます。これからFirebase Functionsのコードを実装し、`firebase deploy`コマンドを使用してデプロイすると、「voicelinebot」プロジェクト内で動作するようになります。

### 確認と次のステップ

– プロジェクトが正しく関連付けられると、Firebase CLIはディレクトリにプロジェクト関連の設定ファイル(`firebase.json`や`.firebaserc`)を生成します。
– 必要なCloud Functionsの実装を行った後、次のコマンドでデプロイできます:

“`bash
firebase deploy –only functions
“`

– デプロイが成功すると、Firebaseコンソールで「voicelinebot」プロジェクトのCloud Functionsセクションから関数の状態やログを確認できます。

これで、「voicelinebot」プロジェクトを使用してLINE Botの機能を実装するための準備が整いました。今後の開発作業に進んでください。

❯ Use an existing project 

でEnter

? Select a default Firebase project for this directory: 
  myfirebaseprojectapp-273fc (MyFirebaseProjectApp) 
  myportfolioproject-5e36a (MyPortfolioProject ) 
  mysecondsite-9c25e (MysecondSite) 
  raspberrypi-ea1b6 (raspberryPi) 
  raspberypi-293821 (raspberypi) 
❯ voicelinebot (voicelinebot) 

プロジェクトの一覧表示となるので

 voicelinebot (voicelinebot) 

を選択しEnter

? Which Firebase features do you want to set up for this directory? Press Space 
to select features, then Enter to confirm your choices. Functions: Configure a 
Cloud Functions directory and its files

=== Project Setup

First, let's associate this project directory with a Firebase project.
You can create multiple project aliases by running firebase use --add, 
but for now we'll just set up a default project.

? Please select an option: Use an existing project
? Select a default Firebase project for this directory: voicelinebot 
(voicelinebot)
i  Using project voicelinebot (voicelinebot)

=== Functions Setup
Let's create a new codebase for your functions.
A directory corresponding to the codebase will be created in your project
with sample code pre-configured.

See https://firebase.google.com/docs/functions/organize-functions for
more information on organizing your functions using codebases.

Functions can be deployed with firebase deploy.

? What language would you like to use to write Cloud Functions? 
  JavaScript 
  TypeScript 
❯ Python 

となるので
chatGPTで調べてみた

Firebase FunctionsでPythonを使いたい場合、Firebase CLIの標準オプションでは直接サポートされていません(2024年現在、Firebase Functionsは主にJavaScriptとTypeScriptでの実装をサポートしています)。そのため、Firebase Functionsの選択肢にPythonが表示されるのは異例であり、通常はJavaScriptまたはTypeScriptを選択します。

### Pythonサポートの状況

– Firebase Functionsは、公式にはJavaScript/TypeScriptの実装をサポートしており、Pythonでの実装は公式サポート外です。
– Pythonでクラウド関数を実行する場合、通常はGoogle Cloud Functionsを直接使用し、Firebaseの認証やデータベースと統合します。

### 対応方法

#### 1. JavaScriptまたはTypeScriptで実装

公式のサポートに従い、Firebase Functionsの実装にはJavaScriptまたはTypeScriptを選択します。これにより、公式のドキュメントやサポートを最大限に活用できます。

– JavaScriptで実装する場合:
– シンプルに開始するにはJavaScriptを選びます。

– TypeScriptで実装する場合:
– 型の安全性を重視する場合、TypeScriptを選択します。

#### 2. Pythonを使用する場合の代替方法

Pythonを使用したい場合、以下の方法でFirebaseと連携することができます。

1. **Google Cloud FunctionsでPythonを使用**:
– Google Cloud FunctionsはPythonでの関数実装をサポートしています。Firebaseの機能(Firestore、Authenticationなど)を使用する場合、Google Cloud FunctionsのエンドポイントとしてPythonコードを実装し、それをFirebaseのイベント(データの変更や認証イベントなど)に関連付けます。

2. **Flaskなどのフレームワークを使用してWebhookを構築**:
– PythonでWebhookを構築し、Firebaseのデータやイベントを処理するWeb APIを作成することも可能です。これをFirebase HostingやCloud Runでホスティングし、Firebase Functionsの代わりに使用します。

### 次のステップ

1. **JavaScript/TypeScriptで実装する場合**:
– Pythonのオプションが表示された場合でも、`JavaScript`または`TypeScript`を選択してプロジェクトを設定します。
– Firebase Functions用に作成されたサンプルコードを基に開発を進めます。

2. **Pythonで実装する場合**:
– Firebase Functionsのプロジェクト設定はスキップし、代わりにGoogle Cloud Functionsや他のPython互換の環境での開発を検討します。

Firebase Functionsでの公式なサポートが必要な場合、現在のところJavaScript/TypeScriptを選ぶのが最も確実な方法です。これを踏まえて、プロジェクトの設定を進めてください。

と出るが

Cloud Functions for FirebaseをPythonで使ってみた

Firebase Functionsで新たにサポートされたPythonをSetupし、更にTypeScriptと共存させる
などの情報もあるので気にせずに
pythonnを選択する

=== Project Setup

First, let's associate this project directory with a Firebase project.
You can create multiple project aliases by running firebase use --add, 
but for now we'll just set up a default project.

? Please select an option: Use an existing project
? Select a default Firebase project for this directory: voicelinebot 
(voicelinebot)
i  Using project voicelinebot (voicelinebot)

=== Functions Setup
Let's create a new codebase for your functions.
A directory corresponding to the codebase will be created in your project
with sample code pre-configured.

See https://firebase.google.com/docs/functions/organize-functions for
more information on organizing your functions using codebases.

Functions can be deployed with firebase deploy.

? What language would you like to use to write Cloud Functions? Python
✔  Wrote functions/requirements.txt
✔  Wrote functions/.gitignore
✔  Wrote functions/main.py
? Do you want to install dependencies now? (Y/n) 

となる
意味は

`Do you want to install dependencies now? (Y/n)` というプロンプトが表示された場合、`Y` を選択することで、Python環境に必要な依存関係が自動的にインストールされます。これにより、作成された`requirements.txt`にリストされているパッケージがインストールされ、PythonのCloud Functionsが正しく動作するようになります。

### 選択の推奨

– **`Y`を選択**:
– `Y`を選択することで、依存関係のインストールが自動で行われ、セットアップがスムーズに進みます。特に、すぐに開発を開始したい場合や、依存関係の管理に慣れていない場合に便利です。

### 次のステップ

1. **依存関係のインストール**:
– `Y`を選択すると、Firebase CLIがPythonのパッケージ管理システム(通常は`pip`)を使用して、`requirements.txt`に記載されたパッケージをインストールします。

2. **関数の実装**:
– `functions/main.py`というファイルが作成されているので、ここに必要な関数のロジックを実装します。このファイルに、LINE BotやOllamaとの連携機能を追加します。

3. **関数のデプロイ**:
– 必要な関数を実装した後、次のコマンドでFirebaseにデプロイします。

“`bash
firebase deploy –only functions
“`

### まとめ

– `Y`を選択して依存関係をインストールし、開発環境を整えましょう。
– `main.py`で関数の実装を行い、テストが終わったらFirebaseにデプロイして、LINE Botなどとの連携を実現します。

これで、Pythonを使用してFirebase Functionsをセットアップし、プロジェクトの開発を進める準備が整いました。

となるので
Y
でEnter

Requirement already satisfied: pip in ./venv/lib/python3.12/site-packages (24.0)
Collecting pip
  Downloading pip-24.2-py3-none-any.whl.metadata (3.6 kB)
Downloading pip-24.2-py3-none-any.whl (1.8 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.8/1.8 MB 8.7 MB/s eta 0:00:00
Installing collected packages: pip
  Attempting uninstall: pip
    Found existing installation: pip 24.0
    Uninstalling pip-24.0:
      Successfully uninstalled pip-24.0
Successfully installed pip-24.2
Collecting firebase_functions~=0.1.0 (from -r requirements.txt (line 1))
  Downloading firebase_functions-0.1.2-py3-none-any.whl.metadata (1.5 kB)
Collecting flask>=2.1.2 (from firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading flask-3.0.3-py3-none-any.whl.metadata (3.2 kB)
Collecting functions-framework>=3.0.0 (from firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading functions_framework-3.8.1-py3-none-any.whl.metadata (16 kB)
Collecting firebase-admin>=6.0.0 (from firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading firebase_admin-6.5.0-py3-none-any.whl.metadata (1.5 kB)
Collecting pyyaml>=6.0 (from firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl.metadata (2.1 kB)
Collecting typing-extensions>=4.4.0 (from firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Using cached typing_extensions-4.12.2-py3-none-any.whl.metadata (3.0 kB)
Collecting cloudevents==1.9.0 (from firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading cloudevents-1.9.0-py3-none-any.whl.metadata (6.5 kB)
Collecting flask-cors>=3.0.10 (from firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading Flask_Cors-4.0.1-py2.py3-none-any.whl.metadata (5.5 kB)
Collecting pyjwt>=2.5.0 (from pyjwt[crypto]>=2.5.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading PyJWT-2.9.0-py3-none-any.whl.metadata (3.0 kB)
Collecting google-events>=0.5.0 (from firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading google_events-0.12.0-py3-none-any.whl.metadata (1.9 kB)
Collecting google-cloud-firestore>=2.11.0 (from firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading google_cloud_firestore-2.18.0-py2.py3-none-any.whl.metadata (5.7 kB)
Collecting deprecation<3.0,>=2.0 (from cloudevents==1.9.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading deprecation-2.1.0-py2.py3-none-any.whl.metadata (4.6 kB)
Collecting cachecontrol>=0.12.6 (from firebase-admin>=6.0.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading cachecontrol-0.14.0-py3-none-any.whl.metadata (3.1 kB)
Collecting google-api-python-client>=1.7.8 (from firebase-admin>=6.0.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading google_api_python_client-2.143.0-py2.py3-none-any.whl.metadata (6.7 kB)
Collecting google-cloud-storage>=1.37.1 (from firebase-admin>=6.0.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading google_cloud_storage-2.18.2-py2.py3-none-any.whl.metadata (9.1 kB)
Collecting google-api-core<3.0.0dev,>=1.22.1 (from google-api-core[grpc]<3.0.0dev,>=1.22.1; platform_python_implementation != "PyPy"->firebase-admin>=6.0.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading google_api_core-2.19.2-py3-none-any.whl.metadata (2.7 kB)
Collecting Werkzeug>=3.0.0 (from flask>=2.1.2->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading werkzeug-3.0.4-py3-none-any.whl.metadata (3.7 kB)
Collecting Jinja2>=3.1.2 (from flask>=2.1.2->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading jinja2-3.1.4-py3-none-any.whl.metadata (2.6 kB)
Collecting itsdangerous>=2.1.2 (from flask>=2.1.2->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading itsdangerous-2.2.0-py3-none-any.whl.metadata (1.9 kB)
Collecting click>=8.1.3 (from flask>=2.1.2->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Using cached click-8.1.7-py3-none-any.whl.metadata (3.0 kB)
Collecting blinker>=1.6.2 (from flask>=2.1.2->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading blinker-1.8.2-py3-none-any.whl.metadata (1.6 kB)
Collecting watchdog>=1.0.0 (from functions-framework>=3.0.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading watchdog-5.0.0-cp312-cp312-macosx_11_0_arm64.whl.metadata (40 kB)
Collecting gunicorn>=22.0.0 (from functions-framework>=3.0.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading gunicorn-23.0.0-py3-none-any.whl.metadata (4.4 kB)
Collecting google-auth!=2.24.0,!=2.25.0,<3.0.0dev,>=2.14.1 (from google-cloud-firestore>=2.11.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading google_auth-2.34.0-py2.py3-none-any.whl.metadata (4.7 kB)
Collecting google-cloud-core<3.0.0dev,>=1.4.1 (from google-cloud-firestore>=2.11.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading google_cloud_core-2.4.1-py2.py3-none-any.whl.metadata (2.7 kB)
Collecting proto-plus<2.0.0dev,>=1.22.0 (from google-cloud-firestore>=2.11.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Using cached proto_plus-1.24.0-py3-none-any.whl.metadata (2.2 kB)
Collecting protobuf!=3.20.0,!=3.20.1,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<6.0.0dev,>=3.20.2 (from google-cloud-firestore>=2.11.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading protobuf-5.27.4-cp38-abi3-macosx_10_9_universal2.whl.metadata (592 bytes)
Collecting cryptography>=3.4.0 (from pyjwt[crypto]>=2.5.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl.metadata (5.4 kB)
Collecting requests>=2.16.0 (from cachecontrol>=0.12.6->firebase-admin>=6.0.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Using cached requests-2.32.3-py3-none-any.whl.metadata (4.6 kB)
Collecting msgpack<2.0.0,>=0.5.2 (from cachecontrol>=0.12.6->firebase-admin>=6.0.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading msgpack-1.0.8-cp312-cp312-macosx_11_0_arm64.whl.metadata (9.1 kB)
Collecting cffi>=1.12 (from cryptography>=3.4.0->pyjwt[crypto]>=2.5.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading cffi-1.17.0-cp312-cp312-macosx_11_0_arm64.whl.metadata (1.5 kB)
Collecting packaging (from deprecation<3.0,>=2.0->cloudevents==1.9.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading packaging-24.1-py3-none-any.whl.metadata (3.2 kB)
Collecting googleapis-common-protos<2.0.dev0,>=1.56.2 (from google-api-core<3.0.0dev,>=1.22.1->google-api-core[grpc]<3.0.0dev,>=1.22.1; platform_python_implementation != "PyPy"->firebase-admin>=6.0.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading googleapis_common_protos-1.65.0-py2.py3-none-any.whl.metadata (1.5 kB)
Collecting grpcio<2.0dev,>=1.33.2 (from google-api-core[grpc]<3.0.0dev,>=1.22.1; platform_python_implementation != "PyPy"->firebase-admin>=6.0.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading grpcio-1.66.0-cp312-cp312-macosx_10_9_universal2.whl.metadata (3.9 kB)
Collecting grpcio-status<2.0.dev0,>=1.33.2 (from google-api-core[grpc]<3.0.0dev,>=1.22.1; platform_python_implementation != "PyPy"->firebase-admin>=6.0.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading grpcio_status-1.66.0-py3-none-any.whl.metadata (1.1 kB)
Collecting httplib2<1.dev0,>=0.19.0 (from google-api-python-client>=1.7.8->firebase-admin>=6.0.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading httplib2-0.22.0-py3-none-any.whl.metadata (2.6 kB)
Collecting google-auth-httplib2<1.0.0,>=0.2.0 (from google-api-python-client>=1.7.8->firebase-admin>=6.0.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading google_auth_httplib2-0.2.0-py2.py3-none-any.whl.metadata (2.2 kB)
Collecting uritemplate<5,>=3.0.1 (from google-api-python-client>=1.7.8->firebase-admin>=6.0.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading uritemplate-4.1.1-py2.py3-none-any.whl.metadata (2.9 kB)
Collecting cachetools<6.0,>=2.0.0 (from google-auth!=2.24.0,!=2.25.0,<3.0.0dev,>=2.14.1->google-cloud-firestore>=2.11.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading cachetools-5.5.0-py3-none-any.whl.metadata (5.3 kB)
Collecting pyasn1-modules>=0.2.1 (from google-auth!=2.24.0,!=2.25.0,<3.0.0dev,>=2.14.1->google-cloud-firestore>=2.11.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading pyasn1_modules-0.4.0-py3-none-any.whl.metadata (3.4 kB)
Collecting rsa<5,>=3.1.4 (from google-auth!=2.24.0,!=2.25.0,<3.0.0dev,>=2.14.1->google-cloud-firestore>=2.11.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading rsa-4.9-py3-none-any.whl.metadata (4.2 kB)
Collecting google-resumable-media>=2.7.2 (from google-cloud-storage>=1.37.1->firebase-admin>=6.0.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading google_resumable_media-2.7.2-py2.py3-none-any.whl.metadata (2.2 kB)
Collecting google-crc32c<2.0dev,>=1.0 (from google-cloud-storage>=1.37.1->firebase-admin>=6.0.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading google-crc32c-1.5.0.tar.gz (12 kB)
  Installing build dependencies ... done
  Getting requirements to build wheel ... done
  Preparing metadata (pyproject.toml) ... done
Collecting MarkupSafe>=2.0 (from Jinja2>=3.1.2->flask>=2.1.2->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl.metadata (3.0 kB)
Collecting pycparser (from cffi>=1.12->cryptography>=3.4.0->pyjwt[crypto]>=2.5.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading pycparser-2.22-py3-none-any.whl.metadata (943 bytes)
Collecting pyparsing!=3.0.0,!=3.0.1,!=3.0.2,!=3.0.3,<4,>=2.4.2 (from httplib2<1.dev0,>=0.19.0->google-api-python-client>=1.7.8->firebase-admin>=6.0.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading pyparsing-3.1.4-py3-none-any.whl.metadata (5.1 kB)
Collecting pyasn1<0.7.0,>=0.4.6 (from pyasn1-modules>=0.2.1->google-auth!=2.24.0,!=2.25.0,<3.0.0dev,>=2.14.1->google-cloud-firestore>=2.11.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading pyasn1-0.6.0-py2.py3-none-any.whl.metadata (8.3 kB)
Collecting charset-normalizer<4,>=2 (from requests>=2.16.0->cachecontrol>=0.12.6->firebase-admin>=6.0.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl.metadata (33 kB)
Collecting idna<4,>=2.5 (from requests>=2.16.0->cachecontrol>=0.12.6->firebase-admin>=6.0.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading idna-3.8-py3-none-any.whl.metadata (9.9 kB)
Collecting urllib3<3,>=1.21.1 (from requests>=2.16.0->cachecontrol>=0.12.6->firebase-admin>=6.0.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading urllib3-2.2.2-py3-none-any.whl.metadata (6.4 kB)
Collecting certifi>=2017.4.17 (from requests>=2.16.0->cachecontrol>=0.12.6->firebase-admin>=6.0.0->firebase_functions~=0.1.0->-r requirements.txt (line 1))
  Downloading certifi-2024.7.4-py3-none-any.whl.metadata (2.2 kB)
Downloading firebase_functions-0.1.2-py3-none-any.whl (76 kB)
Downloading cloudevents-1.9.0-py3-none-any.whl (47 kB)
Downloading firebase_admin-6.5.0-py3-none-any.whl (126 kB)
Downloading flask-3.0.3-py3-none-any.whl (101 kB)
Downloading Flask_Cors-4.0.1-py2.py3-none-any.whl (14 kB)
Downloading functions_framework-3.8.1-py3-none-any.whl (35 kB)
Downloading google_cloud_firestore-2.18.0-py2.py3-none-any.whl (326 kB)
Downloading google_events-0.12.0-py3-none-any.whl (504 kB)
Downloading PyJWT-2.9.0-py3-none-any.whl (22 kB)
Downloading PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl (173 kB)
Using cached typing_extensions-4.12.2-py3-none-any.whl (37 kB)
Downloading blinker-1.8.2-py3-none-any.whl (9.5 kB)
Downloading cachecontrol-0.14.0-py3-none-any.whl (22 kB)
Using cached click-8.1.7-py3-none-any.whl (97 kB)
Downloading cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl (6.2 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.2/6.2 MB 6.7 MB/s eta 0:00:00
Downloading deprecation-2.1.0-py2.py3-none-any.whl (11 kB)
Downloading google_api_core-2.19.2-py3-none-any.whl (139 kB)
Downloading google_api_python_client-2.143.0-py2.py3-none-any.whl (12.2 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 12.2/12.2 MB 9.0 MB/s eta 0:00:00
Downloading google_auth-2.34.0-py2.py3-none-any.whl (200 kB)
Downloading google_cloud_core-2.4.1-py2.py3-none-any.whl (29 kB)
Downloading google_cloud_storage-2.18.2-py2.py3-none-any.whl (130 kB)
Downloading gunicorn-23.0.0-py3-none-any.whl (85 kB)
Downloading itsdangerous-2.2.0-py3-none-any.whl (16 kB)
Downloading jinja2-3.1.4-py3-none-any.whl (133 kB)
Using cached proto_plus-1.24.0-py3-none-any.whl (50 kB)
Downloading protobuf-5.27.4-cp38-abi3-macosx_10_9_universal2.whl (412 kB)
Downloading watchdog-5.0.0-cp312-cp312-macosx_11_0_arm64.whl (88 kB)
Downloading werkzeug-3.0.4-py3-none-any.whl (227 kB)
Downloading cachetools-5.5.0-py3-none-any.whl (9.5 kB)
Downloading cffi-1.17.0-cp312-cp312-macosx_11_0_arm64.whl (178 kB)
Downloading google_auth_httplib2-0.2.0-py2.py3-none-any.whl (9.3 kB)
Downloading google_resumable_media-2.7.2-py2.py3-none-any.whl (81 kB)
Downloading googleapis_common_protos-1.65.0-py2.py3-none-any.whl (220 kB)
Downloading grpcio-1.66.0-cp312-cp312-macosx_10_9_universal2.whl (10.6 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 10.6/10.6 MB 9.9 MB/s eta 0:00:00
Downloading grpcio_status-1.66.0-py3-none-any.whl (14 kB)
Using cached httplib2-0.22.0-py3-none-any.whl (96 kB)
Downloading MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl (18 kB)
Downloading msgpack-1.0.8-cp312-cp312-macosx_11_0_arm64.whl (85 kB)
Downloading pyasn1_modules-0.4.0-py3-none-any.whl (181 kB)
Using cached requests-2.32.3-py3-none-any.whl (64 kB)
Using cached rsa-4.9-py3-none-any.whl (34 kB)
Using cached uritemplate-4.1.1-py2.py3-none-any.whl (10 kB)
Downloading packaging-24.1-py3-none-any.whl (53 kB)
Downloading certifi-2024.7.4-py3-none-any.whl (162 kB)
Downloading charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl (119 kB)
Downloading idna-3.8-py3-none-any.whl (66 kB)
Downloading pyasn1-0.6.0-py2.py3-none-any.whl (85 kB)
Downloading pyparsing-3.1.4-py3-none-any.whl (104 kB)
Downloading urllib3-2.2.2-py3-none-any.whl (121 kB)
Downloading pycparser-2.22-py3-none-any.whl (117 kB)
Building wheels for collected packages: google-crc32c
  Building wheel for google-crc32c (pyproject.toml) ... done
  Created wheel for google-crc32c: filename=google_crc32c-1.5.0-py3-none-any.whl size=13027 sha256=139d975c4c5d667b2152b772b4daab531e73106a48b599ceaf2b867d8514ef6e
  Stored in directory: /Users/snowpool/Library/Caches/pip/wheels/40/b9/2e/089df51c5e6f7cda282c650457a1d9a7a55bf6386d8de8bf3d
Successfully built google-crc32c
Installing collected packages: watchdog, urllib3, uritemplate, typing-extensions, pyyaml, pyparsing, pyjwt, pycparser, pyasn1, protobuf, packaging, msgpack, MarkupSafe, itsdangerous, idna, grpcio, google-crc32c, click, charset-normalizer, certifi, cachetools, blinker, Werkzeug, rsa, requests, pyasn1-modules, proto-plus, Jinja2, httplib2, gunicorn, googleapis-common-protos, google-resumable-media, deprecation, cffi, grpcio-status, google-events, google-auth, flask, cryptography, cloudevents, cachecontrol, google-auth-httplib2, google-api-core, functions-framework, flask-cors, google-cloud-core, google-api-python-client, google-cloud-storage, google-cloud-firestore, firebase-admin, firebase_functions
Successfully installed Jinja2-3.1.4 MarkupSafe-2.1.5 Werkzeug-3.0.4 blinker-1.8.2 cachecontrol-0.14.0 cachetools-5.5.0 certifi-2024.7.4 cffi-1.17.0 charset-normalizer-3.3.2 click-8.1.7 cloudevents-1.9.0 cryptography-43.0.0 deprecation-2.1.0 firebase-admin-6.5.0 firebase_functions-0.1.2 flask-3.0.3 flask-cors-4.0.1 functions-framework-3.8.1 google-api-core-2.19.2 google-api-python-client-2.143.0 google-auth-2.34.0 google-auth-httplib2-0.2.0 google-cloud-core-2.4.1 google-cloud-firestore-2.18.0 google-cloud-storage-2.18.2 google-crc32c-1.5.0 google-events-0.12.0 google-resumable-media-2.7.2 googleapis-common-protos-1.65.0 grpcio-1.66.0 grpcio-status-1.66.0 gunicorn-23.0.0 httplib2-0.22.0 idna-3.8 itsdangerous-2.2.0 msgpack-1.0.8 packaging-24.1 proto-plus-1.24.0 protobuf-5.27.4 pyasn1-0.6.0 pyasn1-modules-0.4.0 pycparser-2.22 pyjwt-2.9.0 pyparsing-3.1.4 pyyaml-6.0.2 requests-2.32.3 rsa-4.9 typing-extensions-4.12.2 uritemplate-4.1.1 urllib3-2.2.2 watchdog-5.0.0

i  Writing configuration info to firebase.json...
i  Writing project information to .firebaserc...
i  Writing gitignore file to .gitignore...

✔  Firebase initialization complete!

となって必要なパッケージがインストールされる

ls

で中身を確認すると

firebase.json	functions

が作成されているのがわかる

とりあえずここまでできたので
次は faster-whisper をマイクからできるようにする

Firebase セットアップ

Firebase セットアップ

実行環境
M1 MacbookAir 16GB

npm install -g firebase-tools

でFirebase CLIをインストール

次に料金体系などを調べておく
これは以前、従量課金でワトソンを使ったら
後で数万円の請求が来たことがあるので
調べるようにしている

使うのは無料枠にする

chatGPTの回答

Firebaseの無料枠と料金プランについて説明します。
Firebaseの無料枠 (Sparkプラン)
Firebaseには無料で利用できるSparkプランがあり、主に以下のサービスに対して無料枠が提供されています:
1. Firebase Hosting:
    * 1GBのストレージ
    * 10GB/月のデータ転送量
2. Firestore (NoSQL データベース):
    * 1GBのストレージ
    * 50,000 回/月の読み取り操作
    * 20,000 回/月の書き込み操作
    * 20,000 回/月の削除操作
3. Realtime Database:
    * 100MBのストレージ
    * 1GB/月のデータ転送量
4. Firebase Authentication:
    * 無制限の認証 (メール/パスワード、匿名、Google、Facebook、Twitterなど)
5. Firebase Cloud Functions:
    * 125,000呼び出し/月
    * 400,000GB-秒の計算時間/月
    * 5GBの出力データ転送量/月
6. Firebase Cloud Messaging:
    * 無制限のプッシュ通知 (送信数に制限なし)
7. Firebase Cloud Storage:
    * 1GBのストレージ
    * 5GB/月のデータ転送量
その他の注意点
* Firebase Authentication はSparkプランで無制限に利用できますが、電話認証(SMS認証)については無料枠があり、それを超えると従量課金が発生します。

次に

Firebase CLIのインストール

Firebase Hostingの設定があったので
有効にしているようだが使わない場合はどうなるか?
を調べる

今回のように、**WebhookサーバーとしてFirebaseを使用する場合、Firebase Hostingは必須ではありません**。Webhookサーバーを構築するためには、主に次のFirebase機能を使用します:

1. **Firebase Cloud Functions**:
– Webhookのエンドポイントを作成するために使用します。Cloud Functionsを使えば、サーバーレスでスケーラブルなWebhookサーバーを簡単にセットアップできます。

2. **Firebase Firestore(またはRealtime Database)**:
– 必要に応じて、Webhookからのデータを保存するために使用します。例えば、音声データのテキスト変換結果や、ユーザーアクションのログを記録するなどの用途です。

### Firebase Hostingが必要ない理由

– **サーバーレスアーキテクチャ**:
– WebhookサーバーのエンドポイントはCloud Functionsで処理されるため、物理的なサーバーや特定のホスティング環境は不要です。

– **静的コンテンツがない**:
– Webhookサーバーは、通常、静的なウェブページを提供するわけではなく、リクエストを受け取って処理するだけのエンドポイントを提供します。したがって、静的コンテンツをホストする必要がない場合、Firebase Hostingは無関係です。

### まとめ

WebhookサーバーをFirebase Cloud Functionsで実装する場合、Firebase Hostingは必要ありません。Firebase Hostingは主に静的ウェブサイトやSPA(シングルページアプリケーション)の公開に使用されるため、Webhookサーバーの構築には直接関係がありません。

そのため、今回はFirebase Hostingを有効にしなくても、Webhookサーバーとしての機能は十分に提供されます。Firebase Cloud Functionsと、必要に応じてFirestoreなどの他のFirebaseサービスを組み合わせて使用すれば、要件を満たすことができます。

ということなので
使うのは
Firebase Cloud Functions
Firebase Firestore(またはRealtime Database)
ということになる

firebase functions(バックエンドサービス)を使うなら
nodeは18 or 20(public beta)が必要らしい

Firebase Functions(Cloud Functions for Firebase)を使用する場合、
現在サポートされているNode.jsのバージョンは18と、最近追加された20(パブリックベータ版)です。
これはFirebaseが提供する最新の機能やセキュリティ強化のためです。

### Firebase FunctionsでのNode.jsバージョンの要件

1. **Node.js 18**:
– これは現在のデフォルトでサポートされている安定版のNode.jsバージョンです。Node.js 18は長期サポート(LTS)リリースであり、セキュリティやパフォーマンスの向上が期待されます。
– Firebase CLIを使用して新しいCloud Functionsプロジェクトを作成する場合、デフォルトでNode.js 18が使用されます。

2. **Node.js 20 (Public Beta)**:
– Node.js 20は最新の機能を試すためのパブリックベータ版として提供されています。新しい言語機能やパフォーマンスの改善が含まれていますが、ベータ版であるため、プロダクション環境での使用には慎重になる必要があります。
– Node.js 20を使用するには、Firebaseプロジェクトの設定で明示的に指定する必要があります。

### Node.jsバージョンを指定する方法

`firebase-functions`のNode.jsバージョンを指定するには、`package.json`ファイルで指定します。以下の例のように設定してください。

```json
{
  "engines": {
    "node": "18"
  }
}
```

または、Node.js 20を使用したい場合は:

```json
{
  "engines": {
    "node": "20"
  }
}
```

### まとめ

– Firebase Functionsで最新の機能とセキュリティを利用するためには、
Node.js 18が推奨されており、これは現在のデフォルトです。
– Node.js 20はパブリックベータ版であり、新しい機能を試したい場合に利用できますが、
プロダクション環境での使用には慎重を要します。
– `package.json`でNode.jsのバージョンを明示的に指定することで、
使用するNode.jsバージョンをコントロールできます。

この情報に基づいて、プロジェクトの要件に最も適したNode.jsのバージョンを選択し、
Firebase Functionsを利用してください。

既に
npm install -g firebase-tools
 を実行しているが nvm install 20 nvm use 20 の後に再度実行する必要があるか?

の答えは

Node.jsのバージョンを変更した後、
`npm install -g firebase-tools`を再度実行する必要は**通常ありません**。
以下に、状況を整理してお答えします。

### なぜ再インストールが不要か

- **グローバルインストール**: `npm install -g firebase-tools`でインストールした`firebase-tools`は、グローバルにインストールされているため、Node.jsのバージョンに依存せず、システム全体で利用できます。
- **互換性**: Firebase CLIは通常、異なるNode.jsのバージョンで互換性があります。したがって、Node.jsのバージョンを切り替えた後でも、既にインストールされているFirebase CLIが動作します。

### nvmでNode.jsのバージョンを変更した場合の注意点

1. **パッケージの互換性**:
   - Node.jsのバージョンを切り替えた場合、特定のプロジェクトが新しいNode.jsバージョンで動作しない場合があります。この場合、プロジェクトの依存関係(`node_modules`)を再インストールする必要がありますが、これは`firebase-tools`の再インストールとは関係ありません。

2. **環境設定**:
   - `nvm use 20`でNode.js 20を使用するようにした場合、シェルセッション内でそのバージョンが有効になります。このシェルセッション内で`firebase-tools`コマンドを使用することに問題はありません。

### Firebase CLIのバージョンを確認する方法

Firebase CLIのバージョンが適切に動作しているか確認したい場合、以下のコマンドでバージョンを確認できます。

```bash
firebase --version
```

### まとめ

- `nvm`でNode.jsのバージョンを切り替えた後に、`firebase-tools`を再インストールする必要は通常ありません。
- Firebase CLIはグローバルにインストールされているため、Node.jsのバージョンを変更しても利用できます。
- `firebase --version`でCLIのバージョンが正常に動作しているかを確認してください。

これにより、`nvm`でのNode.jsバージョンの切り替えとFirebase CLIの使用が円滑に行えるはずです。

とのこと

つまり
WebhookサーバーとしてFirebaseを使用する場合
Firebase Cloud Functionsが必要
これを使用するには
Node.js 20 (Public Beta)
が必要

chatGPTでは

nvm install 20

となっているが
実行すると

zsh: command not found: nvm

M1macにインストールした時には

nodebrew install-binary stable


v22.7.0
がインストールされている

ちなみにバージョンを指定するので

nodebrew ls-remote

で調べると
20の場合は

v20.0.0  v20.1.0  v20.2.0  v20.3.0  v20.3.1  v20.4.0  v20.5.0  v20.5.1
v20.6.0  v20.6.1  v20.7.0  v20.8.0  v20.8.1  v20.9.0  v20.10.0 v20.11.0
v20.11.1 v20.12.0 v20.12.1 v20.12.2 v20.13.0 v20.13.1 v20.14.0 v20.15.0
v20.15.1 v20.16.0 v20.17.0  

となっている

今回は

v20.17.0
をインストールするので

nodebrew install-binary v20.17.0

結果は

Fetching: https://nodejs.org/dist/v20.17.0/node-v20.17.0-darwin-arm64.tar.gz
######################################################################### 100.0%
Installed successfully

使用バージョンを変更

nodebrew use v20.17.0
use v20.17.0

確認のため

node -v

とすれば

v20.17.0

となる

nodebrew ls

の結果は

v20.17.0
v22.7.0

current: v20.17.0

でインストールされているバージョンも確認

ここまでできたので
ゼロから始めるFirebase入門

を参考に

firebase login 

としたら

zsh: command not found: firebase

となる
chatGPTでは再度実行不要と言っていたけど

npm install -g firebase-tools   

を実行しインストール

firebase --version           

を実行すると

13.16.0

と表示される

LLMを使うようにコード変更

実行環境
M1 MacbookAir 16GB

LLMを使うようにコード変更

import re
import csv
from datetime import datetime

# ファイルからテキストを読み込む
with open('ocr.txt', 'r', encoding='utf-8') as file:
  text = file.read()

# 情報を抽出
date_match = re.search(r'\d{4}年\d{1,2}月\d{1,2}日', text)
date = datetime.strptime(date_match.group(), '%Y年%m月%d日').strftime('%Y-%m-%d')

shop_name_match = re.search(r'とれたて食楽部', text)
shop_name = shop_name_match.group()

# 商品情報を正規表現で抽出
items = re.findall(r'内\d+ (.+?) ¥(\d+)', text)

# 現在のタイムスタンプを生成
timestamp = datetime.now().strftime('%Y%m%d%H%M%S')

# 出力ファイル名にタイムスタンプを付ける
csv_filename = f'receipt_data_{timestamp}.csv'

# CSVファイルに出力
with open(csv_filename, mode='w', newline='', encoding='utf-8') as csvfile:
  csv_writer = csv.writer(csvfile)
  # ヘッダー行を書き込み
  csv_writer.writerow(["日付", "店名", "商品名", "数量", "金額"])
  for item in items:
    product_name, price = item
    # 商品名の「/」以降を削除
    product_name_clean = product_name.split('/')[0]
    # 出力: 日付, 店名, 商品名, 数量, 金額
    row = f"{date},{shop_name},{product_name_clean},1,{price}"
    csv_writer.writerow(row.split(','))

print(f"CSVファイル '{csv_filename}' に出力しました。")

の処理を汎用性を持たせるため
プロンプトに変えたい

import re
import csv
from datetime import datetime
import ollama

class OCRToCSV:
    def __init__(self, text):
        self.text = text
        self.date, self.shop_name, self.items = self._extract_info_with_llm()

    def _extract_info_with_llm(self):
        # プロンプトを作成
        prompt = f"""
        テキストから次の情報を抽出してください:
        1. 日付(例: 2024-08-20)
        2. 店名(例: とれたて食楽部)
        3. 商品情報(例: 商品名、数量、金額)
        
        テキスト:
        {self.text}
        
        出力形式:
        日付: 2024-08-20
        店名: とれたて食楽部
        商品情報:
        - 商品名1, 1, 500
        - 商品名2, 1, 1000
        """

        # Ollamaでプロンプトを送信
        res = ollama.chat(
            model="elyza:jp8b",
            messages=[{'role': 'user', 'content': prompt}]
        )

        # 応答から内容を取得
        content = res['message']['content']

        # 応答を解析して必要な情報を取得
        date_match = re.search(r'日付:\s*(\d{4}-\d{2}-\d{2})', content)
        shop_name_match = re.search(r'店名:\s*(.+)', content)
        items_match = re.findall(r'-\s*(.+),\s*(\d+),\s*(\d+)', content)

        date = date_match.group(1) if date_match else None
        shop_name = shop_name_match.group(1) if shop_name_match else None
        items = [(item[0], item[1], item[2]) for item in items_match]

        return date, shop_name, items

    def generate_csv(self, output_dir='.', timestamp=None):
        if timestamp is None:
            timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
        
        csv_filename = f'{output_dir}/receipt_data_{timestamp}.csv'
        
        with open(csv_filename, mode='w', newline='', encoding='utf-8') as csvfile:
            csv_writer = csv.writer(csvfile)
            csv_writer.writerow(["日付", "店名", "商品名", "数量", "金額"])
            for item in self.items:
                product_name, quantity, price = item
                row = f"{self.date},{self.shop_name},{product_name},1,{price}"
                csv_writer.writerow(row.split(','))

        print(f"CSVファイル '{csv_filename}' に出力しました。")
        return csv_filename

# 使用例
# テキストを読み込んでOCRToCSVインスタンスを作成し、CSVを生成
with open('ocr.txt', 'r', encoding='utf-8') as file:
    text = file.read()

ocr_to_csv = OCRToCSV(text)
ocr_to_csv.generate_csv()

としたが

WARNING: All log messages before absl::InitializeLog() is called are written to STDERR I0000 00:00:1724125129.779029 15985491 config.cc:230] gRPC experiments enabled: call_status_override_on_cancellation, event_engine_dns, event_engine_listener, http2_stats_fix, monitoring_experiment, pick_first_new, trace_record_callops, work_serializer_clears_time_cache CSVファイル './receipt_data_20240820123851.csv' に出力しました。 snowpool@kubotasorunoAir ollama % python main_ocr.py WARNING: All log messages before absl::InitializeLog() is called are written to STDERR I0000 00:00:1724125231.314241 15987342 config.cc:230] gRPC experiments enabled: call_status_override_on_cancellation, event_engine_dns, event_engine_listener, http2_stats_fix, monitoring_experiment, pick_first_new, trace_record_callops, work_serializer_clears_time_cache CSVファイル './receipt_data_20240820124054.csv' に出力しました。 となる また cat receipt_data_20240820124054.csv 日付,店名,商品名,数量,金額 となっていて店名などが記載されていない

なので

import re
import csv
from datetime import datetime
import ollama

class OCRToCSV:
    def __init__(self, text):
        self.text = text
        self.date, self.shop_name, self.items = self._extract_info_with_llm()

    def _extract_info_with_llm(self):
        # プロンプトを作成
        prompt = f"""
        テキストから次の情報を抽出してください:
        1. 日付(例: 2024-08-20)
        2. 店名(例: とれたて食楽部)
        3. 商品情報(例: 商品名、数量、金額)
        
        テキスト:
        {self.text}
        
        出力形式:
        日付: 2024-08-20
        店名: とれたて食楽部
        商品情報:
        - 商品名1, 1, 500
        - 商品名2, 1, 1000
        """

        # Ollamaでプロンプトを送信
        res = ollama.chat(
            model="elyza:jp8b",
            messages=[{'role': 'user', 'content': prompt}]
        )

        # 応答内容を表示(デバッグ用)
        content = res['message']['content']
        print("LLMの応答:", content)

        # 応答を解析して必要な情報を取得
        date_match = re.search(r'日付:\s*(\d{4}年\d{1,2}月\d{1,2}日)', content)
        shop_name_match = re.search(r'店名:\s*(.+)', content)
        items_match = re.findall(r'-\s*(.+)、\s*(\d+)', content)

        # 日付を %Y-%m-%d 形式に変換
        date = datetime.strptime(date_match.group(1), '%Y年%m月%d日').strftime('%Y-%m-%d') if date_match else None
        shop_name = shop_name_match.group(1) if shop_name_match else None
        # 商品情報の数量をデフォルトで1に設定
        items = [(item[0], '1', item[1]) for item in items_match]

        return date, shop_name, items

    def generate_csv(self, output_dir='.', timestamp=None):
        if timestamp is None:
            timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
        
        csv_filename = f'{output_dir}/receipt_data_{timestamp}.csv'
        
        with open(csv_filename, mode='w', newline='', encoding='utf-8') as csvfile:
            csv_writer = csv.writer(csvfile)
            csv_writer.writerow(["日付", "店名", "商品名", "数量", "金額"])
            for item in self.items:
                product_name, quantity, price = item
                row = f"{self.date},{self.shop_name},{product_name},{quantity},{price}"
                csv_writer.writerow(row.split(','))

        print(f"CSVファイル '{csv_filename}' に出力しました。")
        return csv_filename

# 使用例
with open('ocr.txt', 'r', encoding='utf-8') as file:
    text = file.read()

ocr_to_csv = OCRToCSV(text)
ocr_to_csv.generate_csv()

へコード変更

これでもダメ

import re
import csv
from datetime import datetime
import ollama

class OCRToCSV:
    def __init__(self, text):
        self.text = text
        self.date, self.shop_name, self.items = self._extract_info_with_llm()

    def _extract_info_with_llm(self):
        # プロンプトを作成
        prompt = f"""
        テキストから次の情報を抽出してください:
        1. 日付(例: 2024-08-20)
        2. 店名(例: とれたて食楽部)
        3. 商品情報(例: 商品名、数量、金額)
        
        テキスト:
        {self.text}
        
        出力形式:
        日付: 2024-08-20
        店名: とれたて食楽部
        商品情報:
        - 商品名1, 1, 500
        - 商品名2, 1, 1000
        """

        # Ollamaでプロンプトを送信
        res = ollama.chat(
            model="elyza:jp8b",
            messages=[{'role': 'user', 'content': prompt}]
        )

        # 応答内容を表示(デバッグ用)
        content = res['message']['content']
        print("LLMの応答:", content)

        # 応答を解析して必要な情報を取得
        date_match = re.search(r'日付:\s*(\d{4}-\d{2}-\d{2})', content)
        shop_name_match = re.search(r'店名:\s*(.+)', content)
        items_match = re.findall(r'-\s*(.+)、\s*(\d+)', content)

        # 日付をそのまま使用
        date = date_match.group(1) if date_match else None
        shop_name = shop_name_match.group(1) if shop_name_match else None
        items = [(item[0], '1', item[1]) for item in items_match]

        return date, shop_name, items

    def generate_csv(self, output_dir='.', timestamp=None):
        if timestamp is None:
            timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
        
        csv_filename = f'{output_dir}/receipt_data_{timestamp}.csv'
        
        with open(csv_filename, mode='w', newline='', encoding='utf-8') as csvfile:
            csv_writer = csv.writer(csvfile)
            csv_writer.writerow(["日付", "店名", "商品名", "数量", "金額"])
            for item in self.items:
                product_name, quantity, price = item
                row = f"{self.date},{self.shop_name},{product_name},{quantity},{price}"
                csv_writer.writerow(row.split(','))

        print(f"CSVファイル '{csv_filename}' に出力しました。")
        return csv_filename

# 使用例
with open('ocr.txt', 'r', encoding='utf-8') as file:
    text = file.read()

ocr_to_csv = OCRToCSV(text)
ocr_to_csv.generate_csv()

に変更することで解決

あとは指定のファイルのみ
main_ocr.pyで固定しているので
これを各ファイルごとにCSVに出力する
もしくはディレクトリ内にあるものを全て対象にするようにする

結局はこれはOCRして商品名を取り出すのがメインの目的で
結果をDBやキーワードリストに使いたい
ということ
ならimagesフォルダの中身を対象に実行するようにすればOK

実行後OCR済みのフォルダに移動するようにすればいい
これをやったらバグった
とりあえずLLMを使わないのならうまく動く

Enchanted LLM

Enchanted LLM

実行環境
M1 MacbookAir 16GB

前提条件として Ollamaが使えること

m1macbookAir へ ollamaを入れる

M1macBookAir 16GBで Ollama を使って日本語に特化した大規模言語モデル Llama-3-ELYZA-JP-8B を動かす
などを参考に

スペック的に docker を動かすのは難しいので
https://apps.apple.com/gb/app/enchanted-llm/id6474268307
から
Enchanted LLM をインストールする

 ollama serve

で起動できるが
停止方法がわからなかった

Ctrl + c で止まるらしい

とりあえず実践

ollama serve
Error: listen tcp 127.0.0.1:11434: bind: address already in use

となる

他で使っているらしいので

lsof -i :11434

としたら

COMMAND   PID     USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
ollama  60614 snowpool    6u  IPv4 0x61d691aa0ace346d      0t0  TCP localhost:11434 (LISTEN)

となって動いていた

どうやら

/bye

だけだと ollamaは停止していなかった

使い方は

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

OllamaをChromeAddonのPage Assistで簡単操作
という方法もあるため
Ubuntuとかで操作するならこちらもありかも

とりあえず今回はEnchanted を使う

Settings で
127.0.0.1:11434
を設定

これで接続される

あとは

USER:
あなたは、文字起こししたテキストを修正する専門のアシスタントです。
以下の三重引用符で囲まれたテキストは、YouTubeの動画を fast-whisper で文字起こししたものです。
多少の誤字脱字があるので、正しいと思われる内容に書き換えてください。
大きくは書き換えないでください。

"""
食べないダイエットでリバウンドを繰り返し、正しく食べることでやっと痩せられた私の一日の食事を解説していきます。
朝はバナナとオールブランで糖質、くるみで脂質、無脂肪ヨーグルトと低糖質低脂質のミルクで脂肪質をとっています。
詳しい作り方はYouTubeで紹介しています。
昼はサラダ、鶏胸肉とお米です。
鶏胸肉は揚げ物なんだけど、脂質が低くておすすめです。
運動量も身長も平均な方であれば、4歩から5歩ぐらいがおすすめです。
夜はサラダと、あやのさんがヘルシーダイエットクラブで教えてくれたバターチキンカレーです。
カレーもノーオイルで自分で手作りしたり、脂質が低いカレールーを使ったり、市販のレトルトカレーでも脂質が低いものがあるので、ダイエット中でもカレーを楽しめます。
市販のものの裏面を見るコツは、YouTubeで詳しく解説しています。
自分の食事の適正量や過不足がわからない方は、数日でいいのでアスケンなどで可視化してみると、気づきがあるかもしれません。"""

とすると

以下は、修正したテキストです。

"""
食べないダイエットでリバウンドを繰り返し、正しく食べることでやっと痩せられた私の一日の食事を解説していきます。
朝はバナナとオールブランで糖質、くるみで脂質、無脂肪ヨーグルトと低糖質・低脂質のミルクで栄養をとっています。
詳しい作り方はYouTubeで紹介しています。
昼はサラダ、鶏胸肉からお米です。鶏胸肉は揚げ物ですが、脂質が低くておすすめです。
運動量も身長も平均な方であれば、4歩から5歩ぐらいがおすすめです。
夜はサラダと、あやのさんがヘルシーダイエットクラブで教えてくれたバターチキンカレーです。カレーもノーオイルで自分で手作りしたり、脂質が低いカレールーを使ったり、市販のレトルトカレーでも脂質が低いものがあるので、ダイエット中でもカレーを楽しめます。
市販の商品の裏面を見るコツはYouTubeで詳しく解説しています。
自分の食事の適正量や過不足がわからない方は、数日アスケンなどで可視化してみると気づきがあるかもしれません。

というように校正されて返ってくる

以下は、修正した箇所です。

1. 「脂質をとっています」→「栄養をとっています」: 正しい表現に書き換えました。
2. 「鶏胸肉は揚げ物なんだけど」→「鶏胸肉からお米です。鶏胸肉は揚げ物ですが」: 文の順序を整理し、正しい情報を提供するために修正しました。
3. 「市販のものの裏面を見るコツ」→「市販の商品の裏面を見るコツ」: 正しい表現に書き換えました。

ちなみにgpt4oだと

食べないダイエットでリバウンドを繰り返し、正しく食べることでやっと痩せられた私の一日の食事を解説していきます。
朝はバナナとオールブランで糖質、くるみで脂質、無脂肪ヨーグルトと低糖質低脂質のミルクでタンパク質をとっています。
詳しい作り方はYouTubeで紹介しています。
昼はサラダ、鶏胸肉とお米です。
鶏胸肉は揚げ物なんだけど、脂質が低くておすすめです。
運動量も身長も平均な方であれば、4歩から5歩ぐらいがおすすめです。
夜はサラダと、あやのさんがヘルシーダイエットクラブで教えてくれたバターチキンカレーです。
カレーもノーオイルで自分で手作りしたり、脂質が低いカレールーを使ったり、市販のレトルトカレーでも脂質が低いものがあるので、ダイエット中でもカレーを楽しめます。
市販のものの裏面を見るコツは、YouTubeで詳しく解説しています。
自分の食事の適正量や過不足がわからない方は、数日でいいのでアスケンなどで可視化してみると、気づきがあるかもしれません。

というように
「脂肪質」を「タンパク質」に修正している

とりあえず音声文字起こしの校正には使えると思う

Detected language 'ja' with probability 0.994422
[0.00s -> 1.78s] 食べないダイエットでリバウンドを繰り返し
[1.78s -> 3.78s] 正しく食べることでやっと痩せられた私の
[3.78s -> 5.48s] 一日の食事を解説していきます
[5.48s -> 7.44s] 朝はバナナとオールブランで糖質
[7.44s -> 8.40s] くるみで脂質
[8.40s -> 9.52s] 無脂肪ヨーグルトと
[9.52s -> 12.00s] 低糖質低脂質のミルクで脂肪質とってます
[12.00s -> 13.94s] 詳しい作り方はYouTubeで紹介してます
[13.94s -> 16.02s] 昼はサラダ、胸からお米です
[16.02s -> 17.92s] 胸からは揚げ物なんだけど
[17.92s -> 19.72s] 脂質が低くておすすめです
[19.72s -> 21.94s] 運動量も身長も平均な方であれば
[21.94s -> 23.72s] 4歩から5歩ぐらいがおすすめです
[23.72s -> 24.68s] 夜はサラダと
[24.68s -> 27.28s] あやのさんがヘルシーダイエットクラブで教えてくれた
[27.28s -> 28.84s] バターチキンカレーです
[28.84s -> 31.40s] カレーもノーオイルで自分で手作りしたりとか
[31.40s -> 33.86s] こういう脂質が低いカレールーを使ったりとか
[33.86s -> 37.04s] 市販のレトルトカレーでも脂質が低いものがあるので
[37.04s -> 39.22s] ダイエット中でもカレーを楽しめます
[39.22s -> 41.08s] 市販のものの裏面を見るコツは
[41.08s -> 42.64s] YouTubeで詳しく解説してます
[42.64s -> 45.68s] 自分の食事の適正量や過不足がわからないよっていう方は
[45.68s -> 46.56s] 数日でいいので
[46.56s -> 48.16s] アスケンなどで可視化してみると
[48.16s -> 49.52s] 気づきがあるかもしれません

を文章のみ抽出

とすれば

以下は、抽出した文章です。

食べないダイエットでリバウンドを繰り返し、正しく食べることでやっと痩せられた私の一日の食事を解説していきます。朝はバナナとオールブランで糖質、くるみで脂質、無脂肪ヨーグルトと低糖質・低脂質のミルクで栄養をとっています。詳しい作り方はYouTubeで紹介しています。昼はサラダ、鶏胸肉からお米です。鶏胸肉は揚げ物なんだけど、脂質が低くておすすめです。運動量も身長も平均な方であれば、4歩から5歩ぐらいがおすすめです。夜はサラダとあやのさんがヘルシーダイエットクラブで教えてくれたバターチキンカレーです。カレーもノーオイルで自分で手作りしたりとか、こういう脂質が低いカレールーを使ったりとか、市販のレトルトカレーでも脂質が低いものがあるので、ダイエット中でもカレーを楽しめます。市販の商品の裏面を見るコツはYouTubeで詳しく解説しています。自分の食事の適正量や過不足がわからない方は、数日でいいのでアスケンなどで可視化してみると気づきがあるかもしれません。

というように文章の修正まで行ってくれる