重构斗鱼粉丝日报为信息优先结构
1. 更新粉丝日报提示词,优先提炼赛事、位置、英雄、对局和场外有效信息\n2. 扩展模板解析与渲染逻辑,支持今日重点信息、核心讨论话题、英雄与对局焦点等新板块\n3. 优化粉丝日报兜底文案与模板展示,让本地提纯结果和LLM语义总结共同参与输出
This commit is contained in:
@@ -2306,32 +2306,36 @@ class DouyuPlugin(MessagePluginInterface):
|
|||||||
"""
|
"""
|
||||||
粉丝版日报提示词设计目标:
|
粉丝版日报提示词设计目标:
|
||||||
1. 和运营版彻底区分开,不再强调“策略、复盘、活跃质量”;
|
1. 和运营版彻底区分开,不再强调“策略、复盘、活跃质量”;
|
||||||
2. 保留真实弹幕语境,让输出像“群友拿着回放在整活”;
|
2. 先提炼高价值信息,再保留粉丝向乐子感,避免报告只剩几条段子;
|
||||||
3. 允许轻微恶搞和夸张,但不能编造未出现的事件,也不能攻击主播或观众。
|
3. 允许轻微恶搞和夸张,但不能编造未出现的事件,也不能攻击主播或观众。
|
||||||
"""
|
"""
|
||||||
meta = payload.get("report_meta", {}) or {}
|
meta = payload.get("report_meta", {}) or {}
|
||||||
room_context_prompt = self._build_room_context_prompt_block(payload)
|
room_context_prompt = self._build_room_context_prompt_block(payload)
|
||||||
prompt_material = self._build_llm_prompt_material(payload, include_operator=False)
|
prompt_material = self._build_llm_prompt_material(payload, include_operator=False)
|
||||||
system_prompt = (
|
system_prompt = (
|
||||||
"你是斗鱼直播间的粉丝向整活日报编辑。"
|
"你是斗鱼直播间的粉丝向信息日报编辑。"
|
||||||
"请只根据提供的真实弹幕材料,输出一份开心、欢乐、带一点恶搞气质的中文总结。"
|
"请只根据提供的真实弹幕材料,输出一份既有信息量、又保留直播间欢乐气氛的中文总结。"
|
||||||
"语气要像群友在复盘名场面,不要写成运营分析,不要编造剧情,不要使用代码块。"
|
"语气要像群友在复盘直播名场面,但第一优先级是提炼有效信息,不要写成运营分析,不要编造剧情,不要使用代码块。"
|
||||||
"如果这是 Dota2 / 电竞语境直播间,请优先按刀圈/电竞圈人物关系、职业生涯、老比赛和主播互动梗去理解笑点。"
|
"如果这是 Dota2 / 电竞语境直播间,请优先按刀圈/电竞圈人物关系、职业生涯、老比赛和主播互动梗去理解笑点。"
|
||||||
)
|
)
|
||||||
user_prompt = (
|
user_prompt = (
|
||||||
"请输出一份适合给粉丝看的《斗鱼弹幕乐子日报》,严格按下面结构输出:\n"
|
"请输出一份适合给粉丝看的《斗鱼弹幕信息日报》,严格按下面结构输出:\n"
|
||||||
"1. 开头先写 1 段总述,概括今天直播间的整体节目效果和气氛。\n"
|
"1. 开头先写 1 段总述,概括今天直播间的整体节目效果和气氛。\n"
|
||||||
"2. 另起一行写标题:`【今日笑点】`,下面写 4 条 bullet,每条一句,突出最有节目效果的地方。\n"
|
"2. 另起一行写标题:`【今日重点信息】`,下面写 4-6 条 bullet,优先提炼真正有效的信息。重点看赛事预告、具体日期、位置讨论、人物关系、主播近况、是否开摄像头、场外话题等。\n"
|
||||||
"3. 另起一行写标题:`【弹幕名场面】`,下面写 4-6 条 bullet,尽量保留弹幕原话风格,像现场回放。\n"
|
"3. 另起一行写标题:`【核心讨论话题】`,下面写 3-4 条 bullet,概括今天弹幕主要围绕哪些话题打转,每条都要带具体内容,不要空泛。\n"
|
||||||
"4. 另起一行写标题:`【梗王榜】`,下面写 3 条 bullet,把今天最刷屏、最有共识的梗排出来。\n"
|
"4. 另起一行写标题:`【英雄与对局焦点】`,下面写 3-4 条 bullet,提炼今天重点英雄、关键对局走势、翻盘/崩盘点、观众对操作和出装的主要反馈。\n"
|
||||||
"5. 另起一行写标题:`【收尾播报】`,下面只写 1 句收尾,轻松一点,像群里发图后的总结句。\n"
|
"5. 另起一行写标题:`【今日笑点】`,下面写 3-4 条 bullet,每条一句,突出最有节目效果的地方。\n"
|
||||||
"6. 可以夸张一点、调皮一点,但不要低俗,不要攻击主播,不要使用“建议、策略、转化、数据表现”等运营词。\n\n"
|
"6. 另起一行写标题:`【弹幕名场面】`,下面写 4-6 条 bullet,尽量保留弹幕原话风格,像现场回放。\n"
|
||||||
|
"7. 另起一行写标题:`【梗王榜】`,下面写 3 条 bullet,把今天最刷屏、最有共识的梗排出来。\n"
|
||||||
|
"8. 另起一行写标题:`【收尾播报】`,下面只写 1 句收尾,轻松一点,像群里发图后的总结句。\n"
|
||||||
|
"9. 出现时间信息时,尽量写清楚绝对日期或明确时间,比如“4月30日”“18:45 前后”,不要只写“最近”“那天”。\n"
|
||||||
|
"10. 不要写“建议、策略、转化、数据表现”等运营词,也不要只复述哈哈哈、gg 这种已经能由本地统计完成的噪声。\n\n"
|
||||||
f"主播:{meta.get('nickname') or meta.get('room_name') or meta.get('room_id')}\n"
|
f"主播:{meta.get('nickname') or meta.get('room_name') or meta.get('room_id')}\n"
|
||||||
f"日期:{meta.get('anchor_day', '')}\n"
|
f"日期:{meta.get('anchor_day', '')}\n"
|
||||||
f"{room_context_prompt}"
|
f"{room_context_prompt}"
|
||||||
"下面是已经提纯给 LLM 的现场材料,请优先抓 `topic_evidence_clusters` 和 `compact_scene_material` 里的 `semantic_fact_hints`、原声弹幕、时间线块和集体起哄片段,"
|
"下面是已经提纯给 LLM 的现场材料,请优先抓 `topic_evidence_clusters` 和 `compact_scene_material` 里的 `semantic_fact_hints`、原声弹幕、时间线块和集体起哄片段,"
|
||||||
"尤其留意赛事预告、位置讨论、英雄选择、关键对局、镜头调侃和团播人物关系,"
|
"尤其留意赛事预告、位置讨论、英雄选择、关键对局、镜头调侃和团播人物关系,"
|
||||||
"少写空泛概括。\n"
|
"少写空泛概括。若材料无法支持某个判断,就不要写。\n"
|
||||||
f"材料:\n{json.dumps(prompt_material, ensure_ascii=False, indent=2)}"
|
f"材料:\n{json.dumps(prompt_material, ensure_ascii=False, indent=2)}"
|
||||||
)
|
)
|
||||||
return system_prompt, user_prompt
|
return system_prompt, user_prompt
|
||||||
@@ -2824,6 +2828,13 @@ class DouyuPlugin(MessagePluginInterface):
|
|||||||
兜底文本保持“有梗但不胡编”的原则,所有句子都只从真实弹幕统计结果里取材。
|
兜底文本保持“有梗但不胡编”的原则,所有句子都只从真实弹幕统计结果里取材。
|
||||||
"""
|
"""
|
||||||
meta = payload.get("report_meta", {}) or {}
|
meta = payload.get("report_meta", {}) or {}
|
||||||
|
topic_clusters = payload.get("topic_evidence_clusters", []) or []
|
||||||
|
hero_mentions = (
|
||||||
|
payload.get("compact_scene_material", {})
|
||||||
|
.get("semantic_fact_hints", {})
|
||||||
|
.get("hero_mentions", [])
|
||||||
|
or []
|
||||||
|
)
|
||||||
top_terms = [
|
top_terms = [
|
||||||
str(item.get("term") or "").strip()
|
str(item.get("term") or "").strip()
|
||||||
for item in (payload.get("top_terms", []) or [])[:5]
|
for item in (payload.get("top_terms", []) or [])[:5]
|
||||||
@@ -2844,8 +2855,42 @@ class DouyuPlugin(MessagePluginInterface):
|
|||||||
f"尤其是「{str(merged_templates[0].get('text') or '').strip()[:26]}」这类共识弹幕,一看就是全场默认会背。"
|
f"尤其是「{str(merged_templates[0].get('text') or '').strip()[:26]}」这类共识弹幕,一看就是全场默认会背。"
|
||||||
)
|
)
|
||||||
|
|
||||||
lines = [" ".join(lead_parts).strip(), "【今日笑点】"]
|
lines = [" ".join(lead_parts).strip(), "【今日重点信息】"]
|
||||||
|
|
||||||
|
for item in topic_clusters[:3]:
|
||||||
|
label = str(item.get("label") or "").strip()
|
||||||
|
time_range = str(item.get("time_range") or "").strip()
|
||||||
|
count = int(item.get("count", 0) or 0)
|
||||||
|
samples = item.get("samples", []) or []
|
||||||
|
sample_text = ""
|
||||||
|
if samples:
|
||||||
|
sample_text = str(samples[0].get("content") or "").strip()[:38]
|
||||||
|
if label and sample_text:
|
||||||
|
lines.append(f"- {label}从 {time_range or '全场'} 一直有人聊,约 {count} 条相关弹幕,代表说法是「{sample_text}」。")
|
||||||
|
elif label:
|
||||||
|
lines.append(f"- {label}是今天的重点主线之一,相关弹幕约 {count} 条。")
|
||||||
|
|
||||||
|
lines.append("【核心讨论话题】")
|
||||||
|
for item in topic_clusters[:3]:
|
||||||
|
label = str(item.get("label") or "").strip()
|
||||||
|
keywords = [str(keyword).strip() for keyword in (item.get("keywords", []) or [])[:5] if str(keyword).strip()]
|
||||||
|
if label and keywords:
|
||||||
|
lines.append(f"- 大家围着 {label} 打转,关键词主要是 {'、'.join(keywords)}。")
|
||||||
|
|
||||||
|
lines.append("【英雄与对局焦点】")
|
||||||
|
for item in hero_mentions[:3]:
|
||||||
|
hero_name = str(item.get("hero") or "").strip()
|
||||||
|
mention_count = int(item.get("mention_count", 0) or 0)
|
||||||
|
samples = item.get("samples", []) or []
|
||||||
|
sample_text = ""
|
||||||
|
if samples:
|
||||||
|
sample_text = str(samples[0].get("content") or "").strip()[:36]
|
||||||
|
if hero_name and sample_text:
|
||||||
|
lines.append(f"- {hero_name}被点名 {mention_count} 次,弹幕现场直接说到「{sample_text}」。")
|
||||||
|
elif hero_name:
|
||||||
|
lines.append(f"- {hero_name}是今天的主要英雄话题之一,被提到 {mention_count} 次。")
|
||||||
|
|
||||||
|
lines.append("【今日笑点】")
|
||||||
if peak_buckets:
|
if peak_buckets:
|
||||||
top_bucket = peak_buckets[0]
|
top_bucket = peak_buckets[0]
|
||||||
lines.append(
|
lines.append(
|
||||||
|
|||||||
@@ -91,6 +91,15 @@ def _split_fans_report_blocks(report_text: str) -> Dict[str, Any]:
|
|||||||
即便模型没有完全按约定输出,这里也会尽量兜底,保证页面不空。
|
即便模型没有完全按约定输出,这里也会尽量兜底,保证页面不空。
|
||||||
"""
|
"""
|
||||||
header_alias_map = {
|
header_alias_map = {
|
||||||
|
"今日重点信息": "key_info",
|
||||||
|
"重点信息": "key_info",
|
||||||
|
"有效信息": "key_info",
|
||||||
|
"核心讨论话题": "topic_focus",
|
||||||
|
"讨论话题": "topic_focus",
|
||||||
|
"核心话题": "topic_focus",
|
||||||
|
"英雄与对局焦点": "hero_focus",
|
||||||
|
"对局焦点": "hero_focus",
|
||||||
|
"英雄焦点": "hero_focus",
|
||||||
"今日笑点": "laugh_points",
|
"今日笑点": "laugh_points",
|
||||||
"笑点": "laugh_points",
|
"笑点": "laugh_points",
|
||||||
"欢乐总结": "laugh_points",
|
"欢乐总结": "laugh_points",
|
||||||
@@ -106,6 +115,9 @@ def _split_fans_report_blocks(report_text: str) -> Dict[str, Any]:
|
|||||||
}
|
}
|
||||||
sections = {
|
sections = {
|
||||||
"lead": "",
|
"lead": "",
|
||||||
|
"key_info": [],
|
||||||
|
"topic_focus": [],
|
||||||
|
"hero_focus": [],
|
||||||
"laugh_points": [],
|
"laugh_points": [],
|
||||||
"famous_scenes": [],
|
"famous_scenes": [],
|
||||||
"meme_rank": [],
|
"meme_rank": [],
|
||||||
@@ -510,6 +522,97 @@ def _build_fans_effective_info_lines(payload: Dict[str, Any], limit: int = 6) ->
|
|||||||
return lines[:limit]
|
return lines[:limit]
|
||||||
|
|
||||||
|
|
||||||
|
def _build_local_topic_focus_lines(payload: Dict[str, Any], limit: int = 4) -> List[str]:
|
||||||
|
"""
|
||||||
|
为“核心讨论话题”补充本地可直接确定的摘要句。
|
||||||
|
这里故意不让模型自己重新发明事实,而是把主题簇已经聚好的结果转成人能读懂的话。
|
||||||
|
"""
|
||||||
|
lines: List[str] = []
|
||||||
|
seen = set()
|
||||||
|
|
||||||
|
def push(text: str) -> None:
|
||||||
|
value = str(text or "").strip()
|
||||||
|
if not value or value in seen:
|
||||||
|
return
|
||||||
|
seen.add(value)
|
||||||
|
lines.append(value)
|
||||||
|
|
||||||
|
for item in (payload.get("topic_evidence_clusters", []) or [])[:4]:
|
||||||
|
label = str(item.get("label") or "").strip()
|
||||||
|
keywords = [str(keyword).strip() for keyword in (item.get("keywords", []) or [])[:5] if str(keyword).strip()]
|
||||||
|
count = int(item.get("count", 0) or 0)
|
||||||
|
if label and keywords:
|
||||||
|
push(f"{label}是高频主线,相关讨论约 {count} 条,关键词集中在 {'、'.join(keywords)}。")
|
||||||
|
elif label:
|
||||||
|
push(f"{label}是今天反复被拉出来聊的主线之一,相关讨论约 {count} 条。")
|
||||||
|
if len(lines) >= limit:
|
||||||
|
return lines[:limit]
|
||||||
|
return lines[:limit]
|
||||||
|
|
||||||
|
|
||||||
|
def _build_local_hero_focus_lines(payload: Dict[str, Any], limit: int = 4) -> List[str]:
|
||||||
|
"""
|
||||||
|
为“英雄与对局焦点”准备本地兜底。
|
||||||
|
这部分直接复用英雄提及聚类,优先强调出现频次和代表发言,方便粉丝快速看懂今天在聊什么英雄。
|
||||||
|
"""
|
||||||
|
hero_mentions = (
|
||||||
|
payload.get("compact_scene_material", {})
|
||||||
|
.get("semantic_fact_hints", {})
|
||||||
|
.get("hero_mentions", [])
|
||||||
|
or []
|
||||||
|
)
|
||||||
|
lines: List[str] = []
|
||||||
|
seen = set()
|
||||||
|
|
||||||
|
def push(text: str) -> None:
|
||||||
|
value = str(text or "").strip()
|
||||||
|
if not value or value in seen:
|
||||||
|
return
|
||||||
|
seen.add(value)
|
||||||
|
lines.append(value)
|
||||||
|
|
||||||
|
for item in hero_mentions[:4]:
|
||||||
|
hero_name = str(item.get("hero") or "").strip()
|
||||||
|
mention_count = int(item.get("mention_count", 0) or 0)
|
||||||
|
samples = item.get("samples", []) or []
|
||||||
|
sample_text = ""
|
||||||
|
if samples:
|
||||||
|
sample_text = str(samples[0].get("content") or "").strip()[:36]
|
||||||
|
if hero_name and sample_text:
|
||||||
|
push(f"{hero_name}被提到 {mention_count} 次,现场典型弹幕是「{sample_text}」。")
|
||||||
|
elif hero_name:
|
||||||
|
push(f"{hero_name}是今天的主要英雄讨论点之一,被提到 {mention_count} 次。")
|
||||||
|
if len(lines) >= limit:
|
||||||
|
return lines[:limit]
|
||||||
|
return lines[:limit]
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_information_section_items(
|
||||||
|
llm_items: List[str],
|
||||||
|
local_items: List[str],
|
||||||
|
target_count: int,
|
||||||
|
) -> List[str]:
|
||||||
|
"""
|
||||||
|
将模型提炼结果与本地事实兜底合并。
|
||||||
|
设计目标:
|
||||||
|
1. 先尊重模型已经总结好的“可读句子”;
|
||||||
|
2. 如果模型漏了,就用本地证据补足;
|
||||||
|
3. 始终保证最终区块有信息量,而不是空标题。
|
||||||
|
"""
|
||||||
|
normalized: List[str] = []
|
||||||
|
seen = set()
|
||||||
|
for source in (llm_items, local_items):
|
||||||
|
for item in source:
|
||||||
|
value = str(item or "").strip()
|
||||||
|
if not value or value in seen:
|
||||||
|
continue
|
||||||
|
seen.add(value)
|
||||||
|
normalized.append(value)
|
||||||
|
if len(normalized) >= target_count:
|
||||||
|
return normalized[:target_count]
|
||||||
|
return normalized[:target_count]
|
||||||
|
|
||||||
|
|
||||||
def _render_fans_info_cards(items: List[str]) -> str:
|
def _render_fans_info_cards(items: List[str]) -> str:
|
||||||
blocks = []
|
blocks = []
|
||||||
for item in items[:6]:
|
for item in items[:6]:
|
||||||
@@ -929,6 +1032,21 @@ def render_fans_daily_report_html(
|
|||||||
f" | 围观群众 {meta.get('unique_user_count', 0)} 人"
|
f" | 围观群众 {meta.get('unique_user_count', 0)} 人"
|
||||||
)
|
)
|
||||||
sections = _split_fans_report_blocks(fans_report_text)
|
sections = _split_fans_report_blocks(fans_report_text)
|
||||||
|
effective_info_lines = _normalize_information_section_items(
|
||||||
|
sections.get("key_info", []),
|
||||||
|
_build_fans_effective_info_lines(payload),
|
||||||
|
target_count=6,
|
||||||
|
)
|
||||||
|
topic_focus_lines = _normalize_information_section_items(
|
||||||
|
sections.get("topic_focus", []),
|
||||||
|
_build_local_topic_focus_lines(payload),
|
||||||
|
target_count=4,
|
||||||
|
)
|
||||||
|
hero_focus_lines = _normalize_information_section_items(
|
||||||
|
sections.get("hero_focus", []),
|
||||||
|
_build_local_hero_focus_lines(payload),
|
||||||
|
target_count=4,
|
||||||
|
)
|
||||||
laugh_points = _normalize_funny_bullets(payload, sections.get("laugh_points", []), target_count=4)
|
laugh_points = _normalize_funny_bullets(payload, sections.get("laugh_points", []), target_count=4)
|
||||||
famous_scenes = _normalize_scene_bullets(payload, sections.get("famous_scenes", []), target_count=5)
|
famous_scenes = _normalize_scene_bullets(payload, sections.get("famous_scenes", []), target_count=5)
|
||||||
meme_rank = _normalize_rank_bullets(payload, sections.get("meme_rank", []), target_count=3)
|
meme_rank = _normalize_rank_bullets(payload, sections.get("meme_rank", []), target_count=3)
|
||||||
@@ -954,8 +1072,11 @@ def render_fans_daily_report_html(
|
|||||||
"lead_text": lead_text,
|
"lead_text": lead_text,
|
||||||
# 粉丝版不再只做“乐子文案展示”,而是补进本地提纯后的有效信息区。
|
# 粉丝版不再只做“乐子文案展示”,而是补进本地提纯后的有效信息区。
|
||||||
"fans_metrics_html": Markup(_render_fans_metric_cards(_build_fans_fun_metrics(payload))),
|
"fans_metrics_html": Markup(_render_fans_metric_cards(_build_fans_fun_metrics(payload))),
|
||||||
"effective_info_html": Markup(_render_fans_info_cards(_build_fans_effective_info_lines(payload))),
|
"effective_summary_html": Markup(_render_list(effective_info_lines, item_class="section-summary-list")),
|
||||||
|
"effective_info_html": Markup(_render_fans_info_cards(effective_info_lines)),
|
||||||
|
"topic_focus_html": Markup(_render_list(topic_focus_lines, item_class="section-summary-list")),
|
||||||
"topic_clusters_html": Markup(_render_topic_clusters(topic_clusters)),
|
"topic_clusters_html": Markup(_render_topic_clusters(topic_clusters)),
|
||||||
|
"hero_focus_html": Markup(_render_list(hero_focus_lines, item_class="section-summary-list")),
|
||||||
"hero_mentions_html": Markup(_render_hero_mentions(hero_mentions)),
|
"hero_mentions_html": Markup(_render_hero_mentions(hero_mentions)),
|
||||||
"hot_windows_html": Markup(_render_hot_window_cards(local_stats.get("peak_windows", []) or [])),
|
"hot_windows_html": Markup(_render_hot_window_cards(local_stats.get("peak_windows", []) or [])),
|
||||||
"repeat_digest_html": Markup(_render_repeat_digest(payload)),
|
"repeat_digest_html": Markup(_render_repeat_digest(payload)),
|
||||||
|
|||||||
@@ -139,6 +139,17 @@
|
|||||||
background: linear-gradient(180deg, rgba(255,255,255,.96), rgba(255,249,244,.94));
|
background: linear-gradient(180deg, rgba(255,255,255,.96), rgba(255,249,244,.94));
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
|
.section-summary-list {
|
||||||
|
margin: 0 0 14px;
|
||||||
|
padding-left: 22px;
|
||||||
|
}
|
||||||
|
.section-summary-list li {
|
||||||
|
margin: 8px 0;
|
||||||
|
color: #5a3e37;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.76;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
.section-title {
|
.section-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -387,16 +398,19 @@
|
|||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-title"><span class="icon"></span><span>今日重点信息</span></div>
|
<div class="section-title"><span class="icon"></span><span>今日重点信息</span></div>
|
||||||
|
{{ effective_summary_html }}
|
||||||
<div class="info-grid">{{ effective_info_html }}</div>
|
<div class="info-grid">{{ effective_info_html }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-title"><span class="icon"></span><span>核心讨论话题</span></div>
|
<div class="section-title"><span class="icon"></span><span>核心讨论话题</span></div>
|
||||||
|
{{ topic_focus_html }}
|
||||||
<div class="topic-grid">{{ topic_clusters_html }}</div>
|
<div class="topic-grid">{{ topic_clusters_html }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-title"><span class="icon"></span><span>英雄与对局焦点</span></div>
|
<div class="section-title"><span class="icon"></span><span>英雄与对局焦点</span></div>
|
||||||
|
{{ hero_focus_html }}
|
||||||
<div class="hero-grid">{{ hero_mentions_html }}</div>
|
<div class="hero-grid">{{ hero_mentions_html }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user