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

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