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

test.py 25 KiB

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