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

1058 lines
31 KiB

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