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

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