このコミットが含まれているのは:
@@ -1,10 +1,14 @@
|
|||||||
|
"""
|
||||||
|
AI ニジカ常時稼動バッチ
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import random
|
import random
|
||||||
from asyncio import Lock
|
from asyncio import Lock
|
||||||
from datetime import date, datetime, time, timedelta
|
from datetime import date, datetime, time, timedelta
|
||||||
from typing import TypedDict, cast
|
from typing import cast
|
||||||
|
|
||||||
import nicolib
|
import nicolib
|
||||||
import queries_to_answers as q2a
|
import queries_to_answers as q2a
|
||||||
@@ -21,11 +25,18 @@ KIRIBAN_VIEWS_COUNTS: list[int] = sorted ({ *range (1_000, 10_000, 1_000),
|
|||||||
reverse = True)
|
reverse = True)
|
||||||
|
|
||||||
kiriban_list: list[tuple[int, VideoInfo, datetime]]
|
kiriban_list: list[tuple[int, VideoInfo, datetime]]
|
||||||
|
|
||||||
|
watched_videos: set[str] = set ()
|
||||||
|
|
||||||
lock = Lock ()
|
lock = Lock ()
|
||||||
|
|
||||||
|
|
||||||
async def main (
|
async def main (
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""
|
||||||
|
メーン処理
|
||||||
|
"""
|
||||||
|
|
||||||
await asyncio.gather (
|
await asyncio.gather (
|
||||||
queries_to_answers (),
|
queries_to_answers (),
|
||||||
report_kiriban (),
|
report_kiriban (),
|
||||||
@@ -35,6 +46,10 @@ async def main (
|
|||||||
|
|
||||||
async def queries_to_answers (
|
async def queries_to_answers (
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""
|
||||||
|
クエリ処理
|
||||||
|
"""
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
loop = asyncio.get_running_loop ()
|
loop = asyncio.get_running_loop ()
|
||||||
await loop.run_in_executor (None, q2a.main)
|
await loop.run_in_executor (None, q2a.main)
|
||||||
@@ -43,6 +58,10 @@ async def queries_to_answers (
|
|||||||
|
|
||||||
async def report_kiriban (
|
async def report_kiriban (
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""
|
||||||
|
キリ番祝ひ
|
||||||
|
"""
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
if not kiriban_list:
|
if not kiriban_list:
|
||||||
await wait_until (time (15, 0))
|
await wait_until (time (15, 0))
|
||||||
@@ -53,15 +72,7 @@ async def report_kiriban (
|
|||||||
(views_count, video_info, uploaded_at) = (
|
(views_count, video_info, uploaded_at) = (
|
||||||
kiriban_list.pop (random.randint (0, len (kiriban_list) - 1)))
|
kiriban_list.pop (random.randint (0, len (kiriban_list) - 1)))
|
||||||
|
|
||||||
since_posted = datetime.now () - uploaded_at
|
|
||||||
_days = since_posted.days
|
|
||||||
_seconds = since_posted.seconds
|
|
||||||
(_hours, _seconds) = divmod (_seconds, 3600)
|
|
||||||
(_mins, _seconds) = divmod (_seconds, 60)
|
|
||||||
|
|
||||||
video_code = video_info['contentId']
|
video_code = video_info['contentId']
|
||||||
uri = f"https://www.nicovideo.jp/watch/{ video_code }"
|
|
||||||
(title, description, _) = nicolib.fetch_embed_info (uri)
|
|
||||||
comments = fetch_comments (video_code)
|
comments = fetch_comments (video_code)
|
||||||
popular_comments = sorted (comments,
|
popular_comments = sorted (comments,
|
||||||
key = lambda c: c.nico_count,
|
key = lambda c: c.nico_count,
|
||||||
@@ -69,8 +80,9 @@ async def report_kiriban (
|
|||||||
latest_comments = sorted (comments,
|
latest_comments = sorted (comments,
|
||||||
key = lambda c: c.posted_at,
|
key = lambda c: c.posted_at,
|
||||||
reverse = True)[:10]
|
reverse = True)[:10]
|
||||||
prompt = f"{ _days }日{ _hours }時間{ _mins }分{ _seconds }秒前にニコニコに投稿された『{ video_info['title'] }』という動画が{ views_count }再生を突破しました。\n"
|
prompt = (f"{ _format_elapsed (uploaded_at) }前にニコニコに投稿された"
|
||||||
prompt += f"コメント数は{ len (comments) }件です。\n"
|
f"『{ video_info['title'] }』という動画が{ views_count }再生を突破しました。\n"
|
||||||
|
f"コメント数は{ len (comments) }件です。\n")
|
||||||
if video_info['tags']:
|
if video_info['tags']:
|
||||||
prompt += f"つけられたタグは「{ '」、「'.join (video_info['tags']) }」です。\n"
|
prompt += f"つけられたタグは「{ '」、「'.join (video_info['tags']) }」です。\n"
|
||||||
if comments:
|
if comments:
|
||||||
@@ -111,6 +123,10 @@ async def report_kiriban (
|
|||||||
|
|
||||||
async def update_kiriban_list (
|
async def update_kiriban_list (
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""
|
||||||
|
キリ番リストの更新
|
||||||
|
"""
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
await wait_until (time (15, 0))
|
await wait_until (time (15, 0))
|
||||||
|
|
||||||
@@ -129,6 +145,20 @@ async def update_kiriban_list (
|
|||||||
def fetch_kiriban_list (
|
def fetch_kiriban_list (
|
||||||
base_date: date,
|
base_date: date,
|
||||||
) -> list[tuple[int, VideoInfo, datetime]]:
|
) -> list[tuple[int, VideoInfo, datetime]]:
|
||||||
|
"""
|
||||||
|
キリ番を迎へた動画のリストを取得する.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
base_date: date
|
||||||
|
基準日
|
||||||
|
|
||||||
|
Return
|
||||||
|
------
|
||||||
|
list[tuple[int, VideoInfo, datetime]]
|
||||||
|
動画リスト(キリ番基準再生数、対象動画情報、投稿日時のタプル)
|
||||||
|
"""
|
||||||
|
|
||||||
_kiriban_list: list[tuple[int, VideoInfo, datetime]] = []
|
_kiriban_list: list[tuple[int, VideoInfo, datetime]] = []
|
||||||
|
|
||||||
latest_fetched_at = cast (date, (VideoHistory
|
latest_fetched_at = cast (date, (VideoHistory
|
||||||
@@ -145,7 +175,7 @@ def fetch_kiriban_list (
|
|||||||
continue
|
continue
|
||||||
previous_views_count: int | None = (
|
previous_views_count: int | None = (
|
||||||
VideoHistory
|
VideoHistory
|
||||||
.where_has ('video', lambda q: q.where ('code', code))
|
.where_has ('video', lambda q, code = code: q.where ('code', code))
|
||||||
.where ('fetched_at', '<', latest_fetched_at)
|
.where ('fetched_at', '<', latest_fetched_at)
|
||||||
.max ('views_count'))
|
.max ('views_count'))
|
||||||
if previous_views_count is None:
|
if previous_views_count is None:
|
||||||
@@ -155,7 +185,8 @@ def fetch_kiriban_list (
|
|||||||
video_info = nicolib.fetch_video_info (code)
|
video_info = nicolib.fetch_video_info (code)
|
||||||
if video_info is not None:
|
if video_info is not None:
|
||||||
_kiriban_list.append ((kiriban_views_count, video_info,
|
_kiriban_list.append ((kiriban_views_count, video_info,
|
||||||
cast (Video, Video.where ('code', code).first ()).uploaded_at))
|
(cast (Video, Video.where ('code', code).first ())
|
||||||
|
.uploaded_at)))
|
||||||
|
|
||||||
return _kiriban_list
|
return _kiriban_list
|
||||||
|
|
||||||
@@ -163,20 +194,87 @@ def fetch_kiriban_list (
|
|||||||
def fetch_comments (
|
def fetch_comments (
|
||||||
video_code: str,
|
video_code: str,
|
||||||
) -> list[Comment]:
|
) -> list[Comment]:
|
||||||
|
"""
|
||||||
|
動画のコメント・リストを取得する.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
video_code: str
|
||||||
|
ニコニコの動画コード
|
||||||
|
|
||||||
|
Return
|
||||||
|
------
|
||||||
|
list[Comment]
|
||||||
|
コメント・リスト
|
||||||
|
"""
|
||||||
|
|
||||||
video = Video.where ('code', video_code).first ()
|
video = Video.where ('code', video_code).first ()
|
||||||
if video is None:
|
if video is None:
|
||||||
return []
|
return []
|
||||||
return video.comments
|
return video.comments
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_latest_deerjika (
|
||||||
|
) -> VideoInfo | None:
|
||||||
|
"""
|
||||||
|
最新のぼざクリ動画を取得する.
|
||||||
|
|
||||||
|
Return
|
||||||
|
------
|
||||||
|
VideoInfo | None
|
||||||
|
動画情報
|
||||||
|
"""
|
||||||
|
|
||||||
|
return nicolib.fetch_latest_video (['伊地知ニジカ',
|
||||||
|
'ぼざろクリーチャーシリーズ',
|
||||||
|
'ぼざろクリーチャーシリーズ外伝'])
|
||||||
|
|
||||||
|
|
||||||
async def report_nico (
|
async def report_nico (
|
||||||
) -> None:
|
) -> None:
|
||||||
...
|
"""
|
||||||
|
ニコニコから最新のぼざクリを取得し,まだ報知してゐなかったら報知する.
|
||||||
|
"""
|
||||||
|
|
||||||
|
while True:
|
||||||
|
latest_deerjika = fetch_latest_deerjika ()
|
||||||
|
if latest_deerjika and latest_deerjika['contentId'] not in watched_videos:
|
||||||
|
video = latest_deerjika
|
||||||
|
watched_videos.add (video['contentId'])
|
||||||
|
|
||||||
|
prompt = f"""ニコニコに『{ video['title'] }』という動画がアップされました。
|
||||||
|
つけられたタグは「{ '」、「'.join (video['tags']) }」です。
|
||||||
|
概要には次のように書かれています:
|
||||||
|
```html
|
||||||
|
{ video['description'] }
|
||||||
|
```
|
||||||
|
このことについて、みんなに告知するとともに、ニジカちゃんの感想を教えてください。"""
|
||||||
|
query = Query ()
|
||||||
|
query.user_id = None
|
||||||
|
query.target_character = Character.DEERJIKA.value
|
||||||
|
query.content = prompt
|
||||||
|
query.query_type = QueryType.NICO_REPORT.value
|
||||||
|
query.model = GPTModel.GPT3_TURBO.value
|
||||||
|
query.sent_at = datetime.now ()
|
||||||
|
query.answered = False
|
||||||
|
query.transfer_data = { 'video_code': video['contentId'] }
|
||||||
|
query.save ()
|
||||||
|
|
||||||
|
await asyncio.sleep (60)
|
||||||
|
|
||||||
|
|
||||||
async def wait_until (
|
async def wait_until (
|
||||||
t: time,
|
t: time,
|
||||||
):
|
) -> None:
|
||||||
|
"""
|
||||||
|
指定した時刻まで待つ.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
t: time
|
||||||
|
次に実行を続行するまでの時刻
|
||||||
|
"""
|
||||||
|
|
||||||
dt = datetime.now ()
|
dt = datetime.now ()
|
||||||
d = dt.date ()
|
d = dt.date ()
|
||||||
if dt.time () >= t:
|
if dt.time () >= t:
|
||||||
@@ -184,9 +282,35 @@ async def wait_until (
|
|||||||
await asyncio.sleep ((datetime.combine (d, t) - dt).total_seconds ())
|
await asyncio.sleep ((datetime.combine (d, t) - dt).total_seconds ())
|
||||||
|
|
||||||
|
|
||||||
|
def _format_elapsed (
|
||||||
|
uploaded_at: datetime,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
指定した時刻から現在までの時間を見やすぃ文字列に変換する.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
uploaded_at: datetime
|
||||||
|
基準日時
|
||||||
|
|
||||||
|
Return
|
||||||
|
------
|
||||||
|
str
|
||||||
|
変換後文字列
|
||||||
|
"""
|
||||||
|
|
||||||
|
delta = datetime.now () - uploaded_at
|
||||||
|
days = delta.days
|
||||||
|
seconds = delta.seconds
|
||||||
|
(hours, seconds) = divmod (seconds, 3600)
|
||||||
|
(mins, seconds) = divmod (seconds, 60)
|
||||||
|
|
||||||
|
return f"{ days }日{ hours }時間{ mins }分{ seconds }秒"
|
||||||
|
|
||||||
|
|
||||||
kiriban_list = (
|
kiriban_list = (
|
||||||
fetch_kiriban_list ((d := datetime.now ()).date ()
|
fetch_kiriban_list ((now := datetime.now ()).date ()
|
||||||
- timedelta (days = d.hour < 15)))
|
- timedelta (days = now.hour < 15)))
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
asyncio.run (main ())
|
asyncio.run (main ())
|
||||||
|
|||||||
+1
-1
サブモジュール nicolib が更新されました: b7a88cc774...9fec1cf5f9
+49
-2
@@ -1,3 +1,7 @@
|
|||||||
|
"""
|
||||||
|
DB の queries テーブルにたまってゐるクエリを AI に処理させ answers テーブルに流す.
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import random
|
import random
|
||||||
@@ -6,12 +10,16 @@ from typing import TypedDict
|
|||||||
|
|
||||||
from nizika_ai.config import DB
|
from nizika_ai.config import DB
|
||||||
from nizika_ai.consts import AnswerType, Character, Platform
|
from nizika_ai.consts import AnswerType, Character, Platform
|
||||||
from nizika_ai.models import Answer, AnsweredFlag, Query, User
|
from nizika_ai.models import Answer, AnsweredFlag, Query
|
||||||
from nizika_ai.talk import Talk
|
from nizika_ai.talk import Talk
|
||||||
|
|
||||||
|
|
||||||
def main (
|
def main (
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""
|
||||||
|
メーン処理
|
||||||
|
"""
|
||||||
|
|
||||||
queries: list[Query] = Query.where ('answered', False).get ()
|
queries: list[Query] = Query.where ('answered', False).get ()
|
||||||
if not queries:
|
if not queries:
|
||||||
return
|
return
|
||||||
@@ -48,6 +56,21 @@ def add_answer (
|
|||||||
user_name: str | None,
|
user_name: str | None,
|
||||||
histories: list[History],
|
histories: list[History],
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""
|
||||||
|
AI の返答を DB に積む.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
query: Query
|
||||||
|
クエリ
|
||||||
|
character: Character
|
||||||
|
返答するキャラクタ
|
||||||
|
user_name: str | None
|
||||||
|
クエリの主
|
||||||
|
histories: list[History]
|
||||||
|
履歴
|
||||||
|
"""
|
||||||
|
|
||||||
message: str | list[dict[str, str | dict[str, str]]]
|
message: str | list[dict[str, str | dict[str, str]]]
|
||||||
if query.image_url:
|
if query.image_url:
|
||||||
message = [{ 'type': 'text', 'text': query.content },
|
message = [{ 'type': 'text', 'text': query.content },
|
||||||
@@ -69,10 +92,19 @@ def add_answer (
|
|||||||
def add_answered_flags (
|
def add_answered_flags (
|
||||||
answer: Answer,
|
answer: Answer,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""
|
||||||
|
返答済フラグを付与する.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
answer: Answer
|
||||||
|
返答モデル
|
||||||
|
"""
|
||||||
|
|
||||||
answer_type: AnswerType
|
answer_type: AnswerType
|
||||||
try:
|
try:
|
||||||
answer_type = AnswerType (answer.answer_type)
|
answer_type = AnswerType (answer.answer_type)
|
||||||
except Exception:
|
except (TypeError, ValueError):
|
||||||
return
|
return
|
||||||
|
|
||||||
if answer_type in (AnswerType.YOUTUBE_REPLY,):
|
if answer_type in (AnswerType.YOUTUBE_REPLY,):
|
||||||
@@ -85,6 +117,17 @@ def add_answered_flag (
|
|||||||
answer: Answer,
|
answer: Answer,
|
||||||
platform: Platform,
|
platform: Platform,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""
|
||||||
|
返答済フラグを付与する.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
answer: Answer
|
||||||
|
返答モデル
|
||||||
|
platform: Platform
|
||||||
|
プラットフォーム
|
||||||
|
"""
|
||||||
|
|
||||||
answered_flag = AnsweredFlag ()
|
answered_flag = AnsweredFlag ()
|
||||||
answered_flag.answer_id = answer.id
|
answered_flag.answer_id = answer.id
|
||||||
answered_flag.platform = platform.value
|
answered_flag.platform = platform.value
|
||||||
@@ -93,6 +136,10 @@ def add_answered_flag (
|
|||||||
|
|
||||||
|
|
||||||
class History (TypedDict):
|
class History (TypedDict):
|
||||||
|
"""
|
||||||
|
会話履歴の 1 要素;ユーザや AI の発話を簡易に保持する型
|
||||||
|
"""
|
||||||
|
|
||||||
role: str
|
role: str
|
||||||
content: str
|
content: str
|
||||||
|
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする