31 コミット

作成者 SHA1 メッセージ 日付
みてるぞ a8239f3de3 #42 2026-01-04 04:40:41 +09:00
みてるぞ 1f46dc3974 #42 2026-01-04 04:15:30 +09:00
みてるぞ b88f4b0ee1 #42 2026-01-04 03:27:25 +09:00
みてるぞ f34dd36b6a #42 2026-01-04 01:04:54 +09:00
みてるぞ 0e1e87ec05 feat: 回答遅延問題対応(#40) (#41)
#40

#40

#40

Co-authored-by: miteruzo <miteruzo@naver.com>
Reviewed-on: #41
2026-01-03 13:23:32 +09:00
みてるぞ 7edc6e6a80 Merge pull request 'AI 移行' (#38) from ai-migration into main
Reviewed-on: #38
2025-12-03 02:02:35 +09:00
みてるぞ cf7eed84bc 軽量化 2025-12-02 00:40:31 +09:00
みてるぞ 05052bbccd 軽量化 2025-12-02 00:17:45 +09:00
みてるぞ 49d887b6cd 軽量化 2025-12-02 00:10:17 +09:00
みてるぞ 9e28c1744e 軽量化 2025-12-01 23:54:09 +09:00
みてるぞ 3eab48c8ef 画面サイズを縮小 2025-12-01 12:40:05 +09:00
みてるぞ 8994105d4e #34 2025-11-30 03:17:15 +09:00
みてるぞ bdf13bf97f ぼちぼちだ2025-11-29 05:15:36 +09:00
みてるぞ 11c2f0c0d4 #37 nizika_ai 最新版に対応 2025-10-21 22:53:14 +09:00
みてるぞ b3ae86033c main.py にリネーム 2025-01-06 23:33:49 +09:00
みてるぞ 88e710572a 現行の分は移行完了したので main.py 削除 2025-01-06 23:32:59 +09:00
みてるぞ 9a68a29e1b #32 完了 2025-01-06 23:22:55 +09:00
みてるぞ 270b4515d8 #35 2025-01-05 17:41:41 +00:00
みてるぞ 9d0b5aff70 Revert "#35"
This reverts commit 41f5a7718f.
2025-01-05 17:37:15 +00:00
みてるぞ 41f5a7718f #35 2025-01-05 17:18:14 +00:00
みてるぞ 4ad5868b63 #34 2025-01-03 05:21:43 +09:00
みてるぞ 37c9947d4a #34 動画再生可能に 2025-01-03 05:03:18 +09:00
みてるぞ 6ee5582a32 不要なファイル削除 2024-12-27 01:03:37 +09:00
みてるぞ 12fbdbc7e2 #35 チャット取得確認用出力追加 2024-12-26 03:51:17 +00:00
みてるぞ a9ba0f697e #33 処理落ち対策 2024-12-26 00:52:43 +09:00
みてるぞ 93fc438d8a #31 layer を GameObject の 属性化 2024-12-25 23:18:36 +09:00
みてるぞ 50281f9120 #31 2024-12-25 01:19:42 +09:00
みてるぞ c6028507ea #31 2024-12-24 01:48:04 +09:00
みてるぞ 9149483dcb #31 2024-12-24 01:39:51 +09:00
みてるぞ a7785fa2c1 Merge branch 'ai-migration' of https://git.miteruzo.com/miteruzo/nizika_broadcast into ai-migration 2024-12-24 01:39:18 +09:00
みてるぞ 49661dad71 #31 2024-12-24 01:39:08 +09:00
24個のファイルの変更1123行の追加1600行の削除
+3
ファイルの表示
@@ -3,3 +3,6 @@ __pycache__
/nizika_talking.wav /nizika_talking.wav
/youtube.py /youtube.py
/log.txt /log.txt
/outputs/**
!/outputs/**/
!/outputs/**/.gitkeep
バイナリ
ファイルの表示
バイナリファイルは表示されません.
バイナリ
ファイルの表示
バイナリファイルは表示されません.

変更前

幅:  |  高さ:  |  サイズ: 945 KiB

変更後

幅:  |  高さ:  |  サイズ: 166 KiB

ファイルの表示
ファイルの表示
バイナリ
ファイルの表示
バイナリファイルは表示されません.

変更後

幅:  |  高さ:  |  サイズ: 463 KiB

バイナリ
ファイルの表示
バイナリファイルは表示されません.
バイナリ
ファイルの表示
バイナリファイルは表示されません.
-8
ファイルの表示
@@ -1,8 +0,0 @@
class CWindow:
WIDTH: int = 1024
HEIGHT: int = 768
class CMath:
PI: float = 3.14159265358979323846
-3
ファイルの表示
@@ -1,7 +1,5 @@
import unicodedata import unicodedata
from common_const import *
class CommonModule: class CommonModule:
@staticmethod @staticmethod
@@ -44,4 +42,3 @@ class CommonModule:
trimmed_left: str = string[cls.index_by_f2c (string, start):] trimmed_left: str = string[cls.index_by_f2c (string, start):]
return trimmed_left[:cls.index_by_f2c (trimmed_left, length)] return trimmed_left[:cls.index_by_f2c (trimmed_left, length)]
-7
ファイルの表示
@@ -1,7 +0,0 @@
# 各変数に適切な値を設定し,ファイル名を connection.py として保存すること
# Organisation ID
OPENAI_ORGANISATION: str = 'org-XXXXXXXXXXXXXXXXXXXXXXXX'
# API Key
OPENAI_API_KEY: str = 'sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
バイナリ
ファイルの表示
バイナリファイルは表示されません.
バイナリ
ファイルの表示
バイナリファイルは表示されません.
+1023 -453
ファイルの表示
ファイル差分が大きすぎるため省略します 差分を読込み
-8
ファイルの表示
@@ -1,8 +0,0 @@
from enum import Enum, auto
class Mode (Enum):
NIZIKA = auto ()
GOATOH = auto ()
DOUBLE = auto ()
バイナリ
ファイルの表示
バイナリファイルは表示されません.
サブモジュール 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 ()
バイナリ
ファイルの表示
バイナリファイルは表示されません.
-280
ファイルの表示
@@ -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 ''))
-793
ファイルの表示
@@ -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 ()
+96
ファイルの表示
@@ -0,0 +1,96 @@
from typing import Any
DEFAULT_W, DEFAULT_H = 1280, 720
FONT_NAME = 'Noto Sans CJK JP'
BASE_FONT = 42
SCROLL_DURATION = 4.
STATIC_DURATION = 3.
MARGIN_X = 20
LANE_PADDING = 6
def ass_time (
t: float,
) -> str:
if t < 0:
t = 0
cs = int (round (t * 100))
s, cs = divmod (cs, 100)
m, s = divmod (s, 60)
h, m = divmod (m, 60)
return f"{h}:{m:02d}:{s:02d}.{cs:02d}"
def escape_ass (
text: str,
) -> str:
text = text.replace ('\n', ' ').replace ('\r', ' ')
text = text.replace ('{', r'\{').replace ('}', r'\}')
text = text.replace ('\\', r'\\')
return text.strip ()
def approx_text_width_px (
text: str,
font_size: float,
) -> float:
return max (40., .62 * font_size * len (text))
def build_ass (
comments: list[dict[str, Any]],
w: int,
h: int,
) -> str:
header = f"""[Script Info]
ScriptType: v4.00+
PlayResX: {w}
PlayResY: {h}
ScaledBorderAndShadow: yes
[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Danmaku,{FONT_NAME},{BASE_FONT},&H00FFFFFF,&H00000000,&H00000000,&H64000000,0,0,0,0,100,100,0,0,1,2,1,7,{MARGIN_X},{MARGIN_X},10,1
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
"""
events: list[str] = []
lane_h = int (BASE_FONT + LANE_PADDING)
lane_count = max (6, (h - 80) // lane_h)
lane_next_free = [0.] * lane_count
top_rows = max (1, (h // 5) // lane_h)
bottom_rows = top_rows
top_next_free = [0.] * top_rows
bottom_next_free = [0.] * bottom_rows
def pick_lane (
next_free: list[float],
start: float,
) -> int:
for i, nf in enumerate (next_free):
if nf <= start:
return i
return min (range (len (next_free)), key = lambda i: next_free[i])
for c in comments:
text = escape_ass (c['content'])
if not text:
continue
start = c['vpos_ms']
end = c['vpos_ms'] + SCROLL_DURATION
lane = pick_lane (lane_next_free, start)
y = 40 + lane * lane_h
tw = approx_text_width_px (text, 1.)
x1 = w + MARGIN_X
x2 = -tw - MARGIN_X
lane_next_free[lane] = start + (SCROLL_DURATION * .65)
override = rf"{{\fs1\c&H00FFFFFF\move({int(x1)},{int(y)},{int(x2)},{int(y)})}}"
events.append (
f"Dialogue: 0,{ass_time(start)},{ass_time(end)},Danmaku,,0,0,0,,{override}{text}")
return header + '\n'.join (events) + '\n'
-3
ファイルの表示
@@ -1,3 +0,0 @@
# 各変数に適切な値を設定し,ファイル名を youtube.py として保存すること
YOUTUBE_ID: str = 'XXXXXXXXXXX' # YouTube の配信 ID