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'' for q in stage_names)} +
季度期数平均份额基础完成率达标率
{q}{stages[q]["count"]}{stages[q]["avg_share"]}{stages[q]["base_completion"]}%{stages[q]["above_base_pct"]}%
+
+
+ +
+

题材对比

+
+
+
+
+
仅含已标注的 {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()