Googleカレンダーの読み上げ

Googleカレンダーの読み上げ

一週間の予定の取得はできたので
次はvoicevox で読み上げをする

今回も docker で起動して実験する

その前に

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']

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

をモジュール化する

カレンダーの予定を取得する関数を別ファイルとして整理し、他のスクリプトからインポートできるようにする

touch calendar_utils.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']

def authenticate():
    """Google Calendar APIの認証を行います。"""
    if os.path.exists('token.json'):
        creds = Credentials.from_authorized_user_file('token.json', SCOPES)
        return creds
    else:
        print("トークンファイルが見つかりません。認証を実行してください。")
        return None

def get_upcoming_events(creds, days=7):
    """指定された期間内のGoogleカレンダーの予定を取得します。"""
    # 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=days)

    # 開始日時と終了日時を設定
    time_min = now.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', [])

    return events

これで
calendar_utils.pyに、カレンダー認証を行うauthenticate関数と、
指定された期間の予定を取得するget_upcoming_events関数を作成

また

def authenticate():
    """Google Calendar APIの認証を行います。"""
    if os.path.exists('token.json'):
        creds = Credentials.from_authorized_user_file('token.json', SCOPES)
        return creds
    else:
        print("トークンファイルが見つかりません。認証を実行してください。")
        return None


Noneを返した場合(トークンがない場合)や、予定がない場合の処理をする

次に
メインスクリプトからモジュールをインポートして使用

touch main_script.py

内容は

from calendar_utils import authenticate, get_upcoming_events

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

if __name__ == '__main__':
    main()

これで

python main_script.py 
2024-10-11T00:06:41.572834+09:00 から 2024-10-14T00:06:41.572834+09:00 までの予定を取得します。
今週の残りの予定はありません。

というように同じ結果が出ればOK

次に voievox
これは以前作成したプロジェクトの中を参考に行う

まず

ssh -i .ssh/ubuntu22 snowpool@192.168.1.69

でログイン

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

で起動

次にコード変更
calendar_utils.pyに音声合成の関数を追加し、カレンダー予定をVoiceVox経由で音声ファイルとして保存

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

# カレンダーAPIのスコープ
SCOPES = ['https://www.googleapis.com/auth/calendar']
VOICEVOX_API_URL = "http://192.168.1.69:50021"  # VoiceVoxのAPIサーバーURL

def authenticate():
    """Google Calendar APIの認証を行います。"""
    if os.path.exists('token.json'):
        creds = Credentials.from_authorized_user_file('token.json', SCOPES)
        return creds
    else:
        print("トークンファイルが見つかりません。認証を実行してください。")
        return None

def get_upcoming_events(creds, days=7):
    """指定された期間内のGoogleカレンダーの予定を取得します。"""
    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=days)
    time_min = now.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', [])

    return events

def synthesize_speech(text, speaker=1):
    """VoiceVox APIを使って音声合成を行い、音声ファイルを生成します。"""
    params = {'text': text, 'speaker': speaker}
    response = requests.post(f"{VOICEVOX_API_URL}/audio_query", params=params)
    if response.status_code == 200:
        query_data = response.json()
        synthesis_response = requests.post(f"{VOICEVOX_API_URL}/synthesis", params={'speaker': speaker}, json=query_data)
        if synthesis_response.status_code == 200:
            filename = f"event_voice_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}.wav"
            with open(filename, "wb") as f:
                f.write(synthesis_response.content)
            print(f"音声ファイルを生成しました: {filename}")
        else:
            print("音声の生成に失敗しました")
    else:
        print("クエリの作成に失敗しました")

次に

カレンダーの予定を取得し、各予定を音声に変換する処理をメインスクリプトに追加

from calendar_utils import authenticate, get_upcoming_events, synthesize_speech

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

if __name__ == '__main__':
    main()

実行すると音声ファイルが作成されるが
2024-10-11 – 診断書の取得
の場合はそのまm数字を読み上げるため
年月日に変換が必要
できれば曜日もほしい

Google Calendar APIでは、曜日自体を直接返す項目はありませんが、
予定の開始日時がISO形式の文字列として返されるので、
これをPythonで処理して曜日を取得することができます。
datetimeオブジェクトを使用すれば、APIから取得した日時を簡単に曜日に変換できます
とのこと

日時を変換する関数を追加し、
YYYY-MM-DD形式の日時を「YYYY年M月D日(曜日)」
のように整形してVoiceVoxで読み上げるようにする

calendar_utils.py

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

SCOPES = ['https://www.googleapis.com/auth/calendar']
VOICEVOX_API_URL = "http://192.168.1.69:50021"  # VoiceVoxのAPIサーバーURL

def authenticate():
    if os.path.exists('token.json'):
        creds = Credentials.from_authorized_user_file('token.json', SCOPES)
        return creds
    else:
        print("トークンファイルが見つかりません。認証を実行してください。")
        return None

def get_upcoming_events(creds, days=7):
    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=days)
    time_min = now.isoformat()
    time_max = end_of_week.isoformat()

    events_result = service.events().list(
        calendarId='primary',
        timeMin=time_min,
        timeMax=time_max,
        singleEvents=True,
        orderBy='startTime'
    ).execute()
    events = events_result.get('items', [])

    return events

