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

987 lines
28 KiB

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