ニジカ 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.

317 lines
9.7 KiB

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