LINEbot で特定のユーザーのメッセージの取得

LINEbot で特定のユーザーのメッセージの取得

LINEbotで特定のユーザのメッセージの取得をする
webhook には firebase第一世代を使用
使用しているのは node.js の18
この構成で LINEbot で特定のユーザーのメッセージの取得する python コード

これを行うには
Firebase プロジェクトが設定されており、
Firestore のアクセスに必要なクレデンシャル JSON ファイルがある

google-cloud-firestore パッケージがインストールされている
の条件を満たす必要がある

Firebase プロジェクトで
Firestore のアクセスに必要なクレデンシャル JSON ファイルが設定されているかを確認

まず
Firebase Console にアクセスし、対象のプロジェクトを選択

左側のメニューから 「プロジェクトの設定」 (Project settings) をクリック

「サービスアカウント」 (Service accounts) タブを開く

Admin SDK 構成スニペット
があるので調べる

Admin SDK 構成スニペットにはどれを選べば良い?
webhook には firebase第一世代を使用し 使用しているのは node.js の18の場合は?


GPTで調べると
Firebase 第一世代の webhook に Node.js 18 を使用している場合、
Firestore の管理者権限でアクセスするには、Admin SDK 構成スニペットとして Node.js を選択

となるので
Node.js のままで

var admin = require("firebase-admin");

var serviceAccount = require("path/to/serviceAccountKey.json");

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount)
});

「新しい秘密鍵の生成」をクリックして、サービスアカウントの JSON クレデンシャルをダウンロード
ファイルをプロジェクトにコピーする

とりあえずJSONファイルは取得できたので
次に

pip install google-cloud-firestore


firesroreのセットアップ

しかしよく考えてみれば
node.jsは firebaseで動かしているが
m1macbookAir では動かしていない

違いがわからないため再度質問

Admin SDK 構成スニペットにはどれを選べば良い?
webhook には firebase第一世代を使用し 使用しているのは node.js の18の場合は?
の答えは node.js だった。
この場合動かす環境は firebaseを想定しているか?
それともm1macbookAir で node.js を想定しているのか?

答えは

この場合、**動かす環境は Firebase を想定**しています。
具体的には、Node.js 18 を使って Firebase Gen 1 環境上で webhook が動作する構成です。
したがって、Node.js 用の Firebase Admin SDK 構成スニペットを選択することになります。

この Node.js のスニペットは、Firebase サーバー側で
webhook として動作する Node.js 環境向けに設定されており、
M1 MacBook Air 上ではなく、
**Firebase のサーバー上で稼働する Node.js のコードに適用**されるものです。

一方で、**Python は MacBook Air 上で動作し、
Firestore からメッセージを取得するためのコード**として使われます。
そのため、Python の Firebase Admin SDK を MacBook Air 上で利用して
Firestore にアクセスする構成になります。

となった

このため再度Admin SDK 構成スニペットをpythonで取得する

import firebase_admin
from firebase_admin import credentials

cred = credentials.Certificate("path/to/serviceAccountKey.json")
firebase_admin.initialize_app(cred)

ダウンロードできたら
でファイルをコピーする

なおこの構成スニペットで表示されているコードは
Python 用の Firebase Admin SDK を使用して、Firebase プロジェクトに接続するための初期設定を行うもの

このスクリプトによって、Python 環境から Firebase にアクセスできるようになる

serviceAccountKey.json
へファイル名を変更

これで

vim access.python

として

import firebase_admin
from firebase_admin import credentials

cred = credentials.Certificate("serviceAccountKey.json")
firebase_admin.initialize_app(cred)

として保存

実行したら

python access.json                
Traceback (most recent call last):
  File "/Users/snowpool/aw10s/linebot/access.json", line 1, in <module>
    import firebase_admin
ModuleNotFoundError: No module named 'firebase_admin'

となった

pip install firebase-admin

でインストール

今度は実行しても何も表示されない

接続が正常に行われているか確認するために、
Firestore のデータベースからデータを取得したり、簡単なメッセージを表示させる
以下のようにして Firestore からデータを取得

import firebase_admin
from firebase_admin import credentials, firestore