def format_date_with_weekday(date_str):
    """ 日付文字列を「YYYY年M月D日(曜日)」形式に変換します """
    date_obj = datetime.datetime.fromisoformat(date_str)
    # 曜日を日本語で取得
    weekday = date_obj.strftime("%A")
    weekday_dict = {
        "Monday": "月",
        "Tuesday": "火",
        "Wednesday": "水",
        "Thursday": "木",
        "Friday": "金",
        "Saturday": "土",
        "Sunday": "日"
    }
    weekday_jp = weekday_dict.get(weekday, weekday)  # 日本語の曜日に変換
    formatted_date = date_obj.strftime(f"%Y年%m月%d日({weekday_jp})")
    return formatted_date

def synthesize_speech(text, speaker=1):
    params = {'text': text, 'speaker': speaker}
    response = requests.post(f"{VOICEVOX_API_URL}/audio_query", params=params)
    if response.status_code == 200:
        query_data = response.json()
        synthesis_response = requests.post(f"{VOICEVOX_API_URL}/synthesis", params={'speaker': speaker}, json=query_data)
        if synthesis_response.status_code == 200:
            filename = f"event_voice_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}.wav"
            with open(filename, "wb") as f:
                f.write(synthesis_response.content)
            print(f"音声ファイルを生成しました: {filename}")
        else:
            print("音声の生成に失敗しました")
    else:
        print("クエリの作成に失敗しました")

として

main_script.py

from calendar_utils import authenticate, get_upcoming_events, synthesize_speech, format_date_with_weekday

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

if __name__ == '__main__':
    main()

として保存

これで実行すると

2024年10月11日(金) - 診断書の取得

というように目的通りの音声が作成された

次は作成した音声を再生できるようにする
この時に今週の予定をお知らせします
という音声ファイルを再生するようにする

これは以前作成したものを使う

touch create_voice.py

import subprocess
import sys

def generate_and_play_audio_from_text(file_path, server_ip):
    # テキストファイルからテキストを読み込む
    with open(file_path, 'r') as file:
        text = file.read()

    # JSONファイルを作成するためのcurlコマンド
    command_json = [
        "curl", "-s", "-X", "POST",
        f"http://{server_ip}/audio_query?speaker=1",
        "--get", "--data-urlencode", f"text={text}"
    ]

    # 音声ファイルを作成するためのcurlコマンド
    command_audio = [
        "curl", "-s", "-H", "Content-Type: application/json", "-X", "POST",
        "-d", "@query.json", f"http://{server_ip}/synthesis?speaker=1"
    ]

    # JSONファイルと音声ファイルを作成
    with open('query.json', 'w') as file:
        subprocess.run(command_json, stdout=file)
    with open('audio_output.wav', 'wb') as file:
        subprocess.run(command_audio, stdout=file)

    # 音声ファイルを再生
    subprocess.run(["afplay", "audio_output.wav"])

if __name__ == "__main__":
    if len(sys.argv) < 3:
        print("Usage: python script.py <file_path> <server_ip>")
        sys.exit(1)
    
    file_path = sys.argv[1]
    server_ip = sys.argv[2]
    generate_and_play_audio_from_text(file_path, server_ip)

として保存

touch voice.txt 

で中身を

今週の予定をお知らせします

として保存

python create_voice.py voice.txt 192.168.1.69:50021

とすれば

audio_output.wav

が作成される

これをnotice.wavにファイル名を変更する

そしてこれを再生するようにする

生成された音声ファイルを再生するには、
Pythonのsubprocessモジュールやplaysoundライブラリなどを利用する方法がある

pip install playsound

でインストール

一週間分の予定を表示した後、保存した音声ファイルを順番に再生する
音声ファイルのリストを作成: 各イベントの音声ファイルを生成した後、
そのファイル名をaudio_filesリストに追加

予定の表示後にファイルを再生:
audio_filesリストの各ファイルを順番に再生しています。
playsound関数を使って音声ファイルを再生し、全ての音声が順番に再生

これで、カレンダーの予定を表示した後に、順次生成された音声ファイルを再生

残りの予定がない場合に音声が再生されないように、
audio_filesリストにファイルが追加されているか確認する条件を追加します。
audio_filesが空でない場合のみ再生処理を行うように、コードを修正

予定がないときは音声ファイルを再生せず、予定がある場合のみリスト内の音声ファイルを再生

予定があり音声ファイルを再生する前に notice.wav を再生

このためには

calendar_utils.py

を修正する

# 音声合成の関数を修正して、生成されたファイル名を返すようにします
def synthesize_speech(text, speaker=1):
    params = {'text': text, 'speaker': speaker}
    response = requests.post(f"{VOICEVOX_API_URL}/audio_query", params=params)
    if response.status_code == 200:
        query_data = response.json()
        synthesis_response = requests.post(f"{VOICEVOX_API_URL}/synthesis", params={'speaker': speaker}, json=query_data)
        if synthesis_response.status_code == 200:
            filename = f"event_voice_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}.wav"
            with open(filename, "wb") as f:
                f.write(synthesis_response.content)
            print(f"音声ファイルを生成しました: {filename}")
            return filename  # 生成されたファイル名を返す
        else:
            print("音声の生成に失敗しました")
            return None
    else:
        print("クエリの作成に失敗しました")
        return None

そして

main_script.py

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

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

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

if __name__ == '__main__':
    main()

として保存

これで実行すると

今週の予定をお知らせします
2024年10月11日(金) - 診断書の取得

というように読み上げてくれる

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です