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

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