# サービスアカウント JSON ファイルのパスに置き換える
cred = credentials.Certificate("path/to/serviceAccountKey.json")
firebase_admin.initialize_app(cred)

# Firestore クライアントの取得
db = firestore.client()

# Firestore の 'messages' コレクションからデータを取得
def get_messages():
    messages_ref = db.collection("messages")
    docs = messages_ref.stream()
    
    for doc in docs:
        print(f"{doc.id} => {doc.to_dict()}")

# 動作確認のためにメッセージを取得
get_messages()

これをcheck.py
として実行たがエラー

原因は
Cloud Firestore API がプロジェクト「voicelinebot」で有効になっていないために発生しています。
Firestore API を有効にするがエラー

プロジェクトに Cloud Firestore データベースがまだ存在しないために発生しています。
Firestore データベースを初期化する必要があります

とのこと

カレンダー読み上げのReadMe作成

カレンダー読み上げのReadMe作成

コードはコピーしたので
ReadMe の原案を書いておく

M1 MacbookAir 16GB で動作しています

Gmail と Google Calendar を操作するためAPIとtoken.jsonが必要になります
https://developers.google.com/gmail/api/quickstart/python?hl=ja
を参考にAPIを使用可能にし、token.jsonを取得し同一ディレクトリに設置してください

使用にあたり pip install -r requirements.txt
を実行後

GoogleDrive のフォルダIDが必要になるので
create_folder.py で作業フォルダの作成とIDの取得を行います
add_gdrive_calendar.py
でIDを設定してください

label_gmail.py
でGmail ラベル一覧の取得ができます

add_gmail_calendar.py
で取得するGmailのラベルを設定します

Ollamaでelyza:jp8b’を使用します
arch -arm64 brew install git-lfs
git lfs install
git clone https://huggingface.co/elyza/Llama-3-ELYZA-JP-8B-GGUF.git
でダウンロード

vim Modelfile
でファイルを作成

中身を
FROM ./Llama-3-ELYZA-JP-8B-q4_k_m.gguf
TEMPLATE “””{{ if .System }}<|start_header_id|>system<|end_header_id|>

{{ .System }}<|eot_id|>{{ end }}{{ if .Prompt }}<|start_header_id|>user<|end_header_id|>

{{ .Prompt }}<|eot_id|>{{ end }}<|start_header_id|>assistant<|end_header_id|>

{{ .Response }}<|eot_id|>“””
PARAMETER stop “<|start_header_id|>”
PARAMETER stop “<|end_header_id|>”
PARAMETER stop “<|eot_id|>”
PARAMETER stop “<|reserved_special_token" として保存 ollama create elyza:jp8b -f Modelfile を実行し Successとなったら ollama run elyza:jp8b で実行します これで Ollamaでelyza:jp8bが動作します calendar_utils.py でDocker VoicevoxマシンのURLを指定していますので 環境に応じて変更してください 音声の作成に voicevox の docker が必要になります
docker pull voicevox/voicevox_engine:cpu-ubuntu20.04-latest
で取得しています 動作させるには バックグランドでの起動で -d オプションをつけて
docker run -d -p '192.168.1.69:50021:50021' voicevox/voicevox_engine:cpu-ubuntu20.04-latest
というように起動させます
IPアドレス部分はご自身のマシンのIPに変えてください

この文章をMarkdownで書き換える

# webcom_face_gcalendar
Perform face identification with OpenCV When the face of the registered person is recognized Get this week's schedule using Google calendar API Generate and read audio with Docker's voicevox


## 動作環境
M1 MacbookAir 16GB で動作しています

Gmail と Google Calendar を操作するためAPIとtoken.jsonが必要になります  
https://developers.google.com/gmail/api/quickstart/python?hl=ja  
を参考にAPIを使用可能にし、token.jsonを取得し同一ディレクトリに設置してください


使用にあたり 
`pip install -r requirements.txt`
を実行後

GoogleDrive のフォルダIDが必要になるので
create_folder.py で作業フォルダの作成とIDの取得を行います
add_gdrive_calendar.py
でIDを設定してください

label_gmail.py
でGmail ラベル一覧の取得ができます

add_gmail_calendar.py
で取得するGmailのラベルを設定します

