Googleカレンダーの読み上げ
一週間の予定の取得はできたので
次はvoicevox で読み上げをする
今回も docker で起動して実験する
その前に
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | import os import datetime import pytz from google.oauth2.credentials import Credentials from googleapiclient.discovery import build # カレンダーAPIのスコープ 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() |
をモジュール化する
カレンダーの予定を取得する関数を別ファイルとして整理し、他のスクリプトからインポートできるようにする
1 | touch calendar_utils.py |
で
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | import os import datetime import pytz from google.oauth2.credentials import Credentials from googleapiclient.discovery import build # カレンダーAPIのスコープ 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関数を作成
また
1 2 3 4 5 6 7 8 | 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を返した場合(トークンがない場合)や、予定がない場合の処理をする
次に
メインスクリプトからモジュールをインポートして使用
1 | touch main_script.py |
内容は
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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() |
これで
1 2 3 | python main_script.py 2024-10-11T00:06:41.572834+09:00 から 2024-10-14T00:06:41.572834+09:00 までの予定を取得します。 今週の残りの予定はありません。 |
というように同じ結果が出ればOK
次に voievox
これは以前作成したプロジェクトの中を参考に行う
まず
1 | ssh -i . ssh /ubuntu22 snowpool@192.168.1.69 |
でログイン
1 | docker run -d -p '192.168.1.69:50021:50021' voicevox /voicevox_engine :cpu-ubuntu20.04-lates |
で起動
次にコード変更
calendar_utils.pyに音声合成の関数を追加し、カレンダー予定をVoiceVox経由で音声ファイルとして保存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | import os import datetime import pytz import requests from google.oauth2.credentials import Credentials from googleapiclient.discovery import build # カレンダーAPIのスコープ 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( "クエリの作成に失敗しました" ) |
次に
カレンダーの予定を取得し、各予定を音声に変換する処理をメインスクリプトに追加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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で読み上げるようにする
1 | calendar_utils.py |
を
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | import os import datetime import pytz import requests from google.oauth2.credentials import Credentials from googleapiclient.discovery import build 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( "クエリの作成に失敗しました" ) |
として
1 | main_script.py |
を
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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() |
として保存
これで実行すると
1 | 2024年10月11日(金) - 診断書の取得 |
というように目的通りの音声が作成された
次は作成した音声を再生できるようにする
この時に今週の予定をお知らせします
という音声ファイルを再生するようにする
これは以前作成したものを使う
1 | touch create_voice.py |
で
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | 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" , "--get" , "--data-urlencode" , f "text={text}" ] # 音声ファイルを作成するためのcurlコマンド command_audio = [ "curl" , "-s" , "-H" , "Content-Type: application/json" , "-X" , "POST" , ] # 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) |
として保存
1 | touch voice.txt |
で中身を
1 | 今週の予定をお知らせします |
として保存
1 | python create_voice.py voice.txt 192.168.1.69:50021 |
とすれば
1 | audio_output.wav |
が作成される
これをnotice.wavにファイル名を変更する
そしてこれを再生するようにする
生成された音声ファイルを再生するには、
Pythonのsubprocessモジュールやplaysoundライブラリなどを利用する方法がある
1 | pip install playsound |
でインストール
一週間分の予定を表示した後、保存した音声ファイルを順番に再生する
音声ファイルのリストを作成: 各イベントの音声ファイルを生成した後、
そのファイル名をaudio_filesリストに追加
予定の表示後にファイルを再生:
audio_filesリストの各ファイルを順番に再生しています。
playsound関数を使って音声ファイルを再生し、全ての音声が順番に再生
これで、カレンダーの予定を表示した後に、順次生成された音声ファイルを再生
残りの予定がない場合に音声が再生されないように、
audio_filesリストにファイルが追加されているか確認する条件を追加します。
audio_filesが空でない場合のみ再生処理を行うように、コードを修正
予定がないときは音声ファイルを再生せず、予定がある場合のみリスト内の音声ファイルを再生
予定があり音声ファイルを再生する前に notice.wav を再生
このためには
1 | calendar_utils.py |
を修正する
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | # 音声合成の関数を修正して、生成されたファイル名を返すようにします 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 |
そして
1 | main_script.py |
を
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | 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() |
として保存
これで実行すると
1 2 | 今週の予定をお知らせします 2024年10月11日(金) - 診断書の取得 |
というように読み上げてくれる