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

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