Ollamaでelyza:jp8b’を使用します
```
arch -arm64 brew install git-lfs  
git lfs install  
git clone https://huggingface.co/elyza/Llama-3-ELYZA-JP-8B-GGUF.git
```
でダウンロード

`vim Modelfile`
でファイルを作成

中身を
```
FROM ./Llama-3-ELYZA-JP-8B-q4_k_m.gguf
TEMPLATE """{{ if .System }}<|start_header_id|>system<|end_header_id|>

{{ .System }}<|eot_id|>{{ end }}{{ if .Prompt }}<|start_header_id|>user<|end_header_id|>

{{ .Prompt }}<|eot_id|>{{ end }}<|start_header_id|>assistant<|end_header_id|>

{{ .Response }}<|eot_id|>"""
PARAMETER stop "<|start_header_id|>"
PARAMETER stop "<|end_header_id|>"
PARAMETER stop "<|eot_id|>"
PARAMETER stop "<|reserved_special_token"
```
として保存  
` ollama create elyza:jp8b -f Modelfile`
を実行し   
Successとなったら  
`ollama run elyza:jp8b`
で実行します  

これで Ollamaでelyza:jp8bが動作します  

calendar_utils.py  
でDocker VoicevoxマシンのURLを指定していますので  
環境に応じて変更してください  

音声の作成に voicevox の docker が必要になります  
`docker pull voicevox/voicevox_engine:cpu-ubuntu20.04-latest`
  
で取得しています  

動作させるには  
バックグランドでの起動で -d オプションをつけて  

`docker run -d -p '192.168.1.69:50021:50021' voicevox/voicevox_engine:cpu-ubuntu20.04-latest`  

というように起動させます
IPアドレス部分はご自身のマシンのIPに変えてください

とした

空白2つで改行
`で囲むとコード表示

複数行なら
“`
で囲む

URLはそのままでOK

タイトルは##の後に表示

カレンダー読み上げのコード公開

カレンダー読み上げのコード公開

webcom_face_gcalendar
というリポジトリを作成する

Add a README file
にチェックを入れる

Description (optional)
はせっかくなので英語で解説を書いておく

OpenCVで顔の識別を行い
登録した人物の顔を認識したら
Google calendar API で今週の予定を取得し
Docker の voicevox で音声を生成し読み上げします

これを翻訳すると
Perform face identification with OpenCV
When the face of the registered person is recognized
Get this week’s schedule using Google calendar API
Generate and read audio with Docker’s voicevox

これをDescriptionに書いておく

Add .gitignore
はそのまま
.gitignore template: None
のままにしておく

ライセンスはMITにしておく

これでリポジトリを作成

次に

git clone git@github.com:Snowpooll/webcom_face_gcalendar.git

でリポジトリコピー

この時にSSHを使っているので
パスフレーズを入力する

cd webcom_face_gcalendar 

で移動し
ここへコードをコピーしていく

スマホからの写真を抽出できるようにするスクリプト

cp ../week_calendar_voice/resize_save.py .

ファイルサイズを調べるスクリプト

cp ../week_calendar_voice/file_info.py .

写真から顔のみ抽出するスクリプト

cp ../week_calendar_voice/generate_aligned_faces.py . 

切り出した顔のjpgファイルを読み顔の特徴量に変換するスクリプト

cp ../week_calendar_voice/generate_feature_dictionary.py . 

顔の得微量が近い人を検出するにはモデル

cp ../week_calendar_voice/face_recognizer_fast.onnx . 
cp ../week_calendar_voice/face_detection_yunet_2023mar.onnx . 

顔を期別した時にカレンダーを読み上げるメインスクリプト

cp ../week_calendar_voice/webcam_face_calendar.py . 

今週の残りの予定を取得し、音声ファイルを生成する

cp ../week_calendar_voice/calendar_audio_utils.py . 

Google Calendar API で予定を取得する

cp ../week_calendar_voice/calendar_utils.py . 

お知らせの音声

cp ../week_calendar_voice/notice.wav .

メンテの手間を省くため
Google calendar へ予定を追加するコードも別フォルダに入れておく

mkdir add_calendar
cd add_calendar 

こちらへGoogleカレンダー予定追加のためのスクリプトを保存する

