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

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
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688
  1. from __future__ import annotations
  2. import math
  3. import random
  4. import sys
  5. from datetime import datetime, timedelta
  6. from enum import Enum, auto
  7. from typing import Callable, TypedDict
  8. import cv2
  9. import emoji
  10. import ephem # type: ignore
  11. import pygame
  12. import pygame.gfxdraw
  13. import pytchat # type: ignore
  14. from cv2 import VideoCapture
  15. from ephem import Moon, Observer, Sun # type: ignore
  16. from pygame import Rect, Surface
  17. from pygame.font import Font
  18. from pygame.mixer import Sound
  19. from pygame.time import Clock
  20. from pytchat.core.pytchat import PytchatCore # type: ignore
  21. from pytchat.processors.default.processor import Chat # type: ignore
  22. from common_module import CommonModule
  23. from nizika_ai.models import Answer, AnsweredFlag, Query, User
  24. from nizika_ai.consts import Platform
  25. pygame.init ()
  26. FPS = 30
  27. SYSTEM_FONT = pygame.font.SysFont ('notosanscjkjp', 24, bold = True)
  28. USER_FONT = pygame.font.SysFont ('notosanscjkjp', 32, italic = True)
  29. DEERJIKA_FONT = pygame.font.SysFont ('07nikumarufont', 50)
  30. def main (
  31. ) -> None:
  32. game = Game ()
  33. Bg (game)
  34. Deerjika (game, DeerjikaPattern.RELAXED, x = CWindow.WIDTH * 3 / 4, y = CWindow.HEIGHT - 120)
  35. Deerjika (game, DeerjikaPattern.RELAXED, x = CWindow.WIDTH * 3 / 4, y = CWindow.HEIGHT - 120)
  36. Deerjika (game, DeerjikaPattern.RELAXED, x = CWindow.WIDTH * 3 / 4, y = CWindow.HEIGHT - 120)
  37. Deerjika (game, DeerjikaPattern.RELAXED, x = CWindow.WIDTH * 3 / 4, y = CWindow.HEIGHT - 120)
  38. Deerjika (game, DeerjikaPattern.RELAXED, x = CWindow.WIDTH * 3 / 4, y = CWindow.HEIGHT - 120)
  39. balloon = Balloon (game)
  40. CurrentTime (game, SYSTEM_FONT)
  41. broadcast = Broadcast ()
  42. try:
  43. Sound ('assets/bgm.mp3').play (loops = -1)
  44. except Exception:
  45. pass
  46. while True:
  47. for event in pygame.event.get ():
  48. if event.type == pygame.QUIT:
  49. pygame.quit ()
  50. sys.exit ()
  51. chat = broadcast.fetch_chat ()
  52. if chat is not None:
  53. chat.message = emoji.emojize (chat.message)
  54. message: str = chat.message
  55. user = (User.where ('platform', Platform.YOUTUBE.value)
  56. .where ('code', chat.author.channelId)
  57. .get ())
  58. # chat.author
  59. game.redraw ()
  60. class Bg:
  61. """
  62. 背景オブゼクト管理用クラス
  63. Attributes:
  64. base (BgBase): 最背面
  65. grass (BgGrass): 草原部分
  66. jojoko (Jojoko): 大月ヨヨコ
  67. kita (KitaSun): き太く陽
  68. """
  69. base: BgBase
  70. grass: BgGrass
  71. jojoko: Jojoko
  72. kita: KitaSun
  73. def __init__ (
  74. self,
  75. game: Game,
  76. ):
  77. self.base = BgBase (game)
  78. self.jojoko = Jojoko (game)
  79. self.kita = KitaSun (game)
  80. self.grass = BgGrass (game)
  81. class DeerjikaPattern (Enum):
  82. """
  83. ニジカの状態
  84. Members:
  85. NORMAL: 通常
  86. RELAXED: 足パタパタ
  87. SLEEPING: 寝ニジカ
  88. DANCING: ダンシング・ニジカ
  89. """
  90. NORMAL = auto ()
  91. RELAXED = auto ()
  92. SLEEPING = auto ()
  93. DANCING = auto ()
  94. class Direction (Enum):
  95. """
  96. クリーチャの向き
  97. Members:
  98. LEFT: 左向き
  99. RIGHT: 右向き
  100. """
  101. LEFT = auto ()
  102. RIGHT = auto ()
  103. class Game:
  104. """
  105. ゲーム・クラス
  106. Attributes:
  107. clock (Clock): Clock オブゼクト
  108. frame (int): フレーム・カウンタ
  109. last_answered_at (datetime): 最後に回答した時刻
  110. now (datetime): 基準日時
  111. redrawers (list[Redrawer]): 再描画するクラスのリスト
  112. screen (Surface): 基底スクリーン
  113. sky (Sky): 天体情報
  114. """
  115. clock: Clock
  116. frame: int
  117. last_answered_at: datetime
  118. now: datetime
  119. redrawers: list[Redrawer]
  120. screen: Surface
  121. sky: Sky
  122. def __init__ (
  123. self,
  124. ):
  125. self.now = datetime.now ()
  126. self.screen = pygame.display.set_mode ((CWindow.WIDTH, CWindow.HEIGHT))
  127. self.clock = Clock ()
  128. self.frame = 0
  129. self.redrawers = []
  130. self._create_sky ()
  131. def redraw (
  132. self,
  133. ) -> None:
  134. self.now = datetime.now ()
  135. self.sky.observer.date = self.now - timedelta (hours = 9)
  136. for redrawer in sorted (self.redrawers, key = lambda x: x['layer']):
  137. if redrawer['obj'].enabled:
  138. redrawer['obj'].redraw ()
  139. pygame.display.update ()
  140. self.clock.tick (FPS)
  141. def _create_sky (
  142. self,
  143. ) -> None:
  144. self.sky = Sky ()
  145. self.sky.observer = Observer ()
  146. self.sky.observer.lat = '35'
  147. self.sky.observer.lon = '139'
  148. class GameObject:
  149. """
  150. 各ゲーム・オブゼクトの基底クラス
  151. Attributes:
  152. arg (float): 回転角度 (rad)
  153. ax (float): X 軸に対する加速度 (px/frame^2)
  154. ay (float): y 軸に対する加速度 (px/frame^2)
  155. enabled (bool): オブゼクトの表示可否
  156. frame (int): フレーム・カウンタ
  157. game (Game): ゲーム基盤
  158. height (int): 高さ (px)
  159. vx (float): x 軸に対する速度 (px/frame)
  160. vy (float): y 軸に対する速度 (px/frame)
  161. width (int): 幅 (px)
  162. x (float): X 座標 (px)
  163. y (float): Y 座標 (px)
  164. """
  165. arg: float = 0
  166. ax: float = 0
  167. ay: float = 0
  168. enabled: bool = True
  169. frame: int
  170. game: Game
  171. height: int
  172. vx: float = 0
  173. vy: float = 0
  174. width: int
  175. x: float
  176. y: float
  177. def __init__ (
  178. self,
  179. game: Game,
  180. layer: int | None = None,
  181. enabled: bool = True,
  182. x: float = 0,
  183. y: float = 0,
  184. ):
  185. self.game = game
  186. self.enabled = enabled
  187. self.frame = 0
  188. if layer is None:
  189. if self.game.redrawers:
  190. layer = max (r['layer'] for r in self.game.redrawers) + 10
  191. else:
  192. layer = 0
  193. self.game.redrawers.append ({ 'layer': layer, 'obj': self })
  194. self.x = x
  195. self.y = y
  196. def redraw (
  197. self,
  198. ) -> None:
  199. self.x += self.vx
  200. self.y += self.vy
  201. self.vx += self.ax
  202. self.vy += self.ay
  203. self.frame += 1
  204. class BgBase (GameObject):
  205. """
  206. 背景
  207. Attributes:
  208. surface (Surface): 背景 Surface
  209. """
  210. surface: Surface
  211. def __init__ (
  212. self,
  213. game: Game,
  214. ):
  215. super ().__init__ (game)
  216. self.surface = pygame.image.load ('assets/bg.jpg')
  217. self.surface = pygame.transform.scale (self.surface, (CWindow.WIDTH, CWindow.HEIGHT))
  218. def redraw (
  219. self,
  220. ) -> None:
  221. self.game.screen.blit (self.surface, (self.x, self.y))
  222. super ().redraw ()
  223. class BgGrass (GameObject):
  224. """
  225. 背景の草原部分
  226. Attributes:
  227. surface (Surface): 草原 Surface
  228. """
  229. surface: Surface
  230. def __init__ (
  231. self,
  232. game: Game,
  233. ):
  234. super ().__init__ (game)
  235. self.game = game
  236. self.surface = pygame.image.load ('assets/bg-grass.png')
  237. self.surface = pygame.transform.scale (self.surface, (CWindow.WIDTH, CWindow.HEIGHT))
  238. def redraw (
  239. self,
  240. ) -> None:
  241. self.game.screen.blit (self.surface, (self.x, self.y))
  242. super ().redraw ()
  243. class Deerjika (GameObject):
  244. """
  245. 伊地知ニジカ
  246. Attributes:
  247. height (int): 高さ (px)
  248. scale (float): 拡大率
  249. surfaces (list[Surface]): ニジカの各フレームを Surface にしたリスト
  250. width (int): 幅 (px)
  251. """
  252. height: int
  253. scale: float = .8
  254. surfaces: list[Surface]
  255. width: int
  256. def __init__ (
  257. self,
  258. game: Game,
  259. pattern: DeerjikaPattern = DeerjikaPattern.NORMAL,
  260. direction: Direction = Direction.LEFT,
  261. layer: int | None = None,
  262. x: float = 0,
  263. y: float = 0,
  264. ):
  265. super ().__init__ (game, layer, x = x, y = y)
  266. self.pattern = pattern
  267. self.direction = direction
  268. match pattern:
  269. case DeerjikaPattern.NORMAL:
  270. ...
  271. case DeerjikaPattern.RELAXED:
  272. match direction:
  273. case Direction.LEFT:
  274. self.width = 1280
  275. self.height = 720
  276. surface = pygame.image.load ('assets/deerjika_relax_left.png')
  277. self.surfaces = []
  278. for x in range (0, surface.get_width (), self.width):
  279. self.surfaces.append (
  280. surface.subsurface (x, 0, self.width, self.height))
  281. case Direction.RIGHT:
  282. ...
  283. def redraw (
  284. self,
  285. ) -> None:
  286. surface = pygame.transform.scale (self.surfaces[self.frame % len (self.surfaces)],
  287. (self.width * self.scale, self.height * self.scale))
  288. self.game.screen.blit (surface, surface.get_rect (center = (self.x, self.y)))
  289. super ().redraw ()
  290. self.x = random.randrange (CWindow.WIDTH)
  291. self.y = random.randrange (CWindow.HEIGHT)
  292. self.arg = math.radians (random.randrange (360))
  293. class CurrentTime (GameObject):
  294. """
  295. 現在日時表示
  296. Attributes:
  297. font (Font): フォント
  298. """
  299. font: Font
  300. def __init__ (
  301. self,
  302. game: Game,
  303. font: Font,
  304. ):
  305. super ().__init__ (game)
  306. self.font = font
  307. def redraw (
  308. self,
  309. ) -> None:
  310. for i in range (4):
  311. self.game.screen.blit (
  312. self.font.render (str (self.game.now), True, (0, 0, 0)),
  313. (i % 2, i // 2 * 2))
  314. super ().redraw ()
  315. class Balloon (GameObject):
  316. """
  317. 吹出し
  318. Attributes:
  319. answer (str): 回答テキスト
  320. image_url (str, None): 画像 URL
  321. length (int): 表示する時間 (frame)
  322. query (str): 質問テキスト
  323. surface (Surface): 吹出し Surface
  324. x_flip (bool): 左右反転フラグ
  325. y_flip (bool): 上下反転フラグ
  326. """
  327. answer: str = ''
  328. image_url: str | None = None
  329. length: int = 300
  330. query: str = ''
  331. surface: Surface
  332. x_flip: bool = False
  333. y_flip: bool = False
  334. def __init__ (
  335. self,
  336. game: Game,
  337. x_flip: bool = False,
  338. y_flip: bool = False,
  339. ):
  340. super ().__init__ (game, enabled = False)
  341. self.x_flip = x_flip
  342. self.y_flip = y_flip
  343. self.surface = pygame.transform.scale (pygame.image.load ('assets/balloon.png'),
  344. (CWindow.WIDTH, CWindow.HEIGHT / 2))
  345. self.surface = pygame.transform.flip (self.surface, self.x_flip, self.y_flip)
  346. def redraw (
  347. self,
  348. ) -> None:
  349. if self.frame >= self.length:
  350. self.enabled = False
  351. self.game.last_answered_at = self.game.now
  352. return
  353. query = self.query
  354. if CommonModule.len_by_full (query) > 21:
  355. query = CommonModule.mid_by_full (query, 0, 19.5) + '...'
  356. answer = Surface ((800, ((CommonModule.len_by_full (self.answer) - 1) // 16 + 1) * 50),
  357. pygame.SRCALPHA)
  358. for i in range (int (CommonModule.len_by_full (self.answer) - 1) // 16 + 1):
  359. answer.blit (DEERJIKA_FONT.render (
  360. CommonModule.mid_by_full (self.answer, 16 * i, 16), True, (192, 0, 0)),
  361. (0, 50 * i))
  362. surface = self.surface.copy ()
  363. surface.blit (USER_FONT.render ('>' + query, True, (0, 0, 0)), (120, 70))
  364. y: int
  365. if self.frame < 30:
  366. y = 0
  367. elif self.frame >= self.length - 90:
  368. y = answer.get_height () - 100
  369. else:
  370. y = int ((answer.get_height () - 100) * (self.frame - 30) / (self.length - 120))
  371. surface.blit (answer, (100, 150), Rect (0, y, 800, 100))
  372. self.game.screen.blit (surface, (0, 0))
  373. super ().redraw ()
  374. def talk (
  375. self,
  376. query: str,
  377. answer: str,
  378. image_url: str | None = None,
  379. length: int = 300,
  380. ) -> None:
  381. self.query = query
  382. self.answer = answer
  383. self.image_url = image_url
  384. self.length = length
  385. self.frame = 0
  386. self.enabled = True
  387. class KitaSun (GameObject):
  388. """
  389. き太く陽
  390. Attributes:
  391. sun (Sun): ephem の太陽オブゼクト
  392. surface (Surface): き太く陽 Surface
  393. """
  394. alt: float
  395. az: float
  396. sun: Sun
  397. surface: Surface
  398. def __init__ (
  399. self,
  400. game: Game,
  401. ):
  402. super ().__init__ (game)
  403. self.surface = pygame.transform.scale (pygame.image.load ('assets/sun.png'), (200, 200))
  404. self.sun = Sun ()
  405. def redraw (
  406. self,
  407. ) -> None:
  408. surface = pygame.transform.rotate (self.surface, -(90 + math.degrees (self.arg)))
  409. self.game.screen.blit (surface, surface.get_rect (center = (self.x, self.y)))
  410. super ().redraw ()
  411. self.sun.compute (self.game.sky.observer)
  412. self.alt = self.sun.alt
  413. self.az = self.sun.az
  414. if abs (self.new_arg - self.arg) > math.radians (15):
  415. self.arg = self.new_arg
  416. self.x = self.new_x
  417. self.y = self.new_y
  418. @property
  419. def new_x (
  420. self,
  421. ) -> float:
  422. return CWindow.WIDTH * (math.degrees (self.az) - 80) / 120
  423. @property
  424. def new_y (
  425. self,
  426. ) -> float:
  427. return ((CWindow.HEIGHT / 2)
  428. - ((CWindow.HEIGHT / 2 + 100) * math.sin (self.alt)
  429. / math.sin (math.radians (60))))
  430. @property
  431. def new_arg (
  432. self,
  433. ) -> float:
  434. return math.atan2 (self.new_y - self.y, self.new_x - self.x)
  435. class Jojoko (GameObject):
  436. """
  437. 大月ヨヨコ
  438. Attributes:
  439. base (Surface): 満月ヨヨコ Surface
  440. moon (Moon): ephem の月オブゼクト
  441. surface (Surface): 缺けたヨヨコ
  442. """
  443. alt: float
  444. az: float
  445. base: Surface
  446. moon: Moon
  447. surface: Surface
  448. def __init__ (
  449. self,
  450. game: Game,
  451. ):
  452. super ().__init__ (game)
  453. self.base = pygame.transform.scale (pygame.image.load ('assets/moon.png'), (200, 200))
  454. self.moon = Moon ()
  455. self.surface = self._get_surface ()
  456. def redraw (
  457. self,
  458. ) -> None:
  459. if self.frame % (FPS * 3600) == 0:
  460. self.surface = self._get_surface ()
  461. surface = pygame.transform.rotate (self.surface, -(90 + math.degrees (self.arg)))
  462. surface.set_colorkey ((0, 255, 0))
  463. self.game.screen.blit (surface, surface.get_rect (center = (self.x, self.y)))
  464. super ().redraw ()
  465. self.moon.compute (self.game.sky.observer)
  466. self.alt = self.moon.alt
  467. self.az = self.moon.az
  468. if abs (self.new_arg - self.arg) > math.radians (15):
  469. self.arg = self.new_arg
  470. self.x = self.new_x
  471. self.y = self.new_y
  472. @property
  473. def phase (
  474. self,
  475. ) -> float:
  476. dt: datetime = ephem.localtime (ephem.previous_new_moon (self.game.sky.observer.date))
  477. return (self.game.now - dt).total_seconds () / 60 / 60 / 24
  478. def _get_surface (
  479. self,
  480. ) -> Surface:
  481. """
  482. ヨヨコを月齢に応じて缺かす.
  483. Returns:
  484. Surface: 缺けたヨヨコ
  485. """
  486. jojoko = self.base.copy ()
  487. for i in range (200):
  488. if 1 <= self.phase < 15:
  489. pygame.gfxdraw.bezier (jojoko, ((0, 100 + i), (100, 180 * self.phase / 7 - 80 + i), (200, 100 + i)), 3, (0, 255, 0))
  490. elif self.phase < 16:
  491. pass
  492. elif self.phase < 30:
  493. pygame.gfxdraw.bezier (jojoko, ((0, 100 - i), (100, 180 * (self.phase - 15) / 7 - 80 - i), (200, 100 - i)), 3, (0, 255, 0))
  494. else:
  495. jojoko.fill ((0, 255, 0))
  496. return jojoko
  497. @property
  498. def new_x (
  499. self,
  500. ) -> float:
  501. return CWindow.WIDTH * (math.degrees (self.az) - 80) / 120
  502. @property
  503. def new_y (
  504. self,
  505. ) -> float:
  506. return ((CWindow.HEIGHT / 2)
  507. - ((CWindow.HEIGHT / 2 + 100) * math.sin (self.alt)
  508. / math.sin (math.radians (60))))
  509. @property
  510. def new_arg (
  511. self,
  512. ) -> float:
  513. return math.atan2 (self.new_y - self.y, self.new_x - self.x)
  514. class Sky:
  515. """
  516. 天体に関する情報を保持するクラス
  517. Attributes:
  518. observer (Observer): 観測値
  519. """
  520. observer: Observer
  521. class CWindow:
  522. """
  523. ウィンドゥに関する定数クラス
  524. Attributes:
  525. WIDTH (int): ウィンドゥ幅
  526. HEIGHT (int): ウィンドゥ高さ
  527. """
  528. WIDTH = 1024
  529. HEIGHT = 768
  530. class Redrawer (TypedDict):
  531. """
  532. 再描画処理を行ふゲーム・オブゼクトとその優先順位のペア
  533. Attributes:
  534. layer (int): レイア
  535. obj (GameObject): ゲーム・オブゼクト
  536. """
  537. layer: int
  538. obj: GameObject
  539. def get_surfaces_from_video (
  540. video_path: str,
  541. ) -> list[Surface]:
  542. cap = VideoCapture (video_path)
  543. if not cap.isOpened ():
  544. return []
  545. fps = cap.get (cv2.CAP_PROP_FPS)
  546. surfaces: list[Surface] = []
  547. while cap.isOpened ():
  548. (ret, frame) = cap.read ()
  549. if not ret:
  550. break
  551. frame = cv2.cvtColor (frame, cv2.COLOR_BGR2RGB)
  552. frame_surface = pygame.surfarray.make_surface (frame)
  553. frame_surface = pygame.transform.rotate (frame_surface, -90)
  554. surfaces.append (pygame.transform.flip (frame_surface, True, False))
  555. cap.release ()
  556. return surfaces
  557. class Broadcast:
  558. chat: PytchatCore
  559. def __init__ (
  560. self,
  561. ):
  562. self.chat = pytchat.create (os.environ['BROADCAST_CODE'])
  563. def fetch_chat (
  564. self,
  565. ) -> Chat | None:
  566. if not self.chat.is_alive ():
  567. return None
  568. chats = self.chat.get ().items
  569. if not chats:
  570. return None
  571. return random.choice (chats)
  572. class Log:
  573. ...
  574. if __name__ == '__main__':
  575. main ()