Firestore データベースの作成

Firestore データベースの作成

Google Cloud Console
へアクセス
「Firestore の開始」または「データベースの作成」ボタンをクリック

Firestore モード」を選択
この時
ネイティブ モード
Datastore モード
の2つがある

APIを使うということで
ネイティブモードを選択

このデータベースには無料枠の割り当てがあります。
各プロジェクトには「(default)」という名前の単一のデータベースがあり、
無料枠割り当ての対象になります。
この割り当てを使い切ると、オペレーション、ストレージ、ネットワークの使用量に基づいて課金されます¹。
ロケーションは料金に影響します

とのこと

(default)
がデータベースIDになっている

次にロケーション

ロケーション タイプには

リージョン
99.99% の可用性 SLA、単一リージョン内で比較的低いレイテンシ

マルチリージョン
99.999% の可用性 SLA、最大の領域にわたり最も高い可用性

コストの違いをGPTで調べたらマルチにすると倍になるらしいので
リージョンを選択する

次にリージョンの設定をする

Firebase のセットアップ画面でリージョンを選ぶ際に、
「asia-northeast1(東京)」を選択すると、日本国内向けに最適なパフォーマンスが得られるらしい

選択したリージョンがユーザーに近いほど、レイテンシが低くなります

とあるので調べた

レイテンシ(Latency) は、ユーザーがデータのリクエストを送ってから、
そのデータが返ってくるまでの遅延時間のことを指します。

具体的には、アプリケーションやサービスに対してユーザーが操作した際に、
その応答が表示されるまでの時間差です。

レイテンシは「応答時間」とも呼ばれることがあり、通常ミリ秒(ms)単位で測定されます。

レイテンシの例
たとえば、LINEbot が Firestore に保存されたメッセージを取得してユーザーに表示する場合、
レイテンシが低いと、メッセージの取得がスムーズで、
ユーザーは素早く応答を受け取ることができます。
しかし、レイテンシが高いと、データの取得や表示が遅れてしまい、ユーザーにとって待ち時間が長く感じられます。

レイテンシが重要な理由
* ユーザー体験の向上: レイテンシが低いほど、アプリの操作がスムーズで、ユーザーは快適に利用できます。
* リアルタイムアプリに重要: リアルタイムでの更新が必要なアプリ
(例: チャットアプリやオンラインゲーム)では、レイテンシが低いことが特に重要です。

どのようにレイテンシを低くするか?
* 近くのリージョンを選択することで、データのリクエストが物理的に近い場所で処理されるため、
レイテンシが低くなります。
* ネットワーク最適化やキャッシュの活用などもレイテンシの低減に役立ちます。
例えば、日本のユーザーが多い場合は、データベースのリージョンを asia-northeast1(東京)にすると、
データの取得時間が短くなり、レイテンシが低くなります。

次に暗号化

「Google が管理する暗号鍵」を選ぶ理由
1. 管理がシンプル: 鍵の生成・ローテーション・バックアップなどがすべて
Google によって自動的に管理されるため、暗号鍵の管理負担が軽減されます。

2. 追加コストなし: Google 管理の暗号鍵はデフォルトで提供されており、追加料金がかかりません。

3. 標準的なセキュリティ: 多くのプロジェクトでは、Google が管理する暗号化で十分なセキュリティが提供されます。

なので
デフォルトのままでOK

次に
セキュリティ ルール
セキュリティ ルールはアクセス制御とデータ検証を提供します。
2 つのプリセット ルールのいずれかから始めて、後でカスタマイズしてください。

本番環境ルール
データはデフォルトで限定公開になります。
モバイルおよびウェブ クライアント ライブラリ アクセスは、
セキュリティ ルールで指定された内容に従ってのみ許可されます。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if false;
    }
  }
}


rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if false;
    }
  }
}

サードパーティによる読み取りと書き取りはすべて拒否されます

テストルール
データはデフォルトでオープン状態になります。
ただし、長期にわたりモバイルおよびウェブ クライアント ライブラリ アクセスを有効にするには、
30 日以内にセキュリティ ルールを更新する必要があります。

これについては本番環境を選択

なお
firebase第1世代だけど rules_version = ‘2’; でOKか気になったので調べる

chatGPTの回答は
**Firebase 第1世代でも `rules_version = ‘2’;` を使用して問題ありません**。

`rules_version = ‘2’` は、最新のセキュリティルール構文で、
Firestore の第1世代と第2世代の両方で利用可能です。
このバージョンでは、より細かいアクセス制御やデータ検証の機能が提供されており、セキュリティルールの記述がシンプルかつ強力になります。

そのため、第1世代の Firebase プロジェクトでも `rules_version = ‘2’;` を指定してルールを作成することを推奨します。

って答えだけどGPTなのでとりあえず試すことにする
全面的な信頼はせず自分で検証も重要
たまに存在しないバージョン指定とかもするので

とりあえずこのルールでDBを作成する

Ollama index

Ollama index チュートリアルを参考に

https://llamahub.ai/

https://docs.llamaindex.ai/en/stable/getting_started/starter_example_local/
のチュートリアルを参考に
MacBook Air で実験

Custom install の
https://docs.llamaindex.ai/en/stable/getting_started/installation/
を参考に

pip install llama-index

If you aren’t using OpenAI, or want a more selective installation, you can install individual packages as needed.
For example, for a local setup with Ollama and HuggingFace embeddings, the installation might look like:

もしOpenAIを使用しない場合や、特定のパッケージのみを選んでインストールしたい場合、必要なパッケージだけを個別にインストールすることができます。
例えば、ローカル環境でOllamaとHuggingFaceの埋め込み機能を使用したセットアップを行う場合、以下のコマンドでインストール

pip install llama-index-core llama-index-readers-file llama-index-llms-ollama llama-index-embeddings-huggingface

これでインストールはOK

次にollama index

pip install llama-index-llms-ollama

でインストール

Ollamaを使ってLlamaIndexでRAGを構築する【Windows 11 + WSL 2 + JupyterLab】
を参考に
Ollama index を使えるようにする

ELYZA-japanese-Llama-2-7b で LlamaIndex を 試す
も参考に

PDFからの情報抽出とクエリ応答を試す

厚生労働省が公開しているモデル就業規則を使う

mkdir -p data/10k/ 
wget https://www.mhlw.go.jp/content/001018385.pdf -O data/10k/001018385.pdf 

これで
ーOオプションをつけて保存先を指定

保存したら
これを
Data/10/フォルダに移動

 touch test.py

でファイルを作成

from llama_index.llms.ollama import Ollama
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.core import SimpleDirectoryReader, VectorStoreIndex
from transformers import AutoTokenizer

# 使用モデルを選択します。
llm = Ollama(model="elyza:jp8b", request_timeout=40.0)

# HuggingFace Embedding モデルの初期化
embed_model = HuggingFaceEmbedding(model_name="all-MiniLM-L6-v2")

# Transformers tokenizerの設定
tokenizer = AutoTokenizer.from_pretrained("sentence-transformers/all-MiniLM-L6-v2", clean_up_tokenization_spaces=False)

# PDFファイルの読み込み
reader = SimpleDirectoryReader(input_files=["./data/001018385.pdf"])
data = reader.load_data()
index = VectorStoreIndex.from_documents(data, embed_model=embed_model)

# クエリエンジンの初期化
query_engine = index.as_query_engine(llm=llm, streaming=True, similarity_top_k=3)

# クエリの実行
response = query_engine.query("就業規則では休みは何日もらえますか?日本語で答えて。")
response.print_response_stream()

# ソースノードの確認
for node in response.source_nodes:
    print(f"Text: {node.node.text[:100]}...")
    print(f"Similarity score: {node.score}")  # 'score'を使用

これを実行すると