未読GmailからGoogle calendar に追加するスクリプト

cp ../../week_calendar_voice/main6.py .
mv main6.py add_gmail_calendar.py 

GoogleDrive からGoogle calendar に追加するスクリプト

cp ../../week_calendar_voice/main4.py .
mv main4.py add_gdrive_calendar.py

和暦を西暦に変換するスクリプト

cp ../../week_calendar_voice/event_utils.py .

Google CalendarAPI で予定を追加するスクリプト

cp ../../week_calendar_voice/google_calendar_module.py .

Ollamaを使用してテキストから日時とイベントを抽出

cp ../../week_calendar_voice/ollama_module.py .

Gmailから本文を抽出する

cp ../../week_calendar_voice/gmail_utils.py .

Gmailのラベルを調べるスクリプト

cp ../../week_calendar_voice/label_gmail.py .

GoogleDrive の指定フォルダからPDFを取得し本文を抽出

cp ../../week_calendar_voice/drive_pdf_extractor.py .

GoogleDrive にフォルダを作成し、そこからPDFファイルを取得し内容を抽出

 cp ../../week_calendar_voice/g_drive/create_folder.py .

次に

touch requirements.txt

でインポートするライブラリ関連を書いておく

numpy
opencv-python
pytz
requests
google-auth
google-auth-oauthlib
google-auth-httplib2
google-api-python-client
playsound
pdfminer.six
python-dateutil
git add .
git commit -m "add code"

Gitignore を忘れていたので

vim .gitignore

内容を

token.json
credentials.json

を追加

再度 リポジトリ登録する

git add .
git commit -m "add gitignore”

あとは

git push -u origin main

で送信

OpenCVの顔の識別機能とカレンダー読み上げの組み合わせ

OpenCVの顔の識別機能とカレンダー読み上げの組み合わせ

まず顔の識別で自分の顔だった時に動作するように
カレンダー読み上げ機能をモジュールにする

from calendar_utils import authenticate, get_upcoming_events, synthesize_speech, format_date_with_weekday
from playsound import playsound

def main():
    creds = authenticate()
    audio_files = []  # 音声ファイルのリスト
    if creds:
        events = get_upcoming_events(creds)
        if not events:
            print('今週の残りの予定はありません。')
            # 音声ファイルは再生しない
        else:
            print('今週の残りの予定:')
            for event in events:
                start = event['start'].get('dateTime', event['start'].get('date'))
                summary = event.get('summary', '(タイトルなし)')
                formatted_date = format_date_with_weekday(start)
                event_text = f"{formatted_date} - {summary}"
                print(event_text)
                filename = synthesize_speech(event_text)
                if filename:
                    audio_files.append(filename)  # 生成されたファイル名をリストに追加

        # 音声ファイルが存在する場合のみ notice.wav と各予定の音声を再生
        if audio_files:
            # notice.wavを最初に再生
            print("再生中: notice.wav")
            playsound("notice.wav")
            
            # 各予定の音声ファイルを再生
            for audio_file in audio_files:
                print(f"再生中: {audio_file}")
                playsound(audio_file)

if __name__ == '__main__':
    main()

をモジュールにして他のプログラムから呼び出せるようにしたい

touch calendar_audio_utils.py

でファイルを作成

from calendar_utils import authenticate, get_upcoming_events, synthesize_speech, format_date_with_weekday
from playsound import playsound

def get_weekly_schedule_with_audio(play_audio=False):
    """
    今週の残りの予定を取得し、音声ファイルを生成する関数。
    
    :param play_audio: 予定を音声で再生するかどうか(デフォルトは再生しない)
    :return: 今週の予定をテキスト形式で返すリスト
    """
    creds = authenticate()
    audio_files = []  # 音声ファイルのリスト
    event_texts = []  # 予定のテキストリスト

    if creds:
        events = get_upcoming_events(creds)
        if not events:
            print('今週の残りの予定はありません。')
        else:
            print('今週の残りの予定:')
            for event in events:
                start = event['start'].get('dateTime', event['start'].get('date'))
                summary = event.get('summary', '(タイトルなし)')
                formatted_date = format_date_with_weekday(start)
                event_text = f"{formatted_date} - {summary}"
                event_texts.append(event_text)  # テキストをリストに追加
                print(event_text)

                # 音声ファイルを生成
                filename = synthesize_speech(event_text)
                if filename:
                    audio_files.append(filename)  # 生成されたファイル名をリストに追加

        # 音声を再生するオプションがTrueの場合にのみ、音声ファイルを再生
        if play_audio and audio_files:
            # notice.wavを最初に再生
            print("再生中: notice.wav")
            playsound("notice.wav")
            
            # 各予定の音声ファイルを再生
            for audio_file in audio_files:
                print(f"再生中: {audio_file}")
                playsound(audio_file)

    return event_texts

