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

273 lines
8.6 KiB

  1. from __future__ import annotations
  2. import asyncio
  3. import random
  4. from datetime import date, datetime, time, timedelta
  5. from typing import TypedDict, cast
  6. import requests
  7. from bs4 import BeautifulSoup
  8. from requests.exceptions import Timeout
  9. import queries_to_answers as q2a
  10. from db.models import Comment, Video, VideoHistory
  11. from nizika_ai.consts import Character, GPTModel, QueryType
  12. from nizika_ai.models import Query
  13. KIRIBAN_VIEWS_COUNTS: list[int] = sorted ({ *range (1_000, 10_000, 1_000),
  14. *range (10_000, 1_000_001, 10_000),
  15. 114_514, 1_940, 2_450, 5_100,
  16. 19_400, 24_500, 51_000, 93_194, 2_424, 242_424, 1_919,
  17. 4_545, 194_245, 245_194, 510_245 },
  18. reverse = True)
  19. kiriban_list: list[tuple[int, VideoInfo, datetime]]
  20. async def main (
  21. ) -> None:
  22. await asyncio.gather (
  23. queries_to_answers (),
  24. report_kiriban (),
  25. report_nico (),
  26. update_kiriban_list ())
  27. async def queries_to_answers (
  28. ) -> None:
  29. while True:
  30. q2a.main ()
  31. await asyncio.sleep (10)
  32. async def report_kiriban (
  33. ) -> None:
  34. while True:
  35. # キリ番祝ひ
  36. (views_count, video_info, uploaded_at) = (
  37. kiriban_list.pop (random.randint (0, len (kiriban_list) - 1)))
  38. since_posted = datetime.now () - uploaded_at
  39. video_code = video_info['contentId']
  40. uri = f"https://www.nicovideo.jp/watch/{ video_code }"
  41. (title, description, _) = fetch_embed_info (uri)
  42. comments = fetch_comments (video_code)
  43. popular_comments = sorted (comments,
  44. key = lambda c: c.nico_count,
  45. reverse = True)[:10]
  46. latest_comments = sorted (comments,
  47. key = lambda c: c.posted_at,
  48. reverse = True)[:10]
  49. prompt = f"{ since_posted.days }日と{ since_posted.seconds }秒前にニコニコに投稿された『{ video_info['title'] }』という動画が{ views_count }再生を突破しました。\n"
  50. prompt += f"コメント数は{ len (comments) }件です。\n"
  51. if video_info['tags']:
  52. prompt += f"つけられたタグは「{ '」、「'.join (video_info['tags']) }」です。\n"
  53. if comments:
  54. prompt += f"人気のコメントは次の通りです:「{ '」、「'.join (c.content for c in popular_comments) }」\n"
  55. prompt += f"最新のコメントは次の通りです:「{ '」、「'.join (c.content for c in latest_comments) }」\n"
  56. prompt += f"""
  57. 概要には次のように書かれています:
  58. ```html
  59. { video_info['description'] }
  60. ```
  61. このことについて、何かお祝いメッセージを下さい。
  62. ただし、そのメッセージ内には再生数の数値を添えてください。
  63. また、つけられたタグ、コメントからどのような動画か想像し、説明してください。"""
  64. query = Query ()
  65. query.user_id = None
  66. query.target_character = Character.DEERJIKA.value
  67. query.content = prompt
  68. query.query_type = QueryType.KIRIBAN.value
  69. query.model = GPTModel.GPT3_TURBO.value
  70. query.sent_at = datetime.now ()
  71. query.answered = False
  72. query.transfer_data = { 'video_code': video_code }
  73. query.save ()
  74. # 待ち時間計算
  75. dt = datetime.now ()
  76. d = dt.date ()
  77. if dt.hour >= 15:
  78. d += timedelta (days = 1)
  79. td = datetime.combine (d, time (15, 0)) - dt
  80. if kiriban_list:
  81. td /= len (kiriban_list)
  82. await asyncio.sleep (td.total_seconds ())
  83. async def update_kiriban_list (
  84. ) -> None:
  85. while True:
  86. await wait_until (time (15, 0))
  87. kiriban_list += fetch_kiriban_list (datetime.now ().date ())
  88. def fetch_kiriban_list (
  89. base_date: date,
  90. ) -> list[tuple[int, VideoInfo, datetime]]:
  91. _kiriban_list: list[tuple[int, VideoInfo, datetime]] = []
  92. latest_fetched_at = cast (date, (VideoHistory
  93. .where ('fetched_at', '<=', base_date)
  94. .max ('fetched_at')))
  95. for kiriban_views_count in KIRIBAN_VIEWS_COUNTS:
  96. targets = { vh.video.code for vh in (VideoHistory
  97. .where ('fetched_at', latest_fetched_at)
  98. .where ('views_count', '>=', kiriban_views_count)
  99. .get ()) }
  100. for code in targets:
  101. if code in [kiriban[1]['contentId'] for kiriban in _kiriban_list]:
  102. continue
  103. previous_views_count: int | None = (
  104. VideoHistory
  105. .where_has ('video', lambda q: q.where ('code', code))
  106. .where ('fetched_at', '<', latest_fetched_at)
  107. .max ('views_count'))
  108. if previous_views_count is None:
  109. previous_views_count = 0
  110. if previous_views_count >= kiriban_views_count:
  111. continue
  112. video_info = fetch_video_info (code)
  113. if video_info is not None:
  114. _kiriban_list.append ((kiriban_views_count, video_info,
  115. cast (Video, Video.where ('code', code).first ()).uploaded_at))
  116. return _kiriban_list
  117. def fetch_video_info (
  118. video_code: str,
  119. ) -> VideoInfo | None:
  120. video_info: dict[str, str | list[str]] = { 'contentId': video_code }
  121. bs = create_bs_from_url (f"https://www.nicovideo.jp/watch/{ video_code }")
  122. if bs is None:
  123. return None
  124. try:
  125. title = bs.find ('title')
  126. if title is None:
  127. return None
  128. video_info['title'] = '-'.join (title.text.split ('-')[:(-1)])[:(-1)]
  129. tags: str = bs.find ('meta', attrs = { 'name': 'keywords' }).get ('content') # type: ignore
  130. video_info['tags'] = tags.split (',')
  131. video_info['description'] = bs.find ('meta', attrs = { 'name': 'description' }).get ('content') # type: ignore
  132. except Exception:
  133. return None
  134. return cast (VideoInfo, video_info)
  135. def create_bs_from_url (
  136. url: str,
  137. params: dict | None = None,
  138. ) -> BeautifulSoup | None:
  139. """
  140. URL から BeautifulSoup インスタンス生成
  141. Parameters
  142. ----------
  143. url: str
  144. 捜査する URL
  145. params: dict
  146. パラメータ
  147. Return
  148. ------
  149. BeautifulSoup | None
  150. BeautifulSoup オブゼクト(失敗したら None)
  151. """
  152. if params is None:
  153. params = { }
  154. try:
  155. req = requests.get (url, params = params, timeout = 60)
  156. except Timeout:
  157. return None
  158. if req.status_code != 200:
  159. return None
  160. req.encoding = req.apparent_encoding
  161. return BeautifulSoup (req.text, 'hecoml.parser')
  162. def fetch_embed_info (
  163. url: str,
  164. ) -> tuple[str, str, str]:
  165. title: str = ''
  166. description: str = ''
  167. thumbnail: str = ''
  168. try:
  169. res = requests.get (url, timeout = 60)
  170. except Timeout:
  171. return ('', '', '')
  172. if res.status_code != 200:
  173. return ('', '', '')
  174. soup = BeautifulSoup (res.text, 'html.parser')
  175. tmp = soup.find ('title')
  176. if tmp is not None:
  177. title = tmp.text
  178. tmp = soup.find ('meta', attrs = { 'name': 'description' })
  179. if tmp is not None and hasattr (tmp, 'get'):
  180. try:
  181. description = cast (str, tmp.get ('content'))
  182. except Exception:
  183. pass
  184. tmp = soup.find ('meta', attrs = { 'name': 'thumbnail' })
  185. if tmp is not None and hasattr (tmp, 'get'):
  186. try:
  187. thumbnail = cast (str, tmp.get ('content'))
  188. except Exception:
  189. pass
  190. return (title, description, thumbnail)
  191. def fetch_comments (
  192. video_code: str,
  193. ) -> list[Comment]:
  194. video = Video.where ('code', video_code).first ()
  195. if video is None:
  196. return []
  197. return video.comments
  198. async def report_nico (
  199. ) -> None:
  200. ...
  201. async def wait_until (
  202. t: time,
  203. ):
  204. dt = datetime.now ()
  205. d = dt.date ()
  206. if dt.time () >= t:
  207. d += timedelta (days = 1)
  208. await asyncio.sleep ((datetime.combine (d, t) - dt).total_seconds ())
  209. class VideoInfo (TypedDict):
  210. contentId: str
  211. title: str
  212. tags: list[str]
  213. description: str
  214. kiriban_list = (
  215. fetch_kiriban_list ((d := datetime.now ()).date ()
  216. - timedelta (days = d.hour < 15)))
  217. if __name__ == '__main__':
  218. asyncio.run (main ())