重做斗鱼粉丝日报信息模板

- 将粉丝日报改为信息优先布局,新增重点信息、话题簇、英雄焦点和热点窗口区块
- 让模板直接展示本地提纯出的有效信息,不再只依赖少量乐子文案
- 补充粉丝日报渲染辅助函数,提升证据簇和高频信息的承载能力
This commit is contained in:
liuwei
2026-04-29 14:55:57 +08:00
parent 31848f67f6
commit 7092cd845c
2 changed files with 398 additions and 98 deletions

View File

@@ -462,6 +462,159 @@ def _render_fans_metric_cards(metrics: List[Dict[str, str]]) -> str:
return "".join(blocks)
def _build_fans_effective_info_lines(payload: Dict[str, Any], limit: int = 6) -> List[str]:
"""
为粉丝日报补一层“有效信息速览”。
这里优先从本地提纯后的主题证据簇中拿事实,不依赖 LLM 自己归纳,
这样模板本身就能稳定承载更多有效信息。
"""
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 [])[:6]:
label = str(item.get("label") or "").strip()
count = int(item.get("count", 0) or 0)
time_range = str(item.get("time_range") or "").strip()
samples = item.get("samples", []) or []
sample_text = ""
if samples:
first_sample = samples[0]
sample_text = str(first_sample.get("content") or "").strip()[:42]
if label and sample_text:
push(f"{label}{time_range or '全场'} 持续被讨论,相关弹幕约 {count} 条,代表内容是「{sample_text}」。")
elif label:
push(f"{label}是今天的高关注话题之一,相关弹幕约 {count} 条。")
if len(lines) >= limit:
return lines[:limit]
hero_mentions = payload.get("compact_scene_material", {}).get("semantic_fact_hints", {}).get("hero_mentions", []) or []
if hero_mentions:
hero_names = [str(item.get("hero") or "").strip() for item in hero_mentions[:4] if str(item.get("hero") or "").strip()]
if hero_names:
push(f"英雄讨论主要集中在 {''.join(hero_names)}")
hot_windows = payload.get("local_stats", {}).get("peak_windows", []) or []
if hot_windows:
top_window = hot_windows[0]
push(
f"最热窗口出现在 {str(top_window.get('start_time') or '')[-8:-3]}"
f"该时段累计弹幕 {int(top_window.get('message_count', 0) or 0)} 条。"
)
return lines[:limit]
def _render_fans_info_cards(items: List[str]) -> str:
blocks = []
for item in items[:6]:
blocks.append(
'<div class="fans-info-card">'
f'<div class="fans-info-text">{_escape(item)}</div>'
'</div>'
)
return "".join(blocks)
def _render_topic_clusters(topic_clusters: List[Dict[str, Any]]) -> str:
blocks = []
for item in topic_clusters[:5]:
label = str(item.get("label") or "").strip()
if not label:
continue
count = int(item.get("count", 0) or 0)
user_count = int(item.get("user_count", 0) or 0)
time_range = str(item.get("time_range") or "").strip()
samples = item.get("samples", []) or []
sample_lines = []
for sample in samples[:3]:
content = str(sample.get("content") or "").strip()
nickname = str(sample.get("nickname") or "").strip() or "观众"
hm = str(sample.get("hm") or "").strip()
if content:
sample_lines.append(f"{hm} {nickname}{content[:42]}")
sample_html = "".join(f'<li>{_escape(line)}</li>' for line in sample_lines)
blocks.append(
'<div class="topic-cluster-card">'
f'<div class="topic-cluster-title">{_escape(label)}</div>'
f'<div class="topic-cluster-meta">{_escape(time_range or "全场")} · {count} 条讨论 · {user_count} 人参与</div>'
f'<ul class="topic-cluster-list">{sample_html}</ul>'
'</div>'
)
return "".join(blocks)
def _render_hero_mentions(hero_mentions: List[Dict[str, Any]]) -> str:
blocks = []
for item in hero_mentions[:4]:
hero_name = str(item.get("hero") or "").strip()
if not hero_name:
continue
mention_count = int(item.get("mention_count", 0) or 0)
user_count = int(item.get("user_count", 0) or 0)
sample_text = ""
samples = item.get("samples", []) or []
if samples:
sample = samples[0]
sample_text = str(sample.get("content") or "").strip()[:46]
blocks.append(
'<div class="hero-mention-card">'
f'<div class="hero-mention-name">{_escape(hero_name)}</div>'
f'<div class="hero-mention-meta">{mention_count} 次提及 / {user_count} 人讨论</div>'
f'<div class="hero-mention-sample">{_escape(sample_text)}</div>'
'</div>'
)
return "".join(blocks)
def _render_hot_window_cards(hot_windows: List[Dict[str, Any]]) -> str:
blocks = []
for item in hot_windows[:4]:
start_time = str(item.get("start_time") or "")[-8:-3]
message_count = int(item.get("message_count", 0) or 0)
user_count = int(item.get("user_count", 0) or 0)
blocks.append(
'<div class="fans-hot-window-card">'
f'<div class="fans-hot-window-time">{_escape(start_time)}</div>'
f'<div class="fans-hot-window-meta">{message_count} 条弹幕 / {user_count} 人参与</div>'
'</div>'
)
return "".join(blocks)
def _render_repeat_digest(payload: Dict[str, Any]) -> str:
local_stats = payload.get("local_stats", {}) or {}
repeated_messages = local_stats.get("top_repeated_messages", []) or []
emotion_bursts = local_stats.get("top_emotion_bursts", []) or []
blocks = []
for item in repeated_messages[:4]:
text = str(item.get("text") or "").strip()
count = int(item.get("count", 0) or 0)
if text:
blocks.append(
'<div class="repeat-chip">'
f'<span class="repeat-chip-text">{_escape(text[:28])}</span>'
f'<span class="repeat-chip-count">{count} 次</span>'
'</div>'
)
for item in emotion_bursts[:4]:
text = str(item.get("text") or "").strip()
count = int(item.get("count", 0) or 0)
if text:
blocks.append(
'<div class="repeat-chip emotion">'
f'<span class="repeat-chip-text">{_escape(text[:18])}</span>'
f'<span class="repeat-chip-count">{count} 次</span>'
'</div>'
)
return "".join(blocks)
def _render_badges(top_badges: List[Dict[str, Any]]) -> str:
blocks = []
for item in top_badges[:6]:
@@ -783,6 +936,14 @@ def render_fans_daily_report_html(
lead_text = str(sections.get("lead") or "").strip()
if not lead_text:
lead_text = "今天这场直播的弹幕主打一个集体上头,观众一边盯着画面,一边忙着把梗越刷越离谱。"
topic_clusters = payload.get("topic_evidence_clusters", []) or []
hero_mentions = (
payload.get("compact_scene_material", {})
.get("semantic_fact_hints", {})
.get("hero_mentions", [])
or []
)
local_stats = payload.get("local_stats", {}) or {}
renderer = HtmlTemplateRenderer()
return renderer.render(
@@ -791,8 +952,13 @@ def render_fans_daily_report_html(
"title_name": title_name,
"subtitle": subtitle,
"lead_text": lead_text,
# 粉丝版刻意弱化“分析感”,下面几块都只展示娱乐化后的结果
# 粉丝版不再只做“乐子文案展示”,而是补进本地提纯后的有效信息区
"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))),
"topic_clusters_html": Markup(_render_topic_clusters(topic_clusters)),
"hero_mentions_html": Markup(_render_hero_mentions(hero_mentions)),
"hot_windows_html": Markup(_render_hot_window_cards(local_stats.get("peak_windows", []) or [])),
"repeat_digest_html": Markup(_render_repeat_digest(payload)),
"laugh_points_html": Markup(_render_list(laugh_points, item_class="funny-list")),
"famous_scenes_html": Markup(_render_fans_scene_cards(famous_scenes)),
"meme_rank_html": Markup(_render_rank_cards(meme_rank)),