テキストファイルを読み込み ollamaでGoogleカレンダーに送れる形式にする

テキストファイルを読み込み ollamaでGoogleカレンダーに送れる形式にする

PDFからテキストと抽出するので

pip install pdf2image pytesseract

Tesseract OCRのインストール

brew install tesseract

日本語言語データを追加

brew install tesseract-lang

とりあえずここまではOK

以前PDFの内容の取り出しはしたことがあるので
肝心の文章から予定を ollamaで取り出しを行う

テキストから日時とイベント情報を抽出します。今回のテキストでは、日付が「10月7日~10月15日」や「10月25日(金)」のように記載されています。これらを正規表現とdateparserライブラリで解析する

pip install dateparser

PDFの前に
テキストファイルの読み込み
テキストをOllamaで解析し、日時と予定を抽出
GoogleカレンダーAPIを使って予定を追加
を行うようにする

これはメールでお知らせすることがあるため

def read_text_file(file_path):
    with open(file_path, 'r', encoding='utf-8') as file:
        text = file.read()
    return text

# 使用例
text_file_path = 'school_notice.txt'  # テキストファイルのパス
text_content = read_text_file(text_file_path)

でテキストファイルを読み込む

Ollamaを使用してテキストを解析し、日時とイベント情報を抽出
PythonからOllamaを呼び出す
Ollamaがローカルで動作している前提で、requestsライブラリを使用してHTTPリクエストを送信

import requests
import json

def parse_text_with_ollama(text):
    # OllamaのAPIエンドポイント
    ollama_url = 'http://localhost:11434/generate'

    # Ollamaに送信するプロンプトを作成
    prompt = f"""
以下の文章から、日時とそれに対応する予定を抽出してください。結果はJSON形式で、"date"と"event"のキーを持つオブジェクトのリストとして返してください。

文章:
{text}

出力例:
[
    {{"date": "2024-10-07", "event": "ペットボトルの準備"}},
    {{"date": "2024-10-25", "event": "準備物の確認"}}
]
"""

    payload = {
        'model': 'your-ollama-model-name',  # 使用するOllamaモデルの名前
        'prompt': prompt
    }

    response = requests.post(ollama_url, json=payload)
    response_text = response.text

    # Ollamaの出力からJSON部分を抽出
    try:
        start_index = response_text.index('[')
        end_index = response_text.rindex(']') + 1
        json_str = response_text[start_index:end_index]
        events = json.loads(json_str)
    except (ValueError, json.JSONDecodeError) as e:
        print("Ollamaからのレスポンスの解析に失敗しました:", e)
        events = []

    return events

# 使用例
events = parse_text_with_ollama(text_content)
print("抽出されたイベント:", events)


抽出されたイベントをGoogleカレンダーに追加
import os
from datetime import datetime, timedelta
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build

def add_events_to_calendar(events):
    SCOPES = ['https://www.googleapis.com/auth/calendar']
    creds = None
    if os.path.exists('token.json'):
        creds = Credentials.from_authorized_user_file('token.json', SCOPES)
    else:
        print("token.json が見つかりません。認証を実行してください。")
        return

    service = build('calendar', 'v3', credentials=creds)

    for event in events:
        # 日付の形式を確認し、必要に応じて変換
        try:
            event_date = datetime.strptime(event['date'], '%Y-%m-%d')
        except ValueError:
            print(f"無効な日付形式: {event['date']}")
            continue

        event_body = {
            'summary': event['event'],
            'start': {
                'date': event_date.strftime('%Y-%m-%d'),
                'timeZone': 'Asia/Tokyo',
            },
            'end': {
                'date': (event_date + timedelta(days=1)).strftime('%Y-%m-%d'),
                'timeZone': 'Asia/Tokyo',
            },
        }

        # イベントをカレンダーに追加
        created_event = service.events().insert(calendarId='primary', body=event_body).execute()
        print(f"イベントが作成されました: {created_event.get('htmlLink')}")

# 使用例
add_events_to_calendar(events)

これらを

import os
import requests
import json
from datetime import datetime, timedelta
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build

def read_text_file(file_path):
    with open(file_path, 'r', encoding='utf-8') as file:
        text = file.read()
    return text

def parse_text_with_ollama(text):
    # OllamaのAPIエンドポイント
    ollama_url = 'http://localhost:11434/generate'

    # Ollamaに送信するプロンプトを作成
    prompt = f"""
以下の文章から、日時とそれに対応する予定を抽出してください。結果はJSON形式で、"date"と"event"のキーを持つオブジェクトのリストとして返してください。

文章:
{text}

出力例:
[
    {{"date": "2024-10-07", "event": "ペットボトルの準備"}},
    {{"date": "2024-10-25", "event": "準備物の確認"}}
]
"""

    payload = {
        'model': 'your-ollama-model-name',  # 使用するOllamaモデルの名前
        'prompt': prompt
    }

    response = requests.post(ollama_url, json=payload)
    response_text = response.text

    # Ollamaの出力からJSON部分を抽出
    try:
        start_index = response_text.index('[')
        end_index = response_text.rindex(']') + 1
        json_str = response_text[start_index:end_index]
        events = json.loads(json_str)
    except (ValueError, json.JSONDecodeError) as e:
        print("Ollamaからのレスポンスの解析に失敗しました:", e)
        events = []

    return events

def add_events_to_calendar(events):
    SCOPES = ['https://www.googleapis.com/auth/calendar']
    creds = None
    if os.path.exists('token.json'):
        creds = Credentials.from_authorized_user_file('token.json', SCOPES)
    else:
        print("token.json が見つかりません。認証を実行してください。")
        return

    service = build('calendar', 'v3', credentials=creds)

    for event in events:
        # 日付の形式を確認し、必要に応じて変換
        try:
            event_date = datetime.strptime(event['date'], '%Y-%m-%d')
        except ValueError:
            print(f"無効な日付形式: {event['date']}")
            continue

        event_body = {
            'summary': event['event'],
            'start': {
                'date': event_date.strftime('%Y-%m-%d'),
                'timeZone': 'Asia/Tokyo',
            },
            'end': {
                'date': (event_date + timedelta(days=1)).strftime('%Y-%m-%d'),
                'timeZone': 'Asia/Tokyo',
            },
        }

        # イベントをカレンダーに追加
        created_event = service.events().insert(calendarId='primary', body=event_body).execute()
        print(f"イベントが作成されました: {created_event.get('htmlLink')}")

# メインの実行部分
text_file_path = 'school_notice.txt'  # テキストファイルのパス
text_content = read_text_file(text_file_path)
events = parse_text_with_ollama(text_content)
print("抽出されたイベント:", events)
add_events_to_calendar(events)

というように1つにすることもできるが
検証と後で他でも使えるように
モジュールにして他でも使えるようにしたい

calendar_module.py

read_text_file(file_path)
parse_text_with_ollama(text, model_name)
add_events_to_calendar(events, calendar_id='primary', token_file='token.json')

を入れる

touch calendar_module.py

でファイルを作成

import os
import requests
import json
from datetime import datetime, timedelta
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build

def read_text_file(file_path):
    """テキストファイルを読み込み、その内容を文字列として返します。"""
    with open(file_path, 'r', encoding='utf-8') as file:
        text = file.read()
    return text

def parse_text_with_ollama(text, model_name='your-ollama-model-name'):
    """
    Ollamaを使用してテキストから日時とイベントを抽出します。

    Args:
        text (str): 解析するテキスト。
        model_name (str): 使用するOllamaモデルの名前。

    Returns:
        list: 抽出されたイベントのリスト。
    """
    # OllamaのAPIエンドポイント
    ollama_url = 'http://localhost:11434/generate'

    # Ollamaに送信するプロンプトを作成
    prompt = f"""
以下の文章から、日時とそれに対応する予定を抽出してください。結果はJSON形式で、"date"と"event"のキーを持つオブジェクトのリストとして返してください。

文章:
{text}

出力例:
[
    {{"date": "2024-10-07", "event": "ペットボトルの準備"}},
    {{"date": "2024-10-25", "event": "準備物の確認"}}
]
"""

    payload = {
        'model': model_name,
        'prompt': prompt
    }

    response = requests.post(ollama_url, json=payload)
    response_text = response.text

    # Ollamaの出力からJSON部分を抽出
    try:
        start_index = response_text.index('[')
        end_index = response_text.rindex(']') + 1
        json_str = response_text[start_index:end_index]
        events = json.loads(json_str)
    except (ValueError, json.JSONDecodeError) as e:
        print("Ollamaからのレスポンスの解析に失敗しました:", e)
        events = []

    return events

def add_events_to_calendar(events, calendar_id='primary', token_file='token.json'):
    """
    抽出されたイベントをGoogleカレンダーに追加します。

    Args:
        events (list): イベントのリスト。
        calendar_id (str): イベントを追加するカレンダーのID。
        token_file (str): 認証トークンファイルのパス。
    """
    SCOPES = ['https://www.googleapis.com/auth/calendar']
    creds = None
    if os.path.exists(token_file):
        creds = Credentials.from_authorized_user_file(token_file, SCOPES)
    else:
        print(f"{token_file} が見つかりません。認証を実行してください。")
        return

    service = build('calendar', 'v3', credentials=creds)

    for event in events:
        # 日付の形式を確認し、必要に応じて変換
        try:
            event_date = datetime.strptime(event['date'], '%Y-%m-%d')
        except ValueError:
            print(f"無効な日付形式: {event['date']}")
            continue

        event_body = {
            'summary': event['event'],
            'start': {
                'date': event_date.strftime('%Y-%m-%d'),
                'timeZone': 'Asia/Tokyo',
            },
            'end': {
                'date': (event_date + timedelta(days=1)).strftime('%Y-%m-%d'),
                'timeZone': 'Asia/Tokyo',
            },
        }

        # イベントをカレンダーに追加
        created_event = service.events().insert(calendarId=calendar_id, body=event_body).execute()
        print(f"イベントが作成されました: {created_event.get('htmlLink')}")

として保存

モジュールを作成したら、別のスクリプトからインポートして使用

 touch main.py

でファイルを作成

from calendar_module import read_text_file, parse_text_with_ollama, add_events_to_calendar

# テキストファイルのパス
text_file_path = 'school_notice.txt'  # 処理するテキストファイルのパス

# テキストの読み込み
text_content = read_text_file(text_file_path)

# Ollamaでテキストを解析(モデル名を指定)
events = parse_text_with_ollama(text_content, model_name='your-ollama-model-name')

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

# Googleカレンダーにイベントを追加
add_events_to_calendar(events, calendar_id='primary', token_file='token.json')

とする

ただし今回はテストなので
とりあえずカレンダーにイベントを追加する部分はコメントアウトし
まずは Ollamaの結果を見る

またモデルには
parse_text_with_ollama関数内のモデル名をelyzaに変更
model_name引数のデフォルト値をelyza:jp8bに設定

ということで

import os
import requests
import json
from datetime import datetime, timedelta
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build

def read_text_file(file_path):
    """テキストファイルを読み込み、その内容を文字列として返します。"""
    with open(file_path, 'r', encoding='utf-8') as file:
        text = file.read()
    return text