/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/transformers/tokenization_utils_base.py:1601: FutureWarning: `clean_up_tokenization_spaces` was not set. It will be set to `True` by default. This behavior will be depracted in transformers v4.45, and will be then set to `False` by default. For more details check this issue: https://github.com/huggingface/transformers/issues/31884
  warnings.warn(

記載されている就業規則より、休息時間の情報が見つかりました。そこには「いかなる場合も、従業員ごとに1日の勤務終了後、次の勤務の開始までに少なくとも、○時間の継続した休息時間を与える」とあります。具体的な日数は記載されていませんが、休息時間が一定の時間保証されていることがわかります。Text: - 65 - 
 ません。しかし、賞与を支給する場合、就業規則に支給対象時期、賞与の算定基準、査
定期間、支払方法等を明確にしておくことが必要です。 
2 就業規則に、賞与の支給対象者を一定の日(例え...
Similarity score: 0.26476020388558674
Text: [例1] インターバル時間と翌日の所定労働時間が重複する部分を働いたものとみなす
場合 
 
(勤務間インターバル) 
第22条  いかなる場合も、従業員ごとに1日の勤務終了後、次の勤務の開始までに少...
Similarity score: 0.2292672424458453
Text: なお、1か月60時間の算定には、法定休日に労働した時間数は含まれませんが、法
定外の休日に行った労働における時間外労働の時間数は含まれます。...
Similarity score: 0.2239520151706951

となる

参考サイトでは

Based on the provided context information, the answer to the query is:

就業規則では休みは3日もらえます。(65)

Just like how the law specifies that an employee is entitled to a maximum of 3 days of rest in a row, it also states that an employee must be given at least 24 hours of continuous rest time after each day of work. However, this rule does not apply in cases of natural disasters or other unavoidable circumstances. (36)-----
Text:	 なお、1か月60時間の算定には、法定休日に労働した時間数は含まれませんが、法 定外の休日に行った労働における時間外労働の時間数は含まれます。 ...
Metadata:	 {'page_label': '55', 'file_name': '001018385.pdf', 'file_path': 'data/10k/001018385.pdf', 'file_type': 'application/pdf', 'file_size': 1058782, 'creation_date': '2024-04-12', 'last_modified_date': '2023-07-07'}
Score:	 0.321
-----
Text:	 - 65 -   ません。しかし、賞与を支給する場合、就業規則に支給対象時期、賞与の算定基準、査 定期間、支払方法等を明確にしておくことが必要です。  2 就業規則に、賞与の支給対象者を一定の日(例えば、6月1日や12月1日、又は賞 与支給日)に在籍した者とする規定を設けることで、期間の途中で退職等し、その日に 在職しない者には支給しないこととすることも可能です。 ...
Metadata:	 {'page_label': '65', 'file_name': '001018385.pdf', 'file_path': 'data/10k/001018385.pdf', 'file_type': 'application/pdf', 'file_size': 1058782, 'creation_date': '2024-04-12', 'last_modified_date': '2023-07-07'}
Score:	 0.306
-----
Text:	 [例1] インターバル時間と翌日の所定労働時間が重複する部分を働いたものとみなす 場合    (勤務間インターバル)  第22条  いかなる場合も、従業員ごとに1日の勤務終了後、次の勤務の開始までに少 なくとも、○時間の継続した休息時間を与える。ただし、災害その他避けることがで きない場合は、この限りではない。 ...
Metadata:	 {'page_label': '36', 'file_name': '001018385.pdf', 'file_path': 'data/10k/001018385.pdf', 'file_type': 'application/pdf', 'file_size': 1058782, 'creation_date': '2024-04-12', 'last_modified_date': '2023-07-07'}
Score:	 0.306

となっているが
PDFを見ても就労規則で休みの項目が見当たらないので
ファイルが更新されているのかもしれない

次に
テキストファイルからの情報抽出とクエリ応答

あらすじ
結束バンド
後藤ひとりは友達を作れない陰キャでいつも一人で過ごしていたが、中学時代にテレビのインタビューを見て、陰キャでもバンドを組んでいれば人気者になれると聞き、ギターの練習を始める。ひとりは毎日コツコツ練習を重ねることでギターの腕前はかなりのものとなっていたが、気がつけば中学は卒業。結局、友達を作るという当初の目標は達成することはできずじまいだった。そして高校に入学したひとりはギター演奏を動画配信し、「ギターヒーロー」としてネットでそこそこの人気を集めていたが、現実では変わらず友達を作れずにいた。ひとりはギターをこれみよがしに持ち、ギタリストだということをアピールするものの効果はなし。黄昏(たそがれ)ていたところ、ギタリストを探していた伊地知虹夏に誘われ、バンド活動をすることとなる。ひとりは虹夏、山田リョウらと共に「結束バンド」として活動を始めるが、友達は欲しいけどコミュ症独りぼっちのひとりは問題行動ばかり起こしていた。結束バンドなのに、結束力ゼロな凸凹バンド活動は幕開けするのだった。そしてそんなひとりに、クラスメイトの喜多郁代が話しかけてきて、彼女が結束バンドの元メンバーだったことを知る。紆余(うよ)曲折の末、郁代はひとりとの交流で結束バンドに戻ることになり、ひとりたちはライブに向けて準備をすることとなる。ひとりはライブチケットのノルマをこなすため、方々を回ってチケットを売ることになるが、人見知りのひとりには難題ですぐに壁にぶち当たってしまう。絶望するひとりは、そこで行き倒れた廣井きくりと遭遇。彼女の計らいで突発的な路上ライブを行なって、観客を魅了する。きくりのお陰でライブチケットのノルマもこなし、結束バンドはライブに臨むのだった。

文化祭ライブ
夏休みに入り、後藤ひとりは知り合いも増えていたが、自分から遊びに誘うことができずに夏休みも終わり、新学期を迎えていた。相も変わらずコミュ症なひとりは文化祭で盛り上がるクラスメイトをよそに、一人黄昏ていた。そんな中、みんなの前で結束バンドのライブをすればクラスで人気者になれるかもと考えたひとりは、喜多郁代の後押しもあり、ライブをすることを決める。文化祭ライブにひとりは大きな不安を抱くが、周囲の励ましと協力によって、ついに文化祭当日を迎える。しかし、ライブ中に機材トラブルが発生し、ひとりはとっさにボトルネック奏法のアドリブを行い、仲間たちはそのフォローに回る。その演奏をみんなから拍手喝采されるひとりだったが、コミュ症のひとりはその晴れ舞台に混乱し、観客席にダイブするという暴挙に出てしまう。ひとまずライブは成功を収めるものの、ひとりの奇行により、結束バンドの名はフリーライターの佐藤愛子の耳に入ることとなる。そして愛子が結束バンドを取材した際に、ひとりが「ギターヒーロー」であることがバレてしまう。愛子はひとりに強い関心を抱くが、結束バンドの演奏に対しては辛らつで、彼女たちを酷評する。そしてひとりたちは、愛子を見返すため、10代限定のロックフェス「未確認ライオット」に参加し、グランプリを目指すことを決める。

デモ審査
未確認ライオットに参加するためには、デモ審査、ウェブ投票、ライブ審査を通過して、最終審査に合格する必要があった。そして結束バンドは、デモ審査のためのMV(ミュージックビデオ)作成を始める。そんな中、結束バンドの面々は、フェスではライバルとなる人気メタルバンド「SIDEROS」に所属する大槻ヨヨコと出会う。ひねくれ者なヨヨコは、結束バンドをライバル視しながらも、彼女たちにアドバイスを送り、ファンたちの助けもあってMVを完成させる。デモ審査の結果が出るのを待つばかりとなり、新たな春を迎える。ひとりたちは無事進級するものの、相も変わらずコミュ症のひとりは奇行が目立っていた。それでも喜多郁代の助けもあり、ひとりは新たにクラスメイトとなった佐々木次子と友人となる。今までのライブ活動が実りはじめ、徐々に活動範囲が広がっていくひとりたちだったが、そこにライブ出演の依頼が舞い込む。初の依頼にワクワクする結束バンドの面々だったが、イベント会場は音楽ジャンルがバラバラのカオス状態。運営サイドがスケジュールを埋めるためだけに、適当にバンドをブッキングされたことが判明する。アウェーな会場に気圧される結束バンドだったが、それぞれが最善を尽くすことでライブは大盛り上がりのうちに終了し、大成功を収める。そして着実に実力をつけた結束バンドのもとに、デモ審査通過の知らせが届くのだった。




メディアミックス
テレビアニメ
2022年テレビアニメ化。2022年10月8日よりTOKYO MX、BS11ほかにて放送。制作はCloverWorks。キャストは、後藤ひとりを青山吉能、伊地知虹夏を鈴代紗弓、山田リョウを水野朔、喜多郁代を長谷川育美が担当している。

劇場版アニメ
テレビアニメの総集編が劇場アニメ化。2024年6月7日に前編『劇場総集編ぼっち・ざ・ろっく! Re:』、8月9日に後編『劇場総集編ぼっち・ざ・ろっく! Re:Re:』が公開。



登場人物・キャラクター
 後藤 ひとり (ごとう ひとり)
秀華高校に通う女子。桃色の髪を無造作に伸ばし、いつもジャージを身につけている。自他共に認める引きこもり一歩手前の「陰キャ」で、承認欲求が人一倍強いにもかかわらず、臆病な性格で人と接するのを極度に苦手としている。そのため、すぐに自分の世界に入って落ち込むという情緒不安定さを見せる。押し入れやダンボールに潜り込む癖があり、「完熟マンゴー仮面」を持ち歩いている。運動も勉強も苦手で、特に勉強はまじめに授業を受けているにもかかわらず、生来の要領の悪さから赤点ギリギリ。自分には何の取り柄もないのを痛感していたため、中学の頃に暗い性格の人間がバンドをやって人気者になったインタビューを読んで、ギターを始める。毎日練習したお陰でギターの腕前はプロ級になったが、結局、その腕前を披露する場に恵まれず、友達を作れないまま中学を卒業。現在は「ギターヒーロー」の名で動画配信している。ギタリストを探していた伊地知虹夏に誘われ、「結束バンド」に加入し、バンド活動を始める。バンド内ではギタリスト兼作詞を担当している。山田リョウに「ぼっち」の愛称を付けられて以降は、その愛称で呼ばれる。ギターの腕前はかなりのものだが、他人と合わせるセッションの経験が皆無なため、バンドではまともに演奏できないでいる。しかし徐々にバンドとして演奏できるようになり、文化祭ライブでは機材トラブルというアクシデントに見舞われながらも、アドリブでボトルネック奏法を行い、ライブを成功に導いた。なお、父親から借りたギターは文化祭で壊れたため、動画配信の広告収入で得たお金を使って、新たなギターを手に入れた。人見知りなため、美容院にもいけず、髪の毛を伸ばし放題で前髪で目をつねに隠している。それに加えて野暮ったいジャージ姿でいるため気づかれていないが、実は同性すら見とれるほどの美少女。黙って着飾っていれば「アイドル事務所に入れる」「ビジュアル担当」と言われるほどのポテンシャルを秘めているが、キメ顔は10秒しか持たず、ふだんの奇行がすべてを台無しにしている。

 伊地知 虹夏 (いじち にじか)
下北沢高校に通う女子。後藤ひとりより1学年上。ライトイエロー色の髪をサイドテールにセットし、派手目のファッションを好んで着ている。幼い頃に母親と死別しており、父親も多忙なためさびしい日々を送っていた。姉の伊地知星歌の影響で、バンド活動にあこがれるようになり、「結束バンド」を結成する。バンドではドラムを担当している。姉がバンドをやめてライブハウスを始めたのも、自分の応援をするためであることに薄々気づいており、バンドで大成功を収めて姉のライブハウスを有名にするのを夢見ている。明るく面倒見のよい性格をしており、初対面の人とも仲よくなれる人当りのよさを持つ。また、生活能力が皆無な山田リョウの面倒を見たり、コミュ障の後藤ひとりにも根気強く付き合ったりしている。ギターヒーローの大ファンで、当初はひとりがギターヒーローであることに気づかなかったが、8月のライブでひとりの演奏する姿を見て、その正体に気づく。のちに佐藤愛子にひとりの正体を暴露されるまでは、ひとりの正体を知る唯一のメンバーだった。ライブでは緊張からライブ後に毎回倒れるひとりの運搬役も担当しているため、知らず間に体力と筋肉が付いてきており、最近は手慣れた手付きでトランクにひとりを詰めて運んでいるが、周囲からは死体を運んでいるように見えるため怖がられている。また、ほかのメンバーに比べて自分が「地味」なことにコンプレックスを抱き、ひそかに筋肉キャラで目立とうと考えている。

 山田 リョウ (やまだ りょう)
下北沢高校に通う女子。後藤ひとりより1学年上。中性的な雰囲気を漂わせた麗人で、青色の髪をボブにセットしている。「結束バンド」のメンバーの一人で、ベースと作曲を担当している。飄々(ひょうひょう)としたマイペースな性格で、「変人」と呼ばれると喜ぶ。趣味は廃墟探索と古着屋巡り。変わり者で一人でいるのが好きだが、後藤ひとりと違って特にコミュ障というわけではない。音楽に関しては独自の価値観を持っており、流行(はや)りに流されるのを嫌い、バンドの個性を重視している。そのため過去に別のバンドに所属していたが、人気が出るにつれて売れ筋の曲ばかりを作るようになったバンドに嫌気が差して脱退し、伊地知虹夏に誘われて結束バンドに加入した経緯がある。バンド経験があるため、演奏技術などは結束バンドの中でも群を抜いて高い。楽器類に小遣いを注ぎ込んでいるためいつも金欠で、食費にすら事欠くことも多い。また計画性も皆無で、新しい楽器を買うため、今の楽器を売り飛ばしたりもしている。ミステリアスな雰囲気を漂わせているため、周囲は勉強ができると思っていたが、実は相当なバカ。高校生にもかかわらず学力は中学生レベルにも及ばず、成績はつねに赤点。ただ天才肌で要領がいいため、高校受験の際は一夜漬けで乗り切った。しかし勉強にのめり込み過ぎると、今度は音楽関係が疎(おろそ)かになるという欠点がある。実家は病院を経営しており、かなりの資産家。両親から溺愛されているが、そんな両親の過保護に反発して、ロックを始めた経緯がある。

 喜多 郁代 (きた いくよ)
秀華高校に通う女子。後藤ひとりとは同学年。ウェーブの入った赤い髪をセミロングに伸ばした美少女で、社交的な性格をしている。いわゆる「陽キャ」で、ひとりとは正反対で要領がよく、友達も多い。一方でなんでも無難にこなせるゆえに特技といったものもなく、自分を平凡な人間と思っているのがコンプレックスとなっている。「郁代」という名前が気に入らず、周囲には名字で呼ぶようにお願いしている。山田リョウを慕っており、彼女に近づくためギターが弾けるとウソをついて、結束バンドに加入した。しかし、土壇場で怖くなって逃げ出しており、そのまま音信不通になっていた。ひとりがギターを弾いている姿を見て、彼女と交流を持ち、ひとりの取り成しで結束バンドに出戻った。バンドではギター兼ボーカルを担当する。実はギターと間違えてベースを買ってしまい、そのせいでいくら練習しても上達しなかったほどの音楽オンチ。歌も上手(うま)いが、カラオケ的な上手さのため歌唱技術そのものはない。しかしひたむきな努力家でもあるため、自らの欠点に気づいてからは、練習を重ねていた。人と接するのが大好きで、自分のSNSはフォロワーが1万5000人以上いる。またその社交性を買われて結束バンドの公式SNSも担当する。喜多郁代の手腕もあって公式SNSは着実に人気を集めているが、美容の話ばっかりしていたせいで、フォロワーからは音楽系アカウント思われず、ライブの告知をした際にはフォロワーから化粧の実演販売をするとカンちがいされていた。2年生ではひとりと違うクラスだったが、3年生ではクラスメイトとなる。ひとりの奇行に頭を悩ませながらも、彼女のフォロー役を担っている。

 廣井 きくり (ひろい きくり)
実力派サイケデリックロックバンド「SICK HACK(シックハック)」に所属する女性。ベース兼ボーカルを担当している。長く伸ばしたあずき色の髪を三つ編みにしている。いつも酔っぱらっている変人で、行き倒れていたところを後藤ひとりに介抱され、彼女と知り合う。新宿ではカリスマ的な人気を誇り、ひとりと路上ライブを行なって、彼女の行く道を指し示した。ひとりの持つポテンシャルに気づいており、彼女が上のレベルに上がってくるのを期待している。実は高校時代はかなり根暗な性格をしており、自分を変えるためバンドを始めたため、ひとりにシンパシーを抱いている。飲酒を始めたのも緊張から逃れるためだったが、現在はそれが行き過ぎて四六時中酔っぱらっている。バンドも人気があり、チケットノルマも余裕で達成し、グッズ販売も好調なためバンド活動だけで生活している。しかしライブ中も酔っぱらって大暴れするため、儲(もう)けは壊した機材の修理費で消えてしまい、いつも生活は困窮している。伊地知星歌は高校時代の先輩で、結束バンドのライブを見て以降はSTARRYに入り浸っている。風呂なしアパートに住んでいるため、STARRYのシャワーを使ったり、タダ飯にありつこうとしているため、星歌からは迷惑がられている。愛用のベースに「スーパーウルトラ酒呑童子EX」と名前を付けて大事にしているが、しょっちゅうどこかに置き忘れている。

 後藤 ふたり (ごとう ふたり)
後藤ひとりの妹。年齢は5歳。姉とは正反対の天真爛漫(らんまん)で無邪気な性格をしている。愛犬のジミヘンと仲がよく、いつもいっしょに遊んでいる。まだ幼いためよくも悪くも素直で、その言動で姉を傷つけたりしている。ナチュラルに姉のことを面倒くさいと思っているが、ギターの腕前は尊敬しており、結束バンドのライブを見た際には姉のことをかっこいいと感じていた。その後、ライブの影響でひとりに教わりながらギターを始めるが、すぐに飽きてしまう。ひとりと後藤ふたりの父によれば、ふたりは家族の中にヒエラルキーを作っており、父親が最下位である。

 清水 イライザ (しみず いらいざ)
バンド「SICK HACK」に所属する女性。ギターを担当している。金髪を長く伸ばした髪型が特徴。イギリス出身で、18歳までイギリスで暮らしていたため、片言の日本語でしゃべる。日本のアニメや漫画が大好きで、「コミマ」に参加するために3年前に来日し、音楽活動を始めた。ギターのテクニックは確かで、その腕前は感情的でありながらロジカルと評されている。ただし、清水イライザとしてはロックバンドよりも、アニソンのコピーバンドをやりたがっている。また、同人作家としても活動している。

 後藤 美智代 (ごとう みちよ)
後藤ひとりの母親。二児の母だが若々しく、言動が明るくノリも軽い。ひとりのことを何かと心配しており、ジャージばかり着ている彼女の服などを買ってきている。ひとりが喜多郁代を家に連れてきた際には大喜びし、謎の行動力を発揮。娘のサポートをするために制服を着て、女子高生になり切って会話に入ろうとした。

 内田 幽々 (うちだ ゆゆ)
メタルバンド「SIDEROS(シデロス)」に所属する少女。ベースを担当している。黒い髪をストレートに伸ばし、口元にホクロがあるのが特徴。

 岩下 志麻 (いわした しま)
バンド「SICK HACK」に所属する女性。ドラムを担当している。髪をボブにした中性的な風貌をしており、ファンからは「志麻さま」と呼ばれている。バンドの中ではまじめな性格の常識人で、酔っぱらって大暴れする廣井きくりのフォローのために謝罪行脚している。ライブでも変拍子に対応したテクニックの高さを見せつけ、バンドをうまくまとめている。喜多郁代はSICK HACKの中では、岩下志麻が一番まともだと思っているが、実は怒ったらめちゃくちゃ怖いらしく、きくりが暴れた後始末のためにライブ後は毎回怒り狂っている。ドラムも、感情のまま叩いていたら向上したようで、きくりは自分のおかげと胸を張っているが、まったく関係ない。

 長谷川 あくび (はせがわ あくび)
メタルバンド「SIDEROS(シデロス)」に所属する少女。ドラムを担当している。いつも口元を黒いマスクで隠している。一人称が「自分」で、語尾に「っす」を付けるしゃべり方をするのが特徴。社交的な性格の常識人で、バンド内では大槻ヨヨコへのツッコミ兼フォローも担っている。ヨヨコを「先輩」と呼び慕っており、ヨヨコとは以心伝心の関係で、彼女のわずかな表情の変化から状態を見極めて的確なツッコミを入れている。結束バンドに対しても、同年代のバンドで親近感を感じており、彼女たちの曲を気に入っている。毎回ライブをめちゃくちゃする廣井きくりに対しては憤りを感じており、彼女が一時期FOLTから姿を消した際には平和を満喫していた。

 佐々木 次子 (ささき つぐこ)
秀華高校に通う3年生の女子。進級した後藤ひとりとクラスメートとなる。愛称は「さっつー」。喜多郁代とは中学時代からの友人関係で、彼女を通してひとりとも知り合う。ヒップホップが大好きで、基本的にヒップホップ以外の曲には興味がない。そのため、郁代のバンドの話にもあまり興味を示さないが、文化祭ライブでひとりの演奏のすごさは目の当たりにしており、ひとりのことは印象に残っていた。

 大槻 ヨヨコ (おおつき よよこ)
人気メタルバンド「SIDEROS(シデロス)」のリーダーを務める少女。ギター兼ボーカルを担当している。ライブハウス「FOLT」で活動しており、廣井きくりを純粋に慕っている数少ない人物の一人。小心者なのに意地っ張りという難儀な性格をしており、弱い自分を見せまいとつい強がりをしてしまう。コミュニケーションを取るのも下手で敵をつくりがちであるため、友達もおらず、バンドメンバーも入れ替わりが激しい。きくりはそんな自分にも構ってくれるために慕っているが、飯をたかられたり、お金を貸したり、便利に使われていることに気づいていない。向上心は旺盛であるため、努力して現在の地位にまで上り詰めたが、未だにライブ前は緊張して眠れなくなるほどあがり症。そのせいで三日寝ずに過ごすこともあるほどで、寝不足のせいでコミュニケーションが疎かになるという悪循環に陥っている。ただし、その緊張さがいい意味でバンドメンバーの緊張を解している副次的効果がある。結束バンドの曲を聞いて、彼女たちのことを気に掛けるようになる。頑なに認めようとはしないが、そのさまはファンそのもので、改善点をまとめて彼女たちに教えるなどマネージャー的な役割を担っている。

 佐藤 愛子 (さとう あいこ)
フリーライターとして活動する女性。ペンネームは「ぽいずん♥やみ」。年齢は23歳だが、童顔で小柄な体型をしているため、中学生でも通る幼い容姿をしている。佐藤愛子自身も幼い容姿は自覚しており、「14歳」と年齢を詐称したこともある。黒い髪をツインアップテールにしている。痛ロリ系のファッションに身を包んでおり、自分でも自分の格好を「痛い奴」と認識している。いいバンドをもっと多くの人に知ってもらいたいとの思いから、音楽系の記事を扱うサイト「ばんらぼ」の運営会社に所属し、記事を書いている。しかし大手のバンドに取材するツテも、音楽雑誌の編集者に就職するツテもないため、最近はアクセス数稼ぎのための記事しか書いておらず辟易している。後藤ひとりの文化祭の奇行が話題となり、その取材のために結束バンドを取材する。その際に、彼女たちの演奏する姿を見て、ひとりがギターヒーローであることに気づき、それをバンドメンバーに暴露した。ギターヒーローのファンだが、結束バンドのバンド演奏は「ガチじゃない」と酷評し、ひとりにバンドを抜けて一人で活動するように勧めた。この行動が結束バンドのメンバーを奮起させ、フェスに参加させるきっかけをつくった。言葉は厳しいが、結束バンドへの評価は的を得ており、ふだんの言動は痛いが音楽に関することには真摯に向き合っている。その後も結束バンドのことは何かと気に掛けており、彼女たちが着々と実力を付けていっているのも認めている。

 ひとりの父 (ひとりのちち)
後藤ひとりの父親。本名は不明で、素顔もなぜか毎回明かされていないために不明。若い頃、ギターを弾いていたらしく、ひとりの中学時代、ギターに興味を覚えた娘に自分のギターを貸した。動画配信のアカウントを家族で共有しているため、ひとりが「ギターヒーロー」として人気を集めていることも知っており、ひとりのために広告収入を貯金していた。のちに、そのお金を新しいギターを購入する資金として渡している。変な特技を数多く持っており、隠れ身の術で壁に隠れていたり、完熟マンゴー仮面を凝ったデザインに改造したりしている。後藤ふたりから家族ヒエラルキーの中で最下位に置かれており、なんとかそれを挽回したいと考えている。

 2号 (にごう)
美術大学の映像学科に在籍する女子。髪をミディアムヘアに整えている。「結束バンド」のファン「2号」を名乗っており、1号とは友人同士で、よく二人で行動している。後藤ひとりと廣井きくりの路上ライブを見て、彼女たちのことを知り、ファンとなった。路上ライブの際にひとりが緊張しているのを心配して応援の言葉を投げかけたが、これが結果的に彼女が観客と向き合う大きなきっかけとなっている。映像制作の勉強をしているため、結束バンドがMVを制作する際に伊地知虹夏から依頼され、1号と共にMV制作を手伝った。

 本城 楓子 (ほんじょう ふうこ)
メタルバンド「SIDEROS(シデロス)」に所属する少女。ギターを担当している。髪をロングに伸ばし、明るい雰囲気を漂わせている。人当たりがよく、同年代の友達は「ちゃん」付けで呼ぶ。大槻ヨヨコのことは「先輩」と呼び慕っているが、一方でコミュニケーション下手なのを面倒くさいとも思っている。

 吉田 銀次郎 (よしだ ぎんじろう)
ライブハウス「FOLT」の店長を務める男性。年齢は37歳。見た目は長髪強面でピアスをつけて目つきが鋭く、初対面の人物は思わず怯えるほどビジュアルが怖い。しかし「心が乙女なおっさん」らしく、女言葉で話すなど物腰は柔らかい。見た目と言動のギャップが激しく、周囲を混乱させることがある。好きな音楽のジャンルはパンクロック系。

 1号 (いちごう)
美術大学の映像学科に在籍する女子。セミロングヘアにしている。「結束バンド」のファン「1号」を名乗っており、2号とは友人同士で、よく二人で行動している。後藤ひとりと廣井きくりの路上ライブを見て、彼女たちのことを知り、ファンとなった。映像制作の勉強をしているため、結束バンドがMVを制作する際に伊地知虹夏から依頼され、2号と共にMV制作を手伝った。かなりの面食いで、かわいい女の子が大好き。ひとりのビジュアルがお気に入りで、彼女がキメ顔になったらテンションが上がるが、そのビジュアルが10秒しか持たなかったためにMVに使うのはあきらめ、ひとりの出番をほとんどカットした。

 PAさん (ぴーえーさん)
ライブハウス「STARRY」のPA(Public Address)エンジニアを務める女性。口ピアスをつけ、黒い髪をストレートロングにしている。伊地知虹夏たちと仲がよく、彼女たちには本名を名乗っているが、覚えられておらず「PAさん」と呼ばれている。ぱっと見はまじめな清楚系に見えるが、かなりいい加減な性格で、高校も朝起きれず1年で中退している。伊地知星歌とは同年代のようで、たまに彼女と独り身同士の闇を吐き出している。

 伊地知 星歌 (いじち せいか)
下北沢のライブハウス「STARRY(スターリー)」の店長を務める女性。誕生日を迎えて30歳になった。伊地知虹夏の姉。ライトイエロー色の髪をストレートロングに伸ばして、やさぐれた雰囲気を漂わせている。シビアな性格をしており、口が非常に悪い。しかし根は優しく、厳しいことを言うのも妹を気づかってのもの。また、かわいいもの好きで、ぬいぐるみがないと眠れないなど乙女な一面を持つ。自分では着ないものの、家にかわいらしい衣装を観賞用としてコレクションしており、メイド服や学校の制服をいくつも所持している。後藤ひとりのギターのテクニックと才能に興味を抱いている。また、ひとりの容姿がかわいらしいのを気に入っており、何かと気に掛けている。現在は解散しているが、過去にバンドをしており、ギターを担当していた。当時はかなり傍若無人な性格をしており、「御茶ノ水の魔王」と呼ばれていた。




その他キーワード
 完熟マンゴー仮面 (かんじゅくまんごーかめん)
後藤ひとりが引きこもるために使う段ボール箱。「完熟マンゴー」と書かれているため、「完熟マンゴー仮面」と呼ばれている。ひとりが人前に立つと緊張するため、人と接するときはおろか、ライブ演奏中でもスキあらばかぶろうとする。また、実はひとりの父が用意しているらしく、少しずつ進化しており、のちに人型ロボットのような外見へと変化している。ひとりは完熟マンゴー仮面がない場合は、ゴミ箱やトランクを代用品としてかぶってひきこもろうとする。

 ギターヒーロー
後藤ひとりが動画配信の際に用いるハンドルネーム。ひとりは「ギターヒーロー」の名で動画配信して、スゴ腕の女子高生ギタリストとしてカリスマ的な人気を集めている。動画配信は素顔を映さない形で行っており、ひとりがギターヒーローであることはほとんどの人たちが知らない。チャンネル登録者もかなり多いらしく、大槻ヨヨコによれば、ドームを2回満員にするほどにチャンネル登録者がいるという。ただ一方で、ひとりの承認欲求が肥大化した原因でもあり、ギターヒーローとしての活動コメントは、「彼氏がいる」「友達とカラオケ」など捏造がひどい。実はアカウントが家族で共有されているため、ひとりの父にはそれらの虚言癖は最初からバレており、のちにその事実に気づく。また佐藤愛子にひとりがギターヒーローであることを暴露されたことをきっかけに周囲の人間にもそれが知れ渡り、以降は激しく後悔してコメントを自粛している。



書誌情報
ぼっち・ざ・ろっく! 6巻 芳文社〈まんがタイムKRコミックス〉
第1巻 (2019-02-27発行、 978-4832270725)
第2巻 (2020-02-27発行、 978-4832271708)
第3巻 (2021-02-25発行、 978-4832272521)
第4巻 (2022-08-26発行、 978-4832273887)
第5巻 (2022-11-26発行、 978-4832274198)
第6巻 (2023-08-25発行、 978-4832274778)
SHARE

の内容を

bocchi.txt	

として保存

from llama_index.llms.ollama import Ollama
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.core import SimpleDirectoryReader, VectorStoreIndex
from transformers import AutoTokenizer

# 使用モデルを選択します。
llm = Ollama(model="elyza:jp8b", request_timeout=40.0)

# HuggingFace Embedding モデルの初期化
embed_model = HuggingFaceEmbedding(model_name="all-MiniLM-L6-v2")

# Transformers tokenizerの設定
tokenizer = AutoTokenizer.from_pretrained("sentence-transformers/all-MiniLM-L6-v2", clean_up_tokenization_spaces=True)

reader = SimpleDirectoryReader(input_files=["./data/10k/bocchi.txt"])
data = reader.load_data()
index = VectorStoreIndex.from_documents(data, embed_model=embed_model)


# クエリエンジンの初期化
query_engine = index.as_query_engine(llm=llm, streaming=True, similarity_top_k=3)

# ストリーミングでレスポンスを取得
response = query_engine.query(
    "文化祭ライブはどうだったのか**日本語**で簡潔に説明してください。 "
    " page reference after each statement."
)
response.print_response_stream()

test_txt.py

として保存

これを実行すると

/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/transformers/tokenization_utils_base.py:1601: FutureWarning: `clean_up_tokenization_spaces` was not set. It will be set to `True` by default. This behavior will be depracted in transformers v4.45, and will be then set to `False` by default. For more details check this issue: https://github.com/huggingface/transformers/issues/31884
  warnings.warn(
文化祭ライブでは、結束バンドが演奏し、機材トラブルが発生したものの、ひとりがとっさにボトルネック奏法のアドリブを行い、成功を収めた。%    

となる

とりあえず動くようになったので
他のファイルなどを使って実験してみる

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 ではなくインストールタイプにしたり
マシンスペックを上げれば短縮できるかもしれない

Gmailから抽出した本文をOllamaに渡しGoogle カレンダーに登録する

Gmailから抽出した本文をOllamaに渡しGoogle カレンダーに登録する

touch main6.py

でファイルを作成

Gmailから抽出したメッセージの本文を、
PDFのテキスト抽出と同様にOllamaに渡すようにコードを変更

Gmailからの本文取得:
gmail_get_latest_unread_message_bodyを使用して、Gmailの本文を取得

Ollamaに渡す:
取得した本文をOllamaに渡して解析

解析結果のフィルタリングとカレンダーへの追加:
PDFからのテキスト処理と同様に、抽出されたイベントをフィルタリングしてカレンダーに追加

以下は、Gmailから抽出した本文をOllamaに渡すようにした修正版コード

from gmail_utils import gmail_init, gmail_get_latest_unread_message_body
from ollama_module import parse_text_with_ollama
from google_calendar_module import add_events_to_calendar
from event_utils import filter_events

# Gmail APIの初期化
service = gmail_init()

# 最新の未読メッセージの本文を取得
label_id = ''  # 取得するラベルID
message_body = gmail_get_latest_unread_message_body(service, label_id)

if message_body != "No unread messages found.":
    print("メール本文:")
    print(message_body)
    
    # Ollamaでメール本文を解析
    raw_events = parse_text_with_ollama(message_body, model_name='elyza:jp8b')
    
    # 抽出されたイベントを表示
    print("抽出されたイベント:")
    for event in raw_events:
        print(event)

    # イベントのフィルタリングとフォーマット
    events = filter_events(raw_events)

    # 有効なイベントを表示
    print("有効なイベント:")
    for event in events:
        print(event)

    # 有効なイベントがある場合のみGoogleカレンダーに追加
    if events:
        add_events_to_calendar(events, calendar_id='primary', token_file='token.json', credentials_file='credentials.json')
    else:
        print("有効なイベントがありません。")
else:
    print("未読メッセージが見つかりませんでした。")

実行すると

「Request had insufficient authentication scopes」

これは、Google Calendar APIへのリクエストに対して適切な認証スコープが不足している

現在使用しているスコープがカレンダーイベントの追加(writeアクセス)を許可していないため

スコープに
https://www.googleapis.com/auth/calendar.events
または
https://www.googleapis.com/auth/calendar
を追加し、Googleカレンダーへの書き込み権限を付与する

gmail_utils.pyを修正する

# gmail_utils.py
import os.path
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
import base64
import dateutil.parser

# スコープの設定
SCOPES = ['https://www.googleapis.com/auth/gmail.readonly']

def decode(encoded):
    decoded_bytes = base64.urlsafe_b64decode(encoded.encode('ASCII'))
    decoded_message = decoded_bytes.decode('utf-8')
    return decoded_message

def gmail_get_latest_unread_message_body(service, labelIdsValue):
    messages = service.users().messages()
    msg_list = messages.list(userId='me', labelIds=[labelIdsValue], q="is:unread", maxResults=1).execute()

    if 'messages' not in msg_list:
        return "No unread messages found."

    msg = msg_list['messages'][0]
    msg_id = msg['id']
    msg = messages.get(userId='me', id=msg_id, format='full').execute()

    body = ""
    if 'parts' in msg['payload']:
        for part in msg['payload']['parts']:
            if part['mimeType'] == 'text/plain' and part['body']['size'] > 0:
                body = decode(part['body']['data'])
                break
    else:
        body = decode(msg['payload']['body']['data'])

    return body  # 本文のみを返す

def gmail_get_messages_body_date(msg):
    headers = msg['payload']['headers']
    date_header = next(header['value'] for header in headers if header['name'].lower() == 'date')
    date = dateutil.parser.parse(date_header).strftime("%Y-%m-%d %H:%M:%S")
    return date

def gmail_init():
    creds = None
    token_path = '../mail_auto/token.json'  # token.jsonのパスを指定
    if os.path.exists(token_path):
        creds = Credentials.from_authorized_user_file(token_path, SCOPES)
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(
                'credentials.json', SCOPES)
            creds = flow.run_local_server(port=0)
        with open('token.json', 'w') as token:
            token.write(creds.to_json())
    service = build('gmail', 'v1', credentials=creds)
    return service

を書き換えてみる

実験結果のログを見ると
Gmail関連の動作をさせるとエラーになる
このため、tokenが重複しているとエラーを起こす可能性があるのかもしれない

Gmailから取得し
Calendar書き込みするとエラーになり
その後はカレンダー機能を使う他のものも動作しなくなる

しかし一度token.jsonを削除し、再度認証すれば
カレンダー機能は使えるようになっている

とりあえず認証関連はこれで動くので
再度tokenを発行すれば解決するはず

先に顔認証したら週間予定を読み上げるのを作成する

gmailから本文を抽出しカレンダー書き込み

gmailから本文を抽出しカレンダー書き込み

PDFからはできるようになったので
次はGmailから本文を抽出しテキストとして取り出すようにする

以前、gmailの未読の最新1件を取得し
本文とURLを抽出するものを作成したので
このソースを変更し
本文を抽出するようにする

なお、実行前にラベルIDを調べる必要がある
Schoolというラベルを作成し
このIDを調べる

cd ../mail_auto
python base,py

を実行し
結果の中から
Schoolのものを探す

コードは

from __future__ import print_function
import os.path
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials

# If modifying these scopes, delete the file token.json.
SCOPES = ['https://www.googleapis.com/auth/gmail.readonly']

import base64, email #デコード用
import dateutil.parser

#token.jsonを設定
tokenPath = "token.json"

#credentials.jsonを設定
credentialsPath = "credentials.json"


#メール本文のデコード
def decode(encoded):
   decoded = base64.urlsafe_b64decode(encoded).decode()
   return decoded


#メール本文の内容を配列で取得する関数
def gmail_get_messages_body(service, labelIdsValue):
   mailBody = []
   
   # メッセージの一覧を取得
   messages = service.users().messages()
   msg_list = messages.list(userId='me', labelIds=labelIdsValue).execute() 
   # msg_list = messages.list(userId='me', labelIds=labelIdsValue ,maxResults=456).execute() #最大値指定
   
   # 取得したメッセージの一覧を配列に格納
   for msg in msg_list['messages']:
       
       #メールの受信日時を取得
       date = gmail_get_messages_body_date(messages,msg)
       
       topid = msg['id']     
       msg = messages.get(userId='me', id=topid).execute()
       
       if(msg["payload"]["body"]["size"]!=0):
           mailBody.append(date+"<br>"+decode(msg["payload"]["body"]["data"])) 
       else:
           #メールによっては"parts"属性の中に本文がある場合もある
           mailBody.append(date+"<br>"+decode(msg["payload"]["parts"][0]["body"]["data"])) 

   return mailBody
   

#gmail_get_messages_body関数内で受信日時を取得する関数
def gmail_get_messages_body_date(messages,msg):
   msg_id = msg['id']
   m = messages.get(userId='me', id=msg_id, format='raw').execute()
   raw = base64.urlsafe_b64decode(m['raw'])
   
   # Emailを解析する
   eml = email.message_from_bytes(raw)
   
   date = dateutil.parser.parse(eml.get('Date')).strftime("%Y-%m-%d_%H-%M-%S")
   return date

#ラベルのIDやnameを表示する関数
def gmail_display_label(service):
   results = service.users().labels().list(userId='me').execute()
   labels = results.get('labels', [])

   if not labels:
       print('No labels found.')
   else:
       print('Labels:')
       for label in labels:
           print(label)


#初期化(from quickstart.py)
def gmail_init():
   creds = None
   # The file token.json stores the user's access and refresh tokens, and is
   # created automatically when the authorization flow completes for the first
   # time.
   if os.path.exists(tokenPath):
       creds = Credentials.from_authorized_user_file(tokenPath, SCOPES)
   # If there are no (valid) credentials available, let the user log in.
   if not creds or not creds.valid:
       if creds and creds.expired and creds.refresh_token:
           creds.refresh(Request())
       else:
           flow = InstalledAppFlow.from_client_secrets_file(
               credentialsPath, SCOPES)
           creds = flow.run_local_server(port=0)
       # Save the credentials for the next run
       with open(tokenPath, 'w') as token:
           token.write(creds.to_json())
           
   service = build('gmail', 'v1', credentials=creds)
   return service


#ここからやりたい処理を書く
service = gmail_init()

#quickstart.pyと同じ処理にしてみた
results = service.users().labels().list(userId='me').execute()
labels = results.get('labels', [])

if not labels:
   print('No labels found.')
else:
   print('Labels:')
   for label in labels:
       print(label['name'])
       
gmail_display_label(service)

grep を使い

python base,py| grep School
で探す

School
{'id': '', 'name': 'School', 'type': 'user'}

となったので
あとはこれを元にコードを書き換えていく

cp ../mail_voice/gmail_utils.py .

でコードをコピー

次にこのコードをURLを抽出せず本文のみ抽出するようにコード変更

# gmail_utils.py
import os.path
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
import base64
import dateutil.parser

# スコープの設定
SCOPES = ['https://www.googleapis.com/auth/gmail.readonly']

def decode(encoded):
    decoded_bytes = base64.urlsafe_b64decode(encoded.encode('ASCII'))
    decoded_message = decoded_bytes.decode('utf-8')
    return decoded_message

def gmail_get_latest_unread_message_body(service, labelIdsValue):
    messages = service.users().messages()
    msg_list = messages.list(userId='me', labelIds=[labelIdsValue], q="is:unread", maxResults=1).execute()

    if 'messages' not in msg_list:
        return "No unread messages found."

    msg = msg_list['messages'][0]
    msg_id = msg['id']
    msg = messages.get(userId='me', id=msg_id, format='full').execute()

    body = ""
    if 'parts' in msg['payload']:
        for part in msg['payload']['parts']:
            if part['mimeType'] == 'text/plain' and part['body']['size'] > 0:
                body = decode(part['body']['data'])
                break
    else:
        body = decode(msg['payload']['body']['data'])

    return body  # 本文のみを返す

def gmail_get_messages_body_date(msg):
    headers = msg['payload']['headers']
    date_header = next(header['value'] for header in headers if header['name'].lower() == 'date')
    date = dateutil.parser.parse(date_header).strftime("%Y-%m-%d %H:%M:%S")
    return date

def gmail_init():
    creds = None
    if os.path.exists('token.json'):
        creds = Credentials.from_authorized_user_file('token.json', SCOPES)
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(
                'credentials.json', SCOPES)
            creds = flow.run_local_server(port=0)
        with open('token.json', 'w') as token:
            token.write(creds.to_json())
    service = build('gmail', 'v1', credentials=creds)
    return service

次に main4.pyは Google Drive からPDFを取得し
抽出する機能だったので
新たに main5.pyを作成し
Gmail未読のものを対象にするようにしてみる

touch main5.py

GPTで生成したコードからラベルIDを変更する

# main2.py
from gmail_utils import gmail_init, gmail_get_latest_unread_message_body
from ollama_module import parse_text_with_ollama

# Gmail APIの初期化
service = gmail_init()

# 最新の未読メッセージの本文を取得
label_id = ''  # 取得するラベルID
message_body = gmail_get_latest_unread_message_body(service, label_id)

if message_body != "No unread messages found.":
    print("メール本文:")
    print(message_body)
    
    # Ollamaでメール本文を解析
    events = parse_text_with_ollama(message_body, model_name='elyza:jp8b')
    
    # 抽出されたイベントを表示
    print("抽出されたイベント:")
    for event in events:
        print(event)
else:
    print("未読メッセージが見つかりませんでした。")

次にGmail でSchoolラベルの中から予定があるメールを試しに1つ未読にして実験する

結果

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/week_calendar_voice/main5.py", line 6, in <module>
    service = gmail_init()
  File "/Users/snowpool/aw10s/week_calendar_voice/gmail_utils.py", line 52, in gmail_init
    creds.refresh(Request())
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/google/oauth2/credentials.py", line 335, in refresh
    ) = reauth.refresh_grant(
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/google/oauth2/reauth.py", line 351, in refresh_grant
    _client._handle_error_response(response_data, retryable_error)
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/google/oauth2/_client.py", line 73, in _handle_error_response
    raise exceptions.RefreshError(
google.auth.exceptions.RefreshError: ('invalid_scope: Bad Request', {'error': 'invalid_scope', 'error_description': 'Bad Request'})

となる

これはtoken.jsonがあってないため
なので以前gmail関連のプロジェクトを作成したので
そちらを使うようにコード変更

# gmail_utils.py
import os.path
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
import base64
import dateutil.parser

# スコープの設定
SCOPES = ['https://www.googleapis.com/auth/gmail.readonly']

def decode(encoded):
    decoded_bytes = base64.urlsafe_b64decode(encoded.encode('ASCII'))
    decoded_message = decoded_bytes.decode('utf-8')
    return decoded_message

def gmail_get_latest_unread_message_body(service, labelIdsValue):
    messages = service.users().messages()
    msg_list = messages.list(userId='me', labelIds=[labelIdsValue], q="is:unread", maxResults=1).execute()

    if 'messages' not in msg_list:
        return "No unread messages found."

    msg = msg_list['messages'][0]
    msg_id = msg['id']
    msg = messages.get(userId='me', id=msg_id, format='full').execute()

    body = ""
    if 'parts' in msg['payload']:
        for part in msg['payload']['parts']:
            if part['mimeType'] == 'text/plain' and part['body']['size'] > 0:
                body = decode(part['body']['data'])
                break
    else:
        body = decode(msg['payload']['body']['data'])

    return body  # 本文のみを返す

def gmail_get_messages_body_date(msg):
    headers = msg['payload']['headers']
    date_header = next(header['value'] for header in headers if header['name'].lower() == 'date')
    date = dateutil.parser.parse(date_header).strftime("%Y-%m-%d %H:%M:%S")
    return date

def gmail_init():
    creds = None
    token_path = '../mail_auto/token.json'  # token.jsonのパスを指定
    if os.path.exists(token_path):
        creds = Credentials.from_authorized_user_file(token_path, SCOPES)
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(
                'credentials.json', SCOPES)
            creds = flow.run_local_server(port=0)
        with open('token.json', 'w') as token:
            token.write(creds.to_json())
    service = build('gmail', 'v1', credentials=creds)
    return service

再度実行するとイベントの抽出ができた

次にこれをカレンダーに書き込むようにする

日付形式の問題解決とイベント抽出とフィルタリング

日付形式の問題解決とイベント抽出とフィルタリング

レスポンスに含まれている日付形式が「令和6年10月26日」などの和暦表記や、「2024-10-08」などのISO表記と混在している

このような場合、統一的に処理するためのフォーマット変換を行う必要がある

from datetime import datetime
import re

def convert_japanese_date(japanese_date):
    # 和暦の例: "令和6年10月26日"
    pattern = r"令和(\d+)年(\d+)月(\d+)日"
    match = re.match(pattern, japanese_date)
    if match:
        year = int(match.group(1)) + 2018  # 令和元年は2019年に相当
        month = int(match.group(2))
        day = int(match.group(3))
        return f"{year}-{month:02d}-{day:02d}"
    # ISO形式のチェック
    try:
        datetime.strptime(japanese_date, '%Y-%m-%d')
        return japanese_date
    except ValueError:
        return None  # 無効な日付形式

また
日付形式の変換後、無効な日付やイベント名が空のエントリをフィルタリングするように変更

eventsリストから有効なエントリのみを抽出

def filter_events(events):
    valid_events = []
    for event in events:
        date = convert_japanese_date(event['date'])
        if date and event['event']:
            valid_events.append({'date': date, 'event': event['event']})
        else:
            print(f"無効な日付形式: {event['date']}")
    return valid_events

日付の変換やイベントのフィルタリングを他のスクリプトでも使うため
モジュール化する

event_utils.pyというモジュールを作成し
これに記述する

touch event_utils.py

内容は

from datetime import datetime
import re

def convert_japanese_date(japanese_date):
    pattern = r"令和(\d+)年(\d+)月(\d+)日"
    match = re.match(pattern, japanese_date)
    if match:
        year = int(match.group(1)) + 2018  # 令和元年は2019年に相当
        month = int(match.group(2))
        day = int(match.group(3))
        return f"{year}-{month:02d}-{day:02d}"
    # ISO形式のチェック
    try:
        datetime.strptime(japanese_date, '%Y-%m-%d')
        return japanese_date
    except ValueError:
        return None  # 無効な日付形式

def filter_events(events):
    valid_events = []
    for event in events:
        date = convert_japanese_date(event['date'])
        if date and event['event']:
            valid_events.append({'date': date, 'event': event['event']})
        else:
            print(f"無効な日付形式: {event['date']}")
    return valid_events

これをmain4.pyで
event_utils.pyをインポートして使用する

from drive_pdf_extractor import extract_texts_from_folder
from ollama_module import parse_text_with_ollama
from google_calendar_module import add_events_to_calendar
from event_utils import convert_japanese_date, filter_events

# フォルダIDを指定して処理を開始
folder_id = "" #Folder ID
texts = extract_texts_from_folder(folder_id)

if not texts:
    print("フォルダ内に解析するテキストがありません。")
else:
    for text_content in texts:
        raw_events = parse_text_with_ollama(text_content, model_name='elyza:jp8b')
        print("抽出されたイベント:")
        for event in raw_events:
            print(event)

        # イベントのフィルタリングとフォーマット
        events = filter_events(raw_events)

        print("有効なイベント:")
        for event in events:
            print(event)

        if events:
            add_events_to_calendar(events, calendar_id='primary', token_file='token.json', credentials_file='credentials.json')
        else:
            print("有効なイベントがありません。")

これで
eventsリストからNoneのイベントを削除するフィルタリング処理が追加
Noneを削除したイベントのみをカレンダーに追加

events = [event for event in events if event['event'] is not None]

の部分がその処理

また
eventsリストが空でないかを確認し、有効なイベントがある場合のみカレンダーに追加
もしイベントがなければ、カレンダーには何も追加されず、
「有効なイベントがありません。」というメッセージが表示される

このチェックによって、無駄な処理が減り、空のイベントがカレンダーに追加されるのを防ぐ

if events:
    add_events_to_calendar(events, calendar_id='primary', token_file='token.json', credentials_file='credentials.json')
else:
    print("有効なイベントがありません。")

これで実行すると
運動会のイベントが失敗している

これは
OllamaからのレスポンスがJSON形式として期待されているのに対し、
不正なJSON形式
(複数のJSONオブジェクトがコンマで区切られている形式や、余分なテキストが含まれている形式)
が返ってきていることが考えられます。

これを解決するために、
Ollamaのレスポンスを受け取る際に、
例外処理や、JSON以外の余計な部分を取り除く前処理を追加

レスポンスを前処理する関数を追加し、JSON形式に整形
JSON解析エラーの例外処理を追加して、不正な形式のデータをスキップ

llamaからのレスポンスを受け取って前処理し、JSON形式に変換するには

import json
import re

def parse_ollama_response(response_text):
    # 正規表現でJSON配列形式のみを抽出
    json_pattern = re.compile(r'\[.*?\]', re.DOTALL)
    match = json_pattern.search(response_text)
    if not match:
        print("JSON形式のレスポンスが見つかりません。")
        return []

    json_data = match.group(0)  # 最初のJSON形式の部分を取得

    try:
        events = json.loads(json_data)
        return events
    except json.JSONDecodeError as e:
        print("Ollamaからのレスポンスの解析に失敗しました:", str(e))
        return []

parse_ollama_response関数を
parse_text_with_ollama関数の後処理として使用し、Ollamaからのレスポンスを整形
これにより、Ollamaからの不正なレスポンス形式を取り除く

from drive_pdf_extractor import extract_texts_from_folder
from ollama_module import parse_text_with_ollama
from google_calendar_module import add_events_to_calendar
import json
import re

# OllamaからのレスポンスをJSON形式に変換
def parse_ollama_response(response_text):
    # JSON配列形式のみを抽出する正規表現
    json_pattern = re.compile(r'\[.*?\]', re.DOTALL)
    match = json_pattern.search(response_text)
    if not match:
        print("JSON形式のレスポンスが見つかりません。")
        return []

    json_data = match.group(0)  # 最初のJSON形式の部分を取得

    try:
        events = json.loads(json_data)
        return events
    except json.JSONDecodeError as e:
        print("Ollamaからのレスポンスの解析に失敗しました:", str(e))
        return []

# Google DriveのSchoolフォルダID
folder_id = "" 

# フォルダ内のPDFファイルからテキストを抽出
texts = extract_texts_from_folder(folder_id)

# テキストが抽出できているか確認
if not texts:
    print("フォルダ内に解析するテキストがありません。")
else:
    for text_content in texts:
        # Ollamaでテキストを解析(モデル名を指定)
        raw_response = parse_text_with_ollama(text_content, model_name='elyza:jp8b')
        
        # Ollamaのレスポンスを前処理してJSONに変換
        events = parse_ollama_response(raw_response)

        print("抽出されたイベント:")
        for event in events:
            print(event)

        # Noneのイベントを除外
        events = [event for event in events if event['event']]

        # フィルタリング後のイベントを表示
        print("有効なイベント:")
        for event in events:
            print(event)

        # 有効なイベントがある場合のみGoogleカレンダーに追加
        if events:
            add_events_to_calendar(events, calendar_id='primary', token_file='token.json', credentials_file='credentials.json')
        else:
            print("有効なイベントがありません。")

としたが

TypeError: expected string or bytes-like object というエラーは、
response_textのデータ型が文字列やバイト列ではない場合に発生

これは、response_textが予期せず辞書(dict)型や他の非文字列データ型である場合に起こることがよくある

response_textの型が文字列であることを確認し、
もし辞書型など別の型が返ってきている場合には、JSONエンコードして文字列に変換する処理を追加

レスポンスが辞書型か確認し、文字列に変換
parse_text_with_ollamaから返ってくるレスポンスが文字列であるかをチェックし、
もし辞書型など別の型であれば文字列に変換する処理を追加

型チェックを行う
parse_ollama_response関数内で
response_textが文字列であることをチェックし、文字列でない場合には変換

以下修正

import json
import re

def parse_ollama_response(response_text):
    # レスポンスが辞書型なら文字列に変換
    if isinstance(response_text, dict):
        response_text = json.dumps(response_text)  # 辞書をJSON文字列に変換
    elif not isinstance(response_text, str):
        print("不明なデータ形式のレスポンスを受け取りました:", type(response_text))
        return []
    
    # JSON配列形式のみを抽出する正規表現
    json_pattern = re.compile(r'\[.*?\]', re.DOTALL)
    match = json_pattern.search(response_text)
    if not match:
        print("JSON形式のレスポンスが見つかりません。")
        return []

    json_data = match.group(0)  # 最初のJSON形式の部分を取得

    try:
        events = json.loads(json_data)
        return events
    except json.JSONDecodeError as e:
        print("Ollamaからのレスポンスの解析に失敗しました:", str(e))
        return []

という処理を追加

response_textが辞書型かを確認します。辞書型の場合はjson.dumps()を使って文字列に変換

次に、正規表現を使用してレスポンス内のJSON配列形式を抽出

json.loadsを使って抽出した部分をJSONとしてパースし、
もしエラーが発生した場合にはエラーメッセージを表示

逆にエラーになった

このため一度

from drive_pdf_extractor import extract_texts_from_folder
from ollama_module import parse_text_with_ollama
from google_calendar_module import add_events_to_calendar
from event_utils import convert_japanese_date, filter_events

# フォルダIDを指定して処理を開始
folder_id = ""
texts = extract_texts_from_folder(folder_id)

if not texts:
    print("フォルダ内に解析するテキストがありません。")
else:
    for text_content in texts:
        raw_events = parse_text_with_ollama(text_content, model_name='elyza:jp8b')
        print("抽出されたイベント:")
        for event in raw_events:
            print(event)

        # イベントのフィルタリングとフォーマット
        events = filter_events(raw_events)

        print("有効なイベント:")
        for event in events:
            print(event)

        if events:
            add_events_to_calendar(events, calendar_id='primary', token_file='token.json', credentials_file='credentials.json')
        else:
            print("有効なイベントがありません。")

に戻したら
となって無事にイベントが追加された

次は
メールから読み込んで実行できるようにする

GoogleDriveの指定フォルダからファイルを取得し処理のモジュール化と処理結果を ollamaへ渡す

GoogleDriveの指定フォルダからファイルを取得し処理のモジュール化と処理結果を ollamaへ渡す

Google DriveからPDFを取得し、テキストを抽出する機能をモジュールに分ける

drive_pdf_extractor.py

を作成する

touch drive_pdf_extractor.py

でファイルを作成し

import os
from googleapiclient.discovery import build
from google.oauth2.credentials import Credentials
from googleapiclient.http import MediaIoBaseDownload
from pdfminer.high_level import extract_text
from io import BytesIO

# Google Drive APIの認証
def authenticate_drive():
    SCOPES = ['https://www.googleapis.com/auth/drive']
    creds = None
    if os.path.exists('token.json'):
        creds = Credentials.from_authorized_user_file('token.json', SCOPES)
    else:
        print("認証トークンが見つかりません。認証を実行してください。")
        return None
    return build('drive', 'v3', credentials=creds)

# フォルダ内のPDFファイルリストを取得
def list_pdf_files_in_folder(service, folder_id):
    """Google Driveフォルダ内のPDFファイルのリストを取得します"""
    query = f"'{folder_id}' in parents and mimeType='application/pdf'"
    results = service.files().list(q=query, fields="files(id, name)").execute()
    files = results.get('files', [])
    return files

# Google DriveからPDFファイルをダウンロード
def download_pdf_from_drive(service, file_id):
    """Google DriveからPDFファイルをダウンロードし、バイナリデータを返します"""
    request = service.files().get_media(fileId=file_id)
    file_data = BytesIO()
    downloader = MediaIoBaseDownload(file_data, request)
    done = False
    while not done:
        status, done = downloader.next_chunk()
        print(f"Download Progress: {int(status.progress() * 100)}%")
    file_data.seek(0)
    return file_data

# PDFからテキストを抽出
def extract_text_from_pdf(pdf_data):
    """PDFファイルデータからテキストを抽出します"""
    text = extract_text(pdf_data)
    return text

# 指定したフォルダ内のすべてのPDFファイルのテキストを抽出
def extract_texts_from_folder(folder_id):
    """フォルダ内のPDFファイルからテキストを抽出し、リストとして返します"""
    service = authenticate_drive()
    if not service:
        return []

    pdf_files = list_pdf_files_in_folder(service, folder_id)
    if not pdf_files:
        print("指定されたフォルダにPDFファイルが見つかりません。")
        return []

    texts = []
    for pdf_file in pdf_files:
        print(f"Processing file: {pdf_file['name']}")
        pdf_data = download_pdf_from_drive(service, pdf_file['id'])
        pdf_text = extract_text_from_pdf(pdf_data)
        if pdf_text:
            texts.append(pdf_text)
        else:
            print(f"{pdf_file['name']} からテキストを抽出できませんでした。")
    return texts

として保存

 touch main3.py

from drive_pdf_extractor import extract_texts_from_folder
from ollama_module import parse_text_with_ollama
from google_calendar_module import add_events_to_calendar

# Google DriveのSchoolフォルダID
folder_id = ""

# フォルダ内のPDFファイルからテキストを抽出
texts = extract_texts_from_folder(folder_id)

# テキストが抽出できているか確認
if not texts:
    print("フォルダ内に解析するテキストがありません。")
else:
    for text_content in texts:
        # Ollamaでテキストを解析(モデル名を指定)
        events = parse_text_with_ollama(text_content, model_name='elyza:jp8b')

        # 抽出されたイベントを表示
        print("抽出されたイベント:")
        for event in events:
            print(event)

        # eventがNoneのものを削除
        events = [event for event in events if event['event'] is not None]

        # フィルタリング後のイベントを表示
        print("有効なイベント:")
        for event in events:
            print(event)

        # 有効なイベントがある場合のみGoogleカレンダーに追加
        if events:
            add_events_to_calendar(events, calendar_id='primary', token_file='token.json', credentials_file='credentials.json')
        else:
            print("有効なイベントがありません。")

として保存

しかし認証エラーになる

これは google drive API の時に作成した token.jsonが
Google calendar API の権限と異なるため
なので参照する token.json を変更

import os
from googleapiclient.discovery import build
from google.oauth2.credentials import Credentials
from googleapiclient.http import MediaIoBaseDownload
from pdfminer.high_level import extract_text
from io import BytesIO

# Google Drive APIの認証
def authenticate_drive():
    SCOPES = ['https://www.googleapis.com/auth/drive.readonly']
    token_path = 'g_drive/token.json'  # token.jsonのパスを変更
    creds = None
    if os.path.exists(token_path):
        creds = Credentials.from_authorized_user_file(token_path, SCOPES)
    else:
        print("認証トークンが見つかりません。認証を実行してください。")
        return None
    return build('drive', 'v3', credentials=creds)

# 他の関数も引き続き同様
# フォルダ内のPDFファイルリストを取得
def list_pdf_files_in_folder(service, folder_id):
    query = f"'{folder_id}' in parents and mimeType='application/pdf'"
    results = service.files().list(q=query, fields="files(id, name)").execute()
    files = results.get('files', [])
    return files

# Google DriveからPDFファイルをダウンロード
def download_pdf_from_drive(service, file_id):
    request = service.files().get_media(fileId=file_id)
    file_data = BytesIO()
    downloader = MediaIoBaseDownload(file_data, request)
    done = False
    while not done:
        status, done = downloader.next_chunk()
        print(f"Download Progress: {int(status.progress() * 100)}%")
    file_data.seek(0)
    return file_data

# PDFからテキストを抽出
def extract_text_from_pdf(pdf_data):
    text = extract_text(pdf_data)
    return text

# 指定したフォルダ内のすべてのPDFファイルのテキストを抽出
def extract_texts_from_folder(folder_id):
    service = authenticate_drive()
    if not service:
        return []

    pdf_files = list_pdf_files_in_folder(service, folder_id)
    if not pdf_files:
        print("指定されたフォルダにPDFファイルが見つかりません。")
        return []

    texts = []
    for pdf_file in pdf_files:
        print(f"Processing file: {pdf_file['name']}")
        pdf_data = download_pdf_from_drive(service, pdf_file['id'])
        pdf_text = extract_text_from_pdf(pdf_data)
        if pdf_text:
            texts.append(pdf_text)
        else:
            print(f"{pdf_file['name']} からテキストを抽出できませんでした。")
    return texts

というように

drive_pdf_extractor.py

を修正

修正したのは

token_path = 'g_drive/token.json'  # token.jsonのパスを変更

というように参照先を変えた

これで
Main3.pyを実行したが

レスポンスに含まれている日付形式が「令和6年10月26日」などの和暦表記や、
「2024-10-08」などのISO表記と混在しているため

統一的に処理するためのフォーマット変換を行い
日付形式の変換後、無効な日付やイベント名が空のエントリをフィルタリングするように変更する必要がある