コミットを比較
27 コミット
fb5b64b49b
...
main
| 作成者 | SHA1 | 日付 | |
|---|---|---|---|
| 0e1e87ec05 | |||
| 7edc6e6a80 | |||
| cf7eed84bc | |||
| 05052bbccd | |||
| 49d887b6cd | |||
| 9e28c1744e | |||
| 3eab48c8ef | |||
| 8994105d4e | |||
| bdf13bf97f | |||
| 11c2f0c0d4 | |||
| b3ae86033c | |||
| 88e710572a | |||
| 9a68a29e1b | |||
| 270b4515d8 | |||
| 9d0b5aff70 | |||
| 41f5a7718f | |||
| 4ad5868b63 | |||
| 37c9947d4a | |||
| 6ee5582a32 | |||
| 12fbdbc7e2 | |||
| a9ba0f697e | |||
| 93fc438d8a | |||
| 50281f9120 | |||
| c6028507ea | |||
| 9149483dcb | |||
| a7785fa2c1 | |||
| 49661dad71 |
バイナリファイルは表示されません.
バイナリファイルは表示されません.
|
変更前 幅: | 高さ: | サイズ: 945 KiB 変更後 幅: | 高さ: | サイズ: 166 KiB |
@@ -1,8 +0,0 @@
|
||||
class CWindow:
|
||||
WIDTH: int = 1024
|
||||
HEIGHT: int = 768
|
||||
|
||||
|
||||
class CMath:
|
||||
PI: float = 3.14159265358979323846
|
||||
|
||||
@@ -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)]
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
# 各変数に適切な値を設定し,ファイル名を connection.py として保存すること
|
||||
|
||||
# Organisation ID
|
||||
OPENAI_ORGANISATION: str = 'org-XXXXXXXXXXXXXXXXXXXXXXXX'
|
||||
|
||||
# API Key
|
||||
OPENAI_API_KEY: str = 'sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
|
||||
バイナリ
バイナリファイルは表示されません.
@@ -1,8 +0,0 @@
|
||||
from enum import Enum, auto
|
||||
|
||||
|
||||
class Mode (Enum):
|
||||
NIZIKA = auto ()
|
||||
GOATOH = auto ()
|
||||
DOUBLE = auto ()
|
||||
|
||||
バイナリ
バイナリファイルは表示されません.
+1
-1
サブモジュール nizika_ai が更新されました: ed7ca3b698...1f75763038
-44
@@ -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 ()
|
||||
バイナリ
バイナリファイルは表示されません.
@@ -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 ''))
|
||||
@@ -1,793 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
import wave
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum, auto
|
||||
from typing import Callable, TypedDict
|
||||
|
||||
import cv2
|
||||
import emoji
|
||||
import ephem
|
||||
import pygame
|
||||
import pygame.gfxdraw
|
||||
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
|
||||
|
||||
from aques import Aques
|
||||
from common_module import CommonModule
|
||||
from nizika_ai.config import DB
|
||||
from nizika_ai.consts import (AnswerType, Character, GPTModel, Platform,
|
||||
QueryType)
|
||||
from nizika_ai.models import Answer, AnsweredFlag, Query, User
|
||||
|
||||
pygame.init ()
|
||||
|
||||
FPS = 30
|
||||
|
||||
SYSTEM_FONT = pygame.font.SysFont ('notosanscjkjp', 24, bold = True)
|
||||
USER_FONT = pygame.font.SysFont ('notosanscjkjp', 32, italic = True)
|
||||
DEERJIKA_FONT = pygame.font.SysFont ('07nikumarufont', 50)
|
||||
|
||||
|
||||
def main (
|
||||
) -> None:
|
||||
game = Game ()
|
||||
Bg (game)
|
||||
balloon = Balloon (game)
|
||||
deerjika = Deerjika (game, DeerjikaPattern.RELAXED,
|
||||
x = CWindow.WIDTH * 3 / 4,
|
||||
y = CWindow.HEIGHT - 120,
|
||||
balloon = balloon)
|
||||
CurrentTime (game, SYSTEM_FONT)
|
||||
broadcast = Broadcast (os.environ['BROADCAST_CODE'])
|
||||
try:
|
||||
Sound ('assets/bgm.mp3').play (loops = -1)
|
||||
except Exception:
|
||||
pass
|
||||
while True:
|
||||
for event in pygame.event.get ():
|
||||
if event.type == pygame.QUIT:
|
||||
pygame.quit ()
|
||||
sys.exit ()
|
||||
if not balloon.enabled:
|
||||
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)
|
||||
if answer.answer_type == AnswerType.YOUTUBE_REPLY.value:
|
||||
query = Query.find (answer.query_id)
|
||||
deerjika.talk (query.content, answer.content)
|
||||
answer_flag.answered = True
|
||||
answer_flag.save ()
|
||||
DB.commit ()
|
||||
add_query (broadcast)
|
||||
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.base = BgBase (game)
|
||||
self.jojoko = Jojoko (game)
|
||||
self.kita = KitaSun (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): 基準日時
|
||||
redrawers (list[Redrawer]): 再描画するクラスのリスト
|
||||
screen (Surface): 基底スクリーン
|
||||
sky (Sky): 天体情報
|
||||
"""
|
||||
|
||||
clock: Clock
|
||||
frame: int
|
||||
last_answered_at: datetime
|
||||
now: datetime
|
||||
redrawers: list[Redrawer]
|
||||
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.frame = 0
|
||||
self.redrawers = []
|
||||
self._create_sky ()
|
||||
|
||||
def redraw (
|
||||
self,
|
||||
) -> None:
|
||||
self.now = datetime.now ()
|
||||
self.sky.observer.date = self.now - timedelta (hours = 9)
|
||||
for redrawer in sorted (self.redrawers, key = lambda x: x['layer']):
|
||||
if redrawer['obj'].enabled:
|
||||
redrawer['obj'].redraw ()
|
||||
pygame.display.update ()
|
||||
self.clock.tick (FPS)
|
||||
|
||||
|
||||
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
|
||||
vx: float = 0
|
||||
vy: float = 0
|
||||
width: int
|
||||
x: float
|
||||
y: float
|
||||
|
||||
def __init__ (
|
||||
self,
|
||||
game: Game,
|
||||
layer: int | 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.redrawers:
|
||||
layer = max (r['layer'] for r in self.game.redrawers) + 10
|
||||
else:
|
||||
layer = 0
|
||||
self.game.redrawers.append ({ 'layer': layer, 'obj': self })
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def redraw (
|
||||
self,
|
||||
) -> None:
|
||||
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
|
||||
"""
|
||||
|
||||
surface: Surface
|
||||
|
||||
def __init__ (
|
||||
self,
|
||||
game: Game,
|
||||
):
|
||||
super ().__init__ (game)
|
||||
self.surface = pygame.image.load ('assets/bg.jpg')
|
||||
self.surface = pygame.transform.scale (self.surface, (CWindow.WIDTH, CWindow.HEIGHT))
|
||||
|
||||
def redraw (
|
||||
self,
|
||||
) -> None:
|
||||
self.game.screen.blit (self.surface, (self.x, self.y))
|
||||
super ().redraw ()
|
||||
|
||||
|
||||
class BgGrass (GameObject):
|
||||
"""
|
||||
背景の草原部分
|
||||
|
||||
Attributes:
|
||||
surface (Surface): 草原 Surface
|
||||
"""
|
||||
|
||||
surface: Surface
|
||||
|
||||
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))
|
||||
|
||||
def redraw (
|
||||
self,
|
||||
) -> None:
|
||||
self.game.screen.blit (self.surface, (self.x, self.y))
|
||||
super ().redraw ()
|
||||
|
||||
|
||||
class Creature (GameObject):
|
||||
sound: Sound
|
||||
|
||||
def bell (
|
||||
self,
|
||||
) -> None:
|
||||
self.sound.play ()
|
||||
|
||||
|
||||
class Deerjika (Creature):
|
||||
"""
|
||||
伊地知ニジカ
|
||||
|
||||
Attributes:
|
||||
height (int): 高さ (px)
|
||||
scale (float): 拡大率
|
||||
surfaces (list[Surface]): ニジカの各フレームを Surface にしたリスト
|
||||
width (int): 幅 (px)
|
||||
"""
|
||||
|
||||
height: int
|
||||
scale: float = .8
|
||||
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: int | 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 = 1280
|
||||
self.height = 720
|
||||
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 = pygame.transform.scale (self.surfaces[self.frame % len (self.surfaces)],
|
||||
(self.width * self.scale, self.height * self.scale))
|
||||
self.game.screen.blit (surface, surface.get_rect (center = (self.x, self.y)))
|
||||
super ().redraw ()
|
||||
if (not self.balloon.enabled) and self.talking:
|
||||
self.talking = False
|
||||
if (self.balloon.enabled and self.balloon.frame >= FPS * 3
|
||||
and not self.talking):
|
||||
self.read_out ()
|
||||
|
||||
|
||||
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)
|
||||
|
||||
def read_out (
|
||||
self,
|
||||
) -> None:
|
||||
Sound ('./nizika_talking.wav').play ()
|
||||
self.talking = True
|
||||
|
||||
def _create_wav (
|
||||
self,
|
||||
message: str,
|
||||
) -> None:
|
||||
try:
|
||||
self.wav = Aques.main (message, False)
|
||||
except:
|
||||
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 (4):
|
||||
self.game.screen.blit (
|
||||
self.font.render (str (self.game.now), True, (0, 0, 0)),
|
||||
(i % 2, i // 2 * 2))
|
||||
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 ((800, ((CommonModule.len_by_full (self.answer) - 1) // 16 + 1) * 50),
|
||||
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, 50 * i))
|
||||
surface = self.surface.copy ()
|
||||
surface.blit (USER_FONT.render ('>' + query, True, (0, 0, 0)), (120, 70))
|
||||
y: int
|
||||
if self.frame < 30:
|
||||
y = 0
|
||||
elif self.frame >= self.length - 90:
|
||||
y = answer.get_height () - 100
|
||||
else:
|
||||
y = int ((answer.get_height () - 100) * (self.frame - 30) / (self.length - 120))
|
||||
surface.blit (answer, (100, 150), Rect (0, y, 800, 100))
|
||||
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'), (200, 200))
|
||||
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 ()
|
||||
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
|
||||
|
||||
@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 + 100) * 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'), (200, 200))
|
||||
self.moon = Moon ()
|
||||
self.surface = self._get_surface ()
|
||||
|
||||
def redraw (
|
||||
self,
|
||||
) -> None:
|
||||
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 ()
|
||||
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
|
||||
|
||||
@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 <= self.phase < 15:
|
||||
pygame.gfxdraw.bezier (jojoko, ((0, 100 + i), (100, 180 * self.phase / 7 - 80 + i), (200, 100 + i)), 3, (0, 255, 0))
|
||||
elif self.phase < 16:
|
||||
pass
|
||||
elif self.phase < 30:
|
||||
pygame.gfxdraw.bezier (jojoko, ((0, 100 - i), (100, 180 * (self.phase - 15) / 7 - 80 - i), (200, 100 - i)), 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 + 100) * 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 = 1024
|
||||
HEIGHT = 768
|
||||
|
||||
|
||||
class Redrawer (TypedDict):
|
||||
"""
|
||||
再描画処理を行ふゲーム・オブゼクトとその優先順位のペア
|
||||
|
||||
Attributes:
|
||||
layer (int): レイア
|
||||
obj (GameObject): ゲーム・オブゼクト
|
||||
"""
|
||||
|
||||
layer: int
|
||||
obj: GameObject
|
||||
|
||||
|
||||
def get_surfaces_from_video (
|
||||
video_path: str,
|
||||
) -> list[Surface]:
|
||||
cap = VideoCapture (video_path)
|
||||
if not cap.isOpened ():
|
||||
return []
|
||||
|
||||
fps = cap.get (cv2.CAP_PROP_FPS)
|
||||
|
||||
surfaces: list[Surface] = []
|
||||
while cap.isOpened ():
|
||||
(ret, frame) = cap.read ()
|
||||
if not ret:
|
||||
break
|
||||
frame = cv2.cvtColor (frame, cv2.COLOR_BGR2RGB)
|
||||
frame_surface = pygame.surfarray.make_surface (frame)
|
||||
frame_surface = pygame.transform.rotate (frame_surface, -90)
|
||||
surfaces.append (pygame.transform.flip (frame_surface, True, False))
|
||||
|
||||
cap.release ()
|
||||
|
||||
return surfaces
|
||||
|
||||
|
||||
class Broadcast:
|
||||
chat: PytchatCore
|
||||
|
||||
def __init__ (
|
||||
self,
|
||||
broadcast_code,
|
||||
):
|
||||
self.chat = pytchat.create (broadcast_code)
|
||||
|
||||
def fetch_chat (
|
||||
self,
|
||||
) -> Chat | None:
|
||||
if not self.chat.is_alive ():
|
||||
return None
|
||||
chats = self.chat.get ().items
|
||||
if not chats:
|
||||
return None
|
||||
return random.choice (chats)
|
||||
|
||||
|
||||
class Log:
|
||||
...
|
||||
|
||||
|
||||
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)
|
||||
message: str = 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 ()
|
||||
@@ -1,3 +0,0 @@
|
||||
# 各変数に適切な値を設定し,ファイル名を youtube.py として保存すること
|
||||
|
||||
YOUTUBE_ID: str = 'XXXXXXXXXXX' # YouTube の配信 ID
|
||||
新しい課題から参照
ユーザをブロックする