ニジカ AI 共通サービス
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.

374 lines
10 KiB

  1. """
  2. AI ニジカ常時稼動バッチ
  3. """
  4. from __future__ import annotations
  5. import asyncio
  6. import json
  7. import os
  8. import random
  9. import subprocess
  10. from asyncio import Lock
  11. from datetime import date, datetime, time, timedelta
  12. from typing import Any, TypedDict, cast
  13. import nicolib
  14. import queries_to_answers as q2a
  15. from nicolib import VideoInfo
  16. from nizika_ai.config import DB
  17. from nizika_ai.consts import Character, GPTModel, QueryType
  18. from nizika_ai.models import Query
  19. KIRIBAN_VIEWS_COUNTS: list[int] = sorted ({ *range (1_000, 10_000, 1_000),
  20. *range (10_000, 1_000_001, 10_000),
  21. 114_514, 1_940, 2_450, 5_100,
  22. 19_400, 24_500, 51_000, 93_194, 2_424, 242_424, 1_919,
  23. 4_545, 194_245, 245_194, 510_245 },
  24. reverse = True)
  25. kiriban_list: list[tuple[int, VideoInfo, datetime]] = []
  26. watched_videos: set[str] = set ()
  27. lock = Lock ()
  28. async def main (
  29. ) -> None:
  30. """
  31. メーン処理
  32. """
  33. await asyncio.gather (
  34. queries_to_answers (),
  35. report_kiriban (),
  36. report_nico (),
  37. update_kiriban_list (),
  38. report_snack_time (),
  39. report_hot_spring_time (),
  40. reconnect_db ())
  41. async def queries_to_answers (
  42. ) -> None:
  43. """
  44. クエリ処理
  45. """
  46. while True:
  47. loop = asyncio.get_running_loop ()
  48. await loop.run_in_executor (None, q2a.main)
  49. await asyncio.sleep (10)
  50. async def report_kiriban (
  51. ) -> None:
  52. """
  53. キリ番祝ひ
  54. """
  55. while True:
  56. if not kiriban_list:
  57. await wait_until (time (15, 0))
  58. continue
  59. # キリ番祝ひ
  60. async with lock:
  61. (views_count, video_info, uploaded_at) = (
  62. kiriban_list.pop (random.randint (0, len (kiriban_list) - 1)))
  63. video_code = video_info['contentId']
  64. comments = fetch_comments (video_code)
  65. popular_comments = sorted (comments,
  66. key = lambda c: c['nico_count'],
  67. reverse = True)[:10]
  68. latest_comments = sorted (comments,
  69. key = lambda c: c['posted_at'],
  70. reverse = True)[:10]
  71. prompt = (f"{ _format_elapsed (uploaded_at) }前にニコニコに投稿された"
  72. f"『{ video_info['title'] }』という動画が{ views_count }再生を突破しました。\n"
  73. f"コメント数は{ len (comments) }件です。\n")
  74. if video_info['tags']:
  75. prompt += f"つけられたタグは「{ '」、「'.join (video_info['tags']) }」です。\n"
  76. if comments:
  77. prompt += f"人気のコメントは次の通りです:「{ '」、「'.join (c['content'] for c in popular_comments) }」\n"
  78. if latest_comments != popular_comments:
  79. prompt += f"最新のコメントは次の通りです:「{ '」、「'.join (c['content'] for c in latest_comments) }」\n"
  80. prompt += f"""
  81. 概要には次のように書かれています:
  82. ```html
  83. { video_info['description'] }
  84. ```
  85. このことについて、何かお祝いメッセージを下さい。
  86. ただし、そのメッセージ内には再生数の数値を添えてください。
  87. また、つけられたタグ、コメントからどのような動画か想像し、説明してください。"""
  88. _add_query (prompt, QueryType.KIRIBAN, { 'video_code': video_code })
  89. # 待ち時間計算
  90. dt = datetime.now ()
  91. d = dt.date ()
  92. if dt.hour >= 15:
  93. d += timedelta (days = 1)
  94. remain = max (len (kiriban_list), 1)
  95. td = (datetime.combine (d, time (15, 0)) - dt) / remain
  96. # まれに時刻跨ぎでマイナスになるため
  97. if td.total_seconds () < 0:
  98. td = timedelta (seconds = 0)
  99. await asyncio.sleep (td.total_seconds ())
  100. async def update_kiriban_list (
  101. ) -> None:
  102. """
  103. キリ番リストの更新
  104. """
  105. while True:
  106. await wait_until (time (15, 0))
  107. new_list = fetch_kiriban_list (datetime.now ().date ())
  108. if not new_list:
  109. continue
  110. async with lock:
  111. have = { k[1]['contentId'] for k in kiriban_list }
  112. for item in new_list:
  113. if item[1]['contentId'] not in have:
  114. kiriban_list.append (item)
  115. have.add (item[1]['contentId'])
  116. def fetch_kiriban_list (
  117. base_date: date,
  118. ) -> list[tuple[int, VideoInfo, datetime]]:
  119. """
  120. キリ番を迎へた動画のリストを取得する.
  121. Parameters
  122. ----------
  123. base_date: date
  124. 基準日
  125. Return
  126. ------
  127. list[tuple[int, VideoInfo, datetime]]
  128. 動画リスト(キリ番基準再生数、対象動画情報、投稿日時のタプル)
  129. """
  130. result = subprocess.run (
  131. ['python3', '/root/nizika_nico/get_kiriban_list.py',
  132. str (base_date), *map (str, KIRIBAN_VIEWS_COUNTS)],
  133. cwd = '/root/nizika_nico',
  134. env = os.environ,
  135. capture_output = True,
  136. text = True)
  137. kl: list[list[int | str]] = json.loads (result.stdout)
  138. return [(cast (int, k[0]), video_info, str_to_datetime (cast (str, k[2])))
  139. for k in kl
  140. if (video_info := nicolib.fetch_video_info (cast (str, k[1]))) is not None]
  141. def fetch_comments (
  142. video_code: str,
  143. ) -> list[CommentDict]:
  144. """
  145. 動画のコメント・リストを取得する.
  146. Parameters
  147. ----------
  148. video_code: str
  149. ニコニコの動画コード
  150. Return
  151. ------
  152. list[CommentDict]
  153. コメント・リスト
  154. """
  155. result = subprocess.run (
  156. ['python3', 'get_comments_by_video_code.py', video_code],
  157. cwd = '/root/nizika_nico',
  158. env = os.environ,
  159. capture_output = True,
  160. text = True)
  161. rows: list[dict[str, Any]] = json.loads (result.stdout)
  162. comments: list[CommentDict] = []
  163. for row in rows:
  164. row['posted_at'] = str_to_datetime (row['posted_at'])
  165. comments.append (cast (CommentDict, row))
  166. return comments
  167. def fetch_latest_deerjika (
  168. ) -> VideoInfo | None:
  169. """
  170. 最新のぼざクリ動画を取得する.
  171. Return
  172. ------
  173. VideoInfo | None
  174. 動画情報
  175. """
  176. return nicolib.fetch_latest_video (['伊地知ニジカ',
  177. 'ぼざろクリーチャーシリーズ',
  178. 'ぼざろクリーチャーシリーズ外伝'])
  179. async def report_nico (
  180. ) -> None:
  181. """
  182. ニコニコから最新のぼざクリを取得し,まだ報知してゐなかったら報知する.
  183. """
  184. while True:
  185. latest_deerjika = fetch_latest_deerjika ()
  186. if latest_deerjika and latest_deerjika['contentId'] not in watched_videos:
  187. video = latest_deerjika
  188. watched_videos.add (video['contentId'])
  189. prompt = f"""ニコニコに『{ video['title'] }』という動画がアップされました。
  190. つけられたタグは「{ '」、「'.join (video['tags']) }」です。
  191. 概要には次のように書かれています:
  192. ```html
  193. { video['description'] }
  194. ```
  195. このことについて、みんなに告知するとともに、ニジカちゃんの感想を教えてください。"""
  196. _add_query (prompt, QueryType.NICO_REPORT, { 'video_code': video['contentId'] })
  197. await asyncio.sleep (60)
  198. async def wait_until (
  199. t: time,
  200. ) -> None:
  201. """
  202. 指定した時刻まで待つ.
  203. Parameters
  204. ----------
  205. t: time
  206. 次に実行を続行するまでの時刻
  207. """
  208. dt = datetime.now ()
  209. d = dt.date ()
  210. if dt.time () >= t:
  211. d += timedelta (days = 1)
  212. await asyncio.sleep ((datetime.combine (d, t) - dt).total_seconds ())
  213. async def report_snack_time (
  214. ) -> None:
  215. """
  216. おやつタイムを報知する.
  217. """
  218. while True:
  219. await wait_until (time (15, 0))
  220. _add_query ('おやつタイムだ!!!!', QueryType.SNACK_TIME)
  221. async def report_hot_spring_time (
  222. ) -> None:
  223. """
  224. 温泉タイムを報知する.
  225. """
  226. while True:
  227. await wait_until (time (21, 0))
  228. _add_query ('温泉に入ろう!!!', QueryType.HOT_SPRING)
  229. async def reconnect_db (
  230. ) -> None:
  231. while True:
  232. await asyncio.sleep (1800)
  233. try:
  234. DB.reconnect ('mysql')
  235. except Exception as e:
  236. if getattr (e, 'args', [None])[0] == 2006:
  237. print ('堕ちたな(確信).')
  238. else:
  239. raise
  240. def _add_query (
  241. content: str,
  242. query_type: QueryType,
  243. transfer_data: dict | None = None,
  244. ) -> None:
  245. query = Query ()
  246. query.user_id = None
  247. query.target_character = Character.DEERJIKA.value
  248. query.content = content
  249. query.query_type = query_type.value
  250. query.model = GPTModel.GPT4_O.value
  251. query.sent_at = datetime.now ()
  252. query.answered = False
  253. query.transfer_data = transfer_data
  254. query.save ()
  255. def _format_elapsed (
  256. uploaded_at: datetime,
  257. ) -> str:
  258. """
  259. 指定した時刻から現在までの時間を見やすぃ文字列に変換する.
  260. Parameters
  261. ----------
  262. uploaded_at: datetime
  263. 基準日時
  264. Return
  265. ------
  266. str
  267. 変換後文字列
  268. """
  269. delta = datetime.now () - uploaded_at
  270. days = delta.days
  271. seconds = delta.seconds
  272. (hours, seconds) = divmod (seconds, 3600)
  273. (mins, seconds) = divmod (seconds, 60)
  274. return f"{ days }日{ hours }時間{ mins }分{ seconds }秒"
  275. def str_to_datetime (
  276. s: str,
  277. ) -> datetime:
  278. formats: list[str] = [
  279. '%Y-%m-%d %H:%M:%S.%f',
  280. '%Y-%m-%d %H:%M:%S']
  281. for f in formats:
  282. try:
  283. return datetime.strptime (s, f)
  284. except ValueError:
  285. pass
  286. raise ValueError ('うんち!w')
  287. class CommentDict (TypedDict):
  288. id: int
  289. video_id: int
  290. comment_no: int
  291. user_id: int
  292. content: str
  293. posted_at: datetime
  294. nico_count: int
  295. vpos_ms: int
  296. kiriban_list = (
  297. fetch_kiriban_list ((now := datetime.now ()).date ()
  298. - timedelta (days = 1 if now.hour < 15 else 0)))
  299. if __name__ == '__main__':
  300. asyncio.run (main ())