From 7092cd845cab1c6e31f4d0bc99deb0eb67be8883 Mon Sep 17 00:00:00 2001 From: liuwei Date: Wed, 29 Apr 2026 14:55:57 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E5=81=9A=E6=96=97=E9=B1=BC=E7=B2=89?= =?UTF-8?q?=E4=B8=9D=E6=97=A5=E6=8A=A5=E4=BF=A1=E6=81=AF=E6=A8=A1=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将粉丝日报改为信息优先布局,新增重点信息、话题簇、英雄焦点和热点窗口区块 - 让模板直接展示本地提纯出的有效信息,不再只依赖少量乐子文案 - 补充粉丝日报渲染辅助函数,提升证据簇和高频信息的承载能力 --- plugins/douyu/report_template.py | 168 ++++++++- .../douyu/templates/daily_fans_report.html | 328 ++++++++++++------ 2 files changed, 398 insertions(+), 98 deletions(-) diff --git a/plugins/douyu/report_template.py b/plugins/douyu/report_template.py index 283eceb..d3597e2 100644 --- a/plugins/douyu/report_template.py +++ b/plugins/douyu/report_template.py @@ -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( + '
' + f'
{_escape(item)}
' + '
' + ) + 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'
  • {_escape(line)}
  • ' for line in sample_lines) + blocks.append( + '
    ' + f'
    {_escape(label)}
    ' + f'
    {_escape(time_range or "全场")} · {count} 条讨论 · {user_count} 人参与
    ' + f'' + '
    ' + ) + 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( + '
    ' + f'
    {_escape(hero_name)}
    ' + f'
    {mention_count} 次提及 / {user_count} 人讨论
    ' + f'
    {_escape(sample_text)}
    ' + '
    ' + ) + 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( + '
    ' + f'
    {_escape(start_time)}
    ' + f'
    {message_count} 条弹幕 / {user_count} 人参与
    ' + '
    ' + ) + 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( + '
    ' + f'{_escape(text[:28])}' + f'{count} 次' + '
    ' + ) + 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( + '
    ' + f'{_escape(text[:18])}' + f'{count} 次' + '
    ' + ) + 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)), diff --git a/plugins/douyu/templates/daily_fans_report.html b/plugins/douyu/templates/daily_fans_report.html index cd16eab..536aa69 100644 --- a/plugins/douyu/templates/daily_fans_report.html +++ b/plugins/douyu/templates/daily_fans_report.html @@ -4,95 +4,90 @@