重构斗鱼粉丝日报信息提纯链路
- 新增本地弹幕文件测试入口,支持直接对样本文件生成提纯结果 - 将本地统计、主题证据簇和语义事实提示接入斗鱼日报LLM材料 - 明确降低情绪刷屏权重,改为优先提取赛事、位置、英雄、对局和场外互动信息
This commit is contained in:
+120
-5
@@ -2173,8 +2173,10 @@ class DouyuPlugin(MessagePluginInterface):
|
||||
os.makedirs(artifact_dir, exist_ok=True)
|
||||
audience_trend = self._build_audience_trend(sessions)
|
||||
room_context = self._build_room_semantic_context(room_id, nickname, room_name, sessions)
|
||||
prepared_all_messages = DouyuDanmuSummaryHelper._prepare_messages(all_messages)
|
||||
compact_source_messages = prepared_all_messages.get("organized_messages", []) or all_messages
|
||||
llm_compact = DouyuDanmuSummaryHelper.build_compact_prompt_assets(
|
||||
all_messages,
|
||||
compact_source_messages,
|
||||
bucket_minutes=5,
|
||||
speaker_limit=80,
|
||||
timeline_limit=24,
|
||||
@@ -2268,8 +2270,8 @@ class DouyuPlugin(MessagePluginInterface):
|
||||
"5. 单独列出 2-3 个热点时段。\n"
|
||||
"6. 整体控制在 600 字以内。\n\n"
|
||||
f"{room_context_prompt}"
|
||||
"下面是已经提纯给 LLM 的材料,其中 `compact_scene_material` 是主阅读区:\n"
|
||||
"请优先依据其中的用户索引、时间线块、整句复读线索和原声样本来写,不要被大段统计信息带偏。\n"
|
||||
"下面是已经提纯给 LLM 的材料,其中 `topic_evidence_clusters` 和 `compact_scene_material.semantic_fact_hints` 是主阅读区:\n"
|
||||
"请优先依据其中的事实证据簇、用户索引、时间线块、整句复读线索和原声样本来写,不要被大段统计信息带偏。\n"
|
||||
f"材料如下:\n{json.dumps(prompt_material, ensure_ascii=False, indent=2)}"
|
||||
)
|
||||
return system_prompt, user_prompt
|
||||
@@ -2294,7 +2296,7 @@ class DouyuPlugin(MessagePluginInterface):
|
||||
f"主播:{meta.get('nickname') or meta.get('room_name') or meta.get('room_id')}\n"
|
||||
f"日期:{meta.get('anchor_day', '')}\n"
|
||||
f"{room_context_prompt}"
|
||||
"下面是已经提纯给 LLM 的现场材料,请优先阅读 `compact_scene_material` 中的时间线块、整句复读线索和原声样本,"
|
||||
"下面是已经提纯给 LLM 的现场材料,请优先阅读 `topic_evidence_clusters` 以及 `compact_scene_material` 中的 `semantic_fact_hints`、时间线块、整句复读线索和原声样本,"
|
||||
"不要写成词频复述。\n"
|
||||
f"材料:\n{json.dumps(prompt_material, ensure_ascii=False, indent=2)}"
|
||||
)
|
||||
@@ -2327,7 +2329,8 @@ class DouyuPlugin(MessagePluginInterface):
|
||||
f"主播:{meta.get('nickname') or meta.get('room_name') or meta.get('room_id')}\n"
|
||||
f"日期:{meta.get('anchor_day', '')}\n"
|
||||
f"{room_context_prompt}"
|
||||
"下面是已经提纯给 LLM 的现场材料,请优先抓 `compact_scene_material` 里的原声弹幕、时间线块和集体起哄片段,"
|
||||
"下面是已经提纯给 LLM 的现场材料,请优先抓 `topic_evidence_clusters` 和 `compact_scene_material` 里的 `semantic_fact_hints`、原声弹幕、时间线块和集体起哄片段,"
|
||||
"尤其留意赛事预告、位置讨论、英雄选择、关键对局、镜头调侃和团播人物关系,"
|
||||
"少写空泛概括。\n"
|
||||
f"材料:\n{json.dumps(prompt_material, ensure_ascii=False, indent=2)}"
|
||||
)
|
||||
@@ -2362,6 +2365,9 @@ class DouyuPlugin(MessagePluginInterface):
|
||||
speaker_index = llm_compact.get("speaker_index", []) or []
|
||||
timeline_digest = llm_compact.get("timeline_digest", []) or []
|
||||
content_cues = llm_compact.get("content_cues", []) or []
|
||||
semantic_fact_hints = llm_compact.get("semantic_fact_hints", {}) or {}
|
||||
fact_topic_clusters = semantic_fact_hints.get("topic_clusters", []) or []
|
||||
hero_mentions = semantic_fact_hints.get("hero_mentions", []) or []
|
||||
|
||||
material: Dict[str, Any] = {
|
||||
"report_meta": {
|
||||
@@ -2382,6 +2388,69 @@ class DouyuPlugin(MessagePluginInterface):
|
||||
"storyline_keywords": self._normalize_text_list(room_context.get("storyline_keywords"))[:10],
|
||||
"style_hints": self._normalize_text_list(room_context.get("style_hints"))[:6],
|
||||
},
|
||||
# 本地统计层:
|
||||
# 1. 这里只放“本地就能确定”的结果;
|
||||
# 2. 让 LLM 只把这些统计当作背景,不再浪费能力去数哈哈哈和复读次数。
|
||||
"local_stats": {
|
||||
"message_count": int(meta.get("message_count", 0) or 0),
|
||||
"unique_user_count": int(meta.get("unique_user_count", 0) or 0),
|
||||
"top_emotion_bursts": [
|
||||
{
|
||||
"text": str(item.get("text") or "").strip(),
|
||||
"count": int(item.get("count", 0) or 0),
|
||||
}
|
||||
for item in content_cues[:12]
|
||||
if str(item.get("kind") or "").strip() == "emotion" and str(item.get("text") or "").strip()
|
||||
][:8],
|
||||
"top_repeated_messages": [
|
||||
{
|
||||
"text": str(item.get("text") or "").strip()[:90],
|
||||
"count": int(item.get("count", 0) or 0),
|
||||
"user_count": int(item.get("user_count", 0) or 0),
|
||||
}
|
||||
for item in (repeated_messages[:12] if repeated_messages else content_cues[:12])
|
||||
if str(item.get("text") or "").strip()
|
||||
][:8],
|
||||
"peak_windows": [
|
||||
{
|
||||
"start_time": str(item.get("start_time") or "").strip(),
|
||||
"message_count": int(item.get("message_count", 0) or 0),
|
||||
"user_count": int(item.get("user_count", 0) or 0),
|
||||
}
|
||||
for item in peak_buckets[:6]
|
||||
],
|
||||
},
|
||||
# 这是后续给 LLM 的主工作区:
|
||||
# 1. 每个簇都代表“今天弹幕里正在讨论的一件事”;
|
||||
# 2. 本地只做聚类和保留证据,不替模型写结论;
|
||||
# 3. LLM 负责从这些簇里提炼赛事、位置、英雄、背景和场外互动信息。
|
||||
"topic_evidence_clusters": [
|
||||
{
|
||||
"label": str(item.get("label") or "").strip(),
|
||||
"count": int(item.get("match_count", 0) or 0),
|
||||
"user_count": int(item.get("user_count", 0) or 0),
|
||||
"time_range": (
|
||||
f"{str(item.get('first_hm') or '').strip()}-{str(item.get('last_hm') or '').strip()}"
|
||||
).strip("-"),
|
||||
"keywords": [
|
||||
str(keyword).strip()
|
||||
for keyword in (item.get("keywords", []) or [])[:8]
|
||||
if str(keyword).strip()
|
||||
],
|
||||
"samples": [
|
||||
{
|
||||
"date": str(sample.get("date") or "").strip(),
|
||||
"hm": str(sample.get("hm") or "").strip(),
|
||||
"nickname": str(sample.get("nickname") or "").strip(),
|
||||
"content": str(sample.get("content") or "").strip()[:100],
|
||||
}
|
||||
for sample in (item.get("samples", []) or [])[:5]
|
||||
if str(sample.get("content") or "").strip()
|
||||
],
|
||||
}
|
||||
for item in fact_topic_clusters[:6]
|
||||
if str(item.get("label") or "").strip()
|
||||
],
|
||||
# 这是新的主材料层,优先级高于传统的 top_terms:
|
||||
# 1. speaker_index 负责承接用户画像,避免在每条样本里重复塞 UUID/牌子/等级;
|
||||
# 2. timeline_digest 让模型按时间推进理解“哪一段开始起哄、哪一段反复刷屏”;
|
||||
@@ -2411,6 +2480,52 @@ class DouyuPlugin(MessagePluginInterface):
|
||||
for item in content_cues[:18]
|
||||
if str(item.get("text") or "").strip()
|
||||
],
|
||||
"semantic_fact_hints": {
|
||||
"topic_clusters": [
|
||||
{
|
||||
"label": str(item.get("label") or "").strip(),
|
||||
"match_count": int(item.get("match_count", 0) or 0),
|
||||
"user_count": int(item.get("user_count", 0) or 0),
|
||||
"first_hm": str(item.get("first_hm") or "").strip(),
|
||||
"last_hm": str(item.get("last_hm") or "").strip(),
|
||||
"keywords": [
|
||||
str(keyword).strip()
|
||||
for keyword in (item.get("keywords", []) or [])[:8]
|
||||
if str(keyword).strip()
|
||||
],
|
||||
"samples": [
|
||||
{
|
||||
"date": str(sample.get("date") or "").strip(),
|
||||
"hm": str(sample.get("hm") or "").strip(),
|
||||
"nickname": str(sample.get("nickname") or "").strip(),
|
||||
"content": str(sample.get("content") or "").strip()[:100],
|
||||
}
|
||||
for sample in (item.get("samples", []) or [])[:5]
|
||||
if str(sample.get("content") or "").strip()
|
||||
],
|
||||
}
|
||||
for item in fact_topic_clusters[:6]
|
||||
if str(item.get("label") or "").strip()
|
||||
],
|
||||
"hero_mentions": [
|
||||
{
|
||||
"hero": str(item.get("hero") or "").strip(),
|
||||
"mention_count": int(item.get("mention_count", 0) or 0),
|
||||
"user_count": int(item.get("user_count", 0) or 0),
|
||||
"samples": [
|
||||
{
|
||||
"hm": str(sample.get("hm") or "").strip(),
|
||||
"nickname": str(sample.get("nickname") or "").strip(),
|
||||
"content": str(sample.get("content") or "").strip()[:100],
|
||||
}
|
||||
for sample in (item.get("samples", []) or [])[:4]
|
||||
if str(sample.get("content") or "").strip()
|
||||
],
|
||||
}
|
||||
for item in hero_mentions[:6]
|
||||
if str(item.get("hero") or "").strip()
|
||||
],
|
||||
},
|
||||
"timeline_digest": [
|
||||
{
|
||||
"date": str(item.get("date") or "").strip(),
|
||||
|
||||
Reference in New Issue
Block a user