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

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