看板升级: L3分析引擎原型v0.1 + 视觉方向定稿 + 页面布局设计

- 新增 l3_analysis_prototype.py:读Excel 25期收视 + GT 20期AI标签,
  计算5个分析模块(指标卡/走势/季度/题材/编导),输出HTML+ECharts报告
- 制片人审定视觉方向:磨砂玻璃/大倒角/1100px限宽/暖色渐变
- 页面布局定稿:指标卡→走势图→L4诊断(走势图下方)→季度+题材→编导→象限图
- 范围选择器定方案A(按自然年分页+年内dataZoom滑块)
- 新增设计参考图3张(功能区划/幅面参考/页面风格)
- 更新两份CLAUDE.md状态栏+进度+关键决策+交接备注

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
simonkoson
2026-06-26 20:19:05 +08:00
parent ffb507a87c
commit 93db565c10
7 changed files with 896 additions and 20 deletions
+2 -2
View File
@@ -13,8 +13,8 @@
## 🔖 状态栏(每次结束 session 前必须更新这三行) ## 🔖 状态栏(每次结束 session 前必须更新这三行)
- **最后更新**Claude(顾问)| 2026-05-27 - **最后更新**Claude(顾问)| 2026-06-26
- **当前状态一句话**:Phase 3 知识库基础设施已完成至 Task 3(树形视图按来源分组,commit `3409d48`);主干暂停、资源转子项目,下一刀未定 - **当前状态一句话**:Phase 3 知识库已完成;看板升级子项目 L3 分析引擎原型 v0.1 已出(HTML 可视化+制片人审定视觉方向),L2 打标收摊+003 迁移到位。主干暂停
- **下一个动手的人从这里开始**:见下方「⏩ 交接备注」 - **下一个动手的人从这里开始**:见下方「⏩ 交接备注」
--- ---
+1
View File
@@ -1,5 +1,6 @@
.env .env
experiments/*.json experiments/*.json
output/
__pycache__/ __pycache__/
*.pyc *.pyc
.venv/ .venv/
+44 -18
View File
@@ -13,7 +13,7 @@
## 🔖 状态栏(每次结束 session 前必须更新这三行) ## 🔖 状态栏(每次结束 session 前必须更新这三行)
- **最后更新**Claude Opus(顾问)| 2026-06-26 - **最后更新**Claude Opus(顾问)| 2026-06-26
- **当前状态一句话**L2 打标收摊。GT v0.5.020期6字段全覆盖)。Schema 加字段方案已写好(003_add_ai_labels.sql + Model),待在数据库上执行。下一步进 L3 分析引擎设计 - **当前状态一句话**L3 分析引擎原型 v0.1 已出(HTML 可视化,5 模块 + L4 模拟诊断),制片人审完视觉方向,页面布局和交互设计方案已定。下一步:原型微调收尾 → 进 React 前端实现
- **下一个动手的人从这里开始**:见下方「⏩ 交接备注」 - **下一个动手的人从这里开始**:见下方「⏩ 交接备注」
--- ---
@@ -109,8 +109,8 @@
|----|------|--------|----------| |----|------|--------|----------|
| L1 | **数据沉淀** | 每期节目的收视数据、文稿全文、编导、选题单持续入库 | 主项目已有 episodes 表基础结构 | | L1 | **数据沉淀** | 每期节目的收视数据、文稿全文、编导、选题单持续入库 | 主项目已有 episodes 表基础结构 |
| L2 | **AI 打标** | 读文稿自动判别:题材分类(4字段)、叙事结构(2档)、开篇钩子(3档) | Prompt 1 v0.3(80%) + Prompt 2 v0.2(100%) + Prompt 3 v0.2(90%),三个均达可用水平 | | L2 | **AI 打标** | 读文稿自动判别:题材分类(4字段)、叙事结构(2档)、开篇钩子(3档) | Prompt 1 v0.3(80%) + Prompt 2 v0.2(100%) + Prompt 3 v0.2(90%),三个均达可用水平 |
| L3 | **分析引擎** | 基于双引擎模型,计算走势、编导对比、题材对比、高低样本对照等多维分析 | 待设计 | | L3 | **分析引擎** | 基于双引擎模型,计算走势、编导对比、题材对比、高低样本对照等多维分析 | HTML 原型 v0.1 已出(5 模块),页面布局已定 |
| L4 | **诊断报告** | 调用 LLM 生成文字诊断结论 + 改进建议(对标 PPT 第 8-10 页水平) | 待设计 | | L4 | **诊断报告** | 调用 LLM 生成文字诊断结论 + 改进建议(对标 PPT 第 8-10 页水平) | 位置已定(走势图下方),模拟文字已预览 |
### 1.6 三层分步交付策略(v1 快照定稿) ### 1.6 三层分步交付策略(v1 快照定稿)
@@ -229,14 +229,16 @@ python scripts/summarize.py --model mimo-v2.5-pro --field opening_hook
## 3. 当前进度(动态,核心交接区) ## 3. 当前进度(动态,核心交接区)
- **已完成**三个 Prompt 均达可用水平 + GT v0.5.020期6字段全覆盖)+ schema 加字段方案(SQL + Model 已写好)+ git 大扫除 - **已完成**L3 原型 v0.1(HTML 可视化 + 制片人审定视觉方向),L2 打标收摊,003 迁移到位
- **正在做** - **正在做**L3 原型视觉微调(Cline 执行),页面布局设计讨论中
- **卡点/待解**① 003_add_ai_labels.sql 待在数据库上执行(先 pg_dump 备份);② L3 分析引擎架构设计未启动 - **卡点/待解**无硬卡点。下一步进 React 前端实现前需定最终页面线框图
--- ---
## 4. 已完成(只追加,最新在上) ## 4. 已完成(只追加,最新在上)
- [2026-06-26 Opus] L3 分析引擎原型 v0.1Python 脚本读 Excel25期)+ ground-truth20期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 编码未写入,不影响功能。Schemaschemas/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.sqlepisodes表+7列)+ episode.py Model同步。summarize.py修复无标注期次崩溃。Git大扫除:doco/programs中间产物移出git、.gitignore补全、deliverables/寄存条入库。commit `cdb213f` - [2026-06-26 Opus] GT v0.5.0:补完10期narrative_structure标注(MiMo跑批85%命中+制片人审定,ep019从AI判主线演进改为并列结构)。20期6字段全覆盖。Schema加字段方案:003_add_ai_labels.sqlepisodes表+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.1ep008补标跨域)。 - [2026-06-25 Opus] Prompt 1 迭代 v0.1→v0.3(全对率40%→80%):v0.2加多选字段显著篇幅门槛+从属零件反例+前沿科技vs横切类比拆清+装备深解同类体系测试;v0.3修正跨域规则(传播分类框非军事术语)+训练舰窄类别判定+装备首发校准。ground-truth v0.4.1ep008补标跨域)。
- [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.220期opening_hook全标注,制片人逐期审定含钩子分析)。summarize.py改为从源GT文件读取。 - [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.220期opening_hook全标注,制片人逐期审定含钩子分析)。summarize.py改为从源GT文件读取。
@@ -269,20 +271,27 @@ python scripts/summarize.py --model mimo-v2.5-pro --field opening_hook
### L1 — 数据沉淀(与主项目交界,需 Opus 审) ### L1 — 数据沉淀(与主项目交界,需 Opus 审)
- [ ] **episodes schema 加字段**方案已就绪(`003_add_ai_labels.sql` + Model),待在数据库执行。先 `pg_dump` 备份 - [x] **episodes schema 加字段**003 迁移已执行(2026-06-26),7 列 + 3 索引到位
- [ ] **文稿入库流程** — 每期文稿全文入知识库(主项目 Phase 3 已有知识库基础设施)。 - [ ] **文稿入库流程** — 每期文稿全文入知识库(主项目 Phase 3 已有知识库基础设施)。
- [ ] **收视数据导入** — 真实收视 Excel 批量导入 + 清测试数据。 - [ ] **收视数据导入** — 真实收视 Excel 批量导入 + 清测试数据。
### L3 — 分析引擎(待设计 ### L3 — 分析引擎(原型已出,进入前端实现
- [ ] **分析维度定义** — 对标 Excel 8 个 Sheet:核心指标卡走势图、编导对比、题材对比、阶段对比、高低样本对照、诊断结论 - [x] **HTML 原型 v0.1** — 5 模块计算验证通过(指标卡/走势/季度/题材/编导),25 期数据全部读入
- [ ] **计算逻辑实现**滚动均值、达标率、分组统计、样本排序等 - [x] **视觉方向定稿**磨砂玻璃+大倒角+1100px 限宽+暖色渐变底+三色判定+题材饼图柱图组合
- [ ] **API 设计**支持按季度/自定义周期查询,返回结构化分析数据 - [x] **页面布局定稿**指标卡 → 走势图 → L4 诊断报告 → 季度对比+题材对比 → 编导对比 → 双引擎象限图
- [ ] **范围选择器(方案 A 已定)** — 按自然年分页,年内用 ECharts dataZoom 滑块选范围,下方所有模块联动刷新。历史年份(如 2025)仅展示走势+编导(无 AI 标签的模块标灰)。
- [ ] **单期详情页入口** — 走势图柱子可点击跳转到该期节目分析页(独立子项目)。
- [ ] **计算逻辑实现(React** — 将原型 Python 计算逻辑搬进 backend API。
- [ ] **API 设计** — 支持按年度+自定义期次范围查询,返回结构化分析数据。
### L4 — 诊断报告(待设计 ### L4 — 诊断报告(位置已定,待实现
- [x] **位置定稿** — 走势图下方,摘要-详情模式(看板页固定高度干条 + 点击展开详情页全文)。
- [ ] **自适应色调 Prompt 机制** — 根据选定范围的达标率/均值动态切换分析角度:优秀期→复盘总结模式;问题期→病因诊断模式;混合→综合对比模式。
- [ ] **多 LLM 共识选型测试** — 用样板数据测试 MiMo / GLM 5.2 / 其他模型的诊断分析能力,确定阵容。 - [ ] **多 LLM 共识选型测试** — 用样板数据测试 MiMo / GLM 5.2 / 其他模型的诊断分析能力,确定阵容。
- [ ] **诊断 Prompt 撰写** — 输入结构化分析数据,输出自然语言诊断结论 + 改进建议。需融入双引擎理论框架。 - [ ] **诊断 Prompt 撰写** — 输入结构化分析数据,输出自然语言诊断结论 + 改进建议。需融入双引擎理论框架。
- [ ] **外部因素关联分析** — 结合互联网搜索(同时段竞争节目、社会热点)丰富诊断深度。
- [ ] **共识聚合逻辑** — 多家结果如何合并/投票/呈现。 - [ ] **共识聚合逻辑** — 多家结果如何合并/投票/呈现。
- [ ] **UI 置信度三档** — AI 草稿/制片人确认/标题推断一眼可辨。 - [ ] **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] **三个 Prompt 独立**(分类 / 叙事结构 / 开篇钩子),不合并成一个大 Prompt。
- [2026-06-11] **示例集"慢慢长大"**10-30 期全塞 prompt → 30-80 期 pgvector 检索相似 → 80+ 期固定 5 + 动态 5-8。栈里 pgvector 已就位,第二阶段不新增依赖。 - [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 输出带 `<think>...</think>` 前缀。脚本用 `extract_json_from_response` 正则剥掉后再 `json.loads`。**换其他 reasoning modelR1、o1 系列)同样适用。** - [2026-06-11] MiMo / M3 等 reasoning model 输出带 `<think>...</think>` 前缀。脚本用 `extract_json_from_response` 正则剥掉后再 `json.loads`。**换其他 reasoning modelR1、o1 系列)同样适用。**
@@ -361,11 +381,14 @@ python scripts/summarize.py --model mimo-v2.5-pro --field opening_hook
## 7. ⏩ 交接备注(换人/换工具 0 摩擦续上) ## 7. ⏩ 交接备注(换人/换工具 0 摩擦续上)
- **L2 打标已收摊**2026-06-26):三个 Prompt 均可用(分类80%/结构85%/钩子90%),GT v0.5.0 20期6字段全覆盖。边际收益递减,暂停迭代 - **L3 原型已出**2026-06-26):`scripts/l3_analysis_prototype.py` 读 Excel 25 期 + GT 20 期标签,输出 `output/l3_report.html`。5 个模块计算验证通过,视觉方向已定(磨砂玻璃/大倒角/限宽/暖色渐变),L4 位置已定(走势图下方)。原型仍在微调中(Cline 执行视觉改动)
- **Schema 加字段方案已就绪**`backend/sql/003_add_ai_labels.sql`episodes 表 +7 列)+ `backend/app/models/episode.py`(Model 同步)。**待在数据库上执行**:先 `pg_dump` 备份 → 执行 SQL → 验证 - **页面布局已定**(2026-06-26):指标卡 → 走势图 → AI 诊断报告(L4) → 季度+题材对比 → 编导对比 → 双引擎象限图。范围选择器走方案 A(按自然年分页 + 年内 dataZoom 滑块)
- **Git 已清理**2026-06-26):doco/programs 中间产物移出 git(本地保留),.gitignore 补全,`git status` 干净 - **关键数据**2026 年基础目标 0.6448,摸高目标 0.8989。25 期完整收视数据在 `example/2026收视update.xlsx`。5 位编导:张彤枫/付天雨/孙逸昊/穆佩弦/左鑫
- **下一步**:① 在数据库执行 003 迁移 → ② L3 分析引擎架构设计(对标 Excel 8 个 Sheet)→ ③ L4 诊断报告 - **GT↔Excel 映射**ground-truth 的 20 期 ep 编号与 Excel 期次不同(GT 用重映射编号),脚本通过关键词匹配标题。5 期无标签:第1/2/9/24/25期
- **L3/L4 待设计**:分析引擎和诊断报告的架构尚未展开。数据基础(打标+schema)就位后可启动 - **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) - [x] ~~Prompt 1 的 4 分类字段枚举最终口径~~ → 已在 v2 快照锁定(见 §1.4)
- [ ] 多 LLM 共识机制的模型阵容(MiMo + GLM 5.2 + ?)待选型测试后定 - [ ] 多 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.380% - [x] ~~下一步优先级:先推 Prompt 1 还是先推 schema 加字段方案?~~ → Prompt 1 先行,已完成 v0.380%
- [x] ~~Prompt 1 示例集是否需要扩充?~~ → v0.3 已扩至 7 个示例(含防空反导、硅基大脑等易错案例),全对率 80% - [x] ~~Prompt 1 示例集是否需要扩充?~~ → v0.3 已扩至 7 个示例(含防空反导、硅基大脑等易错案例),全对率 80%
Binary file not shown.

After

Width:  |  Height:  |  Size: 814 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

@@ -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"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>军事科技 2026 收视分析 — L3 原型</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
background: linear-gradient(135deg, #f5f0e8 0%, #e8e4d8 30%, #f0ece2 60%, #ebe5d5 100%);
min-height: 100vh;
color: #333;
max-width: 1100px;
margin: 0 auto;
padding: 32px 24px;
}}
h1 {{
text-align: center;
color: #4a6741;
font-size: 22px;
margin-bottom: 4px;
}}
.subtitle {{
text-align: center;
color: #888;
font-size: 13px;
margin-bottom: 24px;
}}
/* 指标卡 */
.metrics-grid {{
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 16px;
margin-bottom: 28px;
}}
.metric-card {{
background: rgba(255,255,255,0.55);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-radius: 16px;
padding: 20px;
text-align: center;
border: 1px solid rgba(255,255,255,0.6);
box-shadow: 0 4px 16px rgba(0,0,0,0.06);
}}
.metric-card .value {{
font-size: 28px;
font-weight: 700;
margin-bottom: 4px;
}}
.metric-card .desc {{
font-size: 12px;
color: #888;
}}
.metric-card .sub {{
font-size: 11px;
color: #aaa;
margin-top: 4px;
}}
/* 图表容器 */
.chart-section {{
background: rgba(255,255,255,0.55);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-radius: 16px;
padding: 24px;
margin-bottom: 24px;
border: 1px solid rgba(255,255,255,0.6);
box-shadow: 0 4px 16px rgba(0,0,0,0.06);
}}
.chart-section h2 {{
font-size: 15px;
color: #4a6741;
margin-bottom: 14px;
padding-left: 8px;
border-left: 3px solid #6b8e6b;
}}
.chart-section h3 {{
margin: 0;
}}
.chart-section ul {{
margin: 0;
list-style-type: disc;
}}
.chart-container {{
width: 100%;
height: 380px;
}}
.chart-container-sm {{
width: 100%;
height: 300px;
}}
/* 双列布局 */
.two-col {{
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}}
/* 阶段对比表格 */
.stage-table {{
width: 100%;
border-collapse: collapse;
font-size: 13px;
}}
.stage-table th {{
background: #f5f3eb;
padding: 8px 12px;
text-align: left;
font-weight: 600;
color: #555;
}}
.stage-table td {{
padding: 8px 12px;
border-bottom: 1px solid #eee;
}}
/* 图例 */
.legend {{
display: flex;
justify-content: center;
gap: 20px;
margin-bottom: 16px;
font-size: 12px;
}}
.legend-item {{
display: flex;
align-items: center;
gap: 4px;
}}
.legend-dot {{
width: 10px;
height: 10px;
border-radius: 50%;
}}
/* 摸高卡片动效 */
@keyframes shimmer {{
0% {{ background-position: -200% center; }}
100% {{ background-position: 200% center; }}
}}
@keyframes pulse-danger {{
0%, 100% {{ box-shadow: 0 0 8px rgba(180,30,30,0.3); }}
50% {{ box-shadow: 0 0 20px rgba(180,30,30,0.6); }}
}}
@keyframes pulse-warn {{
0%, 100% {{ opacity: 1; }}
50% {{ opacity: 0.75; }}
}}
.metric-card-shimmer .value {{
display: inline-block;
}}
.metric-card-pulse-danger {{
border: 2px solid rgba(180,30,30,0.4);
animation: pulse-danger 1.5s infinite;
}}
.metric-card-pulse-warn {{
animation: pulse-warn 2s infinite;
}}
.footer {{
text-align: center;
color: #bbb;
font-size: 11px;
margin-top: 20px;
}}
</style>
</head>
<body>
<h1>军事科技 2026 年度收视分析</h1>
<p class="subtitle">数据范围:第1–25期 | 基础目标 {TARGET_BASE} 摸高目标 {TARGET_HIGH} 生成时间 {datetime.now().strftime("%Y-%m-%d %H:%M")}</p>
<div class="legend">
<div class="legend-item"><div class="legend-dot" style="background:{COLOR_EXCELLENT}"></div>优秀(&gt;摸高)</div>
<div class="legend-item"><div class="legend-dot" style="background:{COLOR_STANDARD}"></div>达标(基础~摸高)</div>
<div class="legend-item"><div class="legend-dot" style="background:{COLOR_IMPROVE}"></div>待提升(&lt;基础)</div>
</div>
<!-- 指标卡 -->
<div class="metrics-grid">
<div class="metric-card">
<div class="value" style="color:{rating_color(metrics['avg_share'])}">{metrics['avg_share']}</div>
<div class="desc">平均份额</div>
<div class="sub">基础完成率 {metrics['base_completion']}%</div>
</div>
<div class="{high_card_class}">
<div class="value" style="{high_card_value_style}">{metrics['high_completion']}%</div>
<div class="desc">摸高目标完成率</div>
</div>
<div class="metric-card">
<div class="value" style="color:{COLOR_EXCELLENT}">{metrics['max_share']}</div>
<div class="desc">最高份额</div>
<div class="sub">第{metrics['max_period']}{metrics['max_title'][:8]}…</div>
</div>
<div class="metric-card">
<div class="value" style="color:{COLOR_IMPROVE}">{metrics['min_share']}</div>
<div class="desc">最低份额</div>
<div class="sub">第{metrics['min_period']}{metrics['min_title'][:8]}…</div>
</div>
<div class="metric-card">
<div class="value">{metrics['above_base_count']}<span style="font-size:16px;color:#888">/{metrics['total_episodes']}</span></div>
<div class="desc">达标期数</div>
<div class="sub">达标率 {metrics['above_base_pct']}% 优秀 {metrics['above_high_count']} 期</div>
</div>
</div>
<!-- 走势图 -->
<div class="chart-section">
<h2>收视走势</h2>
<div id="trendChart" class="chart-container"></div>
</div>
<!-- AI 诊断报告 -->
<div class="chart-section">
<h2>AI 诊断报告</h2>
<div style="display:flex;gap:20px;">
<div style="flex:1;padding:16px;background:rgba(192,88,79,0.06);border-radius:12px;border-left:3px solid #c0584f;">
<h3 style="font-size:14px;color:#c0584f;margin-bottom:10px;">核心发现</h3>
<ul style="font-size:13px;line-height:1.8;color:#444;padding-left:16px;">
<li><b>摸高目标完成率 83.7%</b>,距达标差 16.3 个百分点。Q2 连续 4 期低于基础线(第16-19期),是全年最大失速段。</li>
<li>收视<b>两极分化加剧</b>:最高 1.227(第23期)与最低 0.343(第22期)相差 3.6 倍,说明选题质量波动远大于制作水平波动。</li>
<li><b>近 3 期强势反弹</b>(第23-25期均值 1.105),但需警惕"福建舰效应"消退后能否维持。</li>
</ul>
</div>
<div style="flex:1;padding:16px;background:rgba(91,141,184,0.06);border-radius:12px;border-left:3px solid #5b8db8;">
<h3 style="font-size:14px;color:#5b8db8;margin-bottom:10px;">病因与提振建议</h3>
<ul style="font-size:13px;line-height:1.8;color:#444;padding-left:16px;">
<li><b>轻武器/枪械题材连续拖累</b>:4 期枪械相关节目平均份额仅 0.573,远低于基础线。建议控制同类选题间隔至少 6 期。</li>
<li><b>"横切类比"形态两极分化</b>:好的横切(马年图鉴 0.95、颜值洼地 1.227)收视极高,差的(重金造神器 0.52)极低。关键变量是"切口话题感"。</li>
<li><b>建议增加"装备深解+热点跟进"组合</b>:防空网 0.875、X-76 0.873 证明该组合稳定高产。</li>
</ul>
</div>
</div>
<p style="font-size:11px;color:#bbb;margin-top:12px;text-align:right;">* 以上为 AI 模拟诊断草稿,仅供版面预览,非真实分析结论</p>
</div>
<!-- 双列:阶段对比 + 题材对比 -->
<div class="two-col">
<div class="chart-section">
<h2>季度对比</h2>
<table class="stage-table">
<tr><th>季度</th><th>期数</th><th>平均份额</th><th>基础完成率</th><th>达标率</th></tr>
{''.join(f'<tr><td>{q}</td><td>{stages[q]["count"]}</td><td style="color:{rating_color(stages[q]["avg_share"])};font-weight:600">{stages[q]["avg_share"]}</td><td>{stages[q]["base_completion"]}%</td><td>{stages[q]["above_base_pct"]}%</td></tr>' for q in stage_names)}
</table>
<div id="stageChart" class="chart-container-sm" style="margin-top:12px"></div>
</div>
<div class="chart-section">
<h2>题材对比</h2>
<div style="display:flex;gap:16px">
<div id="topicPieChart" class="chart-container-sm" style="flex:1;height:280px"></div>
<div id="topicBarChart" class="chart-container-sm" style="flex:1;height:280px"></div>
</div>
<div style="font-size:11px;color:#aaa;margin-top:8px;text-align:center">仅含已标注的 {sum(topic_counts)} 期(共 {metrics['total_episodes']} 期,{metrics['total_episodes'] - sum(topic_counts)} 期待标注)</div>
</div>
</div>
<!-- 编导对比 -->
<div class="chart-section">
<h2>编导对比</h2>
<div id="editorChart" class="chart-container-sm"></div>
</div>
<div class="footer">L3 分析引擎原型 v0.1 — TPS 工作台 | 数据草稿,仅供内部讨论</div>
<script>
// ── 走势图 ──
var trendTitles = {json.dumps(titles, ensure_ascii=False)};
var trendChart = echarts.init(document.getElementById('trendChart'));
trendChart.setOption({{
tooltip: {{
trigger: 'axis',
formatter: function(params) {{
var p = params[0].dataIndex;
var editors = {json.dumps(editors, ensure_ascii=False)};
var labels = {json.dumps(labels, ensure_ascii=False)};
var dates = {json.dumps(dates, ensure_ascii=False)};
var shares = {json.dumps(shares)};
var s = '<b>' + trendTitles[p] + '</b><br/>';
s += dates[p] + ' ' + editors[p] + '<br/>';
s += '份额:<b>' + shares[p] + '</b>' + labels[p] + '';
if (params.length > 1 && params[1].value != null) {{
s += '<br/>滚动3期均值:' + params[1].value;
}}
return s;
}}
}},
grid: {{ left: 50, right: 30, top: 80, bottom: 40 }},
xAxis: {{
type: 'category',
data: {json.dumps([f'{t["period"]}' for t in trend])},
axisLabel: {{ fontSize: 11 }}
}},
yAxis: {{
type: 'value',
min: 0.2,
max: 1.4,
axisLabel: {{ fontSize: 11 }}
}},
series: [
{{
name: '收视份额',
type: 'bar',
data: {json.dumps([{"value": s, "itemStyle": {"color": c}} for s, c in zip(shares, colors)])},
barWidth: '50%',
label: {{
show: true,
position: 'top',
rotate: 45,
align: 'left',
fontSize: 10,
color: 'inherit',
opacity: 0.7,
formatter: function(params) {{
var t = trendTitles[params.dataIndex];
var name = t.replace(/^第\\d+期\\s*/, '');
return name.length > 4 ? name.substring(0, 4) + '..' : name;
}}
}},
}},
{{
name: '滚动3期均值',
type: 'line',
data: {json.dumps(rolling3)},
smooth: true,
lineStyle: {{ color: '#e8a838', width: 2 }},
itemStyle: {{ color: '#e8a838' }},
symbolSize: 4,
}},
{{
name: '基础目标',
type: 'line',
data: {json.dumps([TARGET_BASE] * len(trend))},
lineStyle: {{ color: '{COLOR_STANDARD}', type: 'dashed', width: 1.5 }},
symbol: 'none',
tooltip: {{ show: false }},
}},
{{
name: '摸高目标',
type: 'line',
data: {json.dumps([TARGET_HIGH] * len(trend))},
lineStyle: {{ color: '{COLOR_EXCELLENT}', type: 'dashed', width: 1.5 }},
symbol: 'none',
tooltip: {{ show: false }},
}},
]
}});
// ── 阶段对比 ──
var stageChart = echarts.init(document.getElementById('stageChart'));
stageChart.setOption({{
tooltip: {{ trigger: 'axis' }},
grid: {{ left: 50, right: 30, top: 20, bottom: 30 }},
xAxis: {{ type: 'category', data: {json.dumps(stage_names)} }},
yAxis: {{ type: 'value', min: 0.4, max: 1.0 }},
series: [
{{
type: 'bar',
data: {json.dumps([{"value": v, "itemStyle": {"color": rating_color(v)}} for v in stage_avgs])},
barWidth: '40%',
label: {{ show: true, position: 'top', fontSize: 12, fontWeight: 'bold',
formatter: function(p) {{ return p.value.toFixed(4); }} }}
}},
{{
type: 'line',
data: {json.dumps([TARGET_BASE] * len(stage_names))},
lineStyle: {{ color: '{COLOR_STANDARD}', type: 'dashed', width: 1 }},
symbol: 'none',
}},
{{
type: 'line',
data: {json.dumps([TARGET_HIGH] * len(stage_names))},
lineStyle: {{ color: '{COLOR_EXCELLENT}', type: 'dashed', width: 1 }},
symbol: 'none',
}}
]
}});
// ── 题材对比 - 饼图(期数占比)──
var topicPieChart = echarts.init(document.getElementById('topicPieChart'));
topicPieChart.setOption({{
tooltip: {{
trigger: 'item',
formatter: function(params) {{
return params.name + '<br/>期数:' + params.value + ' 期(' + params.percent + '%';
}}
}},
series: [{{
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '55%'],
data: {json.dumps(topic_pie_data, ensure_ascii=False)},
color: {json.dumps(topic_pie_colors)},
label: {{
fontSize: 11,
formatter: '{{b}}\\n{{c}}期 ({{d}}%)'
}},
emphasis: {{
label: {{ fontSize: 14, fontWeight: 'bold' }}
}}
}}]
}});
// ── 题材对比 - 柱图(平均份额排名)──
var topicBarChart = echarts.init(document.getElementById('topicBarChart'));
var formatColors = {json.dumps(FORMAT_COLORS, ensure_ascii=False)};
var topicColorData = {json.dumps([{"value": v, "itemStyle": {"color": FORMAT_COLORS.get(k, "#999")}} for k, v in zip(reversed(topic_names), reversed(topic_avgs))])};
topicBarChart.setOption({{
tooltip: {{
trigger: 'axis',
formatter: function(params) {{
var counts = {json.dumps(topic_counts)};
return params[0].name + '<br/>平均份额:' + params[0].value + '<br/>期数:' + counts[params[0].dataIndex];
}}
}},
grid: {{ left: 80, right: 30, top: 10, bottom: 30 }},
xAxis: {{ type: 'value', min: 0.4, max: 1.0 }},
yAxis: {{
type: 'category',
data: {json.dumps(list(reversed(topic_names)), ensure_ascii=False)},
axisLabel: {{ fontSize: 12 }}
}},
series: [{{
type: 'bar',
data: topicColorData,
barWidth: '50%',
label: {{ show: true, position: 'right', fontSize: 11,
formatter: function(p) {{ return p.value.toFixed(3); }} }}
}}]
}});
// ── 编导对比 ──
var editorChart = echarts.init(document.getElementById('editorChart'));
var editorAvgs = {json.dumps(editor_avgs)};
var editorCounts = {json.dumps(editor_counts)};
var editorDetails = {json.dumps({k: v for k, v in editor_stats.items()}, ensure_ascii=False)};
editorChart.setOption({{
tooltip: {{
trigger: 'axis',
formatter: function(params) {{
var name = params[0].name;
var d = editorDetails[name];
var s = '<b>' + name + '</b>' + d.count + '期)<br/>';
s += '平均份额:<b>' + d.avg_share + '</b><br/>';
s += '最高:' + d.max_share + '' + d.max_title.substring(0,10) + '<br/>';
s += '最低:' + d.min_share + '' + d.min_title.substring(0,10) + '<br/>';
s += '达标率:' + d.above_base_pct + '%';
return s;
}}
}},
grid: {{ left: 70, right: 30, top: 10, bottom: 30 }},
xAxis: {{ type: 'value', min: 0.4, max: 1.0 }},
yAxis: {{
type: 'category',
data: {json.dumps(list(reversed(editor_names)), ensure_ascii=False)},
axisLabel: {{ fontSize: 13 }}
}},
series: [{{
type: 'bar',
data: {json.dumps([{"value": v, "itemStyle": {"color": rating_color(v)}} for v in reversed(editor_avgs)])},
barWidth: '40%',
label: {{ show: true, position: 'right', fontSize: 12, fontWeight: 'bold',
formatter: function(p) {{ return p.value.toFixed(4); }} }}
}},
{{
type: 'line',
data: {json.dumps([TARGET_BASE] * len(editor_names))},
lineStyle: {{ color: '{COLOR_STANDARD}', type: 'dashed', width: 1 }},
symbol: 'none',
}}]
}});
// 响应窗口变化
window.addEventListener('resize', function() {{
trendChart.resize();
stageChart.resize();
topicPieChart.resize();
topicBarChart.resize();
editorChart.resize();
}});
</script>
</body>
</html>"""
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()