def parse_text_with_ollama(text, model_name='elyza:jp8b'):
    """
    Ollamaを使用してテキストから日時とイベントを抽出します。

    Args:
        text (str): 解析するテキスト。
        model_name (str): 使用するOllamaモデルの名前(デフォルトは 'elyza:jp8b')。

    Returns:
        list: 抽出されたイベントのリスト。
    """
    # OllamaのAPIエンドポイント
    ollama_url = 'http://localhost:11434/generate'

    # Ollamaに送信するプロンプトを作成
    prompt = f"""
以下の文章から、日時とそれに対応する予定を抽出してください。結果はJSON形式で、"date"と"event"のキーを持つオブジェクトのリストとして返してください。

文章:
{text}

出力例:
[
    {{"date": "2024-10-07", "event": "ペットボトルの準備"}},
    {{"date": "2024-10-25", "event": "準備物の確認"}}
]
"""

    payload = {
        'model': model_name,
        'prompt': prompt
    }

    response = requests.post(ollama_url, json=payload)
    response_text = response.text

    # Ollamaの出力からJSON部分を抽出
    try:
        # レスポンスをJSONとして直接パース
        events = json.loads(response_text.strip())
    except json.JSONDecodeError:
        try:
            # JSON部分のみを抽出
            start_index = response_text.index('[')
            end_index = response_text.rindex(']') + 1
            json_str = response_text[start_index:end_index]
            events = json.loads(json_str)
        except (ValueError, json.JSONDecodeError) as e:
            print("Ollamaからのレスポンスの解析に失敗しました:", e)
            events = []

    return events

def add_events_to_calendar(events, calendar_id='primary', token_file='token.json'):
    """
    抽出されたイベントをGoogleカレンダーに追加します。

    Args:
        events (list): イベントのリスト。
        calendar_id (str): イベントを追加するカレンダーのID。
        token_file (str): 認証トークンファイルのパス。
    """
    SCOPES = ['https://www.googleapis.com/auth/calendar']
    creds = None
    if os.path.exists(token_file):
        creds = Credentials.from_authorized_user_file(token_file, SCOPES)
    else:
        print(f"{token_file} が見つかりません。認証を実行してください。")
        return

    service = build('calendar', 'v3', credentials=creds)

    for event in events:
        # 日付の形式を確認し、必要に応じて変換
        try:
            event_date = datetime.strptime(event['date'], '%Y-%m-%d')
        except ValueError:
            print(f"無効な日付形式: {event['date']}")
            continue

        event_body = {
            'summary': event['event'],
            'start': {
                'date': event_date.strftime('%Y-%m-%d'),
                'timeZone': 'Asia/Tokyo',
            },
            'end': {
                'date': (event_date + timedelta(days=1)).strftime('%Y-%m-%d'),
                'timeZone': 'Asia/Tokyo',
            },
        }

        # イベントをカレンダーに追加
        created_event = service.events().insert(calendarId=calendar_id, body=event_body).execute()
        print(f"イベントが作成されました: {created_event.get('htmlLink')}")

というように

calendar_module.py

を変更

またmain.pyを

from calendar_module import read_text_file, parse_text_with_ollama, add_events_to_calendar

# テキストファイルのパス
text_file_path = 'school_notice.txt'  # 処理するテキストファイルのパス

# テキストの読み込み
text_content = read_text_file(text_file_path)

# Ollamaでテキストを解析(モデル名を指定)
events = parse_text_with_ollama(text_content, model_name='elyza:jp8b')

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

# Googleカレンダーにイベントを追加
#add_events_to_calendar(events, calendar_id='primary', token_file='token.json')

としておく

touch school_notice.txt

でテキストを
学校からのお知らせ内容をテキストファイルにして実験

しかし

Ollamaからのレスポンスの解析に失敗しました: substring not found
抽出されたイベント: []

となってしまう

機能を分割して問題を探す

以前のコードを見たらリクエストURLが

