# vim: nosmartindent autoindent import json import random import subprocess import sys import time from datetime import datetime, timedelta import emoji import ephem import pygame import pygame.gfxdraw import pytchat from playsound import playsound from pygame.locals import * from aques import Aques from common_const import * from common_module import CommonModule from mode import Mode from talk import Talk from youtube import * class Main: @classmethod def main ( cls, argv: list, argc: int) \ -> None: mode = Mode.NIZIKA match (argc > 1) and argv[1]: case '-g': mode = Mode.GOATOH case '-w': mode = Mode.DOUBLE nizika_mode: bool = mode == Mode.NIZIKA goatoh_mode: bool = mode == Mode.GOATOH double_mode: bool = mode == Mode.DOUBLE print (mode) # ウィンドゥの初期化 pygame.init () screen: pygame.Surface = pygame.display.set_mode ( (CWindow.WIDTH, CWindow.HEIGHT)) # 大月ヨヨコの観測値 observer = ephem.Observer () observer.lat, observer.lon = '35', '139' # き太く陽オブジェクト sun = ephem.Sun () # 大月ヨヨコ・オブジェクト moon = ephem.Moon () # 吹き出し balloon = pygame.transform.scale (pygame.image.load ('talking.png'), (CWindow.WIDTH, 384)) if goatoh_mode: balloon = pygame.transform.flip (balloon, False, True) # 背景(昼) bg_day: pygame.Surface = pygame.transform.scale ( pygame.image.load ('bg.jpg'), (CWindow.WIDTH, CWindow.HEIGHT)) # 背景(夕方) bg_evening: pygame.Surface = pygame.transform.scale ( pygame.image.load ('bg-evening.jpg'), (CWindow.WIDTH, CWindow.HEIGHT)) # 背景(夜) bg_night: pygame.Surface = pygame.transform.scale ( pygame.image.load ('bg-night.jpg'), (CWindow.WIDTH, CWindow.HEIGHT)) # 背景の草 bg_grass: pygame.Surface = pygame.transform.scale ( pygame.image.load ('bg-grass.png'), (CWindow.WIDTH, CWindow.HEIGHT)) # き太く陽 kita: pygame.Surface = pygame.transform.scale ( pygame.image.load ('sun.png'), (200, 200)) # 大月ヨヨコ jojoko: pygame.Surface = pygame.transform.scale ( pygame.image.load ('moon.png'), (200, 200)) # 音声再生器の初期化 pygame.mixer.init (frequency = 44100) # ニジカの “ぬ゛ぅ゛ぅ゛ぅ゛ん゛” noon = pygame.mixer.Sound ('noon.wav') # ゴートうの “ムムムム” mumumumu = pygame.mixer.Sound ('mumumumu.wav') # ゴートうの “クサタベテル!!” kusa = pygame.mixer.Sound ('kusa.wav') # YouTube Chat オブジェクト live_chat = pytchat.create (video_id = YOUTUBE_ID) # デバッグ・メシジのフォント system_font = pygame.font.SysFont ('notosanscjkjp', 24, bold = True) # 視聴者コメントのフォント user_font = pygame.font.SysFont ('notosanscjkjp', 32, italic = True) # ニジカのフォント nizika_font = pygame.font.SysFont ('07nikumarufont', 50) # Youtube Chat から取得したコメントたち chat_items: list = [] # 会話の履歴 histories: list = [] while (True): # 観測地の日づけ更新 observer.date: datetime = datetime.now ().date () # 日の出開始 sunrise_start: datetime = ( (ephem.localtime (observer.previous_rising (sun)) - timedelta (minutes = 30))) # 日の出終了 sunrise_end: datetime = sunrise_start + timedelta (hours = 1) # 日の入開始 sunset_start: datetime = ( (ephem.localtime (observer.next_setting (sun)) - timedelta (minutes = 30))) # 日の入終了 sunset_end: datetime = sunset_start + timedelta (hours = 1) # 時刻つき観測地 observer_with_time: ephem.Observer = observer observer_with_time.date = datetime.now () - timedelta (hours = 9) # 日の角度 sun.compute (observer_with_time) sun_alt: float = CommonModule.rad_to_deg (sun.alt) # 月の角度 moon.compute (observer_with_time) moon_alt: float = CommonModule.rad_to_deg (moon.alt) # 月齢 new_moon_dt: datetime = ephem.localtime ( ephem.previous_new_moon (observer_with_time.date)) moon_days_old: float = ( (datetime.now () - new_moon_dt).total_seconds () / 60 / 60 / 24) # 背景描画 cls.draw_bg (screen, bg_day, bg_evening, bg_night, bg_grass, kita, jojoko, sunrise_start, sunrise_end, sunset_start, sunset_end, sun_alt, moon_alt, moon_days_old) # 左上に時刻表示 for i in range (4): screen.blit ( system_font.render (str (datetime.now ()), True, (0, 0, 0)), (i % 2, i // 2 * 2)) if live_chat.is_alive (): # Chat オブジェクトが有効 # Chat 取得 chat_items: list = live_chat.get ().items if chat_items: # 溜まってゐる Chat からランダムに 1 つ抽出 chat_item = random.choice (chat_items) # 投稿者情報を辞書化 chat_item.author = chat_item.author.__dict__ # 絵文字を復元 chat_item.message = emoji.emojize (chat_item.message) message: str = chat_item.message if nizika_mode: goatoh_talking = False if goatoh_mode: goatoh_talking = True if double_mode: goatoh_talking: bool = random.random () < .5 while True: # ChatGPT API を呼出し,返答を取得 answer: str = Talk.main (message, chat_item.author['name'], histories, goatoh_talking).replace ('\n', ' ') # 履歴に追加 histories = (histories + [{'role': 'user', 'content': message}, {'role': 'assistant', 'content': answer}])[(-12):] # ログ書込み with open ('log.txt', 'a') as f: f.write (f'{datetime.now ()}\t' + f'{json.dumps (chat_item.__dict__)}\t' + f'{answer}\n') # 吹出し描画(ニジカは上,ゴートうは下) if nizika_mode: screen.blit (balloon, (0, 0)) if goatoh_mode: screen.blit (balloon, (0, 384)) if double_mode: screen.blit (pygame.transform.flip ( balloon, not goatoh_talking, False), (0, 0)) # 視聴者コメント描画 screen.blit ( user_font.render ( '> ' + (message if (CommonModule.len_by_full ( message) <= 21) else (CommonModule.mid_by_full ( message, 0, 19.5) + '...')), True, (0, 0, 0)), (120, 70 + 384) if goatoh_mode else (120 + (64 if (double_mode and not goatoh_talking) else 0), 70)) # ニジカの返答描画 screen.blit ( nizika_font.render ( (answer if CommonModule.len_by_full (answer) <= 16 else CommonModule.mid_by_full (answer, 0, 16)), True, (192, 0, 0)), (100, 150 + 384) if goatoh_mode else (100 + (64 if (double_mode and not goatoh_talking) else 0), 150)) if CommonModule.len_by_full (answer) > 16: screen.blit ( nizika_font.render ( (CommonModule.mid_by_full (answer, 16, 16) if CommonModule.len_by_full (answer) <= 32 else (CommonModule.mid_by_full ( answer, 16, 14.5) + '...')), True, (192, 0, 0)), (100, 200 + 384) if goatoh_mode else (100 + (64 if (double_mode and not goatoh_talking) else 0), 200)) pygame.display.update () # 鳴く. if goatoh_talking: if random.random () < .1: kusa.play () else: mumumumu.play () else: noon.play () time.sleep (1.5) # 返答の読上げを WAV ディタとして生成,取得 try: wav: bytearray | None = Aques.main (answer, goatoh_talking) except: wav: None = None # 読上げを再生 if wav is not None: with open ('./nizika_talking.wav', 'wb') as f: f.write (wav) playsound ('./nizika_talking.wav') time.sleep (1) if not double_mode or random.random () < .5: break cls.draw_bg (screen, bg_day, bg_evening, bg_night, bg_grass, kita, jojoko, sunrise_start, sunrise_end, sunset_start, sunset_end, sun_alt, moon_alt, moon_days_old) chat_item.author = {'name': 'ゴートうひとり' if goatoh_talking else '伊地知ニジカ', 'id': '', 'imageUrl': './favicon-goatoh.ico' if goatoh_talking else './favicon.ico'} chat_item.message = histories.pop (-1)['content'] message = chat_item.message goatoh_talking = not goatoh_talking else: # Chat オブジェクトが無効 # 再生成 live_chat = pytchat.create (video_id = YOUTUBE_ID) pygame.display.update () for event in pygame.event.get (): if event.type == QUIT: pygame.quit () sys.exit () @classmethod def draw_bg ( cls, screen: pygame.Surface, bg_day: pygame.Surface, bg_evening: pygame.Surface, bg_night: pygame.Surface, bg_grass: pygame.Surface, kita: pygame.Surface, jojoko_original: pygame.Surface, sunrise_start: datetime, sunrise_end: datetime, sunset_start: datetime, sunset_end: datetime, sun_alt: float, moon_alt: float, moon_days_old: float) \ -> None: sunrise_centre: datetime = ( sunrise_start + (sunrise_end - sunrise_start) / 2) sunset_centre: datetime = ( sunset_start + (sunset_end - sunset_start) / 2) jojoko: pygame.Surface = cls.get_jojoko (jojoko_original, moon_days_old) dt: datetime = datetime.now () if sunrise_centre <= dt < sunset_centre: screen.blit (bg_day, (0, 0)) else: screen.blit (bg_night, (0, 0)) if sunrise_start <= dt < sunrise_end: bg_evening.set_alpha (255 - ((abs (dt - sunrise_centre) * 510) / (sunrise_end - sunrise_centre))) elif sunset_start <= dt < sunset_end: bg_evening.set_alpha (255 - ((abs (dt - sunset_centre) * 510) / (sunset_end - sunset_centre))) else: bg_evening.set_alpha (0) if sunrise_start <= dt < sunset_end: jojoko.set_alpha (255 - 255 / 15 * abs (moon_days_old - 15)) else: jojoko.set_alpha (255) screen.blit (bg_evening, (0, 0)) if -10 <= moon_alt < 40: y = ((CWindow.HEIGHT / 2 + 100) - (CWindow.HEIGHT / 2 + 200) / 30 * moon_alt) screen.blit (jojoko, jojoko.get_rect (center = (400, y))) screen.blit (bg_grass, (0, 0)) if -10 <= sun_alt < 40: y = ((CWindow.HEIGHT / 2 + 100) - (CWindow.HEIGHT / 2 + 200) / 30 * sun_alt) screen.blit (kita, kita.get_rect (center = (400, y))) screen.blit (bg_grass, (0, 0)) @classmethod def get_jojoko ( cls, jojoko_original: pygame.Surface, moon_days_old: float) \ -> pygame.Surface: jojoko: pygame.Surface = jojoko_original.copy () jojoko.set_colorkey ((0, 255, 0)) for i in range (200): if 1 <= moon_days_old < 7: pygame.gfxdraw.bezier (jojoko, ((0, 100 + i), (100, 180 * moon_days_old / 7 - 80 + i), (200, 100 + i)), 3, (0, 255, 0)) elif moon_days_old < 14: pygame.gfxdraw.bezier (jojoko, ((0, 100 + i), (100, 180 * moon_days_old / 7 - 80 + i), (200, 100 + i)), 3, (0, 255, 0)) elif moon_days_old < 15: pass elif moon_days_old < 22: pygame.gfxdraw.bezier (jojoko, ((0, 100 - i), (100, 180 * (moon_days_old - 15) / 7 - 80 - i), (200, 100 - i)), 3, (0, 255, 0)) elif moon_days_old < 30: pygame.gfxdraw.bezier (jojoko, ((0, 100 - i), (100, 180 * (moon_days_old - 15) / 7 - 80 - i), (200, 100 - i)), 3, (0, 255, 0)) else: jojoko.fill ((0, 255, 0)) return jojoko if __name__ == '__main__': Main.main (sys.argv, len (sys.argv))