伊地知ニジカ放送局だぬ゛ん゛. 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.

1015 lines
29 KiB

  1. from __future__ import annotations
  2. import math
  3. import os
  4. import random
  5. import sys
  6. import time
  7. import wave
  8. from datetime import datetime, timedelta
  9. from enum import Enum, auto
  10. from io import BytesIO
  11. from typing import Callable, TypedDict
  12. import cv2
  13. import emoji
  14. import ephem
  15. import numpy as np
  16. import pygame
  17. import pygame.gfxdraw
  18. import pytchat
  19. import requests
  20. from cv2 import VideoCapture
  21. from ephem import Moon, Observer, Sun
  22. from pygame import Rect, Surface
  23. from pygame.font import Font
  24. from pygame.mixer import Sound
  25. from pygame.time import Clock
  26. from pytchat.core.pytchat import PytchatCore
  27. from pytchat.processors.default.processor import Chat
  28. from aques import Aques
  29. from common_module import CommonModule
  30. from nizika_ai.config import DB
  31. from nizika_ai.consts import Character, GPTModel, Platform, QueryType
  32. from nizika_ai.models import Answer, AnsweredFlag, Query, User
  33. pygame.init ()
  34. FPS = 30
  35. SYSTEM_FONT = pygame.font.SysFont ('notosanscjkjp', 11, bold = True)
  36. USER_FONT = pygame.font.SysFont ('notosanscjkjp', 15, italic = True)
  37. DEERJIKA_FONT = pygame.font.SysFont ('07nikumarufont', 23)
  38. def main (
  39. ) -> None:
  40. game = Game ()
  41. Bg (game)
  42. balloon = Balloon (game)
  43. deerjika = Deerjika (game, DeerjikaPattern.RELAXED,
  44. x = CWindow.WIDTH * 3 / 4,
  45. y = CWindow.HEIGHT - 56.25,
  46. balloon = balloon)
  47. snack_time = SnackTime (game)
  48. CurrentTime (game, DEERJIKA_FONT)
  49. broadcast: Broadcast | None = None
  50. try:
  51. broadcast = Broadcast (os.environ['BROADCAST_CODE'])
  52. except KeyError:
  53. broadcast = None
  54. waiting_balloon = (False, '', '')
  55. last_flags_poll: float = 0
  56. while True:
  57. now_m = time.monotonic ()
  58. for event in pygame.event.get ():
  59. if event.type == pygame.QUIT:
  60. pygame.quit ()
  61. sys.exit ()
  62. if (not balloon.enabled) and (not snack_time.enabled):
  63. if waiting_balloon[0]:
  64. deerjika.talk (waiting_balloon[1], waiting_balloon[2])
  65. waiting_balloon = (False, '', '')
  66. if now_m - last_flags_poll >= 10:
  67. last_flags_poll = now_m
  68. log (f"balloon: { balloon.enabled }, snack: { snack_time.enabled }")
  69. try:
  70. DB.begin_transaction ()
  71. answer_flags = (AnsweredFlag.where ('platform', Platform.YOUTUBE.value)
  72. .where ('answered', False)
  73. .get ())
  74. log (f"pending: { len (answer_flags) }")
  75. if answer_flags:
  76. answer_flag = random.choice (answer_flags)
  77. log (f"flag_id: { answer_flag.id }, answer_id: { answer_flag.answer_id }")
  78. answer = Answer.find (answer_flag.answer_id)
  79. match QueryType (answer.query_rel.query_type):
  80. case QueryType.YOUTUBE_COMMENT:
  81. query = Query.find (answer.query_id)
  82. deerjika.talk (query.content, answer.content)
  83. answer_flag.answered = True
  84. answer_flag.save ()
  85. case QueryType.SNACK_TIME:
  86. snack_time.play ()
  87. query = Query.find (answer.query_id)
  88. waiting_balloon = (True, query.content, answer.content)
  89. answer_flag.answered = True
  90. answer_flag.save ()
  91. DB.commit ()
  92. except Exception as ex:
  93. DB.rollback ()
  94. log ('EXCEPTION in poll loop')
  95. print (ex)
  96. try:
  97. if broadcast is not None:
  98. add_query (broadcast)
  99. except Exception as ex:
  100. log ('EXCEPTION in adding a query')
  101. print (ex)
  102. game.redraw ()
  103. class Bg:
  104. """
  105. 背景オブゼクト管理用クラス
  106. Attributes:
  107. base (BgBase): 最背面
  108. grass (BgGrass): 草原部分
  109. jojoko (Jojoko): 大月ヨヨコ
  110. kita (KitaSun): き太く陽
  111. """
  112. base: BgBase
  113. grass: BgGrass
  114. jojoko: Jojoko
  115. kita: KitaSun
  116. def __init__ (
  117. self,
  118. game: Game,
  119. ):
  120. self.kita = KitaSun (game)
  121. self.base = BgBase (game, self.kita.sun, layer = self.kita.layer - 5)
  122. self.jojoko = Jojoko (game)
  123. self.grass = BgGrass (game)
  124. class DeerjikaPattern (Enum):
  125. """
  126. ニジカの状態
  127. Members:
  128. NORMAL: 通常
  129. RELAXED: 足パタパタ
  130. SLEEPING: 寝ニジカ
  131. DANCING: ダンシング・ニジカ
  132. """
  133. NORMAL = auto ()
  134. RELAXED = auto ()
  135. SLEEPING = auto ()
  136. DANCING = auto ()
  137. class Direction (Enum):
  138. """
  139. クリーチャの向き
  140. Members:
  141. LEFT: 左向き
  142. RIGHT: 右向き
  143. """
  144. LEFT = auto ()
  145. RIGHT = auto ()
  146. class Game:
  147. """
  148. ゲーム・クラス
  149. Attributes:
  150. clock (Clock): Clock オブゼクト
  151. frame (int): フレーム・カウンタ
  152. last_answered_at (datetime): 最後に回答した時刻
  153. now (datetime): 基準日時
  154. objects (list[GameObject]): 再描画するクラスのリスト
  155. screen (Surface): 基底スクリーン
  156. sky (Sky): 天体情報
  157. """
  158. bgm: Sound
  159. clock: Clock
  160. fps: float
  161. frame: int
  162. last_answered_at: datetime
  163. now: datetime
  164. objects: list[GameObject]
  165. screen: Surface
  166. sky: Sky
  167. def __init__ (
  168. self,
  169. ):
  170. self.now = datetime.now ()
  171. self.screen = pygame.display.set_mode ((CWindow.WIDTH, CWindow.HEIGHT))
  172. self.clock = Clock ()
  173. self.fps = FPS
  174. self.frame = 0
  175. self.objects = []
  176. self.bgm = Sound ('./assets/bgm.mp3')
  177. self.bgm.set_volume (.15)
  178. self.bgm.play (loops = -1)
  179. self._create_sky ()
  180. def redraw (
  181. self,
  182. ) -> None:
  183. self.now = datetime.now ()
  184. self.sky.observer.date = self.now - timedelta (hours = 9)
  185. for obj in sorted (self.objects, key = lambda obj: obj.layer):
  186. if obj.enabled:
  187. obj.redraw ()
  188. pygame.display.update ()
  189. delta_time = self.clock.tick (FPS) / 1000
  190. self.fps = 1 / delta_time
  191. if delta_time > 1 / FPS:
  192. for _ in range (int (FPS * delta_time) - 1):
  193. for obj in self.objects:
  194. if obj.enabled:
  195. obj.update ()
  196. def _create_sky (
  197. self,
  198. ) -> None:
  199. self.sky = Sky ()
  200. self.sky.observer = Observer ()
  201. self.sky.observer.lat = '35'
  202. self.sky.observer.lon = '139'
  203. class GameObject:
  204. """
  205. 各ゲーム・オブゼクトの基底クラス
  206. Attributes:
  207. arg (float): 回転角度 (rad)
  208. ax (float): X 軸に対する加速度 (px/frame^2)
  209. ay (float): y 軸に対する加速度 (px/frame^2)
  210. enabled (bool): オブゼクトの表示可否
  211. frame (int): フレーム・カウンタ
  212. game (Game): ゲーム基盤
  213. height (int): 高さ (px)
  214. vx (float): x 軸に対する速度 (px/frame)
  215. vy (float): y 軸に対する速度 (px/frame)
  216. width (int): 幅 (px)
  217. x (float): X 座標 (px)
  218. y (float): Y 座標 (px)
  219. """
  220. arg: float = 0
  221. ax: float = 0
  222. ay: float = 0
  223. enabled: bool = True
  224. frame: int
  225. game: Game
  226. height: int
  227. layer: float
  228. vx: float = 0
  229. vy: float = 0
  230. width: int
  231. x: float
  232. y: float
  233. def __init__ (
  234. self,
  235. game: Game,
  236. layer: float | None = None,
  237. enabled: bool = True,
  238. x: float = 0,
  239. y: float = 0,
  240. ):
  241. self.game = game
  242. self.enabled = enabled
  243. self.frame = 0
  244. if layer is None:
  245. if self.game.objects:
  246. layer = max (obj.layer for obj in self.game.objects) + 10
  247. else:
  248. layer = 0
  249. self.layer = layer
  250. self.x = x
  251. self.y = y
  252. self.game.objects.append (self)
  253. def redraw (
  254. self,
  255. ) -> None:
  256. self.update ()
  257. def update (
  258. self,
  259. ) -> None:
  260. self.x += self.vx
  261. self.y += self.vy
  262. self.vx += self.ax
  263. self.vy += self.ay
  264. self.frame += 1
  265. class BgBase (GameObject):
  266. """
  267. 背景
  268. Attributes:
  269. surface (Surface): 背景 Surface
  270. """
  271. bg: Surface
  272. bg_evening: Surface
  273. bg_grass: Surface
  274. bg_night: Surface
  275. sun: Sun
  276. def __init__ (
  277. self,
  278. game: Game,
  279. sun: Sun,
  280. layer: float,
  281. ):
  282. super ().__init__ (game, layer = layer)
  283. self.bg = self._load_image ('./assets/bg.jpg')
  284. self.bg_evening = self._load_image ('./assets/bg-evening.jpg')
  285. self.bg_grass = self._load_image ('./assets/bg-grass.png')
  286. self.bg_night = self._load_image ('./assets/bg-night.jpg')
  287. self.sun = sun
  288. @staticmethod
  289. def _load_image (
  290. path: str,
  291. ) -> Surface:
  292. return pygame.transform.scale (pygame.image.load (path),
  293. (CWindow.WIDTH, CWindow.HEIGHT))
  294. def redraw (
  295. self,
  296. ) -> None:
  297. date_tmp = self.game.sky.observer.date
  298. self.game.sky.observer.date = self.game.now.date ()
  299. sunrise_start: datetime = (
  300. (ephem.localtime (self.game.sky.observer.previous_rising (self.sun))
  301. - timedelta (minutes = 30)))
  302. sunrise_end: datetime = sunrise_start + timedelta (hours = 1)
  303. sunrise_centre: datetime = (
  304. sunrise_start + (sunrise_end - sunrise_start) / 2)
  305. sunset_start: datetime = (
  306. (ephem.localtime (self.game.sky.observer.next_setting (self.sun))
  307. - timedelta (minutes = 30)))
  308. sunset_end: datetime = sunset_start + timedelta (hours = 1)
  309. sunset_centre: datetime = (
  310. sunset_start + (sunset_end - sunset_start) / 2)
  311. self.game.sky.observer.date = date_tmp
  312. surface: Surface = ((self.bg
  313. if (sunrise_centre <= self.game.now < sunset_centre)
  314. else self.bg_night)
  315. .copy ())
  316. if sunrise_start <= self.game.now < sunrise_end:
  317. self.bg_evening.set_alpha (255 - int ((abs (self.game.now - sunrise_centre) * 510)
  318. / (sunrise_end - sunrise_centre)))
  319. elif sunset_start <= self.game.now < sunset_end:
  320. self.bg_evening.set_alpha (255 - int ((abs (self.game.now - sunset_centre) * 510)
  321. / (sunset_end - sunset_centre)))
  322. else:
  323. self.bg_evening.set_alpha (0)
  324. surface.blit (self.bg_evening, (0, 0))
  325. self.game.screen.blit (surface, (self.x, self.y))
  326. super ().redraw ()
  327. class BgGrass (GameObject):
  328. """
  329. 背景の草原部分
  330. Attributes:
  331. surface (Surface): 草原 Surface
  332. """
  333. surface: Surface
  334. def __init__ (
  335. self,
  336. game: Game,
  337. ):
  338. super ().__init__ (game)
  339. self.game = game
  340. self.surface = pygame.image.load ('./assets/bg-grass.png')
  341. self.surface = pygame.transform.scale (self.surface, (CWindow.WIDTH, CWindow.HEIGHT))
  342. def redraw (
  343. self,
  344. ) -> None:
  345. self.game.screen.blit (self.surface, (self.x, self.y))
  346. super ().redraw ()
  347. class Creature (GameObject):
  348. sound: Sound
  349. def bell (
  350. self,
  351. ) -> None:
  352. self.sound.play ()
  353. class Deerjika (Creature):
  354. """
  355. 伊地知ニジカ
  356. Attributes:
  357. height (int): 高さ (px)
  358. scale (float): 拡大率
  359. surfaces (list[Surface]): ニジカの各フレームを Surface にしたリスト
  360. width (int): 幅 (px)
  361. """
  362. FPS = 30
  363. height: int
  364. scale: float = 1
  365. surfaces: list[Surface]
  366. width: int
  367. talking: bool = False
  368. wav: bytearray | None = None
  369. balloon: Balloon
  370. def __init__ (
  371. self,
  372. game: Game,
  373. pattern: DeerjikaPattern = DeerjikaPattern.NORMAL,
  374. direction: Direction = Direction.LEFT,
  375. layer: float | None = None,
  376. x: float = 0,
  377. y: float = 0,
  378. balloon: Balloon | None = None,
  379. ):
  380. if balloon is None:
  381. raise Exception
  382. super ().__init__ (game, layer, x = x, y = y)
  383. self.pattern = pattern
  384. self.direction = direction
  385. self.balloon = balloon
  386. match pattern:
  387. case DeerjikaPattern.NORMAL:
  388. ...
  389. case DeerjikaPattern.RELAXED:
  390. match direction:
  391. case Direction.LEFT:
  392. self.width = 480
  393. self.height = 270
  394. surface = pygame.image.load ('./assets/deerjika_relax_left.png')
  395. self.surfaces = []
  396. for x in range (0, surface.get_width (), self.width):
  397. self.surfaces.append (
  398. surface.subsurface (x, 0, self.width, self.height))
  399. case Direction.RIGHT:
  400. ...
  401. self.sound = Sound ('./assets/noon.wav')
  402. def redraw (
  403. self,
  404. ) -> None:
  405. surface = self.surfaces[self.frame * self.FPS // FPS % len (self.surfaces)]
  406. if abs (self.scale - 1) > .05:
  407. surface = pygame.transform.scale (
  408. surface, (self.width * self.scale, self.height * self.scale))
  409. self.game.screen.blit (surface, surface.get_rect (center = (self.x, self.y)))
  410. super ().redraw ()
  411. def update (
  412. self,
  413. ) -> None:
  414. if (not self.balloon.enabled) and self.talking:
  415. self.talking = False
  416. if (self.balloon.enabled and self.balloon.frame >= FPS * 1.5
  417. and not self.talking):
  418. self.read_out ()
  419. super ().update ()
  420. def talk (
  421. self,
  422. query: str,
  423. answer: str,
  424. ) -> None:
  425. self.bell ()
  426. self._create_wav (answer)
  427. length = 300
  428. if self.wav is not None:
  429. with wave.open ('./nizika_talking.wav', 'rb') as f:
  430. length = int (FPS * (f.getnframes () / f.getframerate () + 4))
  431. self.balloon.talk (query, answer, length = length)
  432. def read_out (
  433. self,
  434. ) -> None:
  435. try:
  436. Sound ('./nizika_talking.wav').play ()
  437. except Exception:
  438. pass
  439. self.talking = True
  440. def _create_wav (
  441. self,
  442. message: str,
  443. ) -> None:
  444. try:
  445. self.wav = Aques.main (message, False)
  446. except Exception:
  447. self.wav = None
  448. if self.wav is None:
  449. return
  450. with open ('./nizika_talking.wav', 'wb') as f:
  451. f.write (self.wav)
  452. class CurrentTime (GameObject):
  453. """
  454. 現在日時表示
  455. Attributes:
  456. font (Font): フォント
  457. """
  458. font: Font
  459. def __init__ (
  460. self,
  461. game: Game,
  462. font: Font,
  463. ):
  464. super ().__init__ (game)
  465. self.font = font
  466. def redraw (
  467. self,
  468. ) -> None:
  469. for i in range (2):
  470. cl = (i * 255, i * 255, i * 255)
  471. self.game.screen.blit (self.font.render (str (self.game.now), True, cl), (-i, -i))
  472. self.game.screen.blit (self.font.render ('%2.3f fps' % self.game.fps, True, cl), (-i, 24 - i))
  473. super ().redraw ()
  474. class Balloon (GameObject):
  475. """
  476. 吹出し
  477. Attributes:
  478. answer (str): 回答テキスト
  479. image_url (str, None): 画像 URL
  480. length (int): 表示する時間 (frame)
  481. query (str): 質問テキスト
  482. surface (Surface): 吹出し Surface
  483. x_flip (bool): 左右反転フラグ
  484. y_flip (bool): 上下反転フラグ
  485. """
  486. answer: str = ''
  487. image_url: str | None = None
  488. length: int = 300
  489. query: str = ''
  490. surface: Surface
  491. x_flip: bool = False
  492. y_flip: bool = False
  493. def __init__ (
  494. self,
  495. game: Game,
  496. x_flip: bool = False,
  497. y_flip: bool = False,
  498. ):
  499. super ().__init__ (game, enabled = False)
  500. self.x_flip = x_flip
  501. self.y_flip = y_flip
  502. self.surface = pygame.transform.scale (pygame.image.load ('./assets/balloon.png'),
  503. (CWindow.WIDTH, CWindow.HEIGHT / 2))
  504. self.surface = pygame.transform.flip (self.surface, self.x_flip, self.y_flip)
  505. def redraw (
  506. self,
  507. ) -> None:
  508. if self.frame >= self.length:
  509. self.enabled = False
  510. self.game.last_answered_at = self.game.now
  511. return
  512. query = self.query
  513. if CommonModule.len_by_full (query) > 21:
  514. query = CommonModule.mid_by_full (query, 0, 19.5) + '...'
  515. answer = Surface (
  516. (375, int (((CommonModule.len_by_full (self.answer) - 1) // 16 + 1) * 23.4375)),
  517. pygame.SRCALPHA)
  518. for i in range (int (CommonModule.len_by_full (self.answer) - 1) // 16 + 1):
  519. answer.blit (DEERJIKA_FONT.render (
  520. CommonModule.mid_by_full (self.answer, 16 * i, 16), True, (192, 0, 0)),
  521. (0, 23.4375 * i))
  522. surface = self.surface.copy ()
  523. surface.blit (USER_FONT.render ('>' + query, True, (0, 0, 0)), (56.25, 32.8125))
  524. y: float
  525. if self.frame < 30:
  526. y = 0
  527. elif self.frame >= self.length - 90:
  528. y = answer.get_height () - 46.875
  529. else:
  530. y = int ((answer.get_height () - 46.875) * (self.frame - 30) / (self.length - 120))
  531. surface.blit (answer, (46.875, 70.3125), Rect (0, y, 375, 46.875))
  532. self.game.screen.blit (surface, (0, 0))
  533. super ().redraw ()
  534. def talk (
  535. self,
  536. query: str,
  537. answer: str,
  538. image_url: str | None = None,
  539. length: int = 300,
  540. ) -> None:
  541. self.query = query
  542. self.answer = answer
  543. self.image_url = image_url
  544. self.length = length
  545. self.frame = 0
  546. self.enabled = True
  547. class KitaSun (GameObject):
  548. """
  549. き太く陽
  550. Attributes:
  551. sun (Sun): ephem の太陽オブゼクト
  552. surface (Surface): き太く陽 Surface
  553. """
  554. alt: float
  555. az: float
  556. sun: Sun
  557. surface: Surface
  558. def __init__ (
  559. self,
  560. game: Game,
  561. ):
  562. super ().__init__ (game)
  563. self.surface = pygame.transform.scale (pygame.image.load ('./assets/sun.png'), (93.75, 93.75))
  564. self.sun = Sun ()
  565. def redraw (
  566. self,
  567. ) -> None:
  568. surface = pygame.transform.rotate (self.surface, -(90 + math.degrees (self.arg)))
  569. self.game.screen.blit (surface, surface.get_rect (center = (self.x, self.y)))
  570. super ().redraw ()
  571. def update (
  572. self,
  573. ) -> None:
  574. self.sun.compute (self.game.sky.observer)
  575. self.alt = self.sun.alt
  576. self.az = self.sun.az
  577. if abs (self.new_arg - self.arg) > math.radians (15):
  578. self.arg = self.new_arg
  579. self.x = self.new_x
  580. self.y = self.new_y
  581. super ().update ()
  582. @property
  583. def new_x (
  584. self,
  585. ) -> float:
  586. return CWindow.WIDTH * (math.degrees (self.az) - 80) / 120
  587. @property
  588. def new_y (
  589. self,
  590. ) -> float:
  591. return ((CWindow.HEIGHT / 2)
  592. - ((CWindow.HEIGHT / 2 + 46.875) * math.sin (self.alt)
  593. / math.sin (math.radians (60))))
  594. @property
  595. def new_arg (
  596. self,
  597. ) -> float:
  598. return math.atan2 (self.new_y - self.y, self.new_x - self.x)
  599. class Jojoko (GameObject):
  600. """
  601. 大月ヨヨコ
  602. Attributes:
  603. base (Surface): 満月ヨヨコ Surface
  604. moon (Moon): ephem の月オブゼクト
  605. surface (Surface): 缺けたヨヨコ
  606. """
  607. alt: float
  608. az: float
  609. base: Surface
  610. moon: Moon
  611. surface: Surface
  612. def __init__ (
  613. self,
  614. game: Game,
  615. ):
  616. super ().__init__ (game)
  617. self.base = pygame.transform.scale (pygame.image.load ('./assets/moon.png'), (93.75, 93.75))
  618. self.moon = Moon ()
  619. self.surface = self._get_surface ()
  620. def redraw (
  621. self,
  622. ) -> None:
  623. self.moon.compute (self.game.sky.observer)
  624. self.alt = self.moon.alt
  625. self.az = self.moon.az
  626. if abs (self.new_arg - self.arg) > math.radians (15):
  627. self.arg = self.new_arg
  628. self.x = self.new_x
  629. self.y = self.new_y
  630. if self.frame % (FPS * 3600) == 0:
  631. self.surface = self._get_surface ()
  632. surface = pygame.transform.rotate (self.surface, -(90 + math.degrees (self.arg)))
  633. surface.set_colorkey ((0, 255, 0))
  634. self.game.screen.blit (surface, surface.get_rect (center = (self.x, self.y)))
  635. super ().redraw ()
  636. @property
  637. def phase (
  638. self,
  639. ) -> float:
  640. dt: datetime = ephem.localtime (ephem.previous_new_moon (self.game.sky.observer.date))
  641. return (self.game.now - dt).total_seconds () / 60 / 60 / 24
  642. def _get_surface (
  643. self,
  644. ) -> Surface:
  645. """
  646. ヨヨコを月齢に応じて缺かす.
  647. Returns:
  648. Surface: 缺けたヨヨコ
  649. """
  650. jojoko = self.base.copy ()
  651. for i in range (200):
  652. if 1 <= self.phase < 15:
  653. 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))
  654. elif self.phase < 16:
  655. pass
  656. elif self.phase < 30:
  657. 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))
  658. else:
  659. jojoko.fill ((0, 255, 0))
  660. return jojoko
  661. @property
  662. def new_x (
  663. self,
  664. ) -> float:
  665. return CWindow.WIDTH * (math.degrees (self.az) - 80) / 120
  666. @property
  667. def new_y (
  668. self,
  669. ) -> float:
  670. return ((CWindow.HEIGHT / 2)
  671. - ((CWindow.HEIGHT / 2 + 46.875) * math.sin (self.alt)
  672. / math.sin (math.radians (60))))
  673. @property
  674. def new_arg (
  675. self,
  676. ) -> float:
  677. return math.atan2 (self.new_y - self.y, self.new_x - self.x)
  678. class Sky:
  679. """
  680. 天体に関する情報を保持するクラス
  681. Attributes:
  682. observer (Observer): 観測値
  683. """
  684. observer: Observer
  685. class CWindow:
  686. """
  687. ウィンドゥに関する定数クラス
  688. Attributes:
  689. WIDTH (int): ウィンドゥ幅
  690. HEIGHT (int): ウィンドゥ高さ
  691. """
  692. WIDTH = 480
  693. HEIGHT = 360
  694. class Broadcast:
  695. chat: PytchatCore
  696. code: str
  697. def __init__ (
  698. self,
  699. broadcast_code,
  700. ):
  701. self.code = broadcast_code
  702. self.chat = pytchat.create (self.code)
  703. def fetch_chat (
  704. self,
  705. ) -> Chat | None:
  706. if not self.chat.is_alive ():
  707. self.chat = pytchat.create (self.code)
  708. return None
  709. chats = self.chat.get ().items
  710. if not chats:
  711. return None
  712. return random.choice (chats)
  713. class Video (GameObject):
  714. fps: int
  715. pausing: bool = False
  716. sound: Sound | None
  717. surfaces: list[Surface]
  718. def __init__ (
  719. self,
  720. game: Game,
  721. path: str,
  722. ):
  723. super ().__init__ (game)
  724. self.pausing = False
  725. (self.surfaces, self.fps) = self._create_surfaces (path)
  726. self.sound = self._create_sound (path)
  727. self.stop ()
  728. def _create_sound (
  729. self,
  730. path: str,
  731. ) -> Sound | None:
  732. bytes_io = BytesIO ()
  733. try:
  734. from pydub import AudioSegment
  735. audio = AudioSegment.from_file (path, format = path.split ('.')[-1])
  736. except ModuleNotFoundError:
  737. return None
  738. audio.export (bytes_io, format = 'wav')
  739. bytes_io.seek (0)
  740. return pygame.mixer.Sound (bytes_io)
  741. def _create_surfaces (
  742. self,
  743. path: str,
  744. ) -> tuple[list[Surface], int]:
  745. cap = self._load (path)
  746. surfaces: list[Surface] = []
  747. if cap is None:
  748. return ([], FPS)
  749. fps = int (cap.get (cv2.CAP_PROP_FPS))
  750. while cap.isOpened ():
  751. frame = self._read_frame (cap)
  752. if frame is None:
  753. break
  754. surfaces.append (self._convert_to_surface (frame))
  755. new_surfaces: list[Surface] = []
  756. for i in range (len (surfaces) * FPS // fps):
  757. new_surfaces.append (surfaces[i * fps // FPS])
  758. return (new_surfaces, fps)
  759. def _load (
  760. self,
  761. path: str,
  762. ) -> VideoCapture | None:
  763. """
  764. OpenCV で動画を読込む.
  765. """
  766. cap = VideoCapture (path)
  767. if cap.isOpened ():
  768. return cap
  769. return None
  770. def _read_frame (
  771. self,
  772. cap: VideoCapture,
  773. ) -> np.ndarray | None:
  774. """
  775. 動画のフレームを読込む.
  776. """
  777. ret: bool
  778. frame: np.ndarray
  779. (ret, frame) = cap.read ()
  780. if ret:
  781. return frame
  782. return None
  783. def _convert_to_surface (
  784. self,
  785. frame: np.ndarray,
  786. ) -> Surface:
  787. frame = cv2.cvtColor (frame, cv2.COLOR_BGR2RGB)
  788. frame_surface = pygame.surfarray.make_surface (frame)
  789. frame_surface = pygame.transform.rotate (frame_surface, -90)
  790. frame_surface = pygame.transform.flip (frame_surface, True, False)
  791. return frame_surface
  792. def play (
  793. self,
  794. ) -> None:
  795. self.enabled = True
  796. self.pausing = False
  797. if self.sound is not None:
  798. self.sound.play ()
  799. def stop (
  800. self,
  801. ) -> None:
  802. self.enabled = False
  803. self.frame = 0
  804. def pause (
  805. self,
  806. ) -> None:
  807. self.pausing = True
  808. def redraw (
  809. self,
  810. ) -> None:
  811. surface = pygame.transform.scale (self.surfaces[self.frame], (self.width, self.height))
  812. self.game.screen.blit (surface, (self.x, self.y))
  813. super ().redraw ()
  814. def update (
  815. self,
  816. ) -> None:
  817. if self.frame >= len (self.surfaces) - 1:
  818. self.stop ()
  819. if self.pausing:
  820. self.frame -= 1
  821. super ().update ()
  822. class NicoVideo (Video):
  823. ...
  824. class SnackTime (Video):
  825. def __init__ (
  826. self,
  827. game: Game,
  828. ):
  829. super ().__init__ (game, './assets/snack_time.mp4')
  830. (self.width, self.height) = (CWindow.HEIGHT * 16 // 9, CWindow.HEIGHT)
  831. (self.x, self.y) = ((CWindow.WIDTH - self.width) / 2, 0)
  832. def fetch_bytes_from_url (
  833. url: str,
  834. ) -> bytes | None:
  835. res = requests.get (url, timeout = 60)
  836. if res.status_code != 200:
  837. return None
  838. return res.content
  839. def add_query (
  840. broadcast: Broadcast,
  841. ) -> None:
  842. chat = broadcast.fetch_chat ()
  843. if chat is None:
  844. return
  845. DB.begin_transaction ()
  846. chat.message = emoji.emojize (chat.message)
  847. user = (User.where ('platform', Platform.YOUTUBE.value)
  848. .where ('code', chat.author.channelId)
  849. .first ())
  850. if user is None:
  851. user = User ()
  852. user.platform = Platform.YOUTUBE.value
  853. user.code = chat.author.channelId
  854. user.name = chat.author.name
  855. user.icon = fetch_bytes_from_url (chat.author.imageUrl)
  856. user.save ()
  857. query = Query ()
  858. query.user_id = user.id
  859. query.target_character = Character.DEERJIKA.value
  860. query.content = chat.message
  861. query.query_type = QueryType.YOUTUBE_COMMENT.value
  862. query.model = GPTModel.GPT3_TURBO.value
  863. query.sent_at = datetime.now ()
  864. query.answered = False
  865. query.save ()
  866. DB.commit ()
  867. def log (
  868. msg: str,
  869. ) -> None:
  870. print (f"[{ datetime.now ().isoformat (sep = ' ', timespec = 'seconds') }] { msg }",
  871. flush = True)
  872. if __name__ == '__main__':
  873. main ()