diff --git a/CLAUDE.md b/CLAUDE.md
index f06661c..4955aaa 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -13,8 +13,8 @@
## 🔖 状态栏(每次结束 session 前必须更新这三行)
- - **最后更新**:Claude(顾问)| 2026-05-27
- - **当前状态一句话**:Phase 3 知识库基础设施已完成至 Task 3(树形视图按来源分组,commit `3409d48`);主干暂停、资源转子项目,下一刀未定。
+ - **最后更新**:Claude(顾问)| 2026-06-26
+ - **当前状态一句话**:Phase 3 知识库已完成;看板升级子项目 L3 分析引擎原型 v0.1 已出(HTML 可视化+制片人审定视觉方向),L2 打标收摊+003 迁移到位。主干暂停。
- **下一个动手的人从这里开始**:见下方「⏩ 交接备注」
---
diff --git a/ai-labeling/.gitignore b/ai-labeling/.gitignore
index 13d9402..7f6fdb9 100644
--- a/ai-labeling/.gitignore
+++ b/ai-labeling/.gitignore
@@ -1,5 +1,6 @@
.env
experiments/*.json
+output/
__pycache__/
*.pyc
.venv/
diff --git a/ai-labeling/CLAUDE.md b/ai-labeling/CLAUDE.md
index f8a075d..6729b22 100644
--- a/ai-labeling/CLAUDE.md
+++ b/ai-labeling/CLAUDE.md
@@ -13,7 +13,7 @@
## 🔖 状态栏(每次结束 session 前必须更新这三行)
- **最后更新**:Claude Opus(顾问)| 2026-06-26
-- **当前状态一句话**:L2 打标收摊。GT v0.5.0(20期6字段全覆盖)。Schema 加字段方案已写好(003_add_ai_labels.sql + Model),待在数据库上执行。下一步进 L3 分析引擎设计。
+- **当前状态一句话**:L3 分析引擎原型 v0.1 已出(HTML 可视化,5 模块 + L4 模拟诊断),制片人审完视觉方向,页面布局和交互设计方案已定。下一步:原型微调收尾 → 进 React 前端实现。
- **下一个动手的人从这里开始**:见下方「⏩ 交接备注」
---
@@ -109,8 +109,8 @@
|----|------|--------|----------|
| L1 | **数据沉淀** | 每期节目的收视数据、文稿全文、编导、选题单持续入库 | 主项目已有 episodes 表基础结构 |
| L2 | **AI 打标** | 读文稿自动判别:题材分类(4字段)、叙事结构(2档)、开篇钩子(3档) | Prompt 1 v0.3(80%) + Prompt 2 v0.2(100%) + Prompt 3 v0.2(90%),三个均达可用水平 |
-| L3 | **分析引擎** | 基于双引擎模型,计算走势、编导对比、题材对比、高低样本对照等多维分析 | 待设计 |
-| L4 | **诊断报告** | 调用 LLM 生成文字诊断结论 + 改进建议(对标 PPT 第 8-10 页水平) | 待设计 |
+| L3 | **分析引擎** | 基于双引擎模型,计算走势、编导对比、题材对比、高低样本对照等多维分析 | HTML 原型 v0.1 已出(5 模块),页面布局已定 |
+| L4 | **诊断报告** | 调用 LLM 生成文字诊断结论 + 改进建议(对标 PPT 第 8-10 页水平) | 位置已定(走势图下方),模拟文字已预览 |
### 1.6 三层分步交付策略(v1 快照定稿)
@@ -229,14 +229,16 @@ python scripts/summarize.py --model mimo-v2.5-pro --field opening_hook
## 3. 当前进度(动态,核心交接区)
-- **已完成**:三个 Prompt 均达可用水平 + GT v0.5.0(20期6字段全覆盖)+ schema 加字段方案(SQL + Model 已写好)+ git 大扫除
-- **正在做**:无
-- **卡点/待解**:① 003_add_ai_labels.sql 待在数据库上执行(先 pg_dump 备份);② L3 分析引擎架构设计未启动
+- **已完成至**:L3 原型 v0.1(HTML 可视化 + 制片人审定视觉方向),L2 打标收摊,003 迁移到位
+- **正在做**:L3 原型视觉微调(Cline 执行),页面布局设计讨论中
+- **卡点/待解**:无硬卡点。下一步进 React 前端实现前需定最终页面线框图
---
## 4. 已完成(只追加,最新在上)
+- [2026-06-26 | Opus] L3 分析引擎原型 v0.1:Python 脚本读 Excel(25期)+ ground-truth(20期AI标签),计算 5 个模块(核心指标卡/走势图/季度对比/题材对比/编导对比),生成 HTML+ECharts 可视化报告。制片人审定:磨砂玻璃+大倒角+1100px 限宽+暖色渐变底;L4 诊断报告位置定在走势图下方(摘要-详情模式);题材饼图+柱图组合;摸高完成率动态变色。脚本 `scripts/l3_analysis_prototype.py`,输出 `output/l3_report.html`。
+- [2026-06-26 | Opus] 003_add_ai_labels.sql 迁移已在 milsci_dev 执行:episodes 表 +7 列(program_format / equipment_domain / scene_tags / tech_tags / narrative_structure / opening_hook / ai_label_confidence)+3 索引。COMMENT 因 Windows GBK 编码未写入,不影响功能。Schema(schemas/episode.py)留待 L3 API 时同步更新。
- [2026-06-26 | Opus] GT v0.5.0:补完10期narrative_structure标注(MiMo跑批85%命中+制片人审定,ep019从AI判主线演进改为并列结构)。20期6字段全覆盖。Schema加字段方案:003_add_ai_labels.sql(episodes表+7列)+ episode.py Model同步。summarize.py修复无标注期次崩溃。Git大扫除:doco/programs中间产物移出git、.gitignore补全、deliverables/寄存条入库。commit `cdb213f`。
- [2026-06-25 | Opus] Prompt 1 迭代 v0.1→v0.3(全对率40%→80%):v0.2加多选字段显著篇幅门槛+从属零件反例+前沿科技vs横切类比拆清+装备深解同类体系测试;v0.3修正跨域规则(传播分类框非军事术语)+训练舰窄类别判定+装备首发校准。ground-truth v0.4.1(ep008补标跨域)。
- [2026-06-25 | Opus] Prompt 3 v0.1→v0.2(开篇钩子,命中率50%→90%):v0.1撰写+脚本opening_hook支持;v0.2修复三问题——阅读范围扩至3段(导视+主持人+首段解说)、强判定从紧迫感硬门槛改为6条独立路径、弱判定区分信息性提问vs悬念式提问。ground-truth v0.4.2(20期opening_hook全标注,制片人逐期审定含钩子分析)。summarize.py改为从源GT文件读取。
@@ -269,20 +271,27 @@ python scripts/summarize.py --model mimo-v2.5-pro --field opening_hook
### L1 — 数据沉淀(与主项目交界,需 Opus 审)
-- [ ] **episodes schema 加字段** — 方案已就绪(`003_add_ai_labels.sql` + Model),待在数据库执行。先 `pg_dump` 备份。
+- [x] **episodes schema 加字段** — 003 迁移已执行(2026-06-26),7 列 + 3 索引到位。
- [ ] **文稿入库流程** — 每期文稿全文入知识库(主项目 Phase 3 已有知识库基础设施)。
- [ ] **收视数据导入** — 真实收视 Excel 批量导入 + 清测试数据。
-### L3 — 分析引擎(待设计)
+### L3 — 分析引擎(原型已出,进入前端实现)
-- [ ] **分析维度定义** — 对标 Excel 8 个 Sheet:核心指标卡、走势图、编导对比、题材对比、阶段对比、高低样本对照、诊断结论。
-- [ ] **计算逻辑实现** — 滚动均值、达标率、分组统计、样本排序等。
-- [ ] **API 设计** — 支持按季度/自定义周期查询,返回结构化分析数据。
+- [x] **HTML 原型 v0.1** — 5 模块计算验证通过(指标卡/走势/季度/题材/编导),25 期数据全部读入。
+- [x] **视觉方向定稿** — 磨砂玻璃+大倒角+1100px 限宽+暖色渐变底+三色判定+题材饼图柱图组合。
+- [x] **页面布局定稿** — 指标卡 → 走势图 → L4 诊断报告 → 季度对比+题材对比 → 编导对比 → 双引擎象限图。
+- [ ] **范围选择器(方案 A 已定)** — 按自然年分页,年内用 ECharts dataZoom 滑块选范围,下方所有模块联动刷新。历史年份(如 2025)仅展示走势+编导(无 AI 标签的模块标灰)。
+- [ ] **单期详情页入口** — 走势图柱子可点击跳转到该期节目分析页(独立子项目)。
+- [ ] **计算逻辑实现(React)** — 将原型 Python 计算逻辑搬进 backend API。
+- [ ] **API 设计** — 支持按年度+自定义期次范围查询,返回结构化分析数据。
-### L4 — 诊断报告(待设计)
+### L4 — 诊断报告(位置已定,待实现)
+- [x] **位置定稿** — 走势图下方,摘要-详情模式(看板页固定高度干条 + 点击展开详情页全文)。
+- [ ] **自适应色调 Prompt 机制** — 根据选定范围的达标率/均值动态切换分析角度:优秀期→复盘总结模式;问题期→病因诊断模式;混合→综合对比模式。
- [ ] **多 LLM 共识选型测试** — 用样板数据测试 MiMo / GLM 5.2 / 其他模型的诊断分析能力,确定阵容。
- [ ] **诊断 Prompt 撰写** — 输入结构化分析数据,输出自然语言诊断结论 + 改进建议。需融入双引擎理论框架。
+- [ ] **外部因素关联分析** — 结合互联网搜索(同时段竞争节目、社会热点)丰富诊断深度。
- [ ] **共识聚合逻辑** — 多家结果如何合并/投票/呈现。
- [ ] **UI 置信度三档** — AI 草稿/制片人确认/标题推断一眼可辨。
@@ -350,6 +359,17 @@ python scripts/summarize.py --model mimo-v2.5-pro --field opening_hook
- [2026-06-11] **三个 Prompt 独立**(分类 / 叙事结构 / 开篇钩子),不合并成一个大 Prompt。
- [2026-06-11] **示例集"慢慢长大"**:10-30 期全塞 prompt → 30-80 期 pgvector 检索相似 → 80+ 期固定 5 + 动态 5-8。栈里 pgvector 已就位,第二阶段不新增依赖。
+### L3/L4 页面设计决策
+
+- [2026-06-26] **范围选择器走方案 A(按自然年分页)**:每年目标清零+基础/摸高数值可能变+2025 年以前无文稿无 AI 标签,跨年混算问题太多。年内用 ECharts dataZoom 滑块选范围,所有模块联动刷新。
+- [2026-06-26] **L4 诊断报告位置:走势图下方**(不是最底部):制片人定调——诊断分析是最有含金量的内容,不能埋在最后。摘要-详情模式:看板页放 3-5 条干条,点击展开详情页看全文。
+- [2026-06-26] **L4 诊断内容自适应选定范围色调**:全红→复盘总结模式;全绿→病因诊断模式(语气重);混合→综合对比模式。
+- [2026-06-26] **页面幅面 max-width 1100px 居中**:参考 Crextio 风格,不撑满全屏。
+- [2026-06-26] **视觉方向**:磨砂玻璃卡片(backdrop-filter: blur)+ 大倒角(16px)+ 暖色渐变底 + 充足留白。参考风格图 `example/页面风格.webp`。
+- [2026-06-26] **摸高完成率动态效果**:≥95% 红色微光闪烁;85-94% 蓝色;80-84% 绿色脉冲;<80% 黑色+红色警报光晕。
+- [2026-06-26] **收视分析看板 ≠ TPS 仪表盘首页**:首页是每天扫一眼的门面(题图/近9期/热点/排播),分析看板是左侧工具栏里的深度分析入口。首页从看板抽关键数字展示。
+- [2026-06-26] **2026 年收视目标**:基础 0.6448,摸高 0.8989。
+
### 技术约束
- [2026-06-11] MiMo / M3 等 reasoning model 输出带 `...` 前缀。脚本用 `extract_json_from_response` 正则剥掉后再 `json.loads`。**换其他 reasoning model(R1、o1 系列)同样适用。**
@@ -361,11 +381,14 @@ python scripts/summarize.py --model mimo-v2.5-pro --field opening_hook
## 7. ⏩ 交接备注(换人/换工具 0 摩擦续上)
-- **L2 打标已收摊**(2026-06-26):三个 Prompt 均可用(分类80%/结构85%/钩子90%),GT v0.5.0 20期6字段全覆盖。边际收益递减,暂停迭代。
-- **Schema 加字段方案已就绪**:`backend/sql/003_add_ai_labels.sql`(episodes 表 +7 列)+ `backend/app/models/episode.py`(Model 同步)。**待在数据库上执行**:先 `pg_dump` 备份 → 执行 SQL → 验证。
-- **Git 已清理**(2026-06-26):doco/programs 中间产物移出 git(本地保留),.gitignore 补全,`git status` 干净。
-- **下一步**:① 在数据库执行 003 迁移 → ② L3 分析引擎架构设计(对标 Excel 8 个 Sheet)→ ③ L4 诊断报告。
-- **L3/L4 待设计**:分析引擎和诊断报告的架构尚未展开。数据基础(打标+schema)就位后可启动。
+- **L3 原型已出**(2026-06-26):`scripts/l3_analysis_prototype.py` 读 Excel 25 期 + GT 20 期标签,输出 `output/l3_report.html`。5 个模块计算验证通过,视觉方向已定(磨砂玻璃/大倒角/限宽/暖色渐变),L4 位置已定(走势图下方)。原型仍在微调中(Cline 执行视觉改动)。
+- **页面布局已定**(2026-06-26):指标卡 → 走势图 → AI 诊断报告(L4) → 季度+题材对比 → 编导对比 → 双引擎象限图。范围选择器走方案 A(按自然年分页 + 年内 dataZoom 滑块)。
+- **关键数据**:2026 年基础目标 0.6448,摸高目标 0.8989。25 期完整收视数据在 `example/2026收视update.xlsx`。5 位编导:张彤枫/付天雨/孙逸昊/穆佩弦/左鑫。
+- **GT↔Excel 映射**:ground-truth 的 20 期 ep 编号与 Excel 期次不同(GT 用重映射编号),脚本通过关键词匹配标题。5 期无标签:第1/2/9/24/25期。
+- **L2 打标已收摊**(2026-06-26):三个 Prompt 均可用(分类80%/结构85%/钩子90%),GT v0.5.0 20期6字段全覆盖。
+- **003 迁移已执行**(2026-06-26):episodes 表 +7 列 +3 索引到位。`schemas/episode.py` 尚未加新字段,留待 L3 API 时同步。
+- **下一步**:① 原型视觉微调收尾;② 页面线框图定稿;③ 进 React 前端实现(Cline 主力,Claude 审关键逻辑)。
+- **单期详情页**:走势图柱子可点击跳转(已登记需求入口,独立子项目,暂不实施)。
---
@@ -373,6 +396,9 @@ python scripts/summarize.py --model mimo-v2.5-pro --field opening_hook
- [x] ~~Prompt 1 的 4 分类字段枚举最终口径~~ → 已在 v2 快照锁定(见 §1.4)
- [ ] 多 LLM 共识机制的模型阵容(MiMo + GLM 5.2 + ?)待选型测试后定
-- [ ] L3 分析引擎是否需要超出 Excel 8 Sheet 的额外分析维度
+- [x] ~~L3 分析引擎是否需要超出 Excel 8 Sheet 的额外分析维度~~ → 暂不增加,先复刻 Excel 能力
+- [x] ~~范围选择器方案 A vs B~~ → 方案 A(按自然年分页),2025 年以前无文稿无 AI 标签不适合跨年混算
+- [x] ~~L4 诊断报告放在页面什么位置~~ → 走势图下方(制片人定调:诊断是最有含金量的内容)
+- [ ] L4 诊断需要哪些外部搜索 API(同时段竞争节目热度等)
- [x] ~~下一步优先级:先推 Prompt 1 还是先推 schema 加字段方案?~~ → Prompt 1 先行,已完成 v0.3(80%)
- [x] ~~Prompt 1 示例集是否需要扩充?~~ → v0.3 已扩至 7 个示例(含防空反导、硅基大脑等易错案例),全对率 80%
diff --git a/ai-labeling/example/功能区划.jpg b/ai-labeling/example/功能区划.jpg
new file mode 100644
index 0000000..ace8559
Binary files /dev/null and b/ai-labeling/example/功能区划.jpg differ
diff --git a/ai-labeling/example/幅面参考.jpg b/ai-labeling/example/幅面参考.jpg
new file mode 100644
index 0000000..42fd072
Binary files /dev/null and b/ai-labeling/example/幅面参考.jpg differ
diff --git a/ai-labeling/example/页面风格.webp b/ai-labeling/example/页面风格.webp
new file mode 100644
index 0000000..453c1b7
Binary files /dev/null and b/ai-labeling/example/页面风格.webp differ
diff --git a/ai-labeling/scripts/l3_analysis_prototype.py b/ai-labeling/scripts/l3_analysis_prototype.py
new file mode 100644
index 0000000..1d30c83
--- /dev/null
+++ b/ai-labeling/scripts/l3_analysis_prototype.py
@@ -0,0 +1,849 @@
+"""
+L3 分析引擎原型 — 第一层"纯算的"
+读 Excel 收视数据 + ground-truth AI 标签,计算 5 个分析模块,输出 HTML 可视化。
+
+模块:
+ 1. 核心指标卡(年度均值、达标率、最高/最低、期数)
+ 2. 走势图(逐期份额 + 基础/摸高目标线 + 滚动3期均值)
+ 3. 阶段对比(Q1 vs Q2 均值、达标率变化)
+ 4. 题材对比(各 program_format 均值份额排名)
+ 5. 编导对比(各编导均值、期数、最高/最低)
+"""
+
+import json
+import openpyxl
+from pathlib import Path
+from datetime import datetime
+from collections import defaultdict
+
+# ── 配置 ──────────────────────────────────────────────
+BASE_DIR = Path(__file__).resolve().parent.parent
+EXCEL_PATH = BASE_DIR / "example" / "2026收视update.xlsx"
+GT_PATH = BASE_DIR / "benchmark-set" / "ground-truth.json"
+OUTPUT_PATH = BASE_DIR / "output" / "l3_report.html"
+
+TARGET_BASE = 0.6448
+TARGET_HIGH = 0.8989
+
+# 三色判定(基于 audience_share vs yearly_targets)
+# 红=份额 > 摸高 = 优秀;蓝=基础 ≤ 份额 ≤ 摸高 = 达标;绿=份额 < 基础 = 待提升
+COLOR_EXCELLENT = "#c0584f" # 红
+COLOR_STANDARD = "#5b8db8" # 蓝
+COLOR_IMPROVE = "#7aa874" # 绿
+
+# 题材配色(饼图用)
+FORMAT_COLORS = {
+ "装备深解": "#5b8db8",
+ "横切类比": "#c0584f",
+ "历史纵深": "#e8a838",
+ "前沿科技": "#7aa874",
+ "事件战例": "#9b7eb8",
+ "人物牵引": "#d4816b",
+}
+
+
+def rating_color(share):
+ if share > TARGET_HIGH:
+ return COLOR_EXCELLENT
+ elif share >= TARGET_BASE:
+ return COLOR_STANDARD
+ else:
+ return COLOR_IMPROVE
+
+
+def rating_label(share):
+ if share > TARGET_HIGH:
+ return "优秀"
+ elif share >= TARGET_BASE:
+ return "达标"
+ else:
+ return "待提升"
+
+
+# ── 1. 读 Excel ──────────────────────────────────────
+def read_excel():
+ wb = openpyxl.load_workbook(EXCEL_PATH, data_only=True)
+ ws = wb[wb.sheetnames[0]]
+
+ episodes = []
+ for row in ws.iter_rows(min_row=4, values_only=True):
+ date_str = row[1]
+ period = row[2]
+ title = row[3]
+ editor = row[4]
+ share = row[5]
+ rating = row[6]
+
+ if date_str is None or period is None:
+ continue
+
+ period_num = int("".join(c for c in str(period) if c.isdigit()))
+ editor_clean = str(editor).replace(" ", "").strip() if editor else ""
+
+ if isinstance(date_str, str):
+ date_str = date_str.strip().replace(" ", "-")
+ air_date = datetime.strptime(date_str, "%Y-%m-%d")
+ else:
+ air_date = date_str
+
+ episodes.append({
+ "period": period_num,
+ "title": str(title).strip(),
+ "editor": editor_clean,
+ "air_date": air_date,
+ "share": float(share) if share else 0,
+ "rating": float(rating) if rating else 0,
+ })
+
+ return sorted(episodes, key=lambda e: e["period"])
+
+
+# ── 2. 读 ground-truth + 匹配 ────────────────────────
+def read_gt_and_match(episodes):
+ with open(GT_PATH, "r", encoding="utf-8") as f:
+ gt = json.load(f)
+
+ gt_episodes = gt["episodes"]
+
+ # 用关键词匹配 GT episode → Excel episode
+ # GT 标题是缩写,Excel 标题是全称
+ TITLE_KEYWORDS = {
+ "防空反导大对决": "防空",
+ "潜艇仿生": "潜艇",
+ "枪械小尺寸大讲究": "小尺寸",
+ "枪王对决": "枪王",
+ "硅基大脑": "硅基",
+ "舰证不凡": "舰",
+ "大国巨舰反差": "巨舰",
+ "马年军事图鉴": "马年",
+ "重金造神器": "重金",
+ "射速决定论": "射速",
+ "空战演进": "空战",
+ "逆袭战局": "逆袭战局",
+ "X-76飞行器": "X76",
+ "启程远航": "启程",
+ "舰炮暴力美学": "舰炮",
+ "无人舰艇": "无人舰艇",
+ "逆袭的鸭翼": "鸭翼",
+ "机枪双雄": "机枪双雄",
+ "玩具总动员": "玩具",
+ "武器颜值洼地": "颜值",
+ }
+
+ for gt_ep in gt_episodes:
+ gt_title = gt_ep["title"]
+ keyword = TITLE_KEYWORDS.get(gt_title, gt_title)
+ matched = False
+ for ep in episodes:
+ if keyword in ep["title"]:
+ ep["program_format"] = gt_ep.get("program_format")
+ ep["equipment_domain"] = gt_ep.get("equipment_domain", [])
+ ep["scene_tags"] = gt_ep.get("scene_tags", [])
+ ep["tech_tags"] = gt_ep.get("tech_tags", [])
+ ep["narrative_structure"] = gt_ep.get("narrative_structure")
+ ep["opening_hook"] = gt_ep.get("opening_hook")
+ matched = True
+ break
+ if not matched:
+ print(f" ⚠ GT ep{gt_ep['ep']} '{gt_title}' 未匹配到 Excel 期次")
+
+ matched_count = sum(1 for e in episodes if "program_format" in e)
+ print(f" GT 匹配结果:{matched_count}/{len(episodes)} 期有 AI 标签")
+ return episodes
+
+
+# ── 3. 计算分析模块 ──────────────────────────────────
+def compute_analysis(episodes):
+ shares = [e["share"] for e in episodes]
+ n = len(shares)
+
+ # ── 模块 1: 核心指标卡 ──
+ avg_share = sum(shares) / n
+ max_ep = max(episodes, key=lambda e: e["share"])
+ min_ep = min(episodes, key=lambda e: e["share"])
+ above_base = sum(1 for s in shares if s >= TARGET_BASE)
+ above_high = sum(1 for s in shares if s > TARGET_HIGH)
+
+ metrics = {
+ "total_episodes": n,
+ "avg_share": round(avg_share, 4),
+ "avg_rating": round(avg_share / TARGET_BASE, 2),
+ "base_completion": round(avg_share / TARGET_BASE * 100, 1),
+ "high_completion": round(avg_share / TARGET_HIGH * 100, 1),
+ "max_share": max_ep["share"],
+ "max_title": max_ep["title"],
+ "max_period": max_ep["period"],
+ "min_share": min_ep["share"],
+ "min_title": min_ep["title"],
+ "min_period": min_ep["period"],
+ "above_base_count": above_base,
+ "above_base_pct": round(above_base / n * 100, 1),
+ "above_high_count": above_high,
+ "above_high_pct": round(above_high / n * 100, 1),
+ }
+
+ # ── 模块 2: 走势图数据 ──
+ trend = []
+ for i, ep in enumerate(episodes):
+ rolling_3 = None
+ if i >= 2:
+ rolling_3 = round(sum(shares[i-2:i+1]) / 3, 4)
+ trend.append({
+ "period": ep["period"],
+ "title": ep["title"],
+ "share": ep["share"],
+ "rating": ep["rating"],
+ "editor": ep["editor"],
+ "air_date": ep["air_date"].strftime("%m-%d"),
+ "rolling_3": rolling_3,
+ "color": rating_color(ep["share"]),
+ "label": rating_label(ep["share"]),
+ })
+
+ # ── 模块 3: 阶段对比(按季度)──
+ quarters = defaultdict(list)
+ for ep in episodes:
+ q = (ep["air_date"].month - 1) // 3 + 1
+ quarters[f"Q{q}"].append(ep["share"])
+
+ stages = {}
+ for q_name in sorted(quarters.keys()):
+ q_shares = quarters[q_name]
+ q_avg = sum(q_shares) / len(q_shares)
+ q_above_base = sum(1 for s in q_shares if s >= TARGET_BASE)
+ stages[q_name] = {
+ "count": len(q_shares),
+ "avg_share": round(q_avg, 4),
+ "base_completion": round(q_avg / TARGET_BASE * 100, 1),
+ "high_completion": round(q_avg / TARGET_HIGH * 100, 1),
+ "above_base_count": q_above_base,
+ "above_base_pct": round(q_above_base / len(q_shares) * 100, 1),
+ }
+
+ # ── 模块 4: 题材对比 ──
+ format_groups = defaultdict(list)
+ for ep in episodes:
+ fmt = ep.get("program_format")
+ if fmt:
+ format_groups[fmt].append(ep["share"])
+
+ topic_stats = {}
+ for fmt, fmt_shares in format_groups.items():
+ topic_stats[fmt] = {
+ "count": len(fmt_shares),
+ "avg_share": round(sum(fmt_shares) / len(fmt_shares), 4),
+ "max_share": max(fmt_shares),
+ "min_share": min(fmt_shares),
+ }
+ topic_stats = dict(sorted(topic_stats.items(), key=lambda x: x[1]["avg_share"], reverse=True))
+
+ # ── 模块 5: 编导对比 ──
+ editor_groups = defaultdict(list)
+ for ep in episodes:
+ editor_groups[ep["editor"]].append(ep)
+
+ editor_stats = {}
+ for editor, ed_eps in editor_groups.items():
+ ed_shares = [e["share"] for e in ed_eps]
+ ed_max = max(ed_eps, key=lambda e: e["share"])
+ ed_min = min(ed_eps, key=lambda e: e["share"])
+ editor_stats[editor] = {
+ "count": len(ed_eps),
+ "avg_share": round(sum(ed_shares) / len(ed_shares), 4),
+ "max_share": ed_max["share"],
+ "max_title": ed_max["title"],
+ "min_share": ed_min["share"],
+ "min_title": ed_min["title"],
+ "above_base_count": sum(1 for s in ed_shares if s >= TARGET_BASE),
+ "above_base_pct": round(sum(1 for s in ed_shares if s >= TARGET_BASE) / len(ed_shares) * 100, 1),
+ }
+ editor_stats = dict(sorted(editor_stats.items(), key=lambda x: x[1]["avg_share"], reverse=True))
+
+ return {
+ "metrics": metrics,
+ "trend": trend,
+ "stages": stages,
+ "topic_stats": topic_stats,
+ "editor_stats": editor_stats,
+ }
+
+
+# ── 4. 生成 HTML ─────────────────────────────────────
+def generate_html(data, episodes):
+ metrics = data["metrics"]
+ trend = data["trend"]
+ stages = data["stages"]
+ topic_stats = data["topic_stats"]
+ editor_stats = data["editor_stats"]
+
+ # 走势图数据
+ periods = [t["period"] for t in trend]
+ titles = [f'第{t["period"]}期 {t["title"]}' for t in trend]
+ shares = [t["share"] for t in trend]
+ rolling3 = [t["rolling_3"] for t in trend]
+ colors = [t["color"] for t in trend]
+ editors = [t["editor"] for t in trend]
+ labels = [t["label"] for t in trend]
+ dates = [t["air_date"] for t in trend]
+
+ # 题材对比数据
+ topic_names = list(topic_stats.keys())
+ topic_avgs = [topic_stats[k]["avg_share"] for k in topic_names]
+ topic_counts = [topic_stats[k]["count"] for k in topic_names]
+
+ # 题材饼图数据
+ topic_pie_data = []
+ topic_pie_colors = []
+ for k in topic_names:
+ color = FORMAT_COLORS.get(k, "#999")
+ topic_pie_data.append({"name": k, "value": topic_stats[k]["count"]})
+ topic_pie_colors.append(color)
+
+ # 编导对比数据
+ editor_names = list(editor_stats.keys())
+ editor_avgs = [editor_stats[k]["avg_share"] for k in editor_names]
+ editor_counts = [editor_stats[k]["count"] for k in editor_names]
+
+ # 阶段对比数据
+ stage_names = list(stages.keys())
+ stage_avgs = [stages[k]["avg_share"] for k in stage_names]
+ stage_base_pcts = [stages[k]["above_base_pct"] for k in stage_names]
+
+ # ── 摸高目标完成率卡片动态样式 ──
+ hc = metrics["high_completion"]
+ if hc >= 95:
+ high_card_class = "metric-card metric-card-shimmer"
+ high_card_value_style = "background: linear-gradient(90deg, #c0584f 40%, #f0a050 50%, #c0584f 60%); background-size: 200%; -webkit-background-clip: text; -webkit-text-fill-color: transparent; animation: shimmer 2s infinite;"
+ elif hc >= 85:
+ high_card_class = "metric-card"
+ high_card_value_style = "color:#5b8db8"
+ elif hc >= 80:
+ high_card_class = "metric-card metric-card-pulse-warn"
+ high_card_value_style = "color:#7aa874"
+ else:
+ high_card_class = "metric-card metric-card-pulse-danger"
+ high_card_value_style = "color:#333"
+
+ html = f"""
+
+
+
+
+军事科技 2026 收视分析 — L3 原型
+
+
+
+
+
+军事科技 2026 年度收视分析
+数据范围:第1–25期 | 基础目标 {TARGET_BASE} | 摸高目标 {TARGET_HIGH} | 生成时间 {datetime.now().strftime("%Y-%m-%d %H:%M")}
+
+
+
+
+
+
+
{metrics['avg_share']}
+
平均份额
+
基础完成率 {metrics['base_completion']}%
+
+
+
{metrics['high_completion']}%
+
摸高目标完成率
+
+
+
{metrics['max_share']}
+
最高份额
+
第{metrics['max_period']}期 {metrics['max_title'][:8]}…
+
+
+
{metrics['min_share']}
+
最低份额
+
第{metrics['min_period']}期 {metrics['min_title'][:8]}…
+
+
+
{metrics['above_base_count']}/{metrics['total_episodes']}
+
达标期数
+
达标率 {metrics['above_base_pct']}% | 优秀 {metrics['above_high_count']} 期
+
+
+
+
+
+
+
+
+
AI 诊断报告
+
+
+
核心发现
+
+ - 摸高目标完成率 83.7%,距达标差 16.3 个百分点。Q2 连续 4 期低于基础线(第16-19期),是全年最大失速段。
+ - 收视两极分化加剧:最高 1.227(第23期)与最低 0.343(第22期)相差 3.6 倍,说明选题质量波动远大于制作水平波动。
+ - 近 3 期强势反弹(第23-25期均值 1.105),但需警惕"福建舰效应"消退后能否维持。
+
+
+
+
病因与提振建议
+
+ - 轻武器/枪械题材连续拖累:4 期枪械相关节目平均份额仅 0.573,远低于基础线。建议控制同类选题间隔至少 6 期。
+ - "横切类比"形态两极分化:好的横切(马年图鉴 0.95、颜值洼地 1.227)收视极高,差的(重金造神器 0.52)极低。关键变量是"切口话题感"。
+ - 建议增加"装备深解+热点跟进"组合:防空网 0.875、X-76 0.873 证明该组合稳定高产。
+
+
+
+
* 以上为 AI 模拟诊断草稿,仅供版面预览,非真实分析结论
+
+
+
+
+
+
季度对比
+
+ | 季度 | 期数 | 平均份额 | 基础完成率 | 达标率 |
+ {''.join(f'| {q} | {stages[q]["count"]} | {stages[q]["avg_share"]} | {stages[q]["base_completion"]}% | {stages[q]["above_base_pct"]}% |
' for q in stage_names)}
+
+
+
+
+
+
题材对比
+
+
仅含已标注的 {sum(topic_counts)} 期(共 {metrics['total_episodes']} 期,{metrics['total_episodes'] - sum(topic_counts)} 期待标注)
+
+
+
+
+
+
+
+
+
+
+"""
+ return html
+
+
+# ── 主流程 ────────────────────────────────────────────
+def main():
+ print("L3 分析引擎原型 v0.1")
+ print("=" * 40)
+
+ print("\n[1/4] 读取 Excel 收视数据…")
+ episodes = read_excel()
+ print(f" 读取 {len(episodes)} 期")
+
+ print("\n[2/4] 匹配 ground-truth AI 标签…")
+ episodes = read_gt_and_match(episodes)
+
+ print("\n[3/4] 计算分析模块…")
+ analysis = compute_analysis(episodes)
+ m = analysis["metrics"]
+ print(f" 年度均值份额:{m['avg_share']}")
+ print(f" 基础完成率:{m['base_completion']}%")
+ print(f" 摸高完成率:{m['high_completion']}%")
+ print(f" 达标期数:{m['above_base_count']}/{m['total_episodes']}({m['above_base_pct']}%)")
+
+ print("\n[4/4] 生成 HTML 报告…")
+ OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
+ html = generate_html(analysis, episodes)
+ with open(OUTPUT_PATH, "w", encoding="utf-8") as f:
+ f.write(html)
+ print(f" [OK] 报告已生成:{OUTPUT_PATH}")
+ print(f" 请在浏览器中打开查看。")
+
+
+if __name__ == "__main__":
+ main()