response = requests.post("http://localhost:11434/api/generate", 

なので

ollama_url = 'http://localhost:11434/api/generate'

に修正

import requests
import json

def parse_text_with_ollama(text, model_name='elyza/jp8b'):
    """
    Ollamaを使用してテキストから日時とイベントを抽出します。

    Args:
        text (str): 解析するテキスト。
        model_name (str): 使用するOllamaモデルの名前(デフォルトは 'elyza/jp8b')。

    Returns:
        list: 抽出されたイベントのリスト。
    """
    # OllamaのAPIエンドポイント(修正)
    ollama_url = 'http://localhost:11434/api/generate'

    # Ollamaに送信するプロンプトを作成
    prompt = f"""
以下の文章から、日時とそれに対応する予定を抽出してください。結果はJSON形式で、"date"と"event"のキーを持つオブジェクトのリストとして返してください。

文章:
{text}

出力例:
[
    {{"date": "2024-10-07", "event": "ペットボトルの準備"}},
    {{"date": "2024-10-25", "event": "準備物の確認"}}
]
"""

    payload = {
        'model': model_name,
        'prompt': prompt
    }

    try:
        response = requests.post(ollama_url, json=payload)
        response.raise_for_status()
        # レスポンスをJSONとしてパース
        response_json = response.json()
        # テキスト部分を取得
        response_text = response_json.get('response', '')
    except requests.exceptions.RequestException as e:
        print("Ollamaへのリクエストに失敗しました:", e)
        return []
    except json.JSONDecodeError as e:
        print("OllamaからのレスポンスがJSON形式ではありません:", e)
        return []

    # Ollamaの出力からJSON部分を抽出
    try:
        start_index = response_text.index('[')
        end_index = response_text.rindex(']') + 1
        json_str = response_text[start_index:end_index]
        events = json.loads(json_str)
    except (ValueError, json.JSONDecodeError) as e:
        print("Ollamaからのレスポンスの解析に失敗しました:", e)
        events = []

    return events

とした

しかし

Ollamaからのレスポンスの解析に失敗しました: Expecting value: line 1 column 2 (char 1)
抽出されたイベント:

となる

ChatGGPTで情報を調べると以下の方になる

Ollamaはストリーミング形式でレスポンスを返しています。
つまり、モデルの生成結果が複数のJSONオブジェクトとして順次送られてきています

{“model”:”elyza:jp8b”,”created_at”:”…”,”response”:”…”,”done”:false}

この”response”フィールドに、モデルが生成したテキストの一部が含まれています。全体のレスポンスを組み立てるには、これらの”response”フィールドを順番に連結する必要があります

requestsライブラリを使用して、ストリーミングレスポンスを処理します。response.iter_lines()を使用して、各行を逐次処理

各行をJSONとしてパースし、”response”フィールドの値を取り出して連結

連結したテキストがJSON文字列(リスト)であることを前提に、json.loads()でパース

となるようにコード変更

import requests
import json

def parse_text_with_ollama(text, model_name='elyza:jp8b'):
    """
    Ollamaを使用してテキストから日時とイベントを抽出します。

    Args:
        text (str): 解析するテキスト。
        model_name (str): 使用するOllamaモデルの名前。

    Returns:
        list: 抽出されたイベントのリスト。
    """
    # OllamaのAPIエンドポイント
    ollama_url = 'http://localhost:11434/api/generate'

    # Ollamaに送信するプロンプトを作成
    prompt = f"""
以下の文章から、日付とそれに対応する予定を抽出してください。結果は純粋なJSON形式で、日本語で、"date"と"event"のキーを持つオブジェクトのリストとして返してください。

文章:
{text}

出力例:
[
    {{"date": "2024-10-07", "event": "ペットボトルの準備"}},
    {{"date": "2024-10-25", "event": "準備物の確認"}}
]

重要事項:
- 出力は純粋なJSON形式で、追加のテキストや説明は含めないでください。
- 日付は"YYYY-MM-DD"の形式で出力してください。
- イベント名は元の文章から適切に抽出してください。
"""

    payload = {
        'model': model_name,
        'prompt': prompt
    }

    try:
        # ストリーミングレスポンスを取得
        response = requests.post(ollama_url, json=payload, stream=True)
        response.raise_for_status()

        # レスポンスのストリームを処理
        response_text = ''
        for line in response.iter_lines():
            if line:
                line_str = line.decode('utf-8')
                # 各行をJSONとしてパース
                line_json = json.loads(line_str)
                # "response"フィールドを連結
                response_text += line_json.get('response', '')

        print("Ollamaのレスポンス:")
        print(response_text)
    except requests.exceptions.RequestException as e:
        print("Ollamaへのリクエストに失敗しました:", e)
        return []
    except json.JSONDecodeError as e:
        print("レスポンスの解析に失敗しました:", e)
        return []

    # 連結したテキストをJSONとして解析
    try:
        events = json.loads(response_text)
    except json.JSONDecodeError as e:
        print("Ollamaからのレスポンスの解析に失敗しました:", e)
        events = []

    return events

とする

実行結果は

{'date': '2024-10-07', 'event': 'ペットボトルの準備'}
{'date': '2024-10-15', 'event': None}
{'date': '2024-10-25', 'event': '準備物の確認'}
{'date': '2024-10-26', 'event': None}

となる

一応はエラーは消えたが他のものを試してみる

台風10号の影響により、9月2日(月)の給食が中止となりました。 弁当の準備をお願いします。 8月30日(金)現時点では、日課の変更はありません。 台風や大雨の状況によっては、変更する場合があります。 4月にコドモンで知らせした、「R6年度 自然災害発生時、警報発表・避難情報発表時等に伴う学校の対処」とおり対応します。 今一度御確認ください。
という文章で実験すると

{'date': '8月30日', 'event': '弁当の準備'}
{'date': '9月2日', 'event': '給食中止'}

とほぼ目的に近いものになる

PTA会員 様  Caros membros do PTA
 
 日頃よりPTA活動に御理解・御協力いただいき、ありがとうございます。
 令和7年度のPTA本部役員候補選考の時期になりました。
 アンケート形式にて本部役員の立候補を募ります。
 添付の文書を御一読いただき、以下のリンクからアンケートに回答をお願いします。
 回答期限は9月30日(月)とします。

だと

Ollamaのレスポンス:
[
    {"date": "2024-09-25", "event": "PTA本部役員候補選出についてのアンケート"},
    {"date": "2024-09-30", "event": "アンケート回答期限"}
]
抽出されたイベント:
{'date': '2024-09-25', 'event': 'PTA本部役員候補選出についてのアンケート'}
{'date': '2024-09-30', 'event': 'アンケート回答期限'}

テキストファイルの内容によっては

抽出されたイベント: {'date': '2024-10-07', 'event': 'ペットボトルの準備'} {'date': '2024-10-15', 'event': None} {'date': '2024-10-25', 'event': '準備物の確認'} {'date': '2024-10-26', 'event': None}

となるため
プロンプトを改善して、モデルにeventがnullにならないように強調

prompt = f"""
以下の文章から、日付とそれに対応する予定を抽出してください。結果は純粋なJSON形式で、日本語で、"date"と"event"のキーを持つオブジェクトのリストとして返してください。

文章:
{text}

出力例:
[
    {{"date": "2024-10-07", "event": "ペットボトルの準備"}},
    {{"date": "2024-10-25", "event": "準備物の確認"}}
]

重要事項:
- 出力は純粋なJSON形式で、追加のテキストや説明は含めないでください。
- 日付は"YYYY-MM-DD"の形式で出力してください。
- **イベント名が存在しない場合、そのエントリを出力しないでください。**
- イベント名は元の文章から適切に抽出してください。
"""



リスト内包表記を使用:eventsリストからevent['event']がNoneでないイベントだけを新しいリストにします。
is not Noneを使用:event['event']がNoneでないことを確認

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

これらを変更するので

import requests
import json

def parse_text_with_ollama(text, model_name='elyza:jp8b'):
    """
    Ollamaを使用してテキストから日時とイベントを抽出します。

    Args:
        text (str): 解析するテキスト。
        model_name (str): 使用するOllamaモデルの名前。

    Returns:
        list: 抽出されたイベントのリスト。
    """
    # OllamaのAPIエンドポイント
    ollama_url = 'http://localhost:11434/api/generate'

    # Ollamaに送信するプロンプトを作成
    prompt = f"""
以下の文章から、日付とそれに対応する予定を抽出してください。結果は純粋なJSON形式で、日本語で、"date"と"event"のキーを持つオブジェクトのリストとして返してください。

文章:
{text}

出力例:
[
    {{"date": "2024-10-07", "event": "ペットボトルの準備"}},
    {{"date": "2024-10-25", "event": "準備物の確認"}}
]

重要事項:
- 出力は純粋なJSON形式で、追加のテキストや説明は含めないでください。
- 日付は"YYYY-MM-DD"の形式で出力してください。
- イベント名は元の文章から適切に抽出してください。
"""

    payload = {
        'model': model_name,
        'prompt': prompt
    }

    try:
        # ストリーミングレスポンスを取得
        response = requests.post(ollama_url, json=payload, stream=True)
        response.raise_for_status()

        # レスポンスのストリームを処理
        response_text = ''
        for line in response.iter_lines():
            if line:
                line_str = line.decode('utf-8')
                # 各行をJSONとしてパース
                line_json = json.loads(line_str)
                # "response"フィールドを連結
                response_text += line_json.get('response', '')

        print("Ollamaのレスポンス:")
        print(response_text)
    except requests.exceptions.RequestException as e:
        print("Ollamaへのリクエストに失敗しました:", e)
        return []
    except json.JSONDecodeError as e:
        print("レスポンスの解析に失敗しました:", e)
        return []

    # 連結したテキストをJSONとして解析
    try:
        events = json.loads(response_text)
    except json.JSONDecodeError as e:
        print("Ollamaからのレスポンスの解析に失敗しました:", e)
        events = []

    return events

import requests
import json

def parse_text_with_ollama(text, model_name='elyza:jp8b'):
    """
    Ollamaを使用してテキストから日時とイベントを抽出します。

    Args:
        text (str): 解析するテキスト。
        model_name (str): 使用するOllamaモデルの名前。

    Returns:
        list: 抽出されたイベントのリスト。
    """
    # OllamaのAPIエンドポイント
    ollama_url = 'http://localhost:11434/api/generate'

    # Ollamaに送信するプロンプトを作成
    prompt = f"""
以下の文章から、日付とそれに対応する予定を抽出してください。結果は純粋なJSON形式で、日本語で、"date"と"event"のキーを持つオブジェクトのリストとして返してください。

文章:
{text}

出力例:
[
    {{"date": "2024-10-07", "event": "ペットボトルの準備"}},
    {{"date": "2024-10-25", "event": "準備物の確認"}}
]

重要事項:
- 出力は純粋なJSON形式で、追加のテキストや説明は含めないでください。
- 日付は"YYYY-MM-DD"の形式で出力してください。
- **イベント名が存在しない場合、そのエントリを出力しないでください。**
- イベント名は元の文章から適切に抽出してください。
"""

    payload = {
        'model': model_name,
        'prompt': prompt
    }

    try:
        # ストリーミングレスポンスを取得
        response = requests.post(ollama_url, json=payload, stream=True)
        response.raise_for_status()

        # レスポンスのストリームを処理
        response_text = ''
        for line in response.iter_lines():
            if line:
                line_str = line.decode('utf-8')
                # 各行をJSONとしてパース
                line_json = json.loads(line_str)
                # "response"フィールドを連結
                response_text += line_json.get('response', '')

        print("Ollamaのレスポンス:")
        print(response_text)
    except requests.exceptions.RequestException as e:
        print("Ollamaへのリクエストに失敗しました:", e)
        return []
    except json.JSONDecodeError as e:
        print("レスポンスの解析に失敗しました:", e)
        return []

    # 連結したテキストをJSONとして解析
    try:
        events = json.loads(response_text)
    except json.JSONDecodeError as e:
        print("Ollamaからのレスポンスの解析に失敗しました:", e)
        events = []

    return events

にして

from ollama_module import parse_text_with_ollama
from google_calendar_module import add_events_to_calendar

# テキストファイルのパス
text_file_path = 'school_notice.txt'  # 処理するテキストファイルのパス

# テキストの読み込み
with open(text_file_path, 'r', encoding='utf-8') as file:
    text_content = file.read()

# Ollamaでテキストを解析(モデル名を指定)
events = parse_text_with_ollama(text_content, model_name='elyza:jp8b')

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

# Googleカレンダーにイベントを追加
# add_events_to_calendar(events, calendar_id='primary', token_file='token.json', credentials_file='credentials.json')

from ollama_module import parse_text_with_ollama
from google_calendar_module import add_events_to_calendar

# テキストファイルのパス
text_file_path = 'school_notice.txt'  # 処理するテキストファイルのパス

# テキストの読み込み
with open(text_file_path, 'r', encoding='utf-8') as file:
    text_content = file.read()

# 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カレンダーの今週の予定を取得する

Googleカレンダーの今週の予定を取得する

実行環境
M1 MacbookAir 16GB

pip install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib pytz

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

touch get_week.py

でファイルを作成

import os
import datetime
import pytz
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build

# カレンダーAPIのスコープ
SCOPES = ['https://www.googleapis.com/auth/calendar.readonly']

def main():
    """今週のGoogleカレンダーの予定を取得して表示します。"""
    creds = None
    # 既存のトークンファイルを使用
    if os.path.exists('token.json'):
        creds = Credentials.from_authorized_user_file('token.json', SCOPES)
    else:
        print("トークンファイルが見つかりません。認証を実行してください。")
        return

    # Google Calendar APIサービスを構築
    service = build('calendar', 'v3', credentials=creds)

    # タイムゾーンの設定(日本時間)
    tz = pytz.timezone('Asia/Tokyo')

    # 現在の日時を取得
    now = datetime.datetime.now(tz)

    # 今週の開始日(月曜日)と終了日(日曜日)を計算
    start_of_week = now - datetime.timedelta(days=now.weekday())
    end_of_week = start_of_week + datetime.timedelta(days=7)

    # RFC3339形式に変換
    time_min = start_of_week.isoformat()
    time_max = end_of_week.isoformat()

    print(f"{time_min} から {time_max} までの予定を取得します。")

    # イベントを取得
    events_result = service.events().list(
        calendarId='primary',
        timeMin=time_min,
        timeMax=time_max,
        singleEvents=True,
        orderBy='startTime'
    ).execute()
    events = events_result.get('items', [])

    if not events:
        print('今週の予定はありません。')
    else:
        print('今週の予定:')
        for event in events:
            start = event['start'].get('dateTime', event['start'].get('date'))
            summary = event.get('summary', '(タイトルなし)')
            print(f"{start} - {summary}")

if __name__ == '__main__':
    main()

実行結果は

2024-09-30T23:52:51.254684+09:00 から 2024-10-07T23:52:51.254684+09:00 までの予定を取得します。
今週の予定:
2024-10-05T09:00:00+09:00 - APIを使って追加したイベント

となる

一週間の予定は月曜日からの予定となっている

次は実行した日より前の予定は出力しないようにコード変更する

現在のコードでは、time_min(開始時刻)が「今週の始まり(週の月曜日)」

これを「現在の日時」に変更することで、実行した日より前の予定を除外

time_minをnow(現在の日時)に変更

import os
import datetime
import pytz
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build

# カレンダーAPIのスコープ
SCOPES = ['https://www.googleapis.com/auth/calendar.readonly']

def main():
    """今週のGoogleカレンダーの予定を取得して表示します。"""
    creds = None
    # 既存のトークンファイルを使用
    if os.path.exists('token.json'):
        creds = Credentials.from_authorized_user_file('token.json', SCOPES)
    else:
        print("トークンファイルが見つかりません。認証を実行してください。")
        return

    # Google Calendar APIサービスを構築
    service = build('calendar', 'v3', credentials=creds)

    # タイムゾーンの設定(日本時間)
    tz = pytz.timezone('Asia/Tokyo')

    # 現在の日時を取得
    now = datetime.datetime.now(tz)

    # 今週の開始日(月曜日)と終了日(日曜日)を計算
    start_of_week = now - datetime.timedelta(days=now.weekday())
    end_of_week = start_of_week + datetime.timedelta(days=7)

    # RFC3339形式に変換
    time_min = start_of_week.isoformat()
    time_max = end_of_week.isoformat()

    print(f"{time_min} から {time_max} までの予定を取得します。")

    # イベントを取得
    events_result = service.events().list(
        calendarId='primary',
        timeMin=time_min,
        timeMax=time_max,
        singleEvents=True,
        orderBy='startTime'
    ).execute()
    events = events_result.get('items', [])

    if not events:
        print('今週の予定はありません。')
    else:
        print('今週の予定:')
        for event in events:
            start = event['start'].get('dateTime', event['start'].get('date'))
            summary = event.get('summary', '(タイトルなし)')
            print(f"{start} - {summary}")

if __name__ == '__main__':
    main()

import os
import datetime
import pytz
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build

# カレンダーAPIのスコープ
SCOPES = ['https://www.googleapis.com/auth/calendar.readonly']

def main():
    """今週の残りのGoogleカレンダーの予定を取得して表示します。"""
    creds = None
    # 既存のトークンファイルを使用
    if os.path.exists('token.json'):
        creds = Credentials.from_authorized_user_file('token.json', SCOPES)
    else:
        print("トークンファイルが見つかりません。認証を実行してください。")
        return

    # Google Calendar APIサービスを構築
    service = build('calendar', 'v3', credentials=creds)

    # タイムゾーンの設定(日本時間)
    tz = pytz.timezone('Asia/Tokyo')

    # 現在の日時を取得
    now = datetime.datetime.now(tz)

    # 今週の終了日(日曜日)を計算
    start_of_week = now - datetime.timedelta(days=now.weekday())
    end_of_week = start_of_week + datetime.timedelta(days=7)

    # time_minを現在の日時に設定
    time_min = now.isoformat()
    # time_maxは今週の終了日時
    time_max = end_of_week.isoformat()

    print(f"{time_min} から {time_max} までの予定を取得します。")

    # イベントを取得
    events_result = service.events().list(
        calendarId='primary',
        timeMin=time_min,
        timeMax=time_max,
        singleEvents=True,
        orderBy='startTime'
    ).execute()
    events = events_result.get('items', [])

    if not events:
        print('今週の残りの予定はありません。')
    else:
        print('今週の残りの予定:')
        for event in events:
            start = event['start'].get('dateTime', event['start'].get('date'))
            summary = event.get('summary', '(タイトルなし)')
            print(f"{start} - {summary}")

if __name__ == '__main__':
    main()

とすることで解決

次はテキストの内容から日時と予定を取り出しGoogleカレンダーへAPIで予定を書き込みできるようにする
また
日時と予定の取り出しはOllamaを使うことで汎用性を持たせることにする

Google カレンダーに予定をpythonで追加する

Google カレンダーに予定をpythonで追加する

実行環境
M1 MacbookAir 16GB

APIなどの登録が必要になるので
[初心者向け] GoogleカレンダーにPythonから予定を追加・編集してみた

を参考に行う

https://developers.google.com/calendar/api/v3/reference/calendarList?hl=ja
がリファレンス

なお情報の取得も後で必要になるので

Googleカレンダー情報を取得する
も参考にする

まずは google カレンダーのAPIを有効にする

流れとしては
* 認証情報の発行
* 認証情報を使ってPythonのプログラムを実行

Google Cloudのコンソールにログイン

プロジェクトはすでに作成しているものを選択
APIとサービスをクリック

APIとサービスを有効にするをクリック

calendar
で検索する
なお日本語でカレンダーとしても出ないので注意

Google Calendar API をクリック
有効にするをクリック

これでOK
認証関連のJSONファイルは以前Gmailで作成してるので
今回もいずれGmailを使うので省略

mkdir week_calendar_voice

で作業ディレクトリを作成

ここに

 cp ../mail_auto/*.json .

で以前作成したプロジェクトから
credentials.json
token.json
をコピーする

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

# カレンダーAPIのスコープ(Google Calendarの読み書き権限)
SCOPES = ['https://www.googleapis.com/auth/calendar']

# 既存の token.json を使用
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())

# Google Calendar APIサービスを構築
service = build('calendar', 'v3', credentials=creds)

# イベントの詳細を設定
event = {
  'summary': 'APIを使って追加したイベント',
  'location': 'オンライン',
  'description': 'Google Calendar APIで追加されたイベントです。',
  'start': {
    'dateTime': '2024-10-05T09:00:00',
    'timeZone': 'Asia/Tokyo',
  },
  'end': {
    'dateTime': '2024-10-05T10:00:00',
    'timeZone': 'Asia/Tokyo',
  },
  'attendees': [
    {'email': 'example@example.com'},
  ],
  'reminders': {
    'useDefault': False,
    'overrides': [
      {'method': 'email', 'minutes': 24 * 60},
      {'method': 'popup', 'minutes': 10},
    ],
  },
}

# カレンダーにイベントを挿入
event = service.events().insert(calendarId='primary', body=event).execute()
print(f"イベントが作成されました: {event.get('htmlLink')}")

がchatgpt で生成されたコードだが

追加する予定は

# イベントの詳細を設定 event = { 'summary': 'APIを使って追加したイベント', 'location': 'オンライン', 'description': 'Google Calendar APIで追加されたイベントです。', 'start': { 'dateTime': '2024-10-05T09:00:00', 'timeZone': 'Asia/Tokyo', }, 'end': { 'dateTime': '2024-10-05T10:00:00', 'timeZone': 'Asia/Tokyo', }, 'attendees': [ {'email': 'example@example.com'}, ], 'reminders': { 'useDefault': False, 'overrides': [ {'method': 'email', 'minutes': 24 * 60}, {'method': 'popup', 'minutes': 10}, ], }, } 

このうち
summaryがカレンダーに表示する予定
locationが場所で住所や会場名を指定することも可能
descriptionが詳細な説明
‘start’: イベントの開始時刻
‘end’: イベントの終了時刻
‘attendees’: 出席者のリスト 多分これは使わない
‘reminders’: リマインダーの設定 これも使わない

とりあえずテストなので
attendersと reminders を削除して予定を追加してみる

 touch add_calendar.py

でファイルを作成

これで実行したら

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/week_calendar_voice/add_calendar.py", line 18, in <module>
    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'})

これはgmailの許可はあるけど
Calendar のAPIのスコープがないのが原因

なので
一度 token.jsonを削除して再度実行したけどだめだった

これは認証関連の問題で
再度

rm token.json

で削除してから

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

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

# トークンファイルのチェック
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())

 touch test2.py

で作成して

python test2.py

を実行することで認証画面になるので
そのまま進めていけば認証が完了する

再度

python add_calendar.py

を実行すれば
無事に予定が追加される

とりあえず予定の追加はできたので
次に予定の取得をする
まずは今週の予定から

firebase.jsonの修正

Firebase Functions(第2世代)は、内部的にCloud Run上で動作します。そのため、エラーメッセージにCloud Runが出てきます
とのこと

第1世代の関数に設定することで解決しそう

なのでfirebase.jsonを修正する

firebase.jsonの修正

firebase.jsonで関数を第1世代に設定する

Firebase Functionsの第1世代(Gen 1)と第2世代(Gen 2)の主な違いについて

### **1. 実行環境の違い**

– **第1世代(Gen 1):**
– **Google Cloud Functions**をベースにしています。
– Node.jsのサポートバージョンは**Node.js 10、12、14、16、18**です。
– メモリは最大**2GB**まで利用可能です。
– **シングルリクエストモデル**で、1つの関数インスタンスは同時に1つのリクエストのみを処理します。

– **第2世代(Gen 2):**
– **Google Cloud Run**をベースにしています。
– Node.jsのサポートバージョンは**Node.js 16、18**です。
– メモリは最大**16GB**まで利用可能です。
– **同時実行**が可能で、1つの関数インスタンスが複数のリクエストを同時に処理できます。

### **2. スケーリングとパフォーマンス**

– **第1世代:**
– スケーリングは自動ですが、**コールドスタート**が発生しやすいです。
– 最大インスタンス数はデフォルトで**1000**です。

– **第2世代:**
– スケーリングがより柔軟で、コールドスタートの影響が少ないです。
– 最大インスタンス数はデフォルトで**1000**ですが、同時実行により効率が向上します。

### **3. トリガーのサポート**

– **第1世代:**
– **すべてのFirebaseトリガー**
(Cloud Firestore、Realtime Database、Authentication、Storageなど)
をサポートしています。

– **第2世代:**
– **一部のトリガーのみ**サポートしています。
現在はHTTPトリガーやCloud Eventに対応していますが、他のトリガーは順次対応予定です。

### **4. 設定とカスタマイズ**

– **第1世代:**
– 設定オプションが限定的で、シンプルな設定が可能です。
– タイムアウトは最大**540秒(9分)**まで設定可能です。

– **第2世代:**
– **より詳細な設定**が可能です。タイムアウトは最大**3600秒(1時間)**まで設定できます。
– **VPC接続**や**同時実行数の設定**など、高度なカスタマイズが可能です。

### **5. 料金体系**

– **第1世代:**
– **無料枠**があり、リクエスト数や実行時間に応じて課金されます。
– **コールドスタート**が多い場合、待機時間も考慮する必要があります。

– **第2世代:**
– Cloud Runの料金体系に基づきます。
– 同時実行が可能なため、**コスト効率が向上**する場合があります。

### **6. 環境変数の取り扱い**

– **第1世代:**
– 環境変数は`functions.config()`を使用して取得します。
– `process.env`から直接取得することは推奨されていません。

– **第2世代:**
– 環境変数は`process.env`から直接取得できます。
– `.env`ファイルを使用した環境変数の管理も可能です。

### **7. デプロイと開発の違い**

– **第1世代:**
– 開発とデプロイがシンプルで、**迅速な開発**が可能です。
– Firebase CLIで簡単にデプロイできます。

– **第2世代:**
– **より高度な設定**が可能ですが、その分デプロイ手順が複雑になる場合があります。
– `firebase.json`で世代を指定する必要があります。

– **第1世代(Gen 1)は、シンプルで素早くデプロイしたい場合に適しています。**
– **第2世代(Gen 2)は、高いパフォーマンスや高度なカスタマイズが必要な場合に適しています。**

**参考情報:**

– [Firebase公式ドキュメント – 第1世代と第2世代の比較](https://firebase.google.com/docs/functions/compare-gen-1-and-gen-2?hl=ja)
– [Firebase Functionsの概要](https://firebase.google.com/docs/functions?hl=ja)

これらを元に書き換える

{
  "functions": [
    {
      "source": "プロジェクト名",
      "codebase": "プロジェクト名",
      "runtime": "nodejs18",
      "ignore": [
        "node_modules",
        ".git",
        "firebase-debug.log",
        "firebase-debug.*.log",
        "*.local"
      ]
    }
  ],
  "functions.deployment": {
    "line-bot": {
      "gen": 1
    }
  }
}

というようにした

これで再度

firebase deploy --only functions

を実行すると

   ╭────────────────────────────────────────────────────────────────────╮
   │                                                                    │
   │                 Update available 13.18.0 → 13.19.0                 │
   │           To update to the latest version using npm, run           │
   │                   npm install -g firebase-tools                    │
   │   For other CLI management options, visit the CLI documentation    │
   │         (https://firebase.google.com/docs/cli#update-cli)          │
   │                                                                    │
   │                                                                    │
   │                                                                    │
   ╰────────────────────────────────────────────────────────────────────╯

この後に
Index.js を編集

const functions = require("firebase-functions");
const express = require("express");
const line = require("@line/bot-sdk");

const config = {
  channelAccessToken: functions.config().line.channel_access_token,
  channelSecret: functions.config().line.channel_secret,
};

const app = express();

app.post("/webhook", line.middleware(config), (req, res) => {
  Promise.all(req.body.events.map(handleEvent))
    .then((result) => res.json(result))
    .catch((err) => {
      console.error(err);
      res.status(500).end();
    });
});

const client = new line.Client(config);

function handleEvent(event) {
  if (event.type !== "message" || event.message.type !== "text") {
    return Promise.resolve(null);
  }

  return client.replyMessage(event.replyToken, {
    type: "text",
    text: event.message.text,
  });
}

exports.webhook = functions.https.onRequest(app);

として保存し

firebase deploy --only functions

を実行

webhookのURLが判明するので

これをLINEアカウントにログインして設定する

Blazeプランの概要

Blazeプランの概要

無料枠について
* 無料枠は毎月リセットされます。
* 無料枠の内容は、以下のサービスごとに定められています。
例:Firebase Functions(Cloud Functions for Firebase)

* 関数の呼び出し回数:200万回/月
* コンピューティング時間:400,000 GB-秒/月
* ネットワーク送信量:5 GB/月
その他のサービスの無料枠
* Firebase Authentication
* 無制限の認証機能が無料で利用可能
* Firebase Realtime Database
* 1 GBのデータストレージ
* 10 GB/月のダウンロード
* Firestore
* 1 GBのデータストレージ
* 5万回の読み取り/日
* 2万回の書き込み/日
* 2万回の削除/日
* Firebase Hosting
* 10 GB/月のデータ送信
* 10 GBのストレージ
* カスタムドメインのSSL証明書が無料

費用の管理
* 無料枠内であれば料金は発生しません。
* 無料枠を超過した場合のみ、使用量に応じて課金されます。
* 予算アラートや費用上限の設定が可能で、予期しない高額な請求を防ぐことができます。

料金の例
Firebase Functions
* 関数の呼び出し:$0.40/100万回(無料枠超過分)
* コンピューティング時間:
* GB-秒あたり$0.0000025(メモリ使用量と実行時間に基づく)
* ネットワーク送信量:
* 北米地域:$0.12/GB(無料枠超過分)
Firestore
* データストレージ:$0.18/GB/月(無料枠超過分)
* 読み取り操作:$0.06/10万回
* 書き込み操作:$0.18/10万回
* 削除操作:$0.02/10万回

これらを元に対策をしておく
従量課金の場合助言はないためリスク管理が必要

Firebase の課金で爆死しないための設定方法
を参考に設定を調べる

Functions は関数の実行時間、関数の呼び出し回数、関数にプロビジョニングしたリソースの数に基づいて課金される

export const api = functions.https.onRequest(app);

の部分を

export const api = functions.runWith({
  maxInstances: 1,
  timeoutSeconds: 30,
  memory: "128MB",
}).https.onRequest(app);

というように

最大インスタンス数1
timeoutSeconds30秒
メモリ128MB
というように設定することで回避できそう

次に
Hosting はストレージが10GBまで無料
データ転送は1ヶ月あたり 10GB まで無料
それを超えると課金が発生
デフォルトでは無制限に保持する設定になっているため
Firebase コンソールの Hostring から設定を変更

これらを元に
個人アプリなら10もあれば十分

転送量は制限できないのでアプリケーションの作りで頑張るしかない

あと
Firestore は以下の内容で課金されます。
* 読んだり、書いたり、削除したりする文書の数。
* 集計クエリで一致したインデックス エントリの数。
クエリに一致する最大 1000 のインデックス
エントリのバッチごとに 1 つのドキュメントの読み取りが課金されます。

* メタデータとインデックスのオーバーヘッドを含む、データベースが使用するストレージの量。
* 使用するネットワーク帯域幅の量。
Firestore は何か制限を設けることができません。


なので上記を参考に書き込みや読み取り方法をチューニングしたり、
インデックスを作成してアプリケーション側をよくしていくしかない

そして
コストアラートを設定する
予算の 50%, 90%, 100% でアラートが来る設定をできるらしい

そして
最悪、Firebaseを止める方法

 firebase hosting:disable

を実行すれば
Hosting は止まる

Functions を止める方法は

 firebase functions:delete {関数名}

Firestore を止める方法は
Firebase コンソールから以下のルールの設定を入れて公開

rules_version = '2';

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

これで読み取り、書き込み全てをブロック

これらを元にまずはアラートを予算1000円で動作するよう設定
https://console.cloud.google.com/billing/

予算を設定できるので1,000円に設定

index.js の内容を

export const api = functions.https.onRequest(app);

から

const runtimeOpts = {
    timeoutSeconds: 30,
    memory: '128MB',
    maxInstances: 1, // 最大同時実行数を5に制限
  };
exports.app = functions
.runWith(runtimeOpts)
.https.onRequest(app);

へ変更し保存

これでBlazeプランに変更する

請求アカウントは予算を作成したものを選択

Firebase の課金で爆死しないための設定

Firebase の課金で爆死しないための設定方法
を参考に設定を調べる

Functions は関数の実行時間、関数の呼び出し回数、関数にプロビジョニングしたリソースの数に基づいて課金される

export const api = functions.https.onRequest(app);

の部分を

export const api = functions.runWith({
  maxInstances: 1,
  timeoutSeconds: 30,
  memory: "128MB",
}).https.onRequest(app);

というように

最大インスタンス数1
timeoutSeconds30秒
メモリ128MB
というように設定することで回避できそう

次に
Hosting はストレージが10GBまで無料
データ転送は1ヶ月あたり 10GB まで無料
それを超えると課金が発生
デフォルトでは無制限に保持する設定になっているため
Firebase コンソールの Hostring から設定を変更

これらを元に
個人アプリなら10もあれば十分

転送量は制限できないのでアプリケーションの作りで頑張るしかない

あと
Firestore は以下の内容で課金されます。
* 読んだり、書いたり、削除したりする文書の数。
* 集計クエリで一致したインデックス エントリの数。
クエリに一致する最大 1000 のインデックス エントリのバッチごとに
1 つのドキュメントの読み取りが課金されます。
* メタデータとインデックスのオーバーヘッドを含む、データベースが使用するストレージの量。
* 使用するネットワーク帯域幅の量。
Firestore は何か制限を設けることができません。


なので上記を参考に書き込みや読み取り方法をチューニングしたり、
インデックスを作成してアプリケーション側をよくしていくしかない

そして
コストアラートを設定する
予算の 50%, 90%, 100% でアラートが来る設定をできるらしい

そして
最悪、Firebaseを止める方法

 firebase hosting:disable

を実行すれば
Hosting は止まる

Functions を止める方法は

 firebase functions:delete {関数名}

Firestore を止める方法は
Firebase コンソールから以下のルールの設定を入れて公開

rules_version = '2';

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

これで読み取り、書き込み全てをブロック

これらを元にまずはアラートを予算1000円で動作するよう設定

考えた方法としては
予算を超えメールが来たら

 firebase functions:delete {関数名}
 firebase hosting:disable

を実行するようにすれば回避できそう

メールから実行するにはgmail API を使えばできるはず
あとはこれを cron で毎分実行し監視しておけば心配は減る

監視スクリプトの実行だけなので
ラズパイ3ぐらいのスペックでいけるはず

あとはどんなメールが来るのか
アドレスとか件名などが分かれば対処ができそう

from __future__ import print_function
import os.path
import subprocess
import base64
import email
from email.header import decode_header
import re

from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request

from googleapiclient.discovery import build

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

# 特定の送信元メールアドレスと件名の条件
FROM_EMAIL = 'cloud-billing-noreply@google.com'
SUBJECT_PATTERN = r'.*の予算アラート: 予算の (\d+)% を使用しました'

# Firebaseの関数名
FUNCTION_NAME = 'yourFunctionName'  # 実際の関数名に置き換えてください

def main():
    creds = None
    # token.jsonが存在する場合、既存の認証情報を読み込む
    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:
            # credentials.jsonを使用して認証
            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())

    # Gmail APIクライアントを構築
    service = build('gmail', 'v1', credentials=creds)

    # 送信元と件名でメールを検索
    query = f'from:{FROM_EMAIL} subject:"予算アラート"'
    results = service.users().messages().list(userId='me', q=query).execute()
    messages = results.get('messages', [])

    if not messages:
        print('該当するメールはありません。')
        return

    for message in messages:
        msg = service.users().messages().get(userId='me', id=message['id'], format='full').execute()
        headers = msg['payload']['headers']

        # 件名を取得
        subject = ''
        for header in headers:
            if header['name'] == 'Subject':
                subject = header['value']
                break

        # 件名のエンコーディングをデコード
        decoded_subject = decode_header(subject)[0][0]
        if isinstance(decoded_subject, bytes):
            decoded_subject = decoded_subject.decode()

        # 件名から使用率を抽出
        match = re.match(SUBJECT_PATTERN, decoded_subject)
        if match:
            usage_percentage = int(match.group(1))
            print(f'予算の使用率: {usage_percentage}%')

            # 使用率が特定の閾値を超えた場合にコマンドを実行
            if usage_percentage >= 90:
                print('Firebase FunctionsとHostingを無効化します。')

                # Firebase Functionsを削除
                subprocess.run(['firebase', 'functions:delete', FUNCTION_NAME, '--force'], check=True)

                # Firebase Hostingを無効化
                subprocess.run(['firebase', 'hosting:disable', '--force'], check=True)

                print('Firebase FunctionsとHostingを無効化しました。')
                return
        else:
            print('件名の形式が一致しません。')

    print('条件に合致するメールは処理済みです。')

if __name__ == '__main__':
    main()

が生成されたコード

なおGmailAPIを使うにあたり制限があるので注意

以下はGPTのレポート

Gmail APIには、サービスの安定性と公平な利用を確保するために、
いくつかの**使用制限(クォータ)**が設けられています。
以下に、Gmail APIの使用制限の詳細と、それがあなたのスクリプトに与える影響についてご説明いたします。

## **Gmail APIの使用制限について**

Gmail APIの使用制限は、大きく分けて以下の2種類があります。

1. **ユーザー単位のレート制限**
2. **プロジェクト単位のレート制限**

これらの制限は、リクエストの頻度や送信可能なメールの数を制限するものです。

### **1. ユーザー単位のレート制限**

– **メールの読み取り、検索、ラベル付けなどの操作に対する制限**があります。
– **1秒あたりのリクエスト数**や**1日あたりのリクエスト数**が制限されます。

### **2. プロジェクト単位のレート制限**

– **APIキーやOAuthクライアントIDを共有する全ユーザーの合計リクエスト数**に対する制限です。
– **1日あたりのリクエスト数**や**1秒あたりのユーザー数**などが制限されます。

## **具体的な使用制限の数値**

2023年10月時点でのGmail APIの主な使用制限は以下のとおりです。
ただし、正確な数値や最新の情報については、[Googleの公式ドキュメント](https://developers.google.com/gmail/api/guides/quota)を参照してください。

### **ユーザー単位の制限**

– **1日あたりのメール送信数**:
通常のGmailアカウントで500通、Google Workspace(旧G Suite)の場合は最大2,000通

– **1秒あたりのリクエスト数**: ユーザーあたり約10リクエスト/秒
– **1日あたりのリクエスト数**: ユーザーあたり約250,000リクエスト/日

### **プロジェクト単位の制限**

– **1日あたりのリクエスト数**: プロジェクト全体で約1,000,000リクエスト/日
– **1秒あたりのユーザー数**: プロジェクト全体で約100ユーザーが同時にリクエスト可能

### **3. APIの使用制限を超えた場合の挙動**

– 制限を超えると、
**HTTPステータスコード429(Too Many Requests)**

**403(User Rate Limit Exceeded)**

のエラーが返されます。
– **対策**:
– **エラー発生時にリトライする際は、指数バックオフを実装**します。

## **使用制限を遵守するためのベストプラクティス**

### **1. キャッシュの活用**

– **データをキャッシュ**することで、同じデータに対するリクエストを減らすことができます。

### **2. リクエストの効率化**

– **必要なフィールドのみを取得**するように、`fields`パラメータを使用します。
– **バッチリクエスト**を使用して、複数の操作をまとめて行います。

### **3. リクエスト頻度の最適化**

– スクリプトの実行間隔を長めに設定し、**必要最低限の頻度でメールをチェック**します。

### **4. エラーハンドリング**

– **レート制限エラーが発生した場合のリトライ戦略**を実装します。
– **指数バックオフ**を使用して、待機時間を徐々に増やします。

### **5. メールのフィルタリング**

– Gmailのウェブインターフェースで**フィルタを設定**し、特定のラベルを付ける。
– スクリプトでは、そのラベルを持つメールのみを取得する。

## **代替案の検討**

### **1. Pub/SubとCloud Functionsの活用**

– **Gmail APIのPush通知**を使用し、メールの到着をリアルタイムで検知します。
– ただし、これはGoogle Workspace(旧G Suite)アカウントが必要です。

### **2. メールの転送とWebhookの活用**

– **Gmailのフィルタ機能を使用して、特定のメールをWebhookに転送**します。
– 直接的な方法ではないため、実現可能性を検討する必要があります。

LINEbotのwebhookをfirebaseで行う

LINEbotのwebhookをfirebaseで行う

LINE botのWebhookをFirebaseで行うためには、
Firebase Cloud Functionsを使ってWebhookをセットアップする

コスト面から考えると
firebaseでpython を使いwebhookにするには
Cloud Run のコストも必要らしい

このためpython から javascriptに変更する

rm -rf functions

で削除する

functionsディレクトリがない状態で、Firebase FunctionsをJavaScriptで再初期化

firebase init functions

を実行

❯ Initialize 

を選択

今回は

Functions can be deployed with firebase deploy.

? What should be the name of this codebase?

となったので調べる

GPTによれば

これはFirebase CLIの新しいバージョンで追加された**コードベース(codebase)**に関する設定です。
以下で詳しく説明し、どのように対応すればよいかご案内します。

## **コードベース(Codebase)とは**

**コードベース**は、Firebase Functionsの新しい機能で、
**同じFirebaseプロジェクト内で複数の関数セットを管理するためのもの**です。
これにより、異なるディレクトリや異なる言語で書かれた関数を、
同じプロジェクト内で個別にデプロイできます。

## **このプロンプトが表示される理由**

`firebase init functions`を実行した際、
または`firebase deploy`を初めて行う際に、CLIがコードベースの名前を尋ねてきます。

特に理由がなければ、デフォルトの名前を使用して問題ありません。
プロジェクトに適した名前を指定することもできます。

### **コードベースを使用するメリット**

– **複数の関数セットを管理**:異なるチームや機能ごとに関数を分離できます。
– **異なる言語のサポート**:同じプロジェクト内で、JavaScriptとTypeScript、
あるいは他の言語の関数を同時に使用できます。

### **コードベースの命名規則**

– **ユニークな名前**:各コードベースはプロジェクト内でユニークな名前を持つ必要があります。
– **わかりやすい名前**:コードベースの内容を反映した名前を付けると、管理が容易になります。

## **まとめ**

– **プロンプトに対して名前を指定**:`default`や`app`などの名前を入力します。
– **`firebase.json`を確認**:コードベースの設定が追加されていることを確認します。
– **デプロイを再試行**:`firebase deploy –only functions`でデプロイします。

とのこと

とりあえず

Default 

にするが

functions.codebase must be unique but 'default' was used more than once.
? What should be the name of this codebase? 

となるので

というように defaultから 別の名前に変更した

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


javascriptを選択

? Do you want to use ESLint to catch probable bugs and enforce style? (y/N) 

となる

GPTによれば

## **プロンプトの意味**

– **ESLint**は、JavaScriptのコード品質を向上させるための**静的コード解析ツール**です。
– **コードのバグやスタイルの問題を検出**し、一貫したコーディングスタイルを維持するのに役立ちます。

## **選択肢**

1. **`y` を入力して ESLint を有効にする**

– **メリット**:
– コードの潜在的なバグを早期に発見できます。
– チーム開発の場合、一貫したコーディングスタイルを維持できます。
– デプロイ前にコードの品質をチェックできます。
– **設定**:
– プロジェクトに`.eslintrc.js`という設定ファイルが追加されます。
– デフォルトのルールセットが適用されますが、必要に応じてカスタマイズ可能です。

2. **`n` を入力して ESLint を無効にする**

– **メリット**:
– 初期設定がシンプルになり、すぐに開発を開始できます。
– ESLintによる警告やエラーを気にせずにコーディングできます。
– **デメリット**:
– コードのバグやスタイルの問題を見逃す可能性があります。

## **おすすめの選択**

– **初心者や個人開発の場合**:
– **`y` を選択することをおすすめします**。ESLintは学習曲線がありますが、コード品質の向上に大いに役立ちます。
– **小規模なプロトタイプや試験的なプロジェクトの場合**:
– **`n` を選択しても問題ありません**。後から必要に応じてESLintを導入できます。

– **ESLintの後からの導入**:
– 後でESLintを導入したい場合は、`npm install eslint –save-dev`でインストールし、設定ファイルを追加することで対応できます。
– **TypeScriptを使用する場合**:
– TypeScriptを選択した場合も同様にESLintを使用できます。

とのことなので
今回は n にする

? Do you want to install dependencies with npm now? (Y/n) 

となるので
Y
で依存関係のインストール

npm warn EBADENGINE Unsupported engine {
npm warn EBADENGINE   package: undefined,
npm warn EBADENGINE   required: { node: '18' },
npm warn EBADENGINE   current: { node: 'v20.17.0', npm: '10.8.2' }
npm warn EBADENGINE }
npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported

added 491 packages, and audited 492 packages in 39s

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

found 0 vulnerabilities
npm notice
npm notice New patch version of npm available! 10.8.2 -> 10.8.3
npm notice Changelog: https://github.com/npm/cli/releases/tag/v10.8.3
npm notice To update run: npm install -g npm@10.8.3
npm notice

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

✔  Firebase initialization complete!

となる

次に追加パッケージのインストール
LINE Bot SDKやExpressを使用する場合、
プロジェクトディレクトリに移動して以下のコマンドを実行

npm install @line/bot-sdk express

実行結果は

npm warn EBADENGINE Unsupported engine {
npm warn EBADENGINE   package: undefined,
npm warn EBADENGINE   required: { node: '18' },
npm warn EBADENGINE   current: { node: 'v20.17.0', npm: '10.8.2' }
npm warn EBADENGINE }

added 7 packages, and audited 499 packages in 3s

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

found 0 vulnerabilities

ログを元に調べたら

– **原因:**
– あなたのシステムでは Node.js バージョン 20.17.0 を使用していますが、パッケージはバージョン 18 を要求しています。
– このバージョンの不一致が警告を引き起こしています。

– 警告はバージョンの不一致を知らせていますが、致命的な問題ではありません。
– パッケージが正常に動作するか確認し、必要に応じて Node.js のバージョンを調整してくだい
とのこと

動くのか調べてみたいので

Node.jsとLINE Bot SDKで作るLINE Bot開発チュートリアル
とかみたけど
使用バージョンとかは載っていない

Expressで作ったLINE BotをVercelでデプロイする方法

LINE BotへのメッセージをGoogle Homeに喋らせてみる
あたりもこのバージョンについては書かれていない

LINE ボットを簡単な構成で作ってみた(TypeScript + Bot SDK ver.8 + Lambda)

2023年10月15日の記事で
前提条件
* LINE公式アカウントを作成していること(無料枠でテスト可能)
* Node.js ver.18の環境を準備済

とあるので、やはり ver18にしないと無理っぽい

nodebrew ls-remote

で調べると

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

となっているので

nodebrew install-binary v18.20.4 

でインストール

nodebrew use v18.20.4


使用バージョンを変更する

node -v

でバージョンを確認
v18.20.4

バージョンを変更した場合 firebase init functions などは再度実行する必要があるか調べる

– **Node.jsのバージョンを変更した場合でも、`firebase init functions`を再実行する必要はありません。**
– **依存関係を再インストールするために、`node_modules`と`package-lock.json`を削除してから`npm install`を実行してください。**
– **`package.json`の`engines`フィールドが正しく設定されていることを確認してください。**
– **デプロイ後にLINE Botが正常に動作するか確認し、問題があればエラーログをチェックしてください。**

ということで

rm -rf node_modules
rm package-lock.json

でver20 でインストールしたパッケージとロックファイルを削除

npm install

依存関係の再インストール

npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported

added 497 packages, and audited 498 packages in 1m

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

found 0 vulnerabilities

となる

これらの警告メッセージの意味

1. npm WARN deprecated
* deprecated(非推奨)とは、そのパッケージがサポートされておらず、将来的に使用しないことが推奨されていることを意味します。
* 具体的には、パッケージが古くなっている、メンテナンスされていない、または既知の問題がある場合に表示されます。

2. 各警告の詳細
a. inflight@1.0.6 の警告

npm WARN deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
* 意味:inflightパッケージはサポートされておらず、メモリリークの問題があるため、使用しないことが推奨されています。
* 対策:代替としてlru-cacheを使用することが提案されています。
b. glob@7.2.3 の警告

npm WARN deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
* 意味:globパッケージのバージョン7.2.3はサポートされておらず、バージョン9未満はサポート外であることを示しています。

とのこと

ということでファイルを調べる

cat package.json 

で調べた

{
"name": "functions",
"description": "Cloud Functions for Firebase",
"scripts": {
"serve": "firebase emulators:start --only functions",
"shell": "firebase functions:shell",
"start": "npm run shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log"
},
"engines": {
"node": "18"
},
"main": "index.js",
"dependencies": {
"@line/bot-sdk": "^9.4.0",
"express": "^4.21.0",
"firebase-admin": "^12.1.0",
"firebase-functions": "^5.0.0"
},
"devDependencies": {
"firebase-functions-test": "^3.1.0"
},
"private": true
}

となっている

直接依存しているパッケージが古いバージョンがあるか調べた

## **結論**

直接依存しているパッケージのいくつかが最新バージョンではないため、**アップデートを行うことで警告が解消される可能性**があります。

## **まとめ**

– **直接依存しているパッケージに古いバージョンがあります**ので、最新バージョンにアップデートしましょう。
– **パッケージのアップデート後**、必ず動作確認とテストを行ってください。
– **エラーや警告が解消される**可能性があります。

となる

なので

npm install @line/bot-sdk@latest express@latest firebase-admin@latest firebase-functions@latest --save
npm install firebase-functions-test@latest --save-dev

を実行し

cat package.json
{
  "name": "functions",
  "description": "Cloud Functions for Firebase",
  "scripts": {
    "serve": "firebase emulators:start --only functions",
    "shell": "firebase functions:shell",
    "start": "npm run shell",
    "deploy": "firebase deploy --only functions",
    "logs": "firebase functions:log"
  },
  "engines": {
    "node": "18"
  },
  "main": "index.js",
  "dependencies": {
    "@line/bot-sdk": "^9.4.0",
    "express": "^4.21.0",
    "firebase-admin": "^12.5.0",
    "firebase-functions": "^6.0.1"
  },
  "devDependencies": {
    "firebase-functions-test": "^3.3.0"
  },
  "private": true
}

で内容を確認

npm install @line/bot-sdk@latest express@latest firebase-functions@latest --save

を行ったが変わらない

rm -rf node_modules package-lock.json

で削除

本当にあってるか調べることにした

https://www.npmjs.com/package/firebase-functions
で調べたら
firebase-functionsは6.01

https://www.npmjs.com/package/@line/bot-sdk

@line/bot-sdk は9.4.0

firebase-admin が12.5.0
https://www.npmjs.com/package/firebase-admin

expressが4.21.0
https://www.npmjs.com/package/express

つまりchatgpt が間違いで
ファイルはあっていた

npm cache clean --force            

でキャッシュファイルをクリア

npm warn using --force Recommended protections disabled.

警告が出たので調べた

警告メッセージの意味
この警告は、npm cache clean コマンドを
–force オプション付きで実行 した場合に表示されるものです。

* –force オプションの意味:通常、npm cache clean はデフォルトでキャッシュを削除しません。
–force オプションを付けることで、キャッシュの強制削除を行います。

* 警告の内容:
Recommended protections disabled.
(推奨される保護機能が無効になりました)と表示されている通り、
キャッシュの強制削除は慎重に行うべき操作であるため、警告が表示されます。
対処方法と次のステップ

1. 警告は無視しても大丈夫か?
* 結論:この警告は 予期されたもの であり、特に問題はありません。
* 理由:npm cache clean –force を実行するときに、必ずこの警告が表示されます。
これはユーザーに対して注意を促すためのものです。

とりあえず無視で良いらしい

npm install

でパッケージの再インストール

ちなみに再度 javascriptで構成したときに
ユニーク名である必要があり
functionsではなく line-bot となっているので
これで置き換えて考えればいい

とりあえずここまでできたので
必要なライブラリのインストールはOK

シークレット情報であるchannelAccessTokenとchannelSecretをFirebaseの環境変数として設定

firebase functions:config:set line.channel_access_token="YOUR_CHANNEL_ACCESS_TOKEN" line.channel_secret="YOUR_CHANNEL_SECRET"

を実行する

しかし

zsh: command not found: firebase

となる

npm install -g firebase-tools

で再度インストール
これは ver18に変更したので再インストールが必要だった

firebase --version

13.18.0
でバージョンを確認

https://www.npmjs.com/search?q=firebase%20
でも確認し、最新版であるのを確認

firebase login

でログインできるのも確認ずみ

再度

firebase functions:config:set line.channel_access_token="YOUR_CHANNEL_ACCESS_TOKEN" line.channel_secret="YOUR_CHANNEL_SECRET"

を実行する

✔  Functions config updated.

Please deploy your functions for the change to take effect by running firebase deploy --only functions

となってセット完了

次に
functions/index.js
の編集
とは言っても functions から フォルダを変更しているので
移動してから
index.js を編集する

/**
 * Import function triggers from their respective submodules:
 *
 * const {onCall} = require("firebase-functions/v2/https");
 * const {onDocumentWritten} = require("firebase-functions/v2/firestore");
 *
 * See a full list of supported triggers at https://firebase.google.com/docs/functions
 */

const {onRequest} = require("firebase-functions/v2/https");
const logger = require("firebase-functions/logger");

// Create and deploy your first functions
// https://firebase.google.com/docs/functions/get-started

// exports.helloWorld = onRequest((request, response) => {
//   logger.info("Hello logs!", {structuredData: true});
//   response.send("Hello from Firebase!");
// });

const functions = require('firebase-functions');
const express = require('express');
const line = require('@line/bot-sdk');

const app = express();

// 環境変数からLINEのチャネルアクセストークンとシークレットを取得
const config = {
  channelAccessToken: functions.config().line.channel_access_token,
  channelSecret: functions.config().line.channel_secret,
};

const client = new line.Client(config);

// 署名検証のミドルウェアを設定
app.post('/callback', line.middleware(config), (req, res) => {
  Promise
    .all(req.body.events.map(handleEvent))
    .then(() => res.end('OK'))
    .catch((err) => {
      console.error(err);
      res.status(500).end();
    });
});

// イベントハンドラーの実装
function handleEvent(event) {
  // メッセージイベント以外は無視
  if (event.type !== 'message' || event.message.type !== 'text') {
    return Promise.resolve(null);
  }

  // 受信したメッセージと同じ内容を返信
  const echo = { type: 'text', text: event.message.text };
  return client.replyMessage(event.replyToken, echo);
}

// Firebase Functionsとしてエクスポート
exports.app = functions.https.onRequest(app);

として保存

firebase deploy --only functions

を実行したが

=== Deploying to 'voicelinebot'...

i  deploying functions
i  functions: preparing codebase default for deployment
i  functions: preparing codebase line-bot for deployment
i  functions: ensuring required API cloudfunctions.googleapis.com is enabled...
i  functions: ensuring required API cloudbuild.googleapis.com is enabled...
i  artifactregistry: ensuring required API artifactregistry.googleapis.com is enabled...
⚠  functions: missing required API cloudbuild.googleapis.com. Enabling now...
⚠  artifactregistry: missing required API artifactregistry.googleapis.com. Enabling now...
✔  functions: required API cloudfunctions.googleapis.com is enabled

Error: Your project voicelinebot must be on the Blaze (pay-as-you-go) plan to complete this command. Required API artifactregistry.googleapis.com can't be enabled until the upgrade is complete. To upgrade, visit the following URL:

となる

以下はGPTでの回答

## **原因の説明**

このエラーは、**Firebaseプロジェクトが無料のSparkプランではなく、
有料のBlaze(従量課金)プランにアップグレードする必要がある**ことを示しています。

具体的な原因は以下のとおりです:

– **Node.js 18を使用したFirebase Functionsのデプロイ**:

– 現在、Firebase Functionsで**Node.js 18**を使用する場合、
**Artifact Registry API**と**Cloud Build API**が必要です。
– これらのAPIは、**Blazeプランでのみ利用可能**です。

– **必要なAPIの有効化**:

– エラーメッセージにある`artifactregistry.googleapis.com`や
`cloudbuild.googleapis.com`は、Blazeプランでないと有効化できません。

## **解決策**

### **オプション1:FirebaseプロジェクトをBlazeプランにアップグレードする**

– **手順**:

1. **Firebaseコンソールにアクセス**:

– エラーメッセージに記載されているURL、または[Firebaseコンソール](https://console.firebase.google.com/)にアクセスします。

2. **プロジェクトを選択**:

3. **プランのアップグレード**:

– 左側のメニューから「**Usage and billing(使用量と請求)**」
または「**Billing(請求)**」を選択します。

– 「**Sparkプラン**」から「**Blazeプラン**」にアップグレードします。
– クレジットカード情報を入力する必要がありますが、**無料利用枠内であれば料金は発生しません**。

– **メリット**:

– **Node.js 18を引き続き使用**できます。
– **最新のランタイム環境**で開発とデプロイが可能です。

– **注意点**:

– **従量課金制**であるため、無料枠を超えると料金が発生します。
– **料金が発生する可能性**があるため、費用管理を適切に行う必要があります。

### **オプション2:Node.jsのバージョンを16にダウングレードする**

– **注意点**:

– **Node.js 16は2024年9月11日にEOL(サポート終了)**となります。
– **将来的にアップグレードが必要**になるため、一時的な解決策となります。

## **補足情報**

### **Blazeプランについて**

– **無料利用枠は引き続き適用**されます。
– **実際に使用した分だけ**料金が発生します。
– **無料枠を超過した場合**のみ課金されます。

### **料金の管理**

– **Firebaseコンソールで予算アラート**を設定できます。
– **費用上限を設定**することで、予期せぬ料金発生を防止できます。

つまりBlazeプラン使用にしないとwebhookはできないらしい

linebotへ送信

linebotへ送信

実行環境
M1 MacbookAir 16GB

Ollamaで修正されたテキストをLINE botに送信するため
既存のLINE Notifyの処理と合わせて、修正された文章をLINE botに送信する処理を実装

LINE botにメッセージを送信するには、LINE Messaging APIを利用する

LINE botにメッセージを送信するためのモジュールを作る

touch line_bot_sender.py

中身は

# line_bot_sender.py
import requests
import json

class LineBotSender:
    def __init__(self, config_file_path):
        self.config = self._load_config(config_file_path)
        self.channel_access_token = self.config["line_bot_channel_access_token"]
        self.user_id = self.config["line_bot_user_id"]

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

    def send_message(self, message):
        url = "https://api.line.me/v2/bot/message/push"
        headers = {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {self.channel_access_token}"
        }
        payload = {
            "to": self.user_id,
            "messages": [
                {
                    "type": "text",
                    "text": message
                }
            ]
        }
        response = requests.post(url, headers=headers, json=payload)
        if response.status_code != 200:
            raise Exception(f"Error sending message to LINE bot: {response.status_code}, {response.text}")

として保存

config.json にLINE botの情報を追加
{
    "line_notify_token": "YOUR_LINE_NOTIFY_TOKEN",
    "ollama_model": "elyza:jp8b",
    "line_bot_channel_access_token": "YOUR_LINE_BOT_CHANNEL_ACCESS_TOKEN",
    "line_bot_user_id": "TARGET_USER_ID"
}

linebot useridがわからなかったため検索

開発者が自分自身のユーザーIDを取得する

を参考に

LINE Developersコンソール

基本情報のプロバイダーIDを入れてみる

次にOllamaで修正されたテキストをLINE botに送信するために、メインスクリプトにLineBotSenderを組み込む

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

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

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

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

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

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

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

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

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

if __name__ == "__main__":
    main()

を書き換えるとプロンプト対処があとで困るので
これをコピーし main2.pyとしてコードを記述

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

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

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

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

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

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

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

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

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

if __name__ == "__main__":
    main()

これで実行したら

入力された音声テキスト一覧:
本日の天候
Traceback (most recent call last):
  File "/Users/snowpool/aw10s/linebot/main2.py", line 63, in <module>
    main()
  File "/Users/snowpool/aw10s/linebot/main2.py", line 58, in main
    line_bot_sender.send_message(f"修正された音声テキスト:\n{message}")
  File "/Users/snowpool/aw10s/linebot/line_bot_sender.py", line 32, in send_message
    raise Exception(f"Error sending message to LINE bot: {response.status_code}, {response.text}")
Exception: Error sending message to LINE bot: 400, {"message":"The property, 'to', in the request body is invalid (line: -, column: -)"}

となった

config.jsonに
  "channel_secret": "YOUR_CHANNEL_SECRET",

を追加する

次に
line_bot_sender.pyを修正する
line_bot_sender.py ファイルで、
send_message メソッド内のリクエストボディに ‘to’ フィールドを追加し、
config.jsonから読み込んだ user_id を設定

    def send_message(self, message):
        url = "https://api.line.me/v2/bot/message/push"
        headers = {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {self.channel_access_token}"
        }
        payload = {
            "to": self.user_id,
            "messages": [
                {
                    "type": "text",
                    "text": message
                }
            ]
        }
        response = requests.post(url, headers=headers, json=payload)
        if response.status_code != 200:
            raise Exception(f"Error sending message to LINE bot: {response.status_code}, {response.text}")

def send_message(self, message):
    headers = {
        'Content-Type': 'application/json',
        'Authorization': f'Bearer {self.channel_access_token}'
    }
    data = {
        "to": self.user_id,  # 追加
        "messages": [
            {
                "type": "text",
                "text": message
            }
        ]
    }
    response = requests.post(self.api_endpoint, headers=headers, json=data)
    if response.status_code != 200:
        raise Exception(f"Error sending message to LINE bot: {response.status_code}, {response.text}")

に変更

line_bot_sender.py のクラス内で user_id を読み込むように変更

# line_bot_sender.py
import requests
import json

class LineBotSender:
    def __init__(self, config_path):
        with open(config_path, 'r') as file:
            config = json.load(file)
        self.channel_access_token = config.get('channel_access_token')
        self.channel_secret = config.get('channel_secret')
        self.user_id = config.get('user_id')  # 追加
        self.api_endpoint = 'https://api.line.me/v2/bot/message/push'  # push APIを使用


    def _load_config(self, config_file_path):
        with open(config_file_path, 'r') as file:
            return json.load(file)
    def send_message(self, message):
        headers = {
            'Content-Type': 'application/json',
            'Authorization': f'Bearer {self.channel_access_token}'
        }
        data = {
            "to": self.user_id,  # 追加
            "messages": [
                {
                    "type": "text",
                    "text": message
                }
            ]
        }
        response = requests.post(self.api_endpoint, headers=headers, json=data)
        if response.status_code != 200:
            raise Exception(f"Error sending message to LINE bot: {response.status_code}, {response.text}")

というようにした

これだけだとまだwebhook設定ができていないのでエラーになる

LINEBotの設定

LINEBotの設定

udemyの講習
ChatGPTを用いたLINEBot開発入門-基本的な機能からPDFを用いたQ&Aまで、クラウド上での開発方法を徹底解説
これによれば
LINEから cloud functionsのURLにリクエスト送信

これはトリガーとしてcloud functions で関数が実行される

今回はLINEからのhttpリクエストをトリガーとして実行される

LINEではユーザがメッセージを送ると
指定したURLにリクエストを送信するwebhookという機能がある

このwebhookを使うことで
Lineから CFにリクエスト送信できる

CFを実行すると
返答文章が生成されるので
それをLINEサーバーに返すことで
ユーザのトークルームに返答が表示される

この場合CFで行っているのは2つ
LINEからのリクエストを受け取る
文章を生成してLINEに返答文を送信

CF側では
リクエストがLINEから来てるのか認証する
あと
返答文を返す時に
LINEのトークルームにメッセージを送る権限があるかを認証する

CFとLINE連携には認証が必要

これは
Channel secret
LINEからのメッセージの証明
Channel access token
LINEに権限を持っている証明
が必要

これらの認証関連は
MessaginAPIを使う

https://developers.line.biz/ja/
にログイン

プロバイダを作成するので
任意の名前を入力

会社・事業者の所在国・地域
を日本にして

チャネル名を設定

チャネル説明

音声入力でLINEを行い、返信も音声で行う

大業種

個人でOK

小業種

個人(ITコンピュータ)
とした

次に
MessagingAPI 設定をクリック

webhookは後でfirebaseを設定してURLを取得する

チャンネルアクセストークンを発行する

チャネル基本設定で
チャネルシークレットの取得ができる

Ollamaで音声入力テキストの修正をする

実行環境
M1 MacbookAir 16GB

Ollamaで音声入力テキストの修正をする

Ollamaを使って日本語として自然な文章に構成する部分を追加

 touch ollama_text_correction.py

でファイルを作成

# ollama_text_correction.py
import requests
import json

class OllamaTextCorrector:
    def __init__(self, config_file_path):
        self.config = self._load_config(config_file_path)
        self.model = self.config.get("ollama_model", "elyza:jp8b")  # 使用するモデルを指定
    
    def _load_config(self, config_file_path):
        with open(config_file_path, 'r') as file:
            config = json.load(file)
        return config
    
    def correct_text(self, text):
        url = "http://localhost:11434/api/generate"  # OllamaのAPIエンドポイント
        headers = {
            "Content-Type": "application/json"
        }
        payload = {
            "model": self.model,
            "prompt": f"以下の文を正しい日本語に構成してください:\n{text}"
        }
        response = requests.post(url, headers=headers, json=payload)
        if response.status_code == 200:
            corrected_text = response.json()["text"]
            return corrected_text.strip()
        else:
            raise Exception(f"Error from Ollama API: {response.status_code}, {response.text}")

config.json にOllamaのモデル設定を追加
{
  "token": "LINE notify の token",
  "ollama_model": "elyza:jp8b"
}

音声入力後にOllamaを使ってテキストを修正するように、メインスクリプトを更新

import sounddevice as sd
from module.module_whisper import FasterWhisperModel
from module.module_recorder import Recorder
import time
from line_notify import LineNotify  # 作成したLineNotifyモジュールをインポート

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

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

    # LINE Notifyのモジュールを初期化(config.jsonからトークンを読み込む)
    line_notify = LineNotify("config.json")

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

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

        if audio_data is None:
            print("無音状態が続いたため、ループを終了します。")
            break  # 無音でループを抜ける
        
        # 音声をテキストに変換
        text = fasterWhispermodel.audio2text(audio_data)
        recognized_texts.append(text)  # テキストをリストに追加
        print(text)

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

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

if __name__ == "__main__":
    main()

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

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

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

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

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

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

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

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

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

if __name__ == "__main__":
    main()

に変更

しかし

[2024-09-14 00:48:22.490] [ctranslate2] [thread 305229] [warning] The compute type inferred from the saved model is float16, but the target device or backend do not support efficient float16 computation. The model weights have been automatically converted to use the float32 compute type instead.
stand by ready OK
recording...
finished
Traceback (most recent call last):
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/requests/models.py", line 963, in json
    return complexjson.loads(self.content.decode(encoding), **kwargs)
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/json/__init__.py", line 346, in loads
    return _default_decoder.decode(s)
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/json/decoder.py", line 340, in decode
    raise JSONDecodeError("Extra data", s, end)
json.decoder.JSONDecodeError: Extra data: line 2 column 1 (char 94)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/linebot/main.py", line 55, in <module>
    main()
  File "/Users/snowpool/aw10s/linebot/main.py", line 38, in main
    corrected_text = text_corrector.correct_text(text)
  File "/Users/snowpool/aw10s/linebot/ollama_text_correction.py", line 26, in correct_text
    corrected_text = response.json()["text"]
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/requests/models.py", line 971, in json
    raise RequestsJSONDecodeError(e.msg, e.doc, e.pos)
requests.exceptions.JSONDecodeError: Extra data: line 2 column 1 (char 94)

となる

ollama_text_correction.py の
correct_text 関数にデバッグ用の出力を追加し、レスポンスの内容をテキストで表示

# ollama_text_correction.py
import requests
import json

class OllamaTextCorrector:
    def __init__(self, config_file_path):
        self.config = self._load_config(config_file_path)
        self.model = self.config.get("ollama_model", "elyza:jp8b")  # 使用するモデルを指定
    
    def _load_config(self, config_file_path):
        with open(config_file_path, 'r') as file:
            config = json.load(file)
        return config
    
    def correct_text(self, text):
        url = "http://localhost:11434/api/generate"  # OllamaのAPIエンドポイント
        headers = {
            "Content-Type": "application/json"
        }
        payload = {
            "model": self.model,
            "prompt": f"以下の文を正しい日本語に構成してください:\n{text}"
        }
        response = requests.post(url, headers=headers, json=payload)
        
        # レスポンスをテキストで表示して確認
        print(f"API Response: {response.text}")

        # レスポンスがJSONとして正しいか確認
        try:
            corrected_text = response.json()["text"]
            return corrected_text.strip()
        except json.JSONDecodeError as e:
            print(f"JSONDecodeError: {e}")
            return None

実行結果は

[2024-09-14 00:50:32.751] [ctranslate2] [thread 306834] [warning] The compute type inferred from the saved model is float16, but the target device or backend do not support efficient float16 computation. The model weights have been automatically converted to use the float32 compute type instead.
stand by ready OK
recording...
finished
API Response: {"model":"elyza:jp8b","created_at":"2024-09-13T15:50:43.828714Z","response":"独","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:43.915193Z","response":"り","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:43.999869Z","response":"言","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:44.084866Z","response":"のような","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:44.170081Z","response":"短","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:44.254747Z","response":"い","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:44.341826Z","response":"文章","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:44.428313Z","response":"ですが","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:44.513551Z","response":"、","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:44.599198Z","response":"問題","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:44.6867Z","response":"ない","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:44.775178Z","response":"です","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:44.864271Z","response":"。","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:44.951287Z","response":"正","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:45.037784Z","response":"しい","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:45.123048Z","response":"日本","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:45.21019Z","response":"語","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:45.297796Z","response":"に","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:45.384251Z","response":"構","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:45.471506Z","response":"成","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:45.56044Z","response":"した","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:45.64597Z","response":"文","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:45.732028Z","response":"は","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:45.821103Z","response":"次","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:45.908953Z","response":"の","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:45.997249Z","response":"通り","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:46.088031Z","response":"です","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:46.183558Z","response":"。\n\n","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:46.277991Z","response":"「","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:46.370159Z","response":"天","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:46.46265Z","response":"気","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:46.563932Z","response":"」","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:46.664274Z","response":"","done":true,"done_reason":"stop","context":[128006,882,128007,271,88852,16144,17161,30512,37656,102800,102433,102158,20230,106391,13153,39926,72315,512,36827,95221,128009,128006,78191,128007,271,106063,31431,78244,120950,106649,16995,83125,119627,5486,109606,100604,38641,1811,37656,102800,102433,102158,20230,106391,13153,56051,17161,15682,33671,16144,121640,38641,3490,13177,36827,95221,10646],"total_duration":6901465958,"load_duration":40178541,"prompt_eval_count":26,"prompt_eval_duration":4023220000,"eval_count":33,"eval_duration":2836184000}

JSONDecodeError: Extra data: line 2 column 1 (char 94)
None
stand by ready OK
recording...
finished
10秒間音声が入力されなかったため、ループを終了します。
Traceback (most recent call last):
  File "/Users/snowpool/aw10s/linebot/main.py", line 55, in <module>
    main()
  File "/Users/snowpool/aw10s/linebot/main.py", line 45, in main
    message = "\n".join(recognized_texts)
TypeError: sequence item 0: expected str instance, NoneType found

となる

このエラーは、2つの問題が原因です。
1. Ollama APIのレスポンスが複数行にわたっていることが原因:
response.json()が一つのJSONオブジェクトを期待しているのに対して、
複数行のJSONレスポンスが返されています。

これは、Ollamaが複数の部分に分けてレスポンスを返しているためです。
2. NoneTypeがrecognized_textsに含まれていることが原因:
correct_text関数でNoneが返され、
recognized_textsリストに追加されているため、TypeErrorが発生しています。

1. Ollamaのレスポンスを段階的に収集する:
複数のJSONオブジェクトが連続して返されている場合、手動でそれを収集し、
一つの完全なテキストに結合する処理を追加します。

2. Noneの扱いを改善する: Noneがリストに追加されないように修正します。

なので

ollama_text_correction.py

を修正

Ollamaのレスポンスを部分的に受け取り、テキストを結合するようにする

# ollama_text_correction.py
import requests
import json

class OllamaTextCorrector:
    def __init__(self, config_file_path):
        self.config = self._load_config(config_file_path)
        self.model = self.config.get("ollama_model", "elyza:jp8b")  # 使用するモデルを指定
    
    def _load_config(self, config_file_path):
        with open(config_file_path, 'r') as file:
            config = json.load(file)
        return config
    
    def correct_text(self, text):
        url = "http://localhost:11434/api/generate"  # OllamaのAPIエンドポイント
        headers = {
            "Content-Type": "application/json"
        }
        payload = {
            "model": self.model,
            "prompt": f"以下の文を正しい日本語に構成してください:\n{text}"
        }
        response = requests.post(url, headers=headers, json=payload)

        # レスポンスをテキストで確認して、複数の部分を結合
        full_response = ""
        for line in response.text.splitlines():
            try:
                json_line = json.loads(line)  # 各行をJSONとして処理
                if "response" in json_line:
                    full_response += json_line["response"]  # テキスト部分を結合
            except json.JSONDecodeError as e:
                print(f"JSONDecodeError: {e}")

        return full_response.strip() if full_response else None

そしてmain.py
Noneがリストに追加されないように修正

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

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

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

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

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

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

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

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

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

if __name__ == "__main__":
    main()

これで実行し
音声を
まともに動きますか
と入力すると

入力された音声テキスト一覧:
「まともに動きますか」は、少々不自然な表現です。
より自然で適切な表現は、「正常に動きますか」「問題なく動きますか」などになります。

「まとも」は通常、「正当・真正」という意味合いで用いられ、
物事の本来あるべき姿や道理に反しないことを示します。
例えば、「彼はまともな理由で解雇されたわけではなかった」のように使います。

一方、「動く」は「正常に機能する・問題なく作動する」という意味合いで用いられます。
ですから、文中で「まとも」を用いる必要性が低く、
より適切な表現を選ぶと自然な日本語になります。

というようになる

意図したのは
「正常に動きますか」「問題なく動きますか
というように変換してほしい

なのでプロンプトを変更する