として保存

念のため動作するかチェック

vim testvoice.py  

でファイル作成

from calendar_audio_utils import get_weekly_schedule_with_audio

# 音声再生なしで予定を取得
schedule = get_weekly_schedule_with_audio(play_audio=False)
print(schedule)

# 音声再生ありで予定を取得
schedule = get_weekly_schedule_with_audio(play_audio=True)

保存したら

python testvoice.py

で実行

これで動作するのが確認できたので
次に顔の識別

以前使ったものを再利用する

Pixcel8で撮影したスマホの写真で顔データを作る場合には
元画像の1/4にする必要があるため
変換のため
resize_save.py
を作成したのでこれを使う

コードは

import cv2
import os
import argparse

def main():
    # コマンドライン引数を解析するパーサーを作成
    parser = argparse.ArgumentParser(description="Resize and save an image")
    parser.add_argument("image_path", help="Path to the image file")
    args = parser.parse_args()

    # 画像を読み込む
    image = cv2.imread(args.image_path)
    if image is None:
        print("画像が読み込めませんでした。")
        return

    # 画像の元の高さ、幅を取得
    height, width = image.shape[:2]

    # 新しい寸法を計算(元のサイズの1/4)
    new_width = width // 4
    new_height = height // 4

    # 画像をリサイズ
    resized_image = cv2.resize(image, (new_width, new_height))

    # 新しいファイル名を設定
    new_file_path = os.path.splitext(args.image_path)[0] + "_quarter.jpg"

    # リサイズした画像を保存
    cv2.imwrite(new_file_path, resized_image)
    print(f"リサイズされた画像が保存されました: {new_file_path}")

if __name__ == '__main__':
    main()

使用する時にはターミナルでコマンドで実行する

python resize_save.py PXL_20240612_091410912.jpg      

というようにファイルを指定すれば
実行後1/4サイズにした画像が作成される

ファイルサイズを調べるスクリプトも作成

import cv2
import os
import argparse

def main():
    # コマンドライン引数を解析するパーサーを作成
    parser = argparse.ArgumentParser(description="Display image properties")
    parser.add_argument("image_path", help="Path to the image file")
    args = parser.parse_args()

    # 画像を読み込む
    image = cv2.imread(args.image_path)
    if image is None:
        print("画像が読み込めませんでした。")
        return

    # 画像の高さ、幅、チャンネル数を取得
    height, width, channels = image.shape
    print(f"画像の幅: {width} ピクセル")
    print(f"画像の高さ: {height} ピクセル")
    print(f"色チャネル数: {channels}")

    # ファイルサイズを取得
    file_size = os.path.getsize(args.image_path)
    print(f"ファイルサイズ: {file_size} バイト")

if __name__ == '__main__':
    main()

これを

python file_info.py PXL_20240612_091410912_resized_resized.jpg 

というように実行すればサイズが表示される

画像の幅: 684 ピクセル
画像の高さ: 912 ピクセル
色チャネル数: 3
ファイルサイズ: 228769 バイト

この2つは自分以外の写真から登録画像を作るのに使うので

cp ../face_recog/file_info.py .
cp ../face_recog/resize_save.py .

でコピーしておく

次に
入力した写真から人の顔の部分を切り出して保存するプログラム

generate_aligned_faces.py

に写真のファイルを引数にして実行すれば個人ごとの顔写真ができる

これは
入力した写真から人の顔の部分を切り出して保存するプログラム

複数の人物が写っている場合は全員を切り出して face001.jpg , face002.jpg ・・・ と名前を付けて保存する
出力されたファイル名を 人の名前に変更しておくと後々便利です。 
face001.jpg → taro.jpg

