ai-labeling: ground-truth v0.3.0 + 脚本扩展 --field 支持 Prompt 1

- ground-truth.json 新增 4 分类字段骨架(program_format/equipment_domain/scene_tags/tech_tags)
- equipment_domain 类型修正为数组(对齐 v2 快照多选定义)
- run_labeling.py 新增 --field 参数,文件名含 field 标记
- summarize.py 新增 --field 参数 + 老文件向后兼容
- CLAUDE.md 补入 v1-v4 快照精华:象限图规格、6 字段枚举、三层交付策略

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
simonkoson
2026-06-24 17:53:05 +08:00
parent bdc59bf787
commit 30931d4446
4 changed files with 174 additions and 62 deletions
+84 -12
View File
@@ -38,18 +38,72 @@
**质量标杆**`example/` 目录下的 PPT(14 页诊断报告)和 Excel(8 个分析 Sheet)是制片人与 Opus 4.8 手工做出的 21 期阶段性分析成果。看板升级要达到**同等分析深度**,并把"一次性手工分析"变成"累积持续自动分析"。 **质量标杆**`example/` 目录下的 PPT(14 页诊断报告)和 Excel(8 个分析 Sheet)是制片人与 Opus 4.8 手工做出的 21 期阶段性分析成果。看板升级要达到**同等分析深度**,并把"一次性手工分析"变成"累积持续自动分析"。
### 1.2 核心理论框架——"双引擎模型" ### 1.2 核心理论框架——"双引擎 + 开篇钩子"
**收视 = 题材热度 × 叙事结构** > 收视 题材热度(横轴)× 叙事结构(纵轴)+ 开篇钩子(独立维度,叠加显示)
- **引擎 1 题材热度(地基)**:热点/新装备/大舰/节日天然有人看。即使叙事并列,热点也能撑住收视(如防空网 0.875、X-76 0.873)。 - **引擎 1 题材热度(地基)**:热点/新装备/大舰/节日天然有人看。即使叙事并列,热点也能撑住收视(如防空网 0.875、X-76 0.873)。
- **引擎 2 叙事结构(放大器)**:有没有贯穿全片的主线悬念。强主线能把冷题材拉到高收视(如仿生潜艇 0.92、组装武器 0.85)。 - **引擎 2 叙事结构(放大器)**:有没有贯穿全片的主线悬念。强主线能把冷题材拉到高收视(如仿生潜艇 0.92、组装武器 0.85)。
- **开篇钩子(独立维度)**:前 1-2 分钟决定观众换不换台。功能与叙事结构不同(开篇=留住,结构=留住之后的体验),AI 打标也分开判。
**两条腿至少占一条**:冷题材 + 并列结构 = 必然低分(如枪械图鉴 0.533)。 **两条腿至少占一条**:冷题材 + 并列结构 = 必然低分(如枪械图鉴 0.533)。
**"话题性"已拆解消化**(v2 快照确认):题材稳态热度→横轴承接,时机加成→scene_tags 承接,切口话题感→program_format"横切类比"部分承接。话题性不独立立维。
这个模型是所有 AI 打标、分析计算、诊断结论的理论基础。开发任何功能都要回到这个公式上来。 这个模型是所有 AI 打标、分析计算、诊断结论的理论基础。开发任何功能都要回到这个公式上来。
### 1.3 系统四层架构 ### 1.3 招牌视觉——双引擎象限图(v2 快照定稿)
| 元素 | 编码 |
|------|------|
| 横轴 | 题材热度(= 该题材类别窗口内均值 − 栏目均值,零点=栏目自身平均线) |
| 纵轴 | 叙事结构 2 档 |
| 气泡大小 | 收视份额 |
| 气泡颜色 | 收视判定三色(红 `#c0584f` 优秀 / 蓝 `#5b8db8` 达标 / 绿 `#7aa874` 待提升) |
| 气泡边框 | 开篇钩子(粗实线=强 / 细实线=中 / 虚线=弱) |
| 悬停 tooltip | 节目名 + 期次 + 份额 + 钩子档 |
| 重叠处理 | 横轴 jitter + 垂直 jitter;同点聚集本身就是诊断信号 |
这张图是整个看板升级的**核心视觉交付物**。制片人已认可视觉方向。
### 1.4 6 字段完整枚举定义(v2 快照锁定,v3 修订叙事结构)
**字段一:`program_format` 主题材(单选)** — 节目内容切口,不是播出形态
| 枚举值 | 含义 |
|--------|------|
| `装备深解` | 围绕一款/一型具体装备深度剖析 |
| `横切类比` | 用某切口(趣味/功能/技术)跨装备类比 |
| `历史纵深` | 沿时间线讲装备/战术演进 |
| `前沿科技` | 新技术在军事装备上的应用 |
| `事件战例` | 围绕某次行动、战役、冲突 |
| `人物牵引` | 用人物作叙事入口和牵引线 |
**字段二:`equipment_domain` 装备域(多选,至少 1 项)**
`陆战装备` / `海军舰船` / `航空航天` / `导弹与反导` / `信息化` / `后勤保障` / `轻武器` / `跨域` / `其他`
注:原"枪械"升级为"轻武器";原"空军防空"改为"导弹与反导""无人作战"从装备域剥离到 tech_tags。
**字段三:`scene_tags` 场景标签(多选,可空)** — 回答"为什么这周播"
`节点策划`(纪念日/周年/节庆/装备节点)/ `热点跟进`(突发新闻)/ `装备首发`(新装备亮相)/ `展会`
注:原"节日策划"从 program_format 移到 scene_tags 并改名"节点策划"。
**字段四:`tech_tags` 技术线标签(多选,可空)** — 新质新域追踪
`无人作战` / `智能化`AI/算法/智能决策)/ `有人无人协同`
**字段五:`narrative_structure` 叙事结构(单选,2 档)**
`主线演进` / `并列结构`v3 从 3 档收为 2 档)
**字段六:`opening_hook` 开篇钩子强度(单选,3 档)**
`强`(新闻/反差/画面震撼/悬念)/ `中`(背景铺陈/装备亮相/历史溯源)/ `弱`(直接讲技术/概念/平铺直叙)
### 1.5 系统四层架构
| 层 | 名称 | 做什么 | 当前状态 | | 层 | 名称 | 做什么 | 当前状态 |
|----|------|--------|----------| |----|------|--------|----------|
@@ -58,19 +112,29 @@
| L3 | **分析引擎** | 基于双引擎模型,计算走势、编导对比、题材对比、高低样本对照等多维分析 | 待设计 | | L3 | **分析引擎** | 基于双引擎模型,计算走势、编导对比、题材对比、高低样本对照等多维分析 | 待设计 |
| L4 | **诊断报告** | 调用 LLM 生成文字诊断结论 + 改进建议(对标 PPT 第 8-10 页水平) | 待设计 | | L4 | **诊断报告** | 调用 LLM 生成文字诊断结论 + 改进建议(对标 PPT 第 8-10 页水平) | 待设计 |
### 1.4 分析周期策略 ### 1.6 三层分步交付策略(v1 快照定稿)
| 层 | 解锁条件 | 交付物 |
|----|---------|--------|
| 第一层"纯算的" | 只需题材字段(program_format + equipment_domain | 核心指标卡、走势图(含基础/摸高目标线+滚动3期均值)、阶段对比、题材对比、编导维度 |
| 第二层"打标后才能算的" | 叙事结构 + 开篇钩子标完 | **双引擎象限图**(招牌视觉)+ 高低样本下钻 |
| 第三层"算不出只能写的" | L3 数据就绪 + LLM API 配通 | AI 季度诊断小结(制片人审改后发布)+ 一句话诊断 + 病因 + 提振方案 |
**编导视角:能力地图,不是排名榜**v1 §2.7)。团队视角看"谁擅长哪种结构/哪类题材",把强项做成全组可复用模板。制片人视角可看完整排名+下钻。底层同一套数据,锁可后开。
### 1.7 分析周期策略
- **默认轨:季度报告**(Q1/Q2/Q3/Q4)。对标台里考核节奏,每季约 12-13 期,数据量合适。 - **默认轨:季度报告**(Q1/Q2/Q3/Q4)。对标台里考核节奏,每季约 12-13 期,数据量合适。
- **灵活轨:自定义周期**。所有栏目人员均可自由选择时间范围查看诊断(如"暑期档 7-8 月"、"最近 10 期")。 - **灵活轨:自定义周期**。所有栏目人员均可自由选择时间范围查看诊断(如"暑期档 7-8 月"、"最近 10 期")。
- 系统按季度自动生成,用户也能手动选范围——两不误。 - 系统按季度自动生成,用户也能手动选范围——两不误。
### 1.5 受众与权限 ### 1.8 受众与权限
- **不只给制片人看**——全栏目人员(制片人、责编、6 名编导)都能查看分析结果、自由选择周期、调用 LLM 查看诊断。 - **不只给制片人看**——全栏目人员(制片人、责编、6 名编导)都能查看分析结果、自由选择周期、调用 LLM 查看诊断。
- AI 打标结果仍须制片人审核后才正式落库(设计哲学红线:AI 给方向不给答案)。 - AI 打标结果仍须制片人审核后才正式落库(设计哲学红线:AI 给方向不给答案)。
- 但分析报告(L4)是基于已审核数据生成的,可直接展示。 - 但分析报告(L4)是基于已审核数据生成的,可直接展示。
### 1.6 样板文件参照表(`example/` 目录) ### 1.9 样板文件参照表(`example/` 目录)
| 文件 | 内容 | 对标系统层 | | 文件 | 内容 | 对标系统层 |
|------|------|-----------| |------|------|-----------|
@@ -82,7 +146,7 @@
| PPT 第 12-13 页 | 选题日历、复盘会议程 | 超出系统范围,供人工参考 | | PPT 第 12-13 页 | 选题日历、复盘会议程 | 超出系统范围,供人工参考 |
| Excel 8 个 Sheet | 核心指标卡、数据明细、走势图、编导对比、题材对比、阶段对比、高低样本、诊断结论 | L3 分析引擎的完整数据底稿 | | Excel 8 个 Sheet | 核心指标卡、数据明细、走势图、编导对比、题材对比、阶段对比、高低样本、诊断结论 | L3 分析引擎的完整数据底稿 |
### 1.7 范围边界 ### 1.10 范围边界
- ✅ 做:Prompt 撰写与迭代、模型选型基准测试、ground-truth 维护、打标脚本、分析引擎设计与实现、诊断报告生成、前端看板页面 - ✅ 做:Prompt 撰写与迭代、模型选型基准测试、ground-truth 维护、打标脚本、分析引擎设计与实现、诊断报告生成、前端看板页面
- ❌ 不做:选题日历自动编排、复盘会自动组织、标题自动改写(这些是人的工作) - ❌ 不做:选题日历自动编排、复盘会自动组织、标题自动改写(这些是人的工作)
@@ -163,14 +227,16 @@ python scripts/summarize.py --model mimo-v2.5-pro
## 3. 当前进度(动态,核心交接区) ## 3. 当前进度(动态,核心交接区)
- **已完成**L2 层 Prompt 2(叙事结构)锁定 + ground-truth v0.2.1 锁定 + 生产模型 mimo-v2.5-pro 确定 - **已完成**L2 层 Prompt 2(叙事结构)锁定 + ground-truth v0.3.0(4 分类字段骨架预留)+ 脚本扩展支持 --field classification + 生产模型 mimo-v2.5-pro 确定
- **正在做**全貌对齐完成,准备进入 Prompt 1 撰写 - **正在做**Prompt 14 分类字段)撰写
- **卡点/待解**Prompt 1 的 4 分类字段枚举需收敛;L3/L4 架构设计尚未启动;诊断报告多 LLM 共识机制待选型测试 - **卡点/待解**:L3/L4 架构设计尚未启动;诊断报告多 LLM 共识机制待选型测试
--- ---
## 4. 已完成(只追加,最新在上) ## 4. 已完成(只追加,最新在上)
- [2026-06-24 Opus+Cline] ground-truth v0.3.0 结构扩展(4 分类字段骨架预留)+ run_labeling.py / summarize.py 扩展 --field 参数支持 classification。脚本可直接跑 Prompt 1,等 prompt 文件就位即用。
- [2026-06-24 Opus] CLAUDE.md 二次更新:精读 v1-v4 四份续接快照,补入双引擎象限图视觉规格、6 字段完整枚举定义、三层分步交付策略、编导能力地图设计、v1-v4 关键决策。
- [2026-06-24 Opus] 全貌对齐:精读 PPT+Excel 样板、明确四层架构(数据沉淀→AI打标→分析引擎→诊断报告)、确认多 LLM 共识机制方向、确认季度+自定义双轨周期、确认全员可见。CLAUDE.md 从打标沙盒文档升级为全局指导文件。 - [2026-06-24 Opus] 全貌对齐:精读 PPT+Excel 样板、明确四层架构(数据沉淀→AI打标→分析引擎→诊断报告)、确认多 LLM 共识机制方向、确认季度+自定义双轨周期、确认全员可见。CLAUDE.md 从打标沙盒文档升级为全局指导文件。
- [2026-06-24 Opus] 模型配置统一更正:M3 配置移除,生产模型改为 mimo-v2.5-pro,更新 run_labeling.py / summarize.py / CLAUDE.md / README.md / .env.example。 - [2026-06-24 Opus] 模型配置统一更正:M3 配置移除,生产模型改为 mimo-v2.5-pro,更新 run_labeling.py / summarize.py / CLAUDE.md / README.md / .env.example。
- [2026-06-11 Opus+Cline] AI 打标流水线跑通:Prompt 2 v0.2 锁定、ground-truth v0.2.1 锁定、M3 100% 命中率、模型选型完成。Git commit `38f3728` + `edfb298` - [2026-06-11 Opus+Cline] AI 打标流水线跑通:Prompt 2 v0.2 锁定、ground-truth v0.2.1 锁定、M3 100% 命中率、模型选型完成。Git commit `38f3728` + `edfb298`
@@ -220,7 +286,7 @@ python scripts/summarize.py --model mimo-v2.5-pro
## 6. 关键决策(跨 session 最易丢) ## 6. 关键决策(跨 session 最易丢)
### 项目全貌(2026-06-24 对齐) ### 项目全貌(v1-v4 快照 + 2026-06-24 对齐)
- [2026-06-24] **看板升级 ≠ AI 打标**。AI 打标只是 L2 层,完整系统是四层:数据沉淀 → AI 打标 → 分析引擎 → 诊断报告。目标是把 PPT+Excel 样板里的阶段性手工分析变成累积持续自动分析。 - [2026-06-24] **看板升级 ≠ AI 打标**。AI 打标只是 L2 层,完整系统是四层:数据沉淀 → AI 打标 → 分析引擎 → 诊断报告。目标是把 PPT+Excel 样板里的阶段性手工分析变成累积持续自动分析。
- [2026-06-24] **双引擎模型是理论基础**:收视 = 题材热度 × 叙事结构。所有打标维度、分析计算、诊断结论都围绕这个公式展开。 - [2026-06-24] **双引擎模型是理论基础**:收视 = 题材热度 × 叙事结构。所有打标维度、分析计算、诊断结论都围绕这个公式展开。
@@ -228,6 +294,12 @@ python scripts/summarize.py --model mimo-v2.5-pro
- [2026-06-24] **分析周期双轨**:默认季度 + 自定义范围。 - [2026-06-24] **分析周期双轨**:默认季度 + 自定义范围。
- [2026-06-24] **不用旧 21 期冷启动**:数据已重新整理清洗,拿新数据重新做。 - [2026-06-24] **不用旧 21 期冷启动**:数据已重新整理清洗,拿新数据重新做。
- [2026-06-24] **诊断报告用多 LLM 共识机制**:因合规限制无法用顶级模型(Opus),改为多个国产模型(MiMo / GLM 5.2 / 待定)分析相同数据取共识,弥补单模型洞察力不足。 - [2026-06-24] **诊断报告用多 LLM 共识机制**:因合规限制无法用顶级模型(Opus),改为多个国产模型(MiMo / GLM 5.2 / 待定)分析相同数据取共识,弥补单模型洞察力不足。
- [2026-06-09] **配色走浅色**:沿用 App 米色底 `#fbf9f1` + 深绿 `#6b8e6b`,不做深色大屏(留 2.0 独立数据大屏蓝本)。
- [2026-06-09] **DataEase 不进 1.0**:栈冲突 + 资源不够。图表用 ECharts / @ant-design/charts 在 React 栈直接复刻。
- [2026-06-09] **编导板块不做排名**(团队视角走"能力地图"),制片人视角可看完整排名+下钻。
- [2026-06-09] **"活的边界"**:实时脉搏(走势/滚动均值)永远滚动;周期复盘(象限/病因/提振)按周期生成。每张图都要有显式时间选择器。
- [2026-06-10] **话题性不独立立维**:稳态热度→横轴,时机→scene_tags,切口→横切类比形态。
- [2026-06-10] **6 字段枚举锁定**(见 §1.4):program_format 6 值 / equipment_domain 9 值多选 / scene_tags 4 值 / tech_tags 3 值 / narrative_structure 2 档 / opening_hook 3 档。
### 叙事结构判别 ### 叙事结构判别
@@ -274,7 +346,7 @@ python scripts/summarize.py --model mimo-v2.5-pro
## 8. 待确认 / 开放问题(需制片人拍板) ## 8. 待确认 / 开放问题(需制片人拍板)
- [ ] Prompt 1 的 4 分类字段枚举最终口径(v1 快照 §三.1 已列草案,需收敛 - [x] ~~Prompt 1 的 4 分类字段枚举最终口径~~ → 已在 v2 快照锁定(见 §1.4
- [ ] 多 LLM 共识机制的模型阵容(MiMo + GLM 5.2 + ?)待选型测试后定 - [ ] 多 LLM 共识机制的模型阵容(MiMo + GLM 5.2 + ?)待选型测试后定
- [ ] L3 分析引擎是否需要超出 Excel 8 Sheet 的额外分析维度 - [ ] L3 分析引擎是否需要超出 Excel 8 Sheet 的额外分析维度
- [ ] 下一步优先级:先推 Prompt 1 还是先推 schema 加字段方案? - [ ] 下一步优先级:先推 Prompt 1 还是先推 schema 加字段方案?
+13 -12
View File
@@ -1,19 +1,20 @@
{ {
"version": "v0.2", "version": "v0.3.0",
"locked_at": "2026-06-11", "locked_at": "2026-06-11",
"locked_by": "制片人刘通 + Opus 复议(二次审定)", "locked_by": "制片人刘通 + Opus 复议(二次审定)",
"fields_covered": ["narrative_structure", "opening_hook"], "fields_covered": ["narrative_structure", "opening_hook"],
"changelog": "v0.1→v0.2: ep10(射速决定论)严格命中性能阶梯主线、ep15(空中坚盾)符合层级承接到体系困局递进,两期从并列结构改为主线演进。ep05/ep13/ep14 经讨论后维持并列结构(共同原因: 段内有承接但全片骨架为并列,已写入 Prompt v0.2 示例 4/5 作为反例教学)。v0.2.1: ep12 三家独立模型一致判并列结构,经制片人复读稿确认后改为并列结构", "fields_reserved": ["program_format", "equipment_domain", "scene_tags", "tech_tags"],
"changelog": "v0.1→v0.2: ep10(射速决定论)严格命中性能阶梯主线、ep15(空中坚盾)符合层级承接到体系困局递进,两期从并列结构改为主线演进。ep05/ep13/ep14 经讨论后维持并列结构(共同原因: 段内有承接但全片骨架为并列,已写入 Prompt v0.2 示例 4/5 作为反例教学)。v0.2.1: ep12 三家独立模型一致判并列结构,经制片人复读稿确认后改为并列结构。v0.3.0: 为 Prompt 1 预留 4 个分类字段位置(program_format/equipment_domain/scene_tags/tech_tags),枚举值待定,暂填 null/[]。未改动现有 narrative_structure/opening_hook 标注结果。v0.3.0补丁: equipment_domain 从 null 改为 [],对齐 v2 快照""定义。",
"episodes": [ "episodes": [
{"ep": 3, "title": "硅基大脑", "share": 0.85, "narrative_structure": "并列结构", "opening_hook": "中"}, {"ep": 3, "title": "硅基大脑", "share": 0.85, "narrative_structure": "并列结构", "opening_hook": "中", "program_format": null, "equipment_domain": [], "scene_tags": [], "tech_tags": []},
{"ep": 4, "title": "潜艇仿生", "share": 0.92, "narrative_structure": "主线演进", "opening_hook": "强"}, {"ep": 4, "title": "潜艇仿生", "share": 0.92, "narrative_structure": "主线演进", "opening_hook": "强", "program_format": null, "equipment_domain": [], "scene_tags": [], "tech_tags": []},
{"ep": 5, "title": "舰证不凡", "share": 0.72, "narrative_structure": "并列结构", "opening_hook": "中"}, {"ep": 5, "title": "舰证不凡", "share": 0.72, "narrative_structure": "并列结构", "opening_hook": "中", "program_format": null, "equipment_domain": [], "scene_tags": [], "tech_tags": []},
{"ep": 7, "title": "马年军事图鉴", "share": 0.95, "narrative_structure": "并列结构", "opening_hook": "强"}, {"ep": 7, "title": "马年军事图鉴", "share": 0.95, "narrative_structure": "并列结构", "opening_hook": "强", "program_format": null, "equipment_domain": [], "scene_tags": [], "tech_tags": []},
{"ep": 10, "title": "射速决定论", "share": 0.703, "narrative_structure": "主线演进", "opening_hook": "弱"}, {"ep": 10, "title": "射速决定论", "share": 0.703, "narrative_structure": "主线演进", "opening_hook": "弱", "program_format": null, "equipment_domain": [], "scene_tags": [], "tech_tags": []},
{"ep": 11, "title": "空战颠覆者", "share": 0.859, "narrative_structure": "主线演进", "opening_hook": "中"}, {"ep": 11, "title": "空战颠覆者", "share": 0.859, "narrative_structure": "主线演进", "opening_hook": "中", "program_format": null, "equipment_domain": [], "scene_tags": [], "tech_tags": []},
{"ep": 12, "title": "逆袭战局", "share": 0.85, "narrative_structure": "并列结构", "opening_hook": "强"}, {"ep": 12, "title": "逆袭战局", "share": 0.85, "narrative_structure": "并列结构", "opening_hook": "强", "program_format": null, "equipment_domain": [], "scene_tags": [], "tech_tags": []},
{"ep": 13, "title": "长短智慧", "share": 0.533, "narrative_structure": "并列结构", "opening_hook": "中"}, {"ep": 13, "title": "长短智慧", "share": 0.533, "narrative_structure": "并列结构", "opening_hook": "中", "program_format": null, "equipment_domain": [], "scene_tags": [], "tech_tags": []},
{"ep": 14, "title": "X-76 飞行器", "share": 0.873, "narrative_structure": "并列结构", "opening_hook": "中"}, {"ep": 14, "title": "X-76 飞行器", "share": 0.873, "narrative_structure": "并列结构", "opening_hook": "中", "program_format": null, "equipment_domain": [], "scene_tags": [], "tech_tags": []},
{"ep": 15, "title": "空中坚盾", "share": 0.875, "narrative_structure": "主线演进", "opening_hook": "强"} {"ep": 15, "title": "空中坚盾", "share": 0.875, "narrative_structure": "主线演进", "opening_hook": "强", "program_format": null, "equipment_domain": [], "scene_tags": [], "tech_tags": []}
] ]
} }
+29 -11
View File
@@ -41,11 +41,16 @@ MODEL_CONFIG = {
ALL_EPISODES = [3, 4, 5, 7, 10, 11, 12, 13, 14, 15] ALL_EPISODES = [3, 4, 5, 7, 10, 11, 12, 13, 14, 15]
FIELD_PROMPT_MAP = {
"narrative": "prompt2_narrative.md",
"classification": "prompt1_classification.md",
}
def load_prompt(field): def load_prompt(field):
if field == "narrative": if field not in FIELD_PROMPT_MAP:
return (PROMPTS_DIR / "prompt2_narrative.md").read_text(encoding="utf-8") raise ValueError(f"Unknown field: {field}, valid: {list(FIELD_PROMPT_MAP.keys())}")
raise ValueError(f"Unknown field: {field}") return (PROMPTS_DIR / FIELD_PROMPT_MAP[field]).read_text(encoding="utf-8")
def load_transcript(ep): def load_transcript(ep):
@@ -65,8 +70,19 @@ def load_ground_truth(ep):
def parse_prompt(template, transcript): def parse_prompt(template, transcript):
"""按 ## SYSTEM / ## USER 分隔符拆解 prompt。
自动剥离 ## SYSTEM 标签之前的标题行。
"""
parts = template.split("## USER") parts = template.split("## USER")
system_prompt = parts[0].replace("# Prompt 2:叙事结构判别", "").strip() # parts[0] 是 system 部分,可能包含标题行 + ## SYSTEM 标签
system_raw = parts[0]
# 如果有 ## SYSTEM 标签,取它之后的内容;否则去除标题行
if "## SYSTEM" in system_raw:
system_prompt = system_raw.split("## SYSTEM", 1)[1].strip()
else:
# 没有 ## SYSTEM 标签时,去掉第一行(标题行)作为降级处理
lines = system_raw.strip().splitlines()
system_prompt = "\n".join(lines[1:]).strip() if len(lines) > 1 else system_raw.strip()
user_prompt = parts[1].strip().replace("{transcript}", transcript) user_prompt = parts[1].strip().replace("{transcript}", transcript)
return system_prompt, user_prompt return system_prompt, user_prompt
@@ -108,19 +124,19 @@ def call_model(model_key, system_prompt, user_prompt):
return extract_json_from_response(raw) return extract_json_from_response(raw)
def run_labeling(ep, model_key): def run_labeling(ep, model_key, field="narrative"):
transcript, fname = load_transcript(ep) transcript, fname = load_transcript(ep)
template = load_prompt("narrative") template = load_prompt(field)
system_prompt, user_prompt = parse_prompt(template, transcript) system_prompt, user_prompt = parse_prompt(template, transcript)
result = call_model(model_key, system_prompt, user_prompt) result = call_model(model_key, system_prompt, user_prompt)
gt = load_ground_truth(ep) gt = load_ground_truth(ep)
ts = datetime.now().strftime("%Y%m%d_%H%M%S") ts = datetime.now().strftime("%Y%m%d_%H%M%S")
out = EXPERIMENTS_DIR / f"{ts}_{model_key}_ep{ep:02d}.json" out = EXPERIMENTS_DIR / f"{ts}_{model_key}_{field}_ep{ep:02d}.json"
out.write_text( out.write_text(
json.dumps({"episode": ep, "filename": fname, "result": result, "ground_truth": gt}, ensure_ascii=False, indent=2), json.dumps({"episode": ep, "filename": fname, "field": field, "result": result, "ground_truth": gt}, ensure_ascii=False, indent=2),
encoding="utf-8", encoding="utf-8",
) )
print(f"完成 ep{ep:02d} -> {out.name}") print(f"完成 ep{ep:02d} [{field}] -> {out.name}")
return result return result
@@ -129,12 +145,14 @@ def main():
parser.add_argument("--ep", type=int, help="单期编号") parser.add_argument("--ep", type=int, help="单期编号")
parser.add_argument("--all", action="store_true", help="跑全部") parser.add_argument("--all", action="store_true", help="跑全部")
parser.add_argument("--model", default="mimo-v2.5-pro", help="模型键名") parser.add_argument("--model", default="mimo-v2.5-pro", help="模型键名")
parser.add_argument("--field", default="narrative", choices=["narrative", "classification"],
help="打标字段: narrative(叙事结构) / classification(4分类)")
args = parser.parse_args() args = parser.parse_args()
if args.all: if args.all:
for ep in ALL_EPISODES: for ep in ALL_EPISODES:
run_labeling(ep, args.model) run_labeling(ep, args.model, args.field)
elif args.ep: elif args.ep:
run_labeling(args.ep, args.model) run_labeling(args.ep, args.model, args.field)
else: else:
parser.print_help() parser.print_help()
+48 -27
View File
@@ -39,11 +39,21 @@ def latest_per_ep(files):
return {ep: info[0] for ep, info in latest.items()} return {ep: info[0] for ep, info in latest.items()}
def run(model): def run(model, field="narrative"):
pattern = str(EXPERIMENTS_DIR / f"*_{model}_*.json") # 文件匹配:新格式有 field 标记,老格式(无 field)向后兼容
files = sorted(Path(p) for p in glob.glob(pattern)) pattern_new = str(EXPERIMENTS_DIR / f"*_{model}_{field}_*.json")
files_new = sorted(Path(p) for p in glob.glob(pattern_new))
# 如果是 narrative 且新格式文件为空,尝试匹配老格式(无 field 标记)
if field == "narrative" and not files_new:
pattern_old = str(EXPERIMENTS_DIR / f"*_{model}_ep*.json")
files_old = [f for f in sorted(Path(p) for p in glob.glob(pattern_old))
if len(f.name.replace(".json", "").split("_")) == 3] # 老格式: ts_model_epNN.json
files = files_old
else:
files = files_new
if not files: if not files:
print(f"未找到 {pattern}") print(f"未找到 {pattern_new}")
return return
ep_files = latest_per_ep(files) ep_files = latest_per_ep(files)
@@ -61,36 +71,47 @@ def run(model):
result = data.get("result") result = data.get("result")
title = gt.get("title", "?") title = gt.get("title", "?")
gt_val = gt.get("narrative_structure", "?")
pred_val = result.get("narrative_structure") if result else None
conf = result.get("confidence", "?") if result else "?"
hit = pred_val == gt_val if pred_val is not None else False
rows.append({"ep": ep, "title": title, "gt": gt_val, "pred": pred_val or "解析失败", "hit": hit, "conf": conf}) if field == "narrative":
gt_val = gt.get("narrative_structure", "?")
pred_val = result.get("narrative_structure") if result else None
conf = result.get("confidence", "?") if result else "?"
hit = pred_val == gt_val if pred_val is not None else False
rows.append({"ep": ep, "title": title, "gt": gt_val, "pred": pred_val or "解析失败", "hit": hit, "conf": conf})
elif field == "classification":
# 骨架预留:等 ground-truth 标注就绪后实现具体比对逻辑
rows.append({"ep": ep, "title": title, "gt": "?", "pred": "classification 待实现", "hit": False, "conf": "?"})
# 打印每行 if field == "narrative":
for r in rows: # 打印每行
mark = "" if r["hit"] else "" for r in rows:
conf_str = f'置信度:{r["conf"]}' if r["conf"] != "?" else "" mark = "" if r["hit"] else ""
print(f' ep{r["ep"]:02d} {r["title"]:<10} | 标准:{r["gt"]:<8} | {model}:{r["pred"]:<8} | {mark} {conf_str}') conf_str = f'置信度:{r["conf"]}' if r["conf"] != "?" else ""
print(f' ep{r["ep"]:02d} {r["title"]:<10} | 标准:{r["gt"]:<8} | {model}:{r["pred"]:<8} | {mark} {conf_str}')
# 汇总 # 汇总
total = len(rows) total = len(rows)
hits = sum(1 for r in rows if r["hit"]) hits = sum(1 for r in rows if r["hit"])
hi_conf = [r for r in rows if r["conf"] == ""] hi_conf = [r for r in rows if r["conf"] == ""]
mid_low = [r for r in rows if r["conf"] in ("", "")] mid_low = [r for r in rows if r["conf"] in ("", "")]
hi_hit = sum(1 for r in hi_conf if r["hit"]) hi_hit = sum(1 for r in hi_conf if r["hit"])
ml_hit = sum(1 for r in mid_low if r["hit"]) ml_hit = sum(1 for r in mid_low if r["hit"])
print(f"\n ===== {model} 命中情况 =====") print(f"\n ===== {model} 命中情况 =====")
print(f" narrative_structure 命中: {hits}/{total} = {hits*100//total}%") print(f" narrative_structure 命中: {hits}/{total} = {hits*100//total}%")
print(f" 自评\"\"置信的命中率: {hi_hit}/{len(hi_conf)}") print(f" 自评\"\"置信的命中率: {hi_hit}/{len(hi_conf)}")
print(f" 自评\"中/低\"置信的命中率: {ml_hit}/{len(mid_low)}") print(f" 自评\"中/低\"置信的命中率: {ml_hit}/{len(mid_low)}")
print(f" 解析失败: {parse_fail}") print(f" 解析失败: {parse_fail}")
elif field == "classification":
print(f"\n ===== {model} classification =====")
print(f" classification 比对逻辑待实现(等 ground-truth 标注就绪)")
print(f" 解析失败: {parse_fail}")
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("--model", required=True, help="模型键名,如 mimo-v2.5-pro / deepseek-v4-pro") parser.add_argument("--model", required=True, help="模型键名,如 mimo-v2.5-pro / deepseek-v4-pro")
parser.add_argument("--field", default="narrative", choices=["narrative", "classification"],
help="打标字段: narrative(叙事结构) / classification(4分类)")
args = parser.parse_args() args = parser.parse_args()
run(args.model) run(args.model, args.field)