伊地知ニジカ放送局だぬ゛ん゛. https://www.youtube.com/@deerjika
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

test.py 20 KiB

4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
4 weeks ago
4 weeks ago
4 weeks ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731
  1. from __future__ import annotations
  2. import math
  3. import os
  4. import random
  5. import sys
  6. from datetime import datetime, timedelta
  7. from enum import Enum, auto
  8. from typing import Callable, TypedDict
  9. import cv2
  10. import emoji
  11. import ephem # type: ignore
  12. import pygame
  13. import pygame.gfxdraw
  14. import pytchat # type: ignore
  15. import requests
  16. from cv2 import VideoCapture
  17. from ephem import Moon, Observer, Sun # type: ignore
  18. from pygame import Rect, Surface
  19. from pygame.font import Font
  20. from pygame.mixer import Sound
  21. from pygame.time import Clock
  22. from pytchat.core.pytchat import PytchatCore # type: ignore
  23. from pytchat.processors.default.processor import Chat # type: ignore
  24. from common_module import CommonModule
  25. from nizika_ai.models import Answer, AnsweredFlag, Query, User
  26. from nizika_ai.consts import AnswerType, Character, GPTModel, Platform, QueryType
  27. pygame.init ()
  28. FPS = 30
  29. SYSTEM_FONT = pygame.font.SysFont ('notosanscjkjp', 24, bold = True)
  30. USER_FONT = pygame.font.SysFont ('notosanscjkjp', 32, italic = True)
  31. DEERJIKA_FONT = pygame.font.SysFont ('07nikumarufont', 50)
  32. def main (
  33. ) -> None:
  34. game = Game ()
  35. Bg (game)
  36. Deerjika (game, DeerjikaPattern.RELAXED, x = CWindow.WIDTH * 3 / 4, y = CWindow.HEIGHT - 120)
  37. Deerjika (game, DeerjikaPattern.RELAXED, x = CWindow.WIDTH * 3 / 4, y = CWindow.HEIGHT - 120)
  38. Deerjika (game, DeerjikaPattern.RELAXED, x = CWindow.WIDTH * 3 / 4, y = CWindow.HEIGHT - 120)
  39. Deerjika (game, DeerjikaPattern.RELAXED, x = CWindow.WIDTH * 3 / 4, y = CWindow.HEIGHT - 120)
  40. Deerjika (game, DeerjikaPattern.RELAXED, x = CWindow.WIDTH * 3 / 4, y = CWindow.HEIGHT - 120)
  41. balloon = Balloon (game)
  42. CurrentTime (game, SYSTEM_FONT)
  43. broadcast = Broadcast (os.environ['BROADCAST_CODE'])
  44. try:
  45. Sound ('assets/bgm.mp3').play (loops = -1)
  46. except Exception:
  47. pass
  48. while True:
  49. for event in pygame.event.get ():
  50. if event.type == pygame.QUIT:
  51. pygame.quit ()
  52. sys.exit ()
  53. answer_flags = (AnsweredFlag.where ('platform', Platform.YOUTUBE.value)
  54. .where ('answered', False)
  55. .get ())
  56. if not balloon.enabled:
  57. if answer_flags:
  58. answer_id: int = random.choice (answer_flags).answer_id
  59. answer = Answer.find (answer_id)
  60. if answer.answer_type == AnswerType.YOUTUBE_REPLY.value:
  61. query = Query.find (answer.query_id)
  62. balloon.talk (query.content, answer.content)
  63. add_query (broadcast)
  64. game.redraw ()
  65. class Bg:
  66. """
  67. 背景オブゼクト管理用クラス
  68. Attributes:
  69. base (BgBase): 最背面
  70. grass (BgGrass): 草原部分
  71. jojoko (Jojoko): 大月ヨヨコ
  72. kita (KitaSun): き太く陽
  73. """
  74. base: BgBase
  75. grass: BgGrass
  76. jojoko: Jojoko
  77. kita: KitaSun
  78. def __init__ (
  79. self,
  80. game: Game,
  81. ):
  82. self.base = BgBase (game)
  83. self.jojoko = Jojoko (game)
  84. self.kita = KitaSun (game)
  85. self.grass = BgGrass (game)
  86. class DeerjikaPattern (Enum):
  87. """
  88. ニジカの状態
  89. Members:
  90. NORMAL: 通常
  91. RELAXED: 足パタパタ
  92. SLEEPING: 寝ニジカ
  93. DANCING: ダンシング・ニジカ
  94. """
  95. NORMAL = auto ()
  96. RELAXED = auto ()
  97. SLEEPING = auto ()
  98. DANCING = auto ()
  99. class Direction (Enum):
  100. """
  101. クリーチャの向き
  102. Members:
  103. LEFT: 左向き
  104. RIGHT: 右向き
  105. """
  106. LEFT = auto ()
  107. RIGHT = auto ()
  108. class Game:
  109. """
  110. ゲーム・クラス
  111. Attributes:
  112. clock (Clock): Clock オブゼクト
  113. frame (int): フレーム・カウンタ
  114. last_answered_at (datetime): 最後に回答した時刻
  115. now (datetime): 基準日時
  116. redrawers (list[Redrawer]): 再描画するクラスのリスト
  117. screen (Surface): 基底スクリーン
  118. sky (Sky): 天体情報
  119. """
  120. clock: Clock
  121. frame: int
  122. last_answered_at: datetime
  123. now: datetime
  124. redrawers: list[Redrawer]
  125. screen: Surface
  126. sky: Sky
  127. def __init__ (
  128. self,
  129. ):
  130. self.now = datetime.now ()
  131. self.screen = pygame.display.set_mode ((CWindow.WIDTH, CWindow.HEIGHT))
  132. self.clock = Clock ()
  133. self.frame = 0
  134. self.redrawers = []
  135. self._create_sky ()
  136. def redraw (
  137. self,
  138. ) -> None:
  139. self.now = datetime.now ()
  140. self.sky.observer.date = self.now - timedelta (hours = 9)
  141. for redrawer in sorted (self.redrawers, key = lambda x: x['layer']):
  142. if redrawer['obj'].enabled:
  143. redrawer['obj'].redraw ()
  144. pygame.display.update ()
  145. self.clock.tick (FPS)
  146. def _create_sky (
  147. self,
  148. ) -> None:
  149. self.sky = Sky ()
  150. self.sky.observer = Observer ()
  151. self.sky.observer.lat = '35'
  152. self.sky.observer.lon = '139'
  153. class GameObject:
  154. """
  155. 各ゲーム・オブゼクトの基底クラス
  156. Attributes:
  157. arg (float): 回転角度 (rad)
  158. ax (float): X 軸に対する加速度 (px/frame^2)
  159. ay (float): y 軸に対する加速度 (px/frame^2)
  160. enabled (bool): オブゼクトの表示可否
  161. frame (int): フレーム・カウンタ
  162. game (Game): ゲーム基盤
  163. height (int): 高さ (px)
  164. vx (float): x 軸に対する速度 (px/frame)
  165. vy (float): y 軸に対する速度 (px/frame)
  166. width (int): 幅 (px)
  167. x (float): X 座標 (px)
  168. y (float): Y 座標 (px)
  169. """
  170. arg: float = 0
  171. ax: float = 0
  172. ay: float = 0
  173. enabled: bool = True
  174. frame: int
  175. game: Game
  176. height: int
  177. vx: float = 0
  178. vy: float = 0
  179. width: int
  180. x: float
  181. y: float
  182. def __init__ (
  183. self,
  184. game: Game,
  185. layer: int | None = None,
  186. enabled: bool = True,
  187. x: float = 0,
  188. y: float = 0,
  189. ):
  190. self.game = game
  191. self.enabled = enabled
  192. self.frame = 0
  193. if layer is None:
  194. if self.game.redrawers:
  195. layer = max (r['layer'] for r in self.game.redrawers) + 10
  196. else:
  197. layer = 0
  198. self.game.redrawers.append ({ 'layer': layer, 'obj': self })
  199. self.x = x
  200. self.y = y
  201. def redraw (
  202. self,
  203. ) -> None:
  204. self.x += self.vx
  205. self.y += self.vy
  206. self.vx += self.ax
  207. self.vy += self.ay
  208. self.frame += 1
  209. class BgBase (GameObject):
  210. """
  211. 背景
  212. Attributes:
  213. surface (Surface): 背景 Surface
  214. """
  215. surface: Surface
  216. def __init__ (
  217. self,
  218. game: Game,
  219. ):
  220. super ().__init__ (game)
  221. self.surface = pygame.image.load ('assets/bg.jpg')
  222. self.surface = pygame.transform.scale (self.surface, (CWindow.WIDTH, CWindow.HEIGHT))
  223. def redraw (
  224. self,
  225. ) -> None:
  226. self.game.screen.blit (self.surface, (self.x, self.y))
  227. super ().redraw ()
  228. class BgGrass (GameObject):
  229. """
  230. 背景の草原部分
  231. Attributes:
  232. surface (Surface): 草原 Surface
  233. """
  234. surface: Surface
  235. def __init__ (
  236. self,
  237. game: Game,
  238. ):
  239. super ().__init__ (game)
  240. self.game = game
  241. self.surface = pygame.image.load ('assets/bg-grass.png')
  242. self.surface = pygame.transform.scale (self.surface, (CWindow.WIDTH, CWindow.HEIGHT))
  243. def redraw (
  244. self,
  245. ) -> None:
  246. self.game.screen.blit (self.surface, (self.x, self.y))
  247. super ().redraw ()
  248. class Deerjika (GameObject):
  249. """
  250. 伊地知ニジカ
  251. Attributes:
  252. height (int): 高さ (px)
  253. scale (float): 拡大率
  254. surfaces (list[Surface]): ニジカの各フレームを Surface にしたリスト
  255. width (int): 幅 (px)
  256. """
  257. height: int
  258. scale: float = .8
  259. surfaces: list[Surface]
  260. width: int
  261. def __init__ (
  262. self,
  263. game: Game,
  264. pattern: DeerjikaPattern = DeerjikaPattern.NORMAL,
  265. direction: Direction = Direction.LEFT,
  266. layer: int | None = None,
  267. x: float = 0,
  268. y: float = 0,
  269. ):
  270. super ().__init__ (game, layer, x = x, y = y)
  271. self.pattern = pattern
  272. self.direction = direction
  273. match pattern:
  274. case DeerjikaPattern.NORMAL:
  275. ...
  276. case DeerjikaPattern.RELAXED:
  277. match direction:
  278. case Direction.LEFT:
  279. self.width = 1280
  280. self.height = 720
  281. surface = pygame.image.load ('assets/deerjika_relax_left.png')
  282. self.surfaces = []
  283. for x in range (0, surface.get_width (), self.width):
  284. self.surfaces.append (
  285. surface.subsurface (x, 0, self.width, self.height))
  286. case Direction.RIGHT:
  287. ...
  288. def redraw (
  289. self,
  290. ) -> None:
  291. surface = pygame.transform.scale (self.surfaces[self.frame % len (self.surfaces)],
  292. (self.width * self.scale, self.height * self.scale))
  293. self.game.screen.blit (surface, surface.get_rect (center = (self.x, self.y)))
  294. super ().redraw ()
  295. self.x = random.randrange (CWindow.WIDTH)
  296. self.y = random.randrange (CWindow.HEIGHT)
  297. self.arg = math.radians (random.randrange (360))
  298. class CurrentTime (GameObject):
  299. """
  300. 現在日時表示
  301. Attributes:
  302. font (Font): フォント
  303. """
  304. font: Font
  305. def __init__ (
  306. self,
  307. game: Game,
  308. font: Font,
  309. ):
  310. super ().__init__ (game)
  311. self.font = font
  312. def redraw (
  313. self,
  314. ) -> None:
  315. for i in range (4):
  316. self.game.screen.blit (
  317. self.font.render (str (self.game.now), True, (0, 0, 0)),
  318. (i % 2, i // 2 * 2))
  319. super ().redraw ()
  320. class Balloon (GameObject):
  321. """
  322. 吹出し
  323. Attributes:
  324. answer (str): 回答テキスト
  325. image_url (str, None): 画像 URL
  326. length (int): 表示する時間 (frame)
  327. query (str): 質問テキスト
  328. surface (Surface): 吹出し Surface
  329. x_flip (bool): 左右反転フラグ
  330. y_flip (bool): 上下反転フラグ
  331. """
  332. answer: str = ''
  333. image_url: str | None = None
  334. length: int = 300
  335. query: str = ''
  336. surface: Surface
  337. x_flip: bool = False
  338. y_flip: bool = False
  339. def __init__ (
  340. self,
  341. game: Game,
  342. x_flip: bool = False,
  343. y_flip: bool = False,
  344. ):
  345. super ().__init__ (game, enabled = False)
  346. self.x_flip = x_flip
  347. self.y_flip = y_flip
  348. self.surface = pygame.transform.scale (pygame.image.load ('assets/balloon.png'),
  349. (CWindow.WIDTH, CWindow.HEIGHT / 2))
  350. self.surface = pygame.transform.flip (self.surface, self.x_flip, self.y_flip)
  351. def redraw (
  352. self,
  353. ) -> None:
  354. if self.frame >= self.length:
  355. self.enabled = False
  356. self.game.last_answered_at = self.game.now
  357. return
  358. query = self.query
  359. if CommonModule.len_by_full (query) > 21:
  360. query = CommonModule.mid_by_full (query, 0, 19.5) + '...'
  361. answer = Surface ((800, ((CommonModule.len_by_full (self.answer) - 1) // 16 + 1) * 50),
  362. pygame.SRCALPHA)
  363. for i in range (int (CommonModule.len_by_full (self.answer) - 1) // 16 + 1):
  364. answer.blit (DEERJIKA_FONT.render (
  365. CommonModule.mid_by_full (self.answer, 16 * i, 16), True, (192, 0, 0)),
  366. (0, 50 * i))
  367. surface = self.surface.copy ()
  368. surface.blit (USER_FONT.render ('>' + query, True, (0, 0, 0)), (120, 70))
  369. y: int
  370. if self.frame < 30:
  371. y = 0
  372. elif self.frame >= self.length - 90:
  373. y = answer.get_height () - 100
  374. else:
  375. y = int ((answer.get_height () - 100) * (self.frame - 30) / (self.length - 120))
  376. surface.blit (answer, (100, 150), Rect (0, y, 800, 100))
  377. self.game.screen.blit (surface, (0, 0))
  378. super ().redraw ()
  379. def talk (
  380. self,
  381. query: str,
  382. answer: str,
  383. image_url: str | None = None,
  384. length: int = 300,
  385. ) -> None:
  386. self.query = query
  387. self.answer = answer
  388. self.image_url = image_url
  389. self.length = length
  390. self.frame = 0
  391. self.enabled = True
  392. class KitaSun (GameObject):
  393. """
  394. き太く陽
  395. Attributes:
  396. sun (Sun): ephem の太陽オブゼクト
  397. surface (Surface): き太く陽 Surface
  398. """
  399. alt: float
  400. az: float
  401. sun: Sun
  402. surface: Surface
  403. def __init__ (
  404. self,
  405. game: Game,
  406. ):
  407. super ().__init__ (game)
  408. self.surface = pygame.transform.scale (pygame.image.load ('assets/sun.png'), (200, 200))
  409. self.sun = Sun ()
  410. def redraw (
  411. self,
  412. ) -> None:
  413. surface = pygame.transform.rotate (self.surface, -(90 + math.degrees (self.arg)))
  414. self.game.screen.blit (surface, surface.get_rect (center = (self.x, self.y)))
  415. super ().redraw ()
  416. self.sun.compute (self.game.sky.observer)
  417. self.alt = self.sun.alt
  418. self.az = self.sun.az
  419. if abs (self.new_arg - self.arg) > math.radians (15):
  420. self.arg = self.new_arg
  421. self.x = self.new_x
  422. self.y = self.new_y
  423. @property
  424. def new_x (
  425. self,
  426. ) -> float:
  427. return CWindow.WIDTH * (math.degrees (self.az) - 80) / 120
  428. @property
  429. def new_y (
  430. self,
  431. ) -> float:
  432. return ((CWindow.HEIGHT / 2)
  433. - ((CWindow.HEIGHT / 2 + 100) * math.sin (self.alt)
  434. / math.sin (math.radians (60))))
  435. @property
  436. def new_arg (
  437. self,
  438. ) -> float:
  439. return math.atan2 (self.new_y - self.y, self.new_x - self.x)
  440. class Jojoko (GameObject):
  441. """
  442. 大月ヨヨコ
  443. Attributes:
  444. base (Surface): 満月ヨヨコ Surface
  445. moon (Moon): ephem の月オブゼクト
  446. surface (Surface): 缺けたヨヨコ
  447. """
  448. alt: float
  449. az: float
  450. base: Surface
  451. moon: Moon
  452. surface: Surface
  453. def __init__ (
  454. self,
  455. game: Game,
  456. ):
  457. super ().__init__ (game)
  458. self.base = pygame.transform.scale (pygame.image.load ('assets/moon.png'), (200, 200))
  459. self.moon = Moon ()
  460. self.surface = self._get_surface ()
  461. def redraw (
  462. self,
  463. ) -> None:
  464. if self.frame % (FPS * 3600) == 0:
  465. self.surface = self._get_surface ()
  466. surface = pygame.transform.rotate (self.surface, -(90 + math.degrees (self.arg)))
  467. surface.set_colorkey ((0, 255, 0))
  468. self.game.screen.blit (surface, surface.get_rect (center = (self.x, self.y)))
  469. super ().redraw ()
  470. self.moon.compute (self.game.sky.observer)
  471. self.alt = self.moon.alt
  472. self.az = self.moon.az
  473. if abs (self.new_arg - self.arg) > math.radians (15):
  474. self.arg = self.new_arg
  475. self.x = self.new_x
  476. self.y = self.new_y
  477. @property
  478. def phase (
  479. self,
  480. ) -> float:
  481. dt: datetime = ephem.localtime (ephem.previous_new_moon (self.game.sky.observer.date))
  482. return (self.game.now - dt).total_seconds () / 60 / 60 / 24
  483. def _get_surface (
  484. self,
  485. ) -> Surface:
  486. """
  487. ヨヨコを月齢に応じて缺かす.
  488. Returns:
  489. Surface: 缺けたヨヨコ
  490. """
  491. jojoko = self.base.copy ()
  492. for i in range (200):
  493. if 1 <= self.phase < 15:
  494. pygame.gfxdraw.bezier (jojoko, ((0, 100 + i), (100, 180 * self.phase / 7 - 80 + i), (200, 100 + i)), 3, (0, 255, 0))
  495. elif self.phase < 16:
  496. pass
  497. elif self.phase < 30:
  498. pygame.gfxdraw.bezier (jojoko, ((0, 100 - i), (100, 180 * (self.phase - 15) / 7 - 80 - i), (200, 100 - i)), 3, (0, 255, 0))
  499. else:
  500. jojoko.fill ((0, 255, 0))
  501. return jojoko
  502. @property
  503. def new_x (
  504. self,
  505. ) -> float:
  506. return CWindow.WIDTH * (math.degrees (self.az) - 80) / 120
  507. @property
  508. def new_y (
  509. self,
  510. ) -> float:
  511. return ((CWindow.HEIGHT / 2)
  512. - ((CWindow.HEIGHT / 2 + 100) * math.sin (self.alt)
  513. / math.sin (math.radians (60))))
  514. @property
  515. def new_arg (
  516. self,
  517. ) -> float:
  518. return math.atan2 (self.new_y - self.y, self.new_x - self.x)
  519. class Sky:
  520. """
  521. 天体に関する情報を保持するクラス
  522. Attributes:
  523. observer (Observer): 観測値
  524. """
  525. observer: Observer
  526. class CWindow:
  527. """
  528. ウィンドゥに関する定数クラス
  529. Attributes:
  530. WIDTH (int): ウィンドゥ幅
  531. HEIGHT (int): ウィンドゥ高さ
  532. """
  533. WIDTH = 1024
  534. HEIGHT = 768
  535. class Redrawer (TypedDict):
  536. """
  537. 再描画処理を行ふゲーム・オブゼクトとその優先順位のペア
  538. Attributes:
  539. layer (int): レイア
  540. obj (GameObject): ゲーム・オブゼクト
  541. """
  542. layer: int
  543. obj: GameObject
  544. def get_surfaces_from_video (
  545. video_path: str,
  546. ) -> list[Surface]:
  547. cap = VideoCapture (video_path)
  548. if not cap.isOpened ():
  549. return []
  550. fps = cap.get (cv2.CAP_PROP_FPS)
  551. surfaces: list[Surface] = []
  552. while cap.isOpened ():
  553. (ret, frame) = cap.read ()
  554. if not ret:
  555. break
  556. frame = cv2.cvtColor (frame, cv2.COLOR_BGR2RGB)
  557. frame_surface = pygame.surfarray.make_surface (frame)
  558. frame_surface = pygame.transform.rotate (frame_surface, -90)
  559. surfaces.append (pygame.transform.flip (frame_surface, True, False))
  560. cap.release ()
  561. return surfaces
  562. class Broadcast:
  563. chat: PytchatCore
  564. def __init__ (
  565. self,
  566. broadcast_code,
  567. ):
  568. self.chat = pytchat.create (broadcast_code)
  569. def fetch_chat (
  570. self,
  571. ) -> Chat | None:
  572. if not self.chat.is_alive ():
  573. return None
  574. chats = self.chat.get ().items
  575. if not chats:
  576. return None
  577. return random.choice (chats)
  578. class Log:
  579. ...
  580. def fetch_bytes_from_url (
  581. url: str,
  582. ) -> bytes | None:
  583. res = requests.get (url, timeout = 60)
  584. if res.status_code != 200:
  585. return None
  586. return res.content
  587. def add_query (
  588. broadcast: Broadcast,
  589. ) -> None:
  590. chat = broadcast.fetch_chat ()
  591. if chat is None:
  592. return
  593. chat.message = emoji.emojize (chat.message)
  594. message: str = chat.message
  595. user = (User.where ('platform', Platform.YOUTUBE.value)
  596. .where ('code', chat.author.channelId)
  597. .first ())
  598. if user is None:
  599. user = User ()
  600. user.platform = Platform.YOUTUBE.value
  601. user.code = chat.author.channelId
  602. user.name = chat.author.name
  603. user.icon = fetch_bytes_from_url (chat.author.imageUrl)
  604. user.save ()
  605. query = Query ()
  606. query.user_id = user.id
  607. query.target_character = Character.DEERJIKA.value
  608. query.content = chat.message
  609. query.query_type = QueryType.YOUTUBE_COMMENT.value
  610. query.model = GPTModel.GPT3_TURBO.value
  611. query.sent_at = datetime.now ()
  612. query.answerd = False
  613. query.save ()
  614. if __name__ == '__main__':
  615. main ()