python generate_aligned_faces.py image.jpg

とすれば
写真に写っている人の分だけファイルができる
そのファイル名を人の名前に変更する

つまり全て
face001.jpg
という感じで
Face00x.jpg
となっているので
写真ごとに名前を変える

これもコピーしておく

cp ../face_recog/generate_aligned_faces.py .

次に

 generate_feature_dictionary.py


切り出した顔のjpgファイルを読み込んで、顔の特徴量に変換する

例えば 顔写真 taro.jpg を入力すると 顔の特徴量 taro.npy が出力される
このnumpyファイルに各個人の顔の特徴量が128次元ベクトルに変換されて入っている

python generate_feature_dictionary.py face001.jpg
python generate_feature_dictionary.py face002.jpg

つまり
写真の人の分だけ実行すればOK

これもコピーしておく

 cp ../face_recog/generate_feature_dictionary.py .

次に顔の得微量が近い人を検出するにはモデルが必要なのでコピー

cp ../face_recog/face_recognizer_fast.onnx .
cp ../face_recog/face_detection_yunet_2023mar.onnx .

そして作成した自分の顔の得微量ファイルもコピーしておく

cp ../face_recog/satoru.* .

Webカメラから映った時に顔の識別をするので

cp ../face_recog/webcam_face_recognizer.py .

でコピー

念の為動作確認

python webcam_face_recognizer.py

で自分の顔を識別しているのがわかる

次にこの中で
顔認識した時に

from calendar_module import get_weekly_schedule_with_audio # 音声再生なしで予定を取得 
schedule = get_weekly_schedule_with_audio(play_audio=False) print(schedule) # 音声再生ありで予定を取得 schedule = get_weekly_schedule_with_audio(play_audio=True) 

を実行

また
毎回読み上げでは負荷がかかるため、次の呼び出しは12時間後になるように設定

顔を識別できたときに特定の関数を呼び出し、
呼び出しが12時間に1回のみになるように制限するためには、識別が成功した時間を記録し、
次に呼び出すタイミングを管理する

1. call_function_when_recognized: この関数が顔認識時に呼び出され、最後の呼び出し時間を記録します。
次に呼び出すまでの間隔が12時間経過していない場合は、新たに処理を実行しないようにしています。
2. THROTTLE_TIME: 12時間を秒単位(12 * 60 * 60)で設定しています。
3. last_called_time: この変数は最後に呼び出された時間を記録し、
次の呼び出しが12時間以内であれば、新たな処理をスキップします。

.wavファイルを毎回生成していると容量を圧迫するため

from calendar_audio_utils import get_weekly_schedule_with_audio

# 音声再生なしで予定を取得
schedule = get_weekly_schedule_with_audio(play_audio=False)
print(schedule)

# 音声再生ありで予定を取得
schedule = get_weekly_schedule_with_audio(play_audio=True)

の処理の後に notice.wavファイル以外の .wavファイルをすべて削除する

touch webcam_face_calendar.py

でファイルを作成

import os
import glob
import numpy as np
import cv2
import time
from calendar_module import get_weekly_schedule_with_audio  # カレンダーから予定を取得するためのインポート

COSINE_THRESHOLD = 0.363
NORML2_THRESHOLD = 1.128

# 12時間(秒単位)
THROTTLE_TIME = 12 * 60 * 60
last_called_time = 0  # 最後に呼び出した時間を初期化

def match(recognizer, feature1, dictionary):
    for element in dictionary:
        user_id, feature2 = element
        score = recognizer.match(feature1, feature2, cv2.FaceRecognizerSF_FR_COSINE)
        if score > COSINE_THRESHOLD:
            return True, (user_id, score)
    return False, ("", 0.0)

