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を発行すれば解決するはず

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

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表記と混在しているため

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

テキストファイルを読み込み 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("有効なイベントがありません。")

とした

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

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

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

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

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

というようになる

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

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