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

261 lines
9.6 KiB

  1. # vim: nosmartindent autoindent
  2. import json
  3. import random
  4. import subprocess
  5. import sys
  6. import time
  7. from datetime import datetime
  8. import emoji
  9. import pygame
  10. import pytchat
  11. from playsound import playsound
  12. from pygame.locals import *
  13. from aques import Aques
  14. from common_const import *
  15. from common_module import CommonModule
  16. from mode import Mode
  17. from talk import Talk
  18. from youtube import *
  19. class Main:
  20. @classmethod
  21. def main (
  22. cls,
  23. argv: list,
  24. argc: int) \
  25. -> None:
  26. mode = Mode.NIZIKA
  27. match (argc > 1) and argv[1]:
  28. case '-g':
  29. mode = Mode.GOATOH
  30. case '-w':
  31. mode = Mode.DOUBLE
  32. nizika_mode: bool = mode == Mode.NIZIKA
  33. goatoh_mode: bool = mode == Mode.GOATOH
  34. double_mode: bool = mode == Mode.DOUBLE
  35. print (mode)
  36. # ウィンドゥの初期化
  37. pygame.init ()
  38. screen: pygame.Surface = pygame.display.set_mode (
  39. (CWindow.WIDTH, CWindow.HEIGHT))
  40. # 吹き出し
  41. balloon = pygame.transform.scale (pygame.image.load ('talking.png'),
  42. (CWindow.WIDTH, 384))
  43. if goatoh_mode:
  44. balloon = pygame.transform.flip (balloon, False, True)
  45. # 背景(夕方)
  46. bg_evening: pygame.Surface = pygame.transform.scale (
  47. pygame.image.load ('bg-evening.jpg'),
  48. (CWindow.WIDTH, CWindow.HEIGHT))
  49. # 背景(夜)
  50. bg_night: pygame.Surface = pygame.transform.scale (
  51. pygame.image.load ('bg-night.jpg'),
  52. (CWindow.WIDTH, CWindow.HEIGHT))
  53. # 音声再生器の初期化
  54. pygame.mixer.init (frequency = 44100)
  55. # ニジカの “ぬ゛ぅ゛ぅ゛ぅ゛ん゛”
  56. noon = pygame.mixer.Sound ('noon.wav')
  57. # ゴートうの “ムムムム”
  58. mumumumu = pygame.mixer.Sound ('mumumumu.wav')
  59. # ゴートうの “クサタベテル!!”
  60. kusa = pygame.mixer.Sound ('kusa.wav')
  61. # YouTube Chat オブジェクト
  62. live_chat = pytchat.create (video_id = YOUTUBE_ID)
  63. # デバッグ・メシジのフォント
  64. system_font = pygame.font.SysFont ('notosanscjkjp', 24, bold = True)
  65. # 視聴者コメントのフォント
  66. user_font = pygame.font.SysFont ('notosanscjkjp', 32, italic = True)
  67. # ニジカのフォント
  68. nizika_font = pygame.font.SysFont ('07nikumarufont', 50)
  69. # Youtube Chat から取得したコメントたち
  70. chat_items: list = []
  71. # 会話の履歴(3 件分保持)
  72. histories: list = []
  73. while (True):
  74. cls.draw_bg (screen, bg_evening, bg_night)
  75. # 左上に時刻表示
  76. for i in range (4):
  77. screen.blit (
  78. system_font.render (str (datetime.now ()), True, (0, 0, 0)),
  79. (i % 2, i // 2 * 2))
  80. if live_chat.is_alive ():
  81. # Chat オブジェクトが有効
  82. # Chat 取得
  83. chat_items: list = live_chat.get ().items
  84. if chat_items:
  85. # 溜まってゐる Chat からランダムに 1 つ抽出
  86. chat_item = random.choice (chat_items)
  87. # 投稿者情報を辞書化
  88. chat_item.author = chat_item.author.__dict__
  89. # 絵文字を復元
  90. chat_item.message = emoji.emojize (chat_item.message)
  91. message: str = chat_item.message
  92. if nizika_mode:
  93. goatoh_talking = False
  94. if goatoh_mode:
  95. goatoh_talking = True
  96. if double_mode:
  97. goatoh_talking: bool = random.random () < .5
  98. while True:
  99. # ChatGPT API を呼出し,返答を取得
  100. answer: str = Talk.main (message, chat_item.author['name'], histories, goatoh_talking).replace ('\n', ' ')
  101. # 履歴に追加
  102. histories = (histories
  103. + [{'role': 'user', 'content': message},
  104. {'role': 'assistant', 'content': answer}])[(-12):]
  105. # ログ書込み
  106. with open ('log.txt', 'a') as f:
  107. f.write (f'{datetime.now ()}\t{json.dumps (chat_item.__dict__)}\t{answer}\n')
  108. # 吹出し描画(ニジカは上,ゴートうは下)
  109. if nizika_mode:
  110. screen.blit (balloon, (0, 0))
  111. if goatoh_mode:
  112. screen.blit (balloon, (0, 384))
  113. if double_mode:
  114. screen.blit (pygame.transform.flip (
  115. balloon,
  116. not goatoh_talking,
  117. False),
  118. (0, 0))
  119. # 視聴者コメント描画
  120. screen.blit (
  121. user_font.render (
  122. '> ' + (message
  123. if (CommonModule.len_by_full (message)
  124. <= 21)
  125. else (CommonModule.mid_by_full (
  126. message, 0, 19.5)
  127. + '...')),
  128. True,
  129. (0, 0, 0)),
  130. (120, 70 + 384) if goatoh_mode else (120 + (64 if (double_mode and not goatoh_talking) else 0), 70))
  131. # ニジカの返答描画
  132. screen.blit (
  133. nizika_font.render (
  134. (answer
  135. if CommonModule.len_by_full (answer) <= 16
  136. else CommonModule.mid_by_full (answer, 0, 16)),
  137. True,
  138. (192, 0, 0)),
  139. (100, 150 + 384) if goatoh_mode else (100 + (64 if (double_mode and not goatoh_talking) else 0), 150))
  140. if CommonModule.len_by_full (answer) > 16:
  141. screen.blit (
  142. nizika_font.render (
  143. (CommonModule.mid_by_full (answer, 16, 16)
  144. if CommonModule.len_by_full (answer) <= 32
  145. else (CommonModule.mid_by_full (
  146. answer, 16, 14.5)
  147. + '...')),
  148. True,
  149. (192, 0, 0)),
  150. (100, 200 + 384) if goatoh_mode else (100 + (64 if (double_mode and not goatoh_talking) else 0), 200))
  151. pygame.display.update ()
  152. # 鳴く.
  153. if goatoh_talking:
  154. if random.random () < .1:
  155. kusa.play ()
  156. else:
  157. mumumumu.play ()
  158. else:
  159. noon.play ()
  160. time.sleep (1.5)
  161. # 返答の読上げを WAV ディタとして生成,取得
  162. try:
  163. wav: bytearray | None = Aques.main (answer, goatoh_talking)
  164. except:
  165. wav: None = None
  166. # 読上げを再生
  167. if wav is not None:
  168. with open ('./nizika_talking.wav', 'wb') as f:
  169. f.write (wav)
  170. playsound ('./nizika_talking.wav')
  171. time.sleep (1)
  172. if not double_mode or random.random () < .5:
  173. break
  174. cls.draw_bg (screen, bg_evening, bg_night)
  175. chat_item.author = {'name': 'ゴートうひとり' if goatoh_talking else '伊地知ニジカ',
  176. 'id': '',
  177. 'imageUrl': './favicon-goatoh.ico' if goatoh_talking else './favicon.ico'}
  178. chat_item.message = histories.pop (-1)['content']
  179. message = chat_item.message
  180. goatoh_talking = not goatoh_talking
  181. else:
  182. # Chat オブジェクトが無効
  183. # 再生成
  184. live_chat = pytchat.create (video_id = YOUTUBE_ID)
  185. pygame.display.update ()
  186. for event in pygame.event.get ():
  187. if event.type == QUIT:
  188. pygame.quit ()
  189. sys.exit ()
  190. @classmethod
  191. def draw_bg (
  192. cls,
  193. screen: pygame.Surface,
  194. bg_evening: pygame.Surface,
  195. bg_night: pygame.Surface) \
  196. -> None:
  197. if 17 <= (h := datetime.now ().hour) < 18:
  198. screen.blit (bg_evening, (0, 0))
  199. elif (h < 6) or (18 <= h):
  200. screen.blit (bg_night, (0, 0))
  201. else:
  202. screen.fill ((0, 255, 0))
  203. if __name__ == '__main__':
  204. Main.main (sys.argv, len (sys.argv))