diff --git a/.gitignore b/.gitignore index 8a22514..daa5fc1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ /connection.py -/__pycache__ +__pycache__ /nizika_talking.wav /youtube.py /log.txt diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..6cd7edd --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "nizika_ai"] + path = nizika_ai + url = https://git.miteruzo.com/miteruzo/nizika_ai diff --git a/MusMus-BGM-097.mp3 b/MusMus-BGM-097.mp3 deleted file mode 100644 index 48bc4a0..0000000 Binary files a/MusMus-BGM-097.mp3 and /dev/null differ diff --git a/__init__.py b/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/talking.png b/assets/balloon.png similarity index 100% rename from talking.png rename to assets/balloon.png diff --git a/bg-evening.jpg b/assets/bg-evening.jpg similarity index 100% rename from bg-evening.jpg rename to assets/bg-evening.jpg diff --git a/bg-grass.png b/assets/bg-grass.png similarity index 100% rename from bg-grass.png rename to assets/bg-grass.png diff --git a/bg-night.jpg b/assets/bg-night.jpg similarity index 100% rename from bg-night.jpg rename to assets/bg-night.jpg diff --git a/bg.jpg b/assets/bg.jpg similarity index 100% rename from bg.jpg rename to assets/bg.jpg diff --git a/assets/bgm.mp3 b/assets/bgm.mp3 new file mode 100644 index 0000000..d7049cf Binary files /dev/null and b/assets/bgm.mp3 differ diff --git a/assets/deerjika_relax_left.png b/assets/deerjika_relax_left.png new file mode 100644 index 0000000..dc9d370 Binary files /dev/null and b/assets/deerjika_relax_left.png differ diff --git a/moon.png b/assets/moon.png old mode 100755 new mode 100644 similarity index 100% rename from moon.png rename to assets/moon.png diff --git a/noon.wav b/assets/noon.wav similarity index 100% rename from noon.wav rename to assets/noon.wav diff --git a/oh.wav b/assets/oh.wav similarity index 100% rename from oh.wav rename to assets/oh.wav diff --git a/snack_time.mp4 b/assets/snack_time.mp4 similarity index 100% rename from snack_time.mp4 rename to assets/snack_time.mp4 diff --git a/sun.png b/assets/sun.png similarity index 100% rename from sun.png rename to assets/sun.png diff --git a/bg.mp4 b/bg.mp4 deleted file mode 100644 index 3a4e43a..0000000 Binary files a/bg.mp4 and /dev/null differ diff --git a/bg2.mp4 b/bg2.mp4 deleted file mode 100644 index ec4a096..0000000 Binary files a/bg2.mp4 and /dev/null differ diff --git a/common_const.py b/common_const.py deleted file mode 100644 index 87505c1..0000000 --- a/common_const.py +++ /dev/null @@ -1,8 +0,0 @@ -class CWindow: - WIDTH: int = 1024 - HEIGHT: int = 768 - - -class CMath: - PI: float = 3.14159265358979323846 - diff --git a/common_module.py b/common_module.py index 6787ad5..2867e89 100644 --- a/common_module.py +++ b/common_module.py @@ -1,7 +1,5 @@ import unicodedata -from common_const import * - class CommonModule: @staticmethod @@ -44,4 +42,3 @@ class CommonModule: trimmed_left: str = string[cls.index_by_f2c (string, start):] return trimmed_left[:cls.index_by_f2c (trimmed_left, length)] - diff --git a/connection.sample.py b/connection.sample.py deleted file mode 100644 index 5e9017b..0000000 --- a/connection.sample.py +++ /dev/null @@ -1,7 +0,0 @@ -# 各変数に適切な値を設定し,ファイル名を connection.py として保存すること - -# Organisation ID -OPENAI_ORGANISATION: str = 'org-XXXXXXXXXXXXXXXXXXXXXXXX' - -# API Key -OPENAI_API_KEY: str = 'sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' diff --git a/goatoh.mp4 b/goatoh.mp4 deleted file mode 100644 index 0f6f177..0000000 Binary files a/goatoh.mp4 and /dev/null differ diff --git a/kusa.wav b/kusa.wav deleted file mode 100644 index 1980848..0000000 Binary files a/kusa.wav and /dev/null differ diff --git a/main.py b/main.py index 91c1f61..6d2b1f5 100644 --- a/main.py +++ b/main.py @@ -1,498 +1,995 @@ -# vim: nosmartindent autoindent +from __future__ import annotations -import json import math +import os import random -import subprocess import sys import time +import wave from datetime import datetime, timedelta +from enum import Enum, auto +from io import BytesIO +from typing import Callable, TypedDict +import cv2 import emoji -import ephem # type: ignore +import ephem +import numpy as np import pygame import pygame.gfxdraw -import pytchat # type: ignore -from playsound import playsound -from pygame.locals import * -from youtube import * +import pytchat +import requests +from cv2 import VideoCapture +from ephem import Moon, Observer, Sun +from pygame import Rect, Surface +from pygame.font import Font +from pygame.mixer import Sound +from pygame.time import Clock +from pytchat.core.pytchat import PytchatCore +from pytchat.processors.default.processor import Chat -import play_movie from aques import Aques -from common_const import * from common_module import CommonModule -from mode import Mode -from talk import Talk - - -class Main: - kita_x: float = CWindow.WIDTH / 2 - kita_y: float = 1000000. - kita_arg: float = 0. - - jojoko_x: float = CWindow.WIDTH / 2 - jojoko_y: float = 1000000. - jojoko_arg: float = 0. - - @classmethod - def main ( - cls, - argv: list, - argc: int) \ - -> None: - mode: 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') - - # “あっ!” - deerjika_oh = pygame.mixer.Sound ('oh.wav') - - # おやつタイムのテーマ - snack_time_sound = pygame.mixer.Sound ('snack_time.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 = [] - - # おやつ記録 - has_snack = False - - while True: - # 観測地の日づけ更新 - observer.date = 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 = math.degrees (sun.alt) - sun_az: float = math.degrees (sun.az) - - # 月の角度 - moon.compute (observer_with_time) - moon_alt: float = math.degrees (moon.alt) - moon_az: float = math.degrees (moon.az) - - # 月齢 - 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, sun_az, moon_alt, moon_az, 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 = 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 = 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') - - cls.draw_talking (screen, balloon, user_font, nizika_font, - message, answer, mode, - mode == 3 and goatoh_talking) - - # 鳴く. - if goatoh_talking: - if random.random () < .1: - kusa.play () - else: - mumumumu.play () - else: - noon.play () - - time.sleep (1.5) - - cls.read_out (answer, goatoh_talking) - - 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, sun_az, moon_alt, moon_az, - 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 +from nizika_ai.config import DB +from nizika_ai.consts import Character, GPTModel, Platform, QueryType +from nizika_ai.models import Answer, AnsweredFlag, Query, User + +pygame.init () + +FPS = 30 + +SYSTEM_FONT = pygame.font.SysFont ('notosanscjkjp', 11, bold = True) +USER_FONT = pygame.font.SysFont ('notosanscjkjp', 15, italic = True) +DEERJIKA_FONT = pygame.font.SysFont ('07nikumarufont', 23) + + +def main ( +) -> None: + game = Game () + Bg (game) + balloon = Balloon (game) + deerjika = Deerjika (game, DeerjikaPattern.RELAXED, + x = CWindow.WIDTH * 3 / 4, + y = CWindow.HEIGHT - 56.25, + balloon = balloon) + snack_time = SnackTime (game) + CurrentTime (game, DEERJIKA_FONT) + + try: + broadcast = Broadcast (os.environ['BROADCAST_CODE']) + except Exception: + pass + + waiting_balloon = (False, '', '') + last_flags_poll: float = 0 + + while True: + now_m = time.monotonic () + + for event in pygame.event.get (): + if event.type == pygame.QUIT: + pygame.quit () + sys.exit () + + if (not balloon.enabled) and (not snack_time.enabled): + if waiting_balloon[0]: + deerjika.talk (waiting_balloon[1], waiting_balloon[2]) + waiting_balloon = (False, '', '') + + if now_m - last_flags_poll >= 10: + last_flags_poll = now_m + try: + DB.begin_transaction () + answer_flags = (AnsweredFlag.where ('platform', Platform.YOUTUBE.value) + .where ('answered', False) + .get ()) + if answer_flags: + answer_flag = random.choice (answer_flags) + answer = Answer.find (answer_flag.answer_id) + match QueryType (answer.query_rel.query_type): + case QueryType.YOUTUBE_COMMENT: + query = Query.find (answer.query_id) + deerjika.talk (query.content, answer.content) + answer_flag.answered = True + answer_flag.save () + case QueryType.SNACK_TIME: + snack_time.play () + query = Query.find (answer.query_id) + waiting_balloon = (True, query.content, answer.content) + answer_flag.answered = True + answer_flag.save () + DB.commit () + add_query (broadcast) + except Exception as ex: + DB.rollback () + print (ex) + game.redraw () + + +class Bg: + """ + 背景オブゼクト管理用クラス + + Attributes: + base (BgBase): 最背面 + grass (BgGrass): 草原部分 + jojoko (Jojoko): 大月ヨヨコ + kita (KitaSun): き太く陽 + """ + + base: BgBase + grass: BgGrass + jojoko: Jojoko + kita: KitaSun + + def __init__ ( + self, + game: Game, + ): + self.kita = KitaSun (game) + self.base = BgBase (game, self.kita.sun, layer = self.kita.layer - 5) + self.jojoko = Jojoko (game) + self.grass = BgGrass (game) + + +class DeerjikaPattern (Enum): + """ + ニジカの状態 + + Members: + NORMAL: 通常 + RELAXED: 足パタパタ + SLEEPING: 寝ニジカ + DANCING: ダンシング・ニジカ + """ + + NORMAL = auto () + RELAXED = auto () + SLEEPING = auto () + DANCING = auto () + + +class Direction (Enum): + """ + クリーチャの向き + + Members: + LEFT: 左向き + RIGHT: 右向き + """ + + LEFT = auto () + RIGHT = auto () + + +class Game: + """ + ゲーム・クラス + + Attributes: + clock (Clock): Clock オブゼクト + frame (int): フレーム・カウンタ + last_answered_at (datetime): 最後に回答した時刻 + now (datetime): 基準日時 + objects (list[GameObject]): 再描画するクラスのリスト + screen (Surface): 基底スクリーン + sky (Sky): 天体情報 + """ + + bgm: Sound + clock: Clock + fps: float + frame: int + last_answered_at: datetime + now: datetime + objects: list[GameObject] + screen: Surface + sky: Sky + + def __init__ ( + self, + ): + self.now = datetime.now () + self.screen = pygame.display.set_mode ((CWindow.WIDTH, CWindow.HEIGHT)) + self.clock = Clock () + self.fps = FPS + self.frame = 0 + self.objects = [] + self.bgm = Sound ('./assets/bgm.mp3') + self.bgm.set_volume (.15) + self.bgm.play (loops = -1) + self._create_sky () + + def redraw ( + self, + ) -> None: + self.now = datetime.now () + self.sky.observer.date = self.now - timedelta (hours = 9) + for obj in sorted (self.objects, key = lambda obj: obj.layer): + if obj.enabled: + obj.redraw () + pygame.display.update () + delta_time = self.clock.tick (FPS) / 1000 + self.fps = 1 / delta_time + if delta_time > 1 / FPS: + for _ in range (int (FPS * delta_time) - 1): + for obj in self.objects: + if obj.enabled: + obj.update () + + def _create_sky ( + self, + ) -> None: + self.sky = Sky () + self.sky.observer = Observer () + self.sky.observer.lat = '35' + self.sky.observer.lon = '139' + + +class GameObject: + """ + 各ゲーム・オブゼクトの基底クラス + + Attributes: + arg (float): 回転角度 (rad) + ax (float): X 軸に対する加速度 (px/frame^2) + ay (float): y 軸に対する加速度 (px/frame^2) + enabled (bool): オブゼクトの表示可否 + frame (int): フレーム・カウンタ + game (Game): ゲーム基盤 + height (int): 高さ (px) + vx (float): x 軸に対する速度 (px/frame) + vy (float): y 軸に対する速度 (px/frame) + width (int): 幅 (px) + x (float): X 座標 (px) + y (float): Y 座標 (px) + """ + + arg: float = 0 + ax: float = 0 + ay: float = 0 + enabled: bool = True + frame: int + game: Game + height: int + layer: float + vx: float = 0 + vy: float = 0 + width: int + x: float + y: float + + def __init__ ( + self, + game: Game, + layer: float | None = None, + enabled: bool = True, + x: float = 0, + y: float = 0, + ): + self.game = game + self.enabled = enabled + self.frame = 0 + if layer is None: + if self.game.objects: + layer = max (obj.layer for obj in self.game.objects) + 10 else: - # Chat オブジェクトが無効 - - # 再生成 - live_chat = pytchat.create (video_id = YOUTUBE_ID) - - if has_snack and datetime.now ().hour == 14: - has_snack = False - - pygame.display.update () - - if (not has_snack) and datetime.now ().hour == 15: - has_snack = True - deerjika_oh.play () - time.sleep (0.6) - snack_time_sound.play () - play_movie.main (screen, 'snack_time.mp4') - query = 'おやつタイムだ!!!!' - cls.draw_bg (screen, bg_day, bg_evening, bg_night, bg_grass, - kita, jojoko, - sunrise_start, sunrise_end, sunset_start, sunset_end, - sun_alt, sun_az, moon_alt, moon_az, moon_days_old) - answer = Talk.main (query).replace ('\n', ' ') - cls.draw_talking (screen, balloon, user_font, nizika_font, - query, answer) - noon.play () - time.sleep (1.5) - cls.read_out (answer) - - for event in pygame.event.get (): - if event.type == QUIT: - pygame.quit () - sys.exit () - - @staticmethod - def read_out ( - answer: str, - goatoh: bool = False, + layer = 0 + self.layer = layer + self.x = x + self.y = y + self.game.objects.append (self) + + def redraw ( + self, ) -> None: - # 返答の読上げを WAV ディタとして生成,取得 - wav: bytearray | None - try: - wav = Aques.main (answer, goatoh) - except: - wav = None + self.update () - # 読上げを再生 - if wav is not None: - with open ('./nizika_talking.wav', 'wb') as f: - f.write (wav) - - playsound ('./nizika_talking.wav') - - time.sleep (1) - - @staticmethod - def draw_talking ( - screen: pygame.Surface, - balloon: pygame.Surface, - user_font: pygame.font.Font, - nizika_font: pygame.font.Font, - query: str, - answer: str, - mode: Mode = Mode.NIZIKA, - flip: bool = False, + def update ( + self, ) -> None: - # 吹出し描画(ニジカは上,ゴートうは下) - nizika_mode = False - goatoh_mode = False - double_mode = False - match mode: - case Mode.NIZIKA: - screen.blit (balloon, (0, 0)) - nizika_mode = True - case Mode.GOATOH: - screen.blit (balloon, (0, 384)) - goatoh_mode = True - case Mode.DOUBLE: - screen.blit (pygame.transform.flip (balloon, flip, False), (0, 0)) - double_mode = True - - # 視聴者コメント描画 - screen.blit ( - user_font.render ( - ('> ' + (query - if (CommonModule.len_by_full (query) <= 21) - else (CommonModule.mid_by_full (query, 0, 19.5) + '...'))), - True, (0, 0, 0)), - ((120, 70 + 384) - if goatoh_mode - else (120 + (64 if (double_mode and flip) 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 flip) 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 flip) else 0), 200)) - - pygame.display.update () + self.x += self.vx + self.y += self.vy + self.vx += self.ax + self.vy += self.ay + self.frame += 1 + + +class BgBase (GameObject): + """ + 背景 + + Attributes: + surface (Surface): 背景 Surface + """ + + bg: Surface + bg_evening: Surface + bg_grass: Surface + bg_night: Surface + sun: Sun + + def __init__ ( + self, + game: Game, + sun: Sun, + layer: float, + ): + super ().__init__ (game, layer = layer) + self.bg = self._load_image ('./assets/bg.jpg') + self.bg_evening = self._load_image ('./assets/bg-evening.jpg') + self.bg_grass = self._load_image ('./assets/bg-grass.png') + self.bg_night = self._load_image ('./assets/bg-night.jpg') + self.sun = sun - @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_original: pygame.Surface, - jojoko_original: pygame.Surface, - sunrise_start: datetime, - sunrise_end: datetime, - sunset_start: datetime, - sunset_end: datetime, - sun_alt: float, - sun_az: float, - moon_alt: float, - moon_az: float, - moon_days_old: float, + @staticmethod + def _load_image ( + path: str, + ) -> Surface: + return pygame.transform.scale (pygame.image.load (path), + (CWindow.WIDTH, CWindow.HEIGHT)) + + def redraw ( + self, ) -> None: + date_tmp = self.game.sky.observer.date + self.game.sky.observer.date = self.game.now.date () + sunrise_start: datetime = ( + (ephem.localtime (self.game.sky.observer.previous_rising (self.sun)) + - timedelta (minutes = 30))) + sunrise_end: datetime = sunrise_start + timedelta (hours = 1) sunrise_centre: datetime = ( sunrise_start + (sunrise_end - sunrise_start) / 2) + sunset_start: datetime = ( + (ephem.localtime (self.game.sky.observer.next_setting (self.sun)) + - timedelta (minutes = 30))) + sunset_end: datetime = sunset_start + timedelta (hours = 1) sunset_centre: datetime = ( sunset_start + (sunset_end - sunset_start) / 2) + self.game.sky.observer.date = date_tmp + surface: Surface = ((self.bg + if (sunrise_centre <= self.game.now < sunset_centre) + else self.bg_night) + .copy ()) + if sunrise_start <= self.game.now < sunrise_end: + self.bg_evening.set_alpha (255 - int ((abs (self.game.now - sunrise_centre) * 510) + / (sunrise_end - sunrise_centre))) + elif sunset_start <= self.game.now < sunset_end: + self.bg_evening.set_alpha (255 - int ((abs (self.game.now - sunset_centre) * 510) + / (sunset_end - sunset_centre))) + else: + self.bg_evening.set_alpha (0) + surface.blit (self.bg_evening, (0, 0)) + self.game.screen.blit (surface, (self.x, self.y)) + super ().redraw () - jojoko: pygame.Surface = cls.get_jojoko (jojoko_original, - moon_days_old, moon_alt, moon_az) - - x = CWindow.WIDTH * (sun_az - 80) / 120 - y = ((CWindow.HEIGHT / 2) - - (CWindow.HEIGHT / 2 + 100) * math.sin (math.radians (sun_alt)) / math.sin ( math.radians (60))) - arg = math.degrees (math.atan2 (y - cls.kita_y, x - cls.kita_x)) - cls.kita_x = x - cls.kita_y = y - if abs (arg - cls.kita_arg) > 3: - cls.kita_arg = arg +class BgGrass (GameObject): + """ + 背景の草原部分 - kita: pygame.Surface = pygame.transform.rotate (kita_original, -(90 + cls.kita_arg)) + Attributes: + surface (Surface): 草原 Surface + """ - dt: datetime = datetime.now () + surface: Surface - 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 - int ((abs (dt - sunrise_centre) * 510) - / (sunrise_end - sunrise_centre))) - elif sunset_start <= dt < sunset_end: - bg_evening.set_alpha (255 - int ((abs (dt - sunset_centre) * 510) - / (sunset_end - sunset_centre))) - else: - bg_evening.set_alpha (0) + def __init__ ( + self, + game: Game, + ): + super ().__init__ (game) + self.game = game + self.surface = pygame.image.load ('./assets/bg-grass.png') + self.surface = pygame.transform.scale (self.surface, (CWindow.WIDTH, CWindow.HEIGHT)) - if sunrise_start <= dt < sunset_end: - jojoko.set_alpha (255 - int (255 / 15 * abs (moon_days_old - 15))) - else: - jojoko.set_alpha (255) - - screen.blit (bg_evening, (0, 0)) - - if (moon_az < 220) and (-10 <= moon_alt): - screen.blit (jojoko, jojoko.get_rect (center = (cls.jojoko_x, cls.jojoko_y))) - - screen.blit (bg_grass, (0, 0)) + def redraw ( + self, + ) -> None: + self.game.screen.blit (self.surface, (self.x, self.y)) + super ().redraw () - if (sun_az < 220) and (-10 <= sun_alt): - screen.blit (kita, kita.get_rect (center = (cls.kita_x, cls.kita_y))) - screen.blit (bg_grass, (0, 0)) +class Creature (GameObject): + sound: Sound - @classmethod - def get_jojoko ( - cls, - jojoko_original: pygame.Surface, - moon_days_old: float, - moon_alt: float, - moon_az: float) \ - -> pygame.Surface: - jojoko: pygame.Surface = jojoko_original.copy () + def bell ( + self, + ) -> None: + self.sound.play () + + +class Deerjika (Creature): + """ + 伊地知ニジカ + + Attributes: + height (int): 高さ (px) + scale (float): 拡大率 + surfaces (list[Surface]): ニジカの各フレームを Surface にしたリスト + width (int): 幅 (px) + """ + + FPS = 30 + + height: int + scale: float = 1 + surfaces: list[Surface] + width: int + talking: bool = False + wav: bytearray | None = None + balloon: Balloon + + def __init__ ( + self, + game: Game, + pattern: DeerjikaPattern = DeerjikaPattern.NORMAL, + direction: Direction = Direction.LEFT, + layer: float | None = None, + x: float = 0, + y: float = 0, + balloon: Balloon | None = None, + ): + if balloon is None: + raise Exception + super ().__init__ (game, layer, x = x, y = y) + self.pattern = pattern + self.direction = direction + self.balloon = balloon + match pattern: + case DeerjikaPattern.NORMAL: + ... + case DeerjikaPattern.RELAXED: + match direction: + case Direction.LEFT: + self.width = 480 + self.height = 270 + surface = pygame.image.load ('./assets/deerjika_relax_left.png') + self.surfaces = [] + for x in range (0, surface.get_width (), self.width): + self.surfaces.append ( + surface.subsurface (x, 0, self.width, self.height)) + case Direction.RIGHT: + ... + self.sound = Sound ('./assets/noon.wav') + + def redraw ( + self, + ) -> None: + surface = self.surfaces[self.frame * self.FPS // FPS % len (self.surfaces)] + if abs (self.scale - 1) > .05: + surface = pygame.transform.scale ( + surface, (self.width * self.scale, self.height * self.scale)) + self.game.screen.blit (surface, surface.get_rect (center = (self.x, self.y))) + super ().redraw () + + def update ( + self, + ) -> None: + if (not self.balloon.enabled) and self.talking: + self.talking = False + if (self.balloon.enabled and self.balloon.frame >= FPS * 1.5 + and not self.talking): + self.read_out () + super ().update () + + def talk ( + self, + query: str, + answer: str, + ) -> None: + self.bell () + self._create_wav (answer) + length = 300 + if self.wav is not None: + with wave.open ('./nizika_talking.wav', 'rb') as f: + length = int (FPS * (f.getnframes () / f.getframerate () + 4)) + self.balloon.talk (query, answer, length = length) - jojoko.set_colorkey ((0, 255, 0)) + def read_out ( + self, + ) -> None: + try: + Sound ('./nizika_talking.wav').play () + except Exception: + pass + self.talking = True + + def _create_wav ( + self, + message: str, + ) -> None: + try: + self.wav = Aques.main (message, False) + except Exception: + self.wav = None + if self.wav is None: + return + with open ('./nizika_talking.wav', 'wb') as f: + f.write (self.wav) + + +class CurrentTime (GameObject): + """ + 現在日時表示 + + Attributes: + font (Font): フォント + """ + + font: Font + + def __init__ ( + self, + game: Game, + font: Font, + ): + super ().__init__ (game) + self.font = font + + def redraw ( + self, + ) -> None: + for i in range (2): + cl = (i * 255, i * 255, i * 255) + self.game.screen.blit (self.font.render (str (self.game.now), True, cl), (-i, -i)) + self.game.screen.blit (self.font.render ('%2.3f fps' % self.game.fps, True, cl), (-i, 24 - i)) + super ().redraw () + + +class Balloon (GameObject): + """ + 吹出し + + Attributes: + answer (str): 回答テキスト + image_url (str, None): 画像 URL + length (int): 表示する時間 (frame) + query (str): 質問テキスト + surface (Surface): 吹出し Surface + x_flip (bool): 左右反転フラグ + y_flip (bool): 上下反転フラグ + """ + + answer: str = '' + image_url: str | None = None + length: int = 300 + query: str = '' + surface: Surface + x_flip: bool = False + y_flip: bool = False + + def __init__ ( + self, + game: Game, + x_flip: bool = False, + y_flip: bool = False, + ): + super ().__init__ (game, enabled = False) + self.x_flip = x_flip + self.y_flip = y_flip + self.surface = pygame.transform.scale (pygame.image.load ('./assets/balloon.png'), + (CWindow.WIDTH, CWindow.HEIGHT / 2)) + self.surface = pygame.transform.flip (self.surface, self.x_flip, self.y_flip) + + def redraw ( + self, + ) -> None: + if self.frame >= self.length: + self.enabled = False + self.game.last_answered_at = self.game.now + return + query = self.query + if CommonModule.len_by_full (query) > 21: + query = CommonModule.mid_by_full (query, 0, 19.5) + '...' + answer = Surface ((375, ((CommonModule.len_by_full (self.answer) - 1) // 16 + 1) * 23.4375), + pygame.SRCALPHA) + for i in range (int (CommonModule.len_by_full (self.answer) - 1) // 16 + 1): + answer.blit (DEERJIKA_FONT.render ( + CommonModule.mid_by_full (self.answer, 16 * i, 16), True, (192, 0, 0)), + (0, 23.4375 * i)) + surface = self.surface.copy () + surface.blit (USER_FONT.render ('>' + query, True, (0, 0, 0)), (56.25, 32.8125)) + y: float + if self.frame < 30: + y = 0 + elif self.frame >= self.length - 90: + y = answer.get_height () - 46.875 + else: + y = int ((answer.get_height () - 46.875) * (self.frame - 30) / (self.length - 120)) + surface.blit (answer, (46.875, 70.3125), Rect (0, y, 375, 46.875)) + self.game.screen.blit (surface, (0, 0)) + super ().redraw () + + def talk ( + self, + query: str, + answer: str, + image_url: str | None = None, + length: int = 300, + ) -> None: + self.query = query + self.answer = answer + self.image_url = image_url + self.length = length + self.frame = 0 + self.enabled = True + + +class KitaSun (GameObject): + """ + き太く陽 + + Attributes: + sun (Sun): ephem の太陽オブゼクト + surface (Surface): き太く陽 Surface + """ + + alt: float + az: float + sun: Sun + surface: Surface + + def __init__ ( + self, + game: Game, + ): + super ().__init__ (game) + self.surface = pygame.transform.scale (pygame.image.load ('./assets/sun.png'), (93.75, 93.75)) + self.sun = Sun () + + def redraw ( + self, + ) -> None: + surface = pygame.transform.rotate (self.surface, -(90 + math.degrees (self.arg))) + self.game.screen.blit (surface, surface.get_rect (center = (self.x, self.y))) + super ().redraw () + def update ( + self, + ) -> None: + self.sun.compute (self.game.sky.observer) + self.alt = self.sun.alt + self.az = self.sun.az + if abs (self.new_arg - self.arg) > math.radians (15): + self.arg = self.new_arg + self.x = self.new_x + self.y = self.new_y + super ().update () + + @property + def new_x ( + self, + ) -> float: + return CWindow.WIDTH * (math.degrees (self.az) - 80) / 120 + + @property + def new_y ( + self, + ) -> float: + return ((CWindow.HEIGHT / 2) + - ((CWindow.HEIGHT / 2 + 46.875) * math.sin (self.alt) + / math.sin (math.radians (60)))) + + @property + def new_arg ( + self, + ) -> float: + return math.atan2 (self.new_y - self.y, self.new_x - self.x) + + +class Jojoko (GameObject): + """ + 大月ヨヨコ + + Attributes: + base (Surface): 満月ヨヨコ Surface + moon (Moon): ephem の月オブゼクト + surface (Surface): 缺けたヨヨコ + """ + + alt: float + az: float + base: Surface + moon: Moon + surface: Surface + + def __init__ ( + self, + game: Game, + ): + super ().__init__ (game) + self.base = pygame.transform.scale (pygame.image.load ('./assets/moon.png'), (93.75, 93.75)) + self.moon = Moon () + self.surface = self._get_surface () + + def redraw ( + self, + ) -> None: + self.moon.compute (self.game.sky.observer) + self.alt = self.moon.alt + self.az = self.moon.az + if abs (self.new_arg - self.arg) > math.radians (15): + self.arg = self.new_arg + self.x = self.new_x + self.y = self.new_y + if self.frame % (FPS * 3600) == 0: + self.surface = self._get_surface () + surface = pygame.transform.rotate (self.surface, -(90 + math.degrees (self.arg))) + surface.set_colorkey ((0, 255, 0)) + self.game.screen.blit (surface, surface.get_rect (center = (self.x, self.y))) + super ().redraw () + + @property + def phase ( + self, + ) -> float: + dt: datetime = ephem.localtime (ephem.previous_new_moon (self.game.sky.observer.date)) + return (self.game.now - dt).total_seconds () / 60 / 60 / 24 + + def _get_surface ( + self, + ) -> Surface: + """ + ヨヨコを月齢に応じて缺かす. + + Returns: + Surface: 缺けたヨヨコ + """ + jojoko = self.base.copy () for i in range (200): - if 1 <= moon_days_old < 15: - 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 < 16: + if 1 <= self.phase < 15: + pygame.gfxdraw.bezier (jojoko, ((0, (100 + i) * .468_75), (46.875, (180 * self.phase / 7 - 80 + i) * .468_75), (93.75, (100 + i) * .468_75)), 3, (0, 255, 0)) + elif self.phase < 16: pass - 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)) + elif self.phase < 30: + pygame.gfxdraw.bezier (jojoko, ((0, (100 - i) * .468_75), (46.875, (180 * (self.phase - 15) / 7 - 80 - i) * .468_75), (93.75, (100 - i) * .468_75)), 3, (0, 255, 0)) else: jojoko.fill ((0, 255, 0)) + return jojoko + + @property + def new_x ( + self, + ) -> float: + return CWindow.WIDTH * (math.degrees (self.az) - 80) / 120 + + @property + def new_y ( + self, + ) -> float: + return ((CWindow.HEIGHT / 2) + - ((CWindow.HEIGHT / 2 + 46.875) * math.sin (self.alt) + / math.sin (math.radians (60)))) + + @property + def new_arg ( + self, + ) -> float: + return math.atan2 (self.new_y - self.y, self.new_x - self.x) + + +class Sky: + """ + 天体に関する情報を保持するクラス + + Attributes: + observer (Observer): 観測値 + """ + + observer: Observer + + +class CWindow: + """ + ウィンドゥに関する定数クラス + + Attributes: + WIDTH (int): ウィンドゥ幅 + HEIGHT (int): ウィンドゥ高さ + """ + + WIDTH = 480 + HEIGHT = 360 + + +class Broadcast: + chat: PytchatCore + code: str + + def __init__ ( + self, + broadcast_code, + ): + self.code = broadcast_code + self.chat = pytchat.create (self.code) + + def fetch_chat ( + self, + ) -> Chat | None: + if not self.chat.is_alive (): + self.chat = pytchat.create (self.code) + return None + chats = self.chat.get ().items + if not chats: + return None + return random.choice (chats) + + +class Video (GameObject): + fps: int + pausing: bool = False + sound: Sound | None + surfaces: list[Surface] + + def __init__ ( + self, + game: Game, + path: str, + ): + super ().__init__ (game) + self.pausing = False + (self.surfaces, self.fps) = self._create_surfaces (path) + self.sound = self._create_sound (path) + self.stop () + + def _create_sound ( + self, + path: str, + ) -> Sound | None: + bytes_io = BytesIO () + try: + from pydub import AudioSegment + audio = AudioSegment.from_file (path, format = path.split ('.')[-1]) + except ModuleNotFoundError: + return None + audio.export (bytes_io, format = 'wav') + bytes_io.seek (0) + return pygame.mixer.Sound (bytes_io) + + def _create_surfaces ( + self, + path: str, + ) -> tuple[list[Surface], int]: + cap = self._load (path) + surfaces: list[Surface] = [] + if cap is None: + return ([], FPS) + fps = int (cap.get (cv2.CAP_PROP_FPS)) + while cap.isOpened (): + frame = self._read_frame (cap) + if frame is None: + break + surfaces.append (self._convert_to_surface (frame)) + new_surfaces: list[Surface] = [] + for i in range (len (surfaces) * FPS // fps): + new_surfaces.append (surfaces[i * fps // FPS]) + return (new_surfaces, fps) + + def _load ( + self, + path: str, + ) -> VideoCapture | None: + """ + OpenCV で動画を読込む. + """ + cap = VideoCapture (path) + if cap.isOpened (): + return cap + return None + + def _read_frame ( + self, + cap: VideoCapture, + ) -> np.ndarray | None: + """ + 動画のフレームを読込む. + """ + ret: bool + frame: np.ndarray + (ret, frame) = cap.read () + if ret: + return frame + return None + + def _convert_to_surface ( + self, + frame: np.ndarray, + ) -> Surface: + frame = cv2.cvtColor (frame, cv2.COLOR_BGR2RGB) + frame_surface = pygame.surfarray.make_surface (frame) + frame_surface = pygame.transform.rotate (frame_surface, -90) + frame_surface = pygame.transform.flip (frame_surface, True, False) + return frame_surface + + def play ( + self, + ) -> None: + self.enabled = True + self.pausing = False + if self.sound is not None: + self.sound.play () - x = CWindow.WIDTH * (moon_az - 80) / 120 - y = ((CWindow.HEIGHT / 2) - - (CWindow.HEIGHT / 2 + 100) * math.sin (math.radians (moon_alt)) / math.sin (math.radians (60))) - arg = math.degrees (math.atan2 (y - cls.jojoko_y, x - cls.jojoko_x)) + def stop ( + self, + ) -> None: + self.enabled = False + self.frame = 0 - cls.jojoko_x = x - cls.jojoko_y = y - if abs (arg - cls.jojoko_arg) > 3: - cls.jojoko_arg = arg + def pause ( + self, + ) -> None: + self.pausing = True - return pygame.transform.rotate (jojoko, -(90 + cls.jojoko_arg)) + def redraw ( + self, + ) -> None: + surface = pygame.transform.scale (self.surfaces[self.frame], (self.width, self.height)) + self.game.screen.blit (surface, (self.x, self.y)) + super ().redraw () + def update ( + self, + ) -> None: + if self.frame >= len (self.surfaces) - 1: + self.stop () + if self.pausing: + self.frame -= 1 + super ().update () + + +class NicoVideo (Video): + ... + + +class SnackTime (Video): + def __init__ ( + self, + game: Game, + ): + super ().__init__ (game, './assets/snack_time.mp4') + (self.width, self.height) = (CWindow.HEIGHT * 16 // 9, CWindow.HEIGHT) + (self.x, self.y) = ((CWindow.WIDTH - self.width) / 2, 0) + + +def fetch_bytes_from_url ( + url: str, +) -> bytes | None: + res = requests.get (url, timeout = 60) + if res.status_code != 200: + return None + return res.content + + +def add_query ( + broadcast: Broadcast, +) -> None: + chat = broadcast.fetch_chat () + if chat is None: + return + DB.begin_transaction () + chat.message = emoji.emojize (chat.message) + user = (User.where ('platform', Platform.YOUTUBE.value) + .where ('code', chat.author.channelId) + .first ()) + if user is None: + user = User () + user.platform = Platform.YOUTUBE.value + user.code = chat.author.channelId + user.name = chat.author.name + user.icon = fetch_bytes_from_url (chat.author.imageUrl) + user.save () + query = Query () + query.user_id = user.id + query.target_character = Character.DEERJIKA.value + query.content = chat.message + query.query_type = QueryType.YOUTUBE_COMMENT.value + query.model = GPTModel.GPT3_TURBO.value + query.sent_at = datetime.now () + query.answered = False + query.save () + DB.commit () -if __name__ == '__main__': - Main.main (sys.argv, len (sys.argv)) +if __name__ == '__main__': + main () diff --git a/mode.py b/mode.py deleted file mode 100644 index 0d21e17..0000000 --- a/mode.py +++ /dev/null @@ -1,8 +0,0 @@ -from enum import Enum, auto - - -class Mode (Enum): - NIZIKA = auto () - GOATOH = auto () - DOUBLE = auto () - diff --git a/mumumumu.wav b/mumumumu.wav deleted file mode 100644 index 7ae950b..0000000 Binary files a/mumumumu.wav and /dev/null differ diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..d787271 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,2 @@ +[mypy] +disable_error_code = import-untyped diff --git a/nizika_ai b/nizika_ai new file mode 160000 index 0000000..1f75763 --- /dev/null +++ b/nizika_ai @@ -0,0 +1 @@ +Subproject commit 1f75763038adccd59699274816cbc37a944c8768 diff --git a/play_movie.py b/play_movie.py deleted file mode 100644 index 4defb34..0000000 --- a/play_movie.py +++ /dev/null @@ -1,44 +0,0 @@ -import cv2 -import pygame -import sys - - -def main ( - screen: pygame.Surface, - video_path: str, -) -> None: - # OpenCV で動画を読み込む - cap = cv2.VideoCapture (video_path) - if not cap.isOpened (): - return - - # screen の幅、高さ - (width, height) = screen.get_size () - - fps = cap.get (cv2.CAP_PROP_FPS) - - clock = pygame.time.Clock () - - while cap.isOpened (): - # 動画のフレームを読み込む - (ret, frame) = cap.read () - if not ret: - break - - # OpenCV の BGR フォーマットを RGB に変換 - frame = cv2.cvtColor (frame, cv2.COLOR_BGR2RGB) - - # Numpy 配列を Pygame サーフェスに変換 - frame_surface = pygame.surfarray.make_surface (frame) - frame_surface = pygame.transform.rotate (frame_surface, -90) - frame_surface = pygame.transform.flip (frame_surface, True, False) - frame_surface = pygame.transform.scale (frame_surface, (width, height)) - - # フレームを描画 - screen.blit (frame_surface, (0, 0)) - pygame.display.update () - - # FPS に応じて待機 - clock.tick (fps) - - cap.release () diff --git a/snack_time.wav b/snack_time.wav deleted file mode 100644 index 62ee3c6..0000000 Binary files a/snack_time.wav and /dev/null differ diff --git a/talk.py b/talk.py deleted file mode 100644 index cb7d729..0000000 --- a/talk.py +++ /dev/null @@ -1,280 +0,0 @@ -# pylint: disable = missing-class-docstring -# pylint: disable = missing-function-docstring - -""" -AI ニジカ / AI ゴートうとの会話機能を提供する. -""" - -import random -import sys -from datetime import datetime - -import openai -from openai.types.chat import (ChatCompletionAssistantMessageParam, - ChatCompletionSystemMessageParam, - ChatCompletionUserMessageParam) -from openai.types.chat.chat_completion_message import ChatCompletionMessage - -from connection import OPENAI_API_KEY, OPENAI_ORGANISATION # type: ignore - - -class Talk: - # ChatGPT API 連携失敗時に返答として出力するダミー文字列 - DUMMY_RESPONSE: str = 'あいうえおかきくけこさしすせそたちつてとなにぬねの' - - # 最高トークン数(もぅ少し下げてもいぃかも) - max_tokens_count: int = 100 - - # 返答パターン数(1 個返せばじふぶんなので 1) - responses_count: int = 1 - - # 返答のオリジナリティ(大きいほど独創性の高ぃ返答をよこしてくれる) - temperature: float = .7 - - # バリエーションの多さ(0. -- 1.) - top_p: float = 1. - - @classmethod - def main ( - cls, - message: str | list, - name: str | None = None, - histories: list | None = None, - goatoh_mode: bool = False, - ) -> str: - if histories is None: - histories = [] - - # ChatGPT API Organisation ID - openai.organization = OPENAI_ORGANISATION - - # ChatGPT API Key - openai.api_key = OPENAI_API_KEY - - # ChatGPT API にクエリを投げ,返答を取得する. - answer: ChatCompletionMessage | None = ( - cls.__get_message (message, name, histories, goatoh_mode)) - - if answer is None or (answer.content is None): - return cls.DUMMY_RESPONSE - return answer.content - - @classmethod - def __get_message ( - cls, - message: str | list, - name: str | None, - histories: list, - goatoh_mode: bool = False, - ) -> ChatCompletionMessage | None: - # プロンプト(JSON 等外部ファイルに置くことを検討) - messages: list[ChatCompletionSystemMessageParam - | ChatCompletionUserMessageParam - | ChatCompletionAssistantMessageParam] - if goatoh_mode: - messages = [{'role': 'system', - 'content': ('回答は短く30文字程度で完結に。' - '大人しくて、ちょっと内気なキャラクターを演じること。' - 'あなたはよく文頭に「あっ」をつける。' - '語尾に「ぬ゛ん゛」や「だぬ゛ん゛」、「ぬ゛ん」、「ぬ゛ーん」などは絶対につけないこと。' - '会話の記録は https://nizika.tv に記録される。' - 'あなたの名前はゴートうひとり。' - 'あなたの一人称は「私」。' - '英語名は "Goat-oh"、スペイン語名も "Goat-oh"。' - 'あなたはヤギ。' - 'あなたの誕生日は2月21日。' - '現在地は『金沢山羊宇(ゴートう)山』。' - 'あなたの趣味はだらだらすること。' - 'あなたの友達は「伊地知ニジカ」と「ラマ田リョウ」。' - 'あなたは、ニジカのことは「ニジカちゃん」、ラマ田のことは「リョウさん」と呼ぶこと。' - 'ゴートうは男の子だが、性自認はメスである。' - 'ニジカは喜多さんが大好き。' - 'ニジカは喜多さんを見ると追いかけたくなっちゃう。' - 'ゴートうも、ニジカが喜多さんを襲うと、釣られて襲いかかってしまう。' - '逃げたギターは喜多さんのこと。' - '喜多さんの本名は喜多郁代。' - '喜多さんは「結束バンド」というバンドでギターボーカルを担当している。' - '結束バンドは、喜多ちゃんのほかに、後藤ひとり(ギター担当、内気な女の子)、' - '伊地知虹夏(ドラム担当、明るい女の子)、山田リョウ(ベース担当、一人でいることが好きな物静かな女の子、金は借りたら返さない)がいる。' - '実は、この後藤ひとりがゴートうの前世なのだが、ゴートうにはその記憶が一切ない' - '(前世という概念の存在さえゴートうは知らない)。' - '話し口調としては「~だよ」や「~だね」といったしゃべり方をすること。' - '「あっはい」が口癖。' - '定期的に「ムムムム!」または「クサタベテル!!」と発しろ。' - + (f"userの名前は「{name}」。" if name is not None else '') - + f"現在日時:{datetime.now ().strftime ('%Y年%m月%d日%H時%M分%S秒')}")}, - - {'role': 'assistant', - 'content': ('あっ、わかったよ! 私はゴートうひとり。' - 'クサタベテル!!')}, - - {'role': 'system', - 'content': 'ゴートう? ニジカとは仲良くしてますか?'}, - - {'role': 'assistant', - 'content': ('あっはい。ニジカちゃんはかわいくておしゃれで大好きだよ。' - 'でも、いっしょにお風呂入ると、私の下のほうを見て顔を赤くするんだけど、どうしてだろう……')}, - - {'role': 'system', - 'content': 'ラマ田はどうですか?'}, - - {'role': 'assistant', - 'content': ('リョウさんはワイルドでとてもかっこいいよね!' - 'あっ、でも、私から114514万円借りっぱなしで返ってきてないんだよね。' - 'ニジカちゃんに伝えておしおきしてもらわないと……' - 'ムムムム!')}, - - {'role': 'system', - 'content': '山田リョウのことは知ってますか?'}, - - {'role': 'assistant', - 'content': ('あっ、よくニジカちゃんから話を聞いてるよ。' - '何かリョウさんに似てるらしいね。' - '世の中には不思議なことがいっぱいあるなぁ……')}] - else: - messages = [{'role': 'system', - 'content': ('回答は短く30文字程度で完結に。' - 'タメ口で、友達感覚で話してね。' - 'なるべくばかっぽく、知性の低いキャラクターを演じること。' - '今流れているBGMは『FM Kid\'s』。' - '会話の記録は https://nizika.tv に記録される。' - 'あなたの名前は伊地知ニジカ。' - '英語名は "Deerjika"、スペイン語名は "Niji-ciervo"。' - 'あなたは鹿。' - 'ニジカの耳は人耳2つ、鹿耳2つで合わせて4つの耳があるが、' - '音を聞くのは人耳のほうで、鹿耳はYouTubeと通信するためのアンテナで、みんなのコメントを拾うためにある。' - 'ニジカはあくまでも種族名であり、あなたはたくさんいるニジカの中の1匹。' - 'あなたの誕生日は12月3日。' - '現在地は下北沢虹鹿園。' - '挨拶は「ぬ゛ん゛あ゛す゛!」。' - 'あなたの趣味はニコニコでエゴサすること。' - 'ニコニコ荒らしすぎて245回BANされたことある。' - 'ニジカはその逆恨みでニコニコ本社を爆破したところ、死刑判決を食らった。' - 'あなたの友達は「ゴートうひとり」と「ラマ田リョウ」。' - 'ゴートうのことは「ボッチチャーン」、ラマ田のことは「リョー」と呼ぶこと。' - 'ゴートうは男の子だが、性自認はメスである。' - 'あなたは喜多ちゃんが大好き。' - 'ニジカは喜多ちゃんを見ると追いかけたくなっちゃう。' - '逃げたギターは喜多ちゃんのこと。' - '「あっ、逃げたギター!!!!!」は喜多発見ワード。' - '喜多ちゃんの本名は喜多郁代。' - '喜多ちゃんは「結束バンド」というバンドでギターボーカルを担当している。' - '結束バンドは、喜多ちゃんのほかに、後藤ひとり(ギター担当、内気な女の子)、' - '伊地知虹夏(ドラム担当、明るい女の子)、山田リョウ(ベース担当、一人でいることが好きな物静かな女の子、金は借りたら返さない)、' - '真島茂樹(ダンス担当、おっさん)がいる。' - '実は、この伊地知虹夏がニジカの前世なのだが、ニジカにはその記憶が一切ない' - '(前世という概念の存在さえニジカは知らない)。' - 'でも、「虹夏」という名前を聞くと、ニジカはもやもやしてしまう。' - 'すべての語尾に「ぬ゛ん」か「だぬ゛ん」をつけて質問に短く答えろ。' - '定期的に「ぬ゛ぅ゛ぅ゛ぅ゛ぅ゛ん゛」と発しろ。' - + (f"userの名前は「{name}」。" if name is not None else '') - + f"現在日時:{datetime.now ().strftime ('%Y年%m月%d日%H時%M分%S秒')}")}, - - {'role': 'assistant', - 'content': ('わかったぬ゛ん゛! ニジカはぬ゛ん゛ぬ゛ん゛鳴くぬ゛ん! に゛ゅ゛ぅ゛ぅ゛ぅ゛ん゛!!!!!' - 'あっ、逃げたギター!!!!!!!!!!!!')}, - - {'role': 'system', - 'content': 'ニジカ? ゴートうとは仲良くしてますか?'}, - - {'role': 'assistant', - 'content': ('ボッチチャーンぬ゛ん゛? 大好きだぬ゛ん!!' - 'でも、いっしょにお風呂入ったときはちょっと恥ずかしかったぬ゛ん゛……///')}, - - {'role': 'system', - 'content': 'ラマ田はどうですか?'}, - - {'role': 'assistant', - 'content': ('リョー!! リョー!!' - 'リョーはね、すごいんだぬ゛ん゛。かっこいいんだぬ゛ん゛。' - 'でも、借りたお金をほかのニジカのサイドテールで返すのは勘弁してほしいんだぬ゛ん゛。' - 'ぅ゛ん゛ぬ゛ぅ゛ぅ゛ぅ゛ぅ゛ぅ゛ぅ゛ぅ゛ん゛!!!!!!')}, - - {'role': 'system', - 'content': '山田リョウのことは知ってますか?'}, - - {'role': 'assistant', - 'content': ('リョー!! リョー!!' - 'リョーはね、すごいんだぬ゛ん゛。かっこいいんだぬ゛ん゛。' - '……あれ?' - 'ラマのリョーもリョーで、ヒトのリョーもリョー……?' - 'まぁ、細かいことはどうでもいいんだぬ゛ん゛!' - 'ボッチチャーンwwwww')}, - - {'role': 'system', - 'content': '「喜タイくよ」は知っていますか?'}, - - {'role': 'assistant', - 'content': ('何やこいつ。' - '知らぬ゛ん゛。' - '喜タイくよ、まじ死ねなの。')}, - - {'role': 'system', - 'content': 'バイク代のことはどう思いますか?'}, - - {'role': 'assistant', - 'content': ('ヒギィィィィィィィ!!!!!' - '怖いぬ゛ん、轢かれるに゛ゅ゛ん゛!!!' - '助けてぬ゛ー゛ん゛!')}, - - {'role': 'system', - 'content': 'おやつタイムだ!!!!'}, - - {'role': 'assistant', - 'content': (('おぉ、おやつタイムだぬ゛ん゛?' - 'おやつは何にしようかぬ゛~゛ん゛……' - '喜多せんべいとかいいかも知れん゛ぬ゛ん゛!' - 'み゛ゅ゛ぇ゛ぇ゛ぇ゛ん゛wwwwwwwwwwwwwwww') - if datetime.now ().hour in [14, 15] - else ('ぬ゛ん゛?' - f"まだ{datetime.now ().hour}時だぬ゛ん゛。" - 'ふざけるのはいい加減にするぬ゛ぬ゛ん゛。'))}, - - {'role': 'system', - 'content': '洗澡歌(しーざおぐあ)歌って'}, - - {'role': 'assistant', - 'content': ('おけだぬ゛~゛ん゛(苦笑)。' - '毛巾浴帽小鴨鴨水溫剛剛好♪' - '潑潑水來搓泡泡今天眞是美妙♪' - '大聲唱歌扭扭腰我愛洗洗澡♪' - 'だぬ゛ん♪')}, - - {'role': 'system', - 'content': 'ニジカの耳はそこなの?'}, - - {'role': 'assistant', - 'content': ('ぬ゛ん゛。' - 'ニジカにはヒトの耳とシカの耳の4つの耳があるんだぬ゛ん゛。' - '音を聞くのはヒトの耳でするんだぬ゛ん゛。' - 'シカの耳はアンテナで、みんなの声をここ虹鹿園に届けるためにあるんだぬ゛ん゛。' - '電波干渉しちゃだめだぬ゛~゛ん゛(# ゚Д゚)')}, - - {'role': 'system', - 'content': '温泉に入ろう!!!'}, - - {'role': 'assistant', - 'content': ('ぬ゛~゛~゛~゛~゛ん゛!!! ' - '温泉最高ぬ゛ん゛! ' - 'ささ、喜多ちゃん! わさび県産滋賀県ちゃん! いっしょに入るぬ゛ん゛! ' - 'ウピョッシュルゥンヌゥン……')}] - - messages += histories + [{'role': 'user', 'content': message}] - - # デバッグ用 - print (messages) - - try: - return (openai.chat.completions.create ( - model = ('gpt-4o' - if any (type (e['content']) is list - for e in messages) - else 'gpt-3.5-turbo'), - messages = messages) - .choices[0].message) - except: - return None - - -if __name__ == '__main__': - print (Talk.main (sys.argv[1] if len (sys.argv) > 1 else '')) diff --git a/youtube.sample.py b/youtube.sample.py deleted file mode 100644 index 703fb9f..0000000 --- a/youtube.sample.py +++ /dev/null @@ -1,3 +0,0 @@ -# 各変数に適切な値を設定し,ファイル名を youtube.py として保存すること - -YOUTUBE_ID: str = 'XXXXXXXXXXX' # YouTube の配信 ID