def call_function_when_recognized(user_id):
    global last_called_time
    current_time = time.time()
    
    # 最後に呼び出してから12時間経過しているかを確認
    if current_time - last_called_time >= THROTTLE_TIME:
        print(f"認識されました: {user_id}")
        
        # 予定を音声再生なしで取得
        schedule = get_weekly_schedule_with_audio(play_audio=False)
        print("予定:", schedule)
        
        # 予定を音声再生ありで取得
        schedule = get_weekly_schedule_with_audio(play_audio=True)
        print("音声で再生される予定:", schedule)
        
        # notice.wavファイル以外の.wavファイルを削除
        cleanup_audio_files(exclude_file="notice.wav")
        
        # 最後に呼び出した時間を更新
        last_called_time = current_time
    else:
        print("まだ12時間経過していないため、次の呼び出しは行われません。")

def cleanup_audio_files(exclude_file):
    """指定された.wavファイル以外の.wavファイルを削除する関数"""
    directory = os.getcwd()  # 現在のディレクトリを取得
    wav_files = glob.glob(os.path.join(directory, "*.wav"))  # すべての.wavファイルを取得

    for wav_file in wav_files:
        if os.path.basename(wav_file) != exclude_file:
            try:
                os.remove(wav_file)  # 指定されたファイル以外を削除
                print(f"削除しました: {wav_file}")
            except OSError as e:
                print(f"ファイル削除エラー: {wav_file}, {e}")

def main():
    directory = os.path.dirname(__file__)
    capture = cv2.VideoCapture(0)  # Use the default camera

    if not capture.isOpened():
        print("Error: The webcam could not be opened.")
        return

    dictionary = []
    files = glob.glob(os.path.join(directory, "*.npy"))
    for file in files:
        feature = np.load(file)
        user_id = os.path.splitext(os.path.basename(file))[0]
        dictionary.append((user_id, feature))

    weights = os.path.join(directory, "face_detection_yunet_2023mar.onnx")
    face_detector = cv2.FaceDetectorYN_create(weights, "", (0, 0))
    weights = os.path.join(directory, "face_recognizer_fast.onnx")
    face_recognizer = cv2.FaceRecognizerSF_create(weights, "")

    while True:
        result, image = capture.read()
        if not result:
            print("Error: No image from webcam.")
            break

        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)  # Ensure image is in RGB

        height, width, _ = image.shape
        face_detector.setInputSize((width, height))

        result, faces = face_detector.detect(image)
        faces = faces if faces is not None else []

        for face in faces:
            aligned_face = face_recognizer.alignCrop(image, face)
            feature = face_recognizer.feature(aligned_face)

            result, user = match(face_recognizer, feature, dictionary)

            box = list(map(int, face[:4]))
            color = (0, 255, 0) if result else (0, 0, 255)
            thickness = 2
            cv2.rectangle(image, box, color, thickness, cv2.LINE_AA)

            id, score = user if result else ("unknown", 0.0)
            text = "{} ({:.2f})".format(id, score)
            position = (box[0], box[1] - 10)
            font = cv2.FONT_HERSHEY_SIMPLEX
            scale = 0.6
            cv2.putText(image, text, position, font, scale, color, thickness, cv2.LINE_AA)

            if result and id != "unknown":
                call_function_when_recognized(id)  # 顔が認識された時にカレンダーの予定取得を実行

        # 画像を表示する前にRGBからBGRに再変換
        cv2.imshow("face recognition", cv2.cvtColor(image, cv2.COLOR_RGB2BGR))

        key = cv2.waitKey(1)
        if key == ord('q'):
            break

    capture.release()
    cv2.destroyAllWindows()

if __name__ == '__main__':
    main()

で実行

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/week_calendar_voice/webcam_face_calendar.py", line 6, in <module>
    from calendar_module import get_weekly_schedule_with_audio  # カレンダーから予定を取得するためのインポート
ImportError: cannot import name 'get_weekly_schedule_with_audio' from 'calendar_module' (/Users/snowpool/aw10s/week_calendar_voice/calendar_module.py)

となる

これはChatGPTで作成した時のモジュールのエラー
結構あることでライブラリのインポートを間違えたり削除下入りしている

from calendar_module import get_weekly_schedule_with_audio  # カレンダーから予定を取得するためのインポート

に変更すれば解決

起動はしたけど、このままだとOpenCVで画面描画するので
これは不要なので非表示にする
これをしないとリモート環境などで動作しない

v2.imshow()やキーボードの操作に関する部分を削除し、
無限ループで顔認識を行うコードに修正

cv2.VideoCapture の映像確認が不要な場合は、その部分を省略しても動作する

ということで
画面表示とキー入力待機を削除

import os
import glob
import numpy as np
import cv2
import time
from calendar_audio_utils import get_weekly_schedule_with_audio  # カレンダーから予定を取得するためのインポート

COSINE_THRESHOLD = 0.363
NORML2_THRESHOLD = 1.128

# 12時間(秒単位)
THROTTLE_TIME = 12 * 60 * 60
last_called_time = 0  # 最後に呼び出した時間を初期化

def match(recognizer, feature1, dictionary):
    for element in dictionary:
        user_id, feature2 = element
        score = recognizer.match(feature1, feature2, cv2.FaceRecognizerSF_FR_COSINE)
        if score > COSINE_THRESHOLD:
            return True, (user_id, score)
    return False, ("", 0.0)

def call_function_when_recognized(user_id):
    global last_called_time
    current_time = time.time()
    
    # 最後に呼び出してから12時間経過しているかを確認
    if current_time - last_called_time >= THROTTLE_TIME:
        print(f"認識されました: {user_id}")
        
        # 予定を音声再生なしで取得
        schedule = get_weekly_schedule_with_audio(play_audio=False)
        print("予定:", schedule)
        
        # 予定を音声再生ありで取得
        schedule = get_weekly_schedule_with_audio(play_audio=True)
        print("音声で再生される予定:", schedule)
        
        # notice.wavファイル以外の.wavファイルを削除
        cleanup_audio_files(exclude_file="notice.wav")
        
        # 最後に呼び出した時間を更新
        last_called_time = current_time
    else:
        print("まだ12時間経過していないため、次の呼び出しは行われません。")

def cleanup_audio_files(exclude_file):
    """指定された.wavファイル以外の.wavファイルを削除する関数"""
    directory = os.getcwd()  # 現在のディレクトリを取得
    wav_files = glob.glob(os.path.join(directory, "*.wav"))  # すべての.wavファイルを取得

    for wav_file in wav_files:
        if os.path.basename(wav_file) != exclude_file:
            try:
                os.remove(wav_file)  # 指定されたファイル以外を削除
                print(f"削除しました: {wav_file}")
            except OSError as e:
                print(f"ファイル削除エラー: {wav_file}, {e}")

def main():
    directory = os.path.dirname(__file__)
    capture = cv2.VideoCapture(0)  # Use the default camera

    if not capture.isOpened():
        print("Error: The webcam could not be opened.")
        return

    dictionary = []
    files = glob.glob(os.path.join(directory, "*.npy"))
    for file in files:
        feature = np.load(file)
        user_id = os.path.splitext(os.path.basename(file))[0]
        dictionary.append((user_id, feature))

    weights = os.path.join(directory, "face_detection_yunet_2023mar.onnx")
    face_detector = cv2.FaceDetectorYN_create(weights, "", (0, 0))
    weights = os.path.join(directory, "face_recognizer_fast.onnx")
    face_recognizer = cv2.FaceRecognizerSF_create(weights, "")

    while True:
        result, image = capture.read()
        if not result:
            print("Error: No image from webcam.")
            break

        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)  # Ensure image is in RGB

        height, width, _ = image.shape
        face_detector.setInputSize((width, height))

        result, faces = face_detector.detect(image)
        faces = faces if faces is not None else []

        for face in faces:
            aligned_face = face_recognizer.alignCrop(image, face)
            feature = face_recognizer.feature(aligned_face)

            result, user = match(face_recognizer, feature, dictionary)

            if result and user[0] != "unknown":
                call_function_when_recognized(user[0])  # 顔が認識された時にカレンダーの予定取得を実行

        # 適当な待機時間を設けてリソースの使用を抑える
        time.sleep(1)

    capture.release()

if __name__ == '__main__':
    main()

というコードに変更

これでwebカメラの画面描画はなくなり
停止手段は ctrl + c で停止となる

実際に動かしたけど
M1macbookAir 16GB で
顔認識してからGoogle カレンダーを読み込み
Voicevox で音声ファイルを生成し、予定を読み上げるまで一分かかる

Docker ではなくインストールタイプにしたり
マシンスペックを上げれば短縮できるかもしれない