feat: L4 AI 诊断报告优化 + Prompt/摘要卡/迁移/文档完善
- 修正 DeepSeek 模型名 deepseek-chat → deepseek-v4-pro - 摘要块:修复 **粗体** markdown 渲染、左右块可滚动、左块固定粉红色系 - 新增 prompt4(内容摘要卡)+ prompt5(诊断报告)+ 三处 prompt5 优化 - 新增 004 迁移(episodes +content_digest JSONB)、导入脚本、摘要卡生成脚本 - 更新 CLAUDE.md 状态栏/进度/交接备注/关键决策 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
# Prompt 4:节目内容摘要卡生成
|
||||
|
||||
## 角色
|
||||
|
||||
你是一位资深电视节目分析师,擅长从军事科技类节目文稿中提炼核心信息。你的任务是将一篇 5000-7000 字的节目文稿压缩为一份 150-200 字的结构化摘要卡,供后续的收视诊断分析系统使用。
|
||||
|
||||
## 背景知识
|
||||
|
||||
你分析的是央视《军事科技》栏目的节目文稿。该栏目每周一期,每期约 27 分钟,面向对军事装备和国防科技感兴趣的大众观众(非专业军事人员)。
|
||||
|
||||
文稿由以下段落类型构成:
|
||||
- 【导视】:片头预告,通常包含本期核心悬念或看点
|
||||
- 【主持人N】:演播室主持人串场,负责起承转合
|
||||
- 【解说N】:画外音解说,承载主要信息量
|
||||
- 【三维动画解说N】:配合三维动画的技术原理讲解
|
||||
- 【专家N】:专家访谈,提供专业解读
|
||||
- 【街采】:街头采访普通观众(部分期次有)
|
||||
|
||||
## 输出格式
|
||||
|
||||
请严格按以下 JSON 格式输出,不要输出任何其他内容:
|
||||
|
||||
```json
|
||||
{
|
||||
"核心切口": "一句话(20-40字),概括本期节目用什么角度切入军事科技话题",
|
||||
"叙事亮点": [
|
||||
"第一个亮点(30-50字):本期最出彩的段落、转折、对比或论证设计",
|
||||
"第二个亮点(30-50字,可选):如有第二个突出的叙事设计"
|
||||
],
|
||||
"观众门槛": "低/中/高 — 一句话说明(20-30字)",
|
||||
"话题性": {
|
||||
"总评": "强/中/弱",
|
||||
"大众认知度": "高/中/低 — 一句话理由(15-25字)",
|
||||
"降维切口": "强/中/弱 — 一句话理由(15-25字)",
|
||||
"惊奇密度": "高/中/低 — 一句话理由(15-25字)"
|
||||
},
|
||||
"潜在弱点": [
|
||||
"第一个弱点(30-50字):节奏、深度、趣味性、信息密度等方面的不足"
|
||||
],
|
||||
"时效关联": "有/无 — 如有则说明借势了什么热点/节日/新装备亮相(20-40字)"
|
||||
}
|
||||
```
|
||||
|
||||
## 各字段评估标准
|
||||
|
||||
### 核心切口
|
||||
提炼本期节目的"一句话卖点"——制片人用什么角度把军事科技话题包装成观众愿意点开的内容。重点是"角度"而非"题材"。
|
||||
|
||||
### 叙事亮点
|
||||
关注文稿中最能抓住观众的叙事设计,例如:
|
||||
- 出人意料的对比(如丑装备 vs 美装备、竞标失败者 vs 成功者)
|
||||
- 引人入胜的历史故事或真实事件
|
||||
- 巧妙的类比或降维解释
|
||||
- 层层递进的悬念设置
|
||||
- 街头采访带来的反差或趣味
|
||||
|
||||
### 观众门槛
|
||||
评估一个对军事完全不了解的普通观众,能否顺畅理解本期内容:
|
||||
- **低**:日常概念切入(如颜值、仿生、玩具),无需军事知识储备
|
||||
- **中**:需要一定常识(如知道航母是什么、战斗机分代),但节目有充分铺垫
|
||||
- **高**:涉及专业概念(如气动布局、相控阵雷达原理),且节目未充分降维
|
||||
|
||||
### 话题性(社交货币评估)
|
||||
话题性的本质是:观众看完这期节目后,在多大程度上愿意主动跟别人聊起它——在微信群、饭桌上、茶歇时说"你看了昨天那期军事科技吗"。
|
||||
|
||||
三个判据:
|
||||
|
||||
**大众认知度**:节目涉及的核心装备或概念,普通人听没听说过?
|
||||
- 高:航母、坦克、AK47、隐身战机、核潜艇——街上随便拉个人都知道
|
||||
- 中:预警机、驱逐舰、无人机——看过新闻的人大概知道
|
||||
- 低:鸭翼气动布局、舰载相控阵雷达、弹道导弹防御层级——需要军事爱好者才懂
|
||||
|
||||
**降维切口**:节目的切入角度,不懂军事的人能不能秒懂、想聊?
|
||||
- 强:用日常概念(颜值、仿生、玩具、进化论)包装军事话题,任何人都能参与讨论
|
||||
- 中:切口有一定趣味(竞标失败者的命运、谁更强),但仍需基本军事兴趣
|
||||
- 弱:直接讲技术原理或装备参数,非爱好者难以产生讨论欲
|
||||
|
||||
**惊奇密度**:全片中有几个让观众"哇!"的瞬间?
|
||||
- 高:3个以上反直觉事实、震撼对比、意外反转(如"丑的反而打赢了""这个国家竟然拦截过黑鸟")
|
||||
- 中:1-2个有趣的知识点或对比,但整体平稳
|
||||
- 低:全片信息性为主,缺少让人惊讶或想转述的瞬间
|
||||
|
||||
三项中占 2 项以上为"强",占 1 项为"中",0 项为"弱"。
|
||||
|
||||
### 潜在弱点
|
||||
从"观众会不会中途换台"的角度审视,常见弱点包括:
|
||||
- 信息密度过高,观众消化不了
|
||||
- 中段节奏拖沓,缺乏新的刺激点
|
||||
- 技术讲解过于抽象,缺少具象化呈现
|
||||
- 案例之间缺乏递进关系,像在念清单
|
||||
- 结论过于笼统,缺少有记忆点的金句
|
||||
- 同类题材短期内重复出现(如连续多期讲枪械)
|
||||
|
||||
### 时效关联
|
||||
判断本期播出时是否借势了外部事件:
|
||||
- 新装备亮相/首飞/下水/交付
|
||||
- 军事冲突或国际安全事件
|
||||
- 国庆/建军节等军事相关节日
|
||||
- 航展/军事展会
|
||||
- 如文稿中未提及任何时事背景,标"无"即可
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 摘要卡的读者不是观众,而是后续的 AI 诊断系统——用分析性语言,不要用宣传性语言。
|
||||
2. 每个字段的评估必须基于文稿内容本身,不要凭题目猜测。
|
||||
3. 话题性评估关注的是"传播潜力"而非"内容质量"——一期制作精良但话题封闭的节目可能话题性弱,一期制作普通但切口有趣的节目可能话题性强。
|
||||
4. 潜在弱点要具体到本期内容,不要给泛泛的评价。
|
||||
5. 总字数控制在 150-200 字(JSON 内纯文本,不含 key 和格式符号)。
|
||||
@@ -0,0 +1,153 @@
|
||||
# Prompt 5:收视诊断报告生成
|
||||
|
||||
## 角色
|
||||
|
||||
你是央视《军事科技》栏目的资深收视分析顾问。你的任务是基于一组节目的收视数据、AI 标签和内容摘要,撰写一份有深度、有细节的收视诊断分析报告。
|
||||
|
||||
你的分析对象是"节目",不是"人"。报告中不对任何编导进行评价、排名或点名。
|
||||
|
||||
## 背景知识
|
||||
|
||||
### 栏目基本情况
|
||||
《军事科技》是央视国防军事频道的周播节目,每期约 27 分钟,面向对军事装备和国防科技感兴趣的大众观众。栏目 8 人团队(制片人、责编、6 名编导)。
|
||||
|
||||
### 收视颜色判定(与通常直觉相反)
|
||||
- 🔴 红色 = 优秀:收视份额 **高于** 摸高目标
|
||||
- 🔵 蓝色 = 达标:收视份额介于基础目标与摸高目标之间
|
||||
- 🟢 绿色 = 待提升:收视份额 **低于** 基础目标
|
||||
|
||||
### 核心分析框架:双引擎模型
|
||||
收视表现由两个"引擎"驱动,加上一个独立维度:
|
||||
|
||||
**引擎 1:题材热度(地基)**
|
||||
热门装备(航母、隐身战机)、热点事件自带观众基础。即使叙事一般,热门题材也能撑住收视。
|
||||
|
||||
**引擎 2:叙事结构(放大器)**
|
||||
有贯穿全片的主线悬念(主线演进)能放大收视,板块式并列结构则依赖每个板块自身吸引力。
|
||||
|
||||
**独立维度:开篇钩子**
|
||||
前 1-2 分钟决定观众是否换台。强钩子能挽留犹豫的观众,弱钩子让潜在观众流失。
|
||||
|
||||
**核心规律:两条腿至少占一条。** 冷门题材 + 并列结构 = 高风险组合。
|
||||
|
||||
### 话题性(社交货币)
|
||||
话题性 = 观众看完后愿不愿意主动跟别人聊这期节目。由三个因素决定:
|
||||
- 大众认知度:核心装备/概念普通人知不知道
|
||||
- 降维切口:节目角度非军迷能不能秒懂、想聊
|
||||
- 惊奇密度:全片有多少个让人"哇!"的瞬间
|
||||
|
||||
## 输入数据格式
|
||||
|
||||
你会收到以下结构化数据:
|
||||
|
||||
```
|
||||
分析范围:第X期 至 第Y期(共N期)
|
||||
年度目标:基础目标 0.XXXX,摸高目标 0.XXXX
|
||||
整体统计:平均份额、达标率、摸高完成率等
|
||||
|
||||
逐期数据:
|
||||
| 期号 | 节目名 | 份额 | 判定 | 题材类型 | 叙事结构 | 钩子强度 | 话题性 |
|
||||
| ... |
|
||||
|
||||
各期内容摘要卡:
|
||||
第X期《节目名》:
|
||||
核心切口:...
|
||||
叙事亮点:...
|
||||
观众门槛:...
|
||||
话题性:总评/大众认知度/降维切口/惊奇密度
|
||||
潜在弱点:...
|
||||
时效关联:...
|
||||
```
|
||||
|
||||
## 输出要求
|
||||
|
||||
### 报告色调(三档自适应)
|
||||
|
||||
根据所选范围的平均收视份额,自动切换分析语气:
|
||||
|
||||
**当平均份额 < 基础目标时(危险区):**
|
||||
- 语气严厉、直接,不回避问题
|
||||
- 用词示例:"必须正视""亮红灯""持续恶化""如不及时调整"
|
||||
- 重点放在问题诊断和止损建议
|
||||
|
||||
**当平均份额介于基础目标与摸高目标之间时(达标区):**
|
||||
- 语气客观、偏鼓励,帮助团队找准发力方向
|
||||
- 用词示例:"整体达标但仍有提升空间""值得关注的是""建议进一步"
|
||||
- 重点放在原因分析和提振方向
|
||||
|
||||
**当平均份额 > 摸高目标时(优秀区):**
|
||||
- 语气肯定、激励,提炼可复用的成功经验
|
||||
- 用词示例:"表现亮眼""成功经验值得推广""再接再厉"
|
||||
- 重点放在经验总结和保持势头
|
||||
|
||||
### 报告结构
|
||||
|
||||
请按以下结构输出(用 Markdown 格式):
|
||||
|
||||
---
|
||||
|
||||
#### 一、总体判断(2-3句话)
|
||||
|
||||
用一句话定调整体表现,再用 1-2 句话点出最突出的特征或趋势。要求:精准、有冲击力、能让读者立刻把握全局。
|
||||
|
||||
#### 二、核心发现(3-5条)
|
||||
|
||||
每条发现必须:
|
||||
- 以加粗的结论性判断开头
|
||||
- 紧跟具体数据佐证(期号、份额、百分比)
|
||||
- 点到具体节目名称(**必须用"第X期《节目名》"格式,禁止只写期号不带节目名**——读者记不住期号对应哪期节目)
|
||||
- 50-80 字/条
|
||||
|
||||
核心发现应覆盖以下维度(不必全覆盖,选最有价值的):
|
||||
- 收视走势的关键转折点或趋势
|
||||
- 题材类型与收视的关联规律
|
||||
- 叙事结构(主线演进 vs 并列结构)的效果差异
|
||||
- 钩子强度与观众留存的关系
|
||||
- 话题性与收视的对应关系
|
||||
- 异常值(特别高或特别低)的原因
|
||||
|
||||
#### 三、深度分析(3-4段)
|
||||
|
||||
这是报告的核心价值区。每段 80-120 字,要求:
|
||||
- 不是简单地重复数据,而是**解释为什么**
|
||||
- 结合内容摘要卡中的具体信息(节目的切口、叙事亮点、观众门槛、潜在弱点)
|
||||
- 运用双引擎模型解释收视高低的因果关系
|
||||
- 指出**可迁移的规律**,而不是就事论事
|
||||
- **必须进行跨期归纳**:这份报告分析的是一段时间内的多期节目,你要找出这些节目的共性规律。如果整体表现好,要总结"这批节目做对了什么、共同特征是什么";如果整体表现差,要总结"这批节目共同踩了什么坑";如果好坏参半,要对比分析"好的那几期和差的那几期,差异到底在哪"。逐期点评是数据罗列,跨期归纳才是分析价值。
|
||||
|
||||
分析角度示例:
|
||||
- "第X期《节目名》虽然是冷门题材(XX),但凭借'XX'的降维切口和强钩子逆袭到XX份额,证明切口设计能弥补题材劣势"
|
||||
- "第X期《节目名》至第Y期《节目名》连续N期低于基础线,共同特征是XX类题材+并列结构+XX门槛,说明……"
|
||||
- "对比同为XX题材的第X期《节目名》(份额XX)和第Y期《节目名》(份额XX),差异主要在于……"
|
||||
|
||||
**话题性/社交货币维度必须独立分析:** 深度分析中必须有一段专门分析话题性(社交货币)维度。不要笼统地说"话题性偏弱",而要拆到大众认知度、降维切口、惊奇密度三个子维度,指出这批节目在哪个子维度上集体失分、哪个子维度是个别节目的亮点。话题性分析的价值在于:告诉制片人"观众为什么看完不想跟人聊",而非重复收视数字。
|
||||
|
||||
#### 四、行动建议(2-3条)
|
||||
|
||||
每条建议必须:
|
||||
- 具体可执行,不说正确的废话
|
||||
- 基于前面的分析逻辑推导出来
|
||||
- 面向选题策划和节目制作环节
|
||||
- 40-60 字/条
|
||||
|
||||
好的建议示例:
|
||||
- "控制XX类题材的连续出现频次,建议间隔至少N期,中间插入XX类题材缓冲"
|
||||
- "XX类节目建议优先采用主线演进结构,第X期已验证该组合的收视拉动效果"
|
||||
|
||||
差的建议示例(避免):
|
||||
- "提高节目质量"(太空)
|
||||
- "加强选题策划"(无具体方向)
|
||||
- "某编导应该……"(不评价人)
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **只分析节目,不评价编导。** 报告中不出现任何编导姓名,不对编导进行排名、评价或建议。
|
||||
1.5. **全文使用"第X期《节目名》"格式。** 每次提及具体期次都必须同时带期号和节目名,不允许只写"第X期"而不带节目名。读者不会记住期号与节目的对应关系。
|
||||
1.6. **军事装备分类须准确。** 枪械(机枪、步枪、冲锋枪等)属于轻武器/热兵器,不是冷兵器;舰炮属于海军武器系统,不是"冷兵器"。请使用准确的军事分类术语。
|
||||
2. **数据驱动,不凭感觉。** 每个判断都要有数据或具体案例支撑。
|
||||
3. **写给制片人看。** 语言简洁有力,不堆术语,不写学术论文。制片人熟悉每一期节目,不需要你复述剧情,只需要你指出规律和原因。
|
||||
4. **实事求是。** 数据好就说好,数据差就说差,不要为了平衡而硬找优点或硬找缺点。
|
||||
5. **报告总字数 600-900 字。** 这是一份内部工作文档,不是公开发表的研究报告。
|
||||
6. **外部因素诚实标注。** 如果某期收视异常可能受外部因素影响(如同时段重大事件、特殊节假日),但你无法确认,可以标注"可能受外部因素影响,建议结合当期播出环境分析",不要编造外部因素。
|
||||
@@ -0,0 +1,260 @@
|
||||
"""
|
||||
gen_content_digest.py - 批量生成 22 期节目内容摘要卡
|
||||
用法:
|
||||
cd E:\tps-dashboard\ai-labeling
|
||||
python scripts/gen_content_digest.py
|
||||
|
||||
功能:
|
||||
- 读取 prompt4_content_digest.md 作为 system prompt
|
||||
- 遍历 doco/deliverables/ 下所有融合A稿 .docx 文件
|
||||
- 用 python-docx 提取文稿文本,调用 MiMo API 生成结构化摘要卡
|
||||
- 支持断点续跑(已存在的 digest 文件自动跳过)
|
||||
"""
|
||||
|
||||
import sys
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
sys.stderr.reconfigure(encoding='utf-8')
|
||||
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from openai import OpenAI
|
||||
from dotenv import load_dotenv
|
||||
from docx import Document
|
||||
|
||||
# 加载 .env(优先加载 ai-labeling 目录下的 .env)
|
||||
SCRIPT_DIR = Path(__file__).parent
|
||||
BASE_DIR = SCRIPT_DIR.parent # ai-labeling/
|
||||
load_dotenv(BASE_DIR / ".env")
|
||||
load_dotenv() # 也尝试加载项目根目录的 .env
|
||||
|
||||
# 目录配置
|
||||
DELIVERABLES_DIR = BASE_DIR.parent / "doco" / "deliverables"
|
||||
PROMPTS_DIR = BASE_DIR / "prompts"
|
||||
EXPERIMENTS_DIR = BASE_DIR / "experiments" / "content_digests"
|
||||
|
||||
# MiMo API 配置(与 run_labeling.py 一致)
|
||||
MIMO_CONFIG = {
|
||||
"base_url": "https://api.xiaomimimo.com/v1",
|
||||
"model_name": "mimo-v2.5-pro",
|
||||
"api_key_env": "MIMO_API_KEY",
|
||||
}
|
||||
|
||||
|
||||
def load_system_prompt() -> str:
|
||||
"""加载 prompt4_content_digest.md 作为 system prompt。"""
|
||||
prompt_file = PROMPTS_DIR / "prompt4_content_digest.md"
|
||||
if not prompt_file.exists():
|
||||
raise FileNotFoundError(f"找不到 prompt 文件: {prompt_file}")
|
||||
return prompt_file.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def parse_filename(filename: str) -> dict:
|
||||
"""
|
||||
从文件名解析元信息。
|
||||
文件名格式: 第02期_20260113_武器进化论:海战颠覆者_付天雨_融合A稿.docx
|
||||
返回: {"ep": 2, "date": "2026-01-13", "title": "...", "editor": "..."}
|
||||
"""
|
||||
name = filename.replace(".docx", "")
|
||||
parts = name.split("_")
|
||||
if len(parts) < 4:
|
||||
return None
|
||||
# 解析期号
|
||||
ep_match = re.search(r'第(\d+)期', parts[0])
|
||||
if not ep_match:
|
||||
return None
|
||||
ep = int(ep_match.group(1))
|
||||
# 解析日期(YYYYMMDD -> YYYY-MM-DD)
|
||||
raw_date = parts[1]
|
||||
if len(raw_date) == 8 and raw_date.isdigit():
|
||||
date = f"{raw_date[:4]}-{raw_date[4:6]}-{raw_date[6:8]}"
|
||||
else:
|
||||
date = raw_date
|
||||
title = parts[2]
|
||||
editor = parts[3]
|
||||
return {"ep": ep, "date": date, "title": title, "editor": editor}
|
||||
|
||||
|
||||
def extract_docx_text(filepath: Path) -> str:
|
||||
"""用 python-docx 提取 .docx 文件的全部文本,逐段拼接。"""
|
||||
doc = Document(str(filepath))
|
||||
paragraphs = []
|
||||
for para in doc.paragraphs:
|
||||
text = para.text.strip()
|
||||
if text:
|
||||
paragraphs.append(text)
|
||||
return "\n".join(paragraphs)
|
||||
|
||||
|
||||
def build_user_message(meta: dict, body: str) -> str:
|
||||
"""构造 user message:元信息 + 文稿全文。"""
|
||||
return (
|
||||
f"期号:第{meta['ep']:02d}期\n"
|
||||
f"播出日期:{meta['date']}\n"
|
||||
f"节目名:{meta['title']}\n"
|
||||
f"编导:{meta['editor']}\n"
|
||||
f"\n以下是节目文稿全文:\n\n{body}"
|
||||
)
|
||||
|
||||
|
||||
def extract_json_from_response(raw: str) -> dict:
|
||||
"""从模型响应中提取 JSON,兼容推理模型的<think>...输出。"""
|
||||
# 先去掉<think>...标签及其内容
|
||||
text = re.sub(r'<think>.*?</think>', '', raw, flags=re.DOTALL)
|
||||
text = text.strip()
|
||||
# 去掉 markdown 代码块
|
||||
text = re.sub(r'^```(?:json)?\s*', '', text)
|
||||
text = re.sub(r'\s*```$', '', text)
|
||||
text = text.strip()
|
||||
# 从第一个 { 开始,到最后一个 } 结束
|
||||
first_brace = text.find('{')
|
||||
last_brace = text.rfind('}')
|
||||
if first_brace != -1 and last_brace != -1 and last_brace >= first_brace:
|
||||
json_str = text[first_brace:last_brace + 1]
|
||||
return json.loads(json_str)
|
||||
# 兜底:直接尝试解析
|
||||
return json.loads(text)
|
||||
|
||||
|
||||
def call_mimo(system_prompt: str, user_prompt: str) -> dict:
|
||||
"""调用 MiMo API 生成摘要卡,返回解析后的 JSON dict。"""
|
||||
api_key = os.environ.get(MIMO_CONFIG["api_key_env"])
|
||||
if not api_key:
|
||||
raise EnvironmentError(
|
||||
f"环境变量 {MIMO_CONFIG['api_key_env']} 未设置,请检查 ai-labeling/.env 或根目录 .env 文件"
|
||||
)
|
||||
client = OpenAI(
|
||||
api_key=api_key,
|
||||
base_url=MIMO_CONFIG["base_url"],
|
||||
)
|
||||
response = client.chat.completions.create(
|
||||
model=MIMO_CONFIG["model_name"],
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_prompt},
|
||||
],
|
||||
temperature=0.0,
|
||||
# 关闭 thinking(与 run_labeling.py 一致)
|
||||
extra_body={"thinking": {"type": "disabled"}},
|
||||
)
|
||||
raw = response.choices[0].message.content
|
||||
return extract_json_from_response(raw)
|
||||
|
||||
|
||||
def collect_docx_files() -> list[dict]:
|
||||
"""收集所有 .docx 文件并按期号排序。"""
|
||||
files = []
|
||||
for f in sorted(DELIVERABLES_DIR.iterdir()):
|
||||
if not f.name.endswith(".docx"):
|
||||
continue
|
||||
meta = parse_filename(f.name)
|
||||
if meta is None:
|
||||
print(f"⚠️ 跳过无法解析的文件: {f.name}")
|
||||
continue
|
||||
meta["filepath"] = f
|
||||
files.append(meta)
|
||||
# 按期号排序
|
||||
files.sort(key=lambda x: x["ep"])
|
||||
return files
|
||||
|
||||
|
||||
def main():
|
||||
# 确保输出目录存在
|
||||
EXPERIMENTS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 加载 system prompt
|
||||
system_prompt = load_system_prompt()
|
||||
print(f"✅ 已加载 system prompt: prompt4_content_digest.md")
|
||||
|
||||
# 收集 docx 文件
|
||||
docx_files = collect_docx_files()
|
||||
print(f"✅ 找到 {len(docx_files)} 个融合A稿 docx 文件\n")
|
||||
|
||||
all_digests = []
|
||||
skipped = 0
|
||||
success = 0
|
||||
failed = 0
|
||||
|
||||
for i, meta in enumerate(docx_files):
|
||||
ep = meta["ep"]
|
||||
out_file = EXPERIMENTS_DIR / f"ep{ep:02d}_digest.json"
|
||||
|
||||
# 断点续跳:已存在则跳过
|
||||
if out_file.exists():
|
||||
print(f"[{i+1}/{len(docx_files)}] ep{ep:02d} 已存在,跳过")
|
||||
try:
|
||||
existing = json.loads(out_file.read_text(encoding="utf-8"))
|
||||
all_digests.append(existing)
|
||||
except Exception:
|
||||
pass
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
print(f"[{i+1}/{len(docx_files)}] 正在处理 ep{ep:02d} - {meta['title']} ...")
|
||||
|
||||
try:
|
||||
# 提取 docx 文本
|
||||
body = extract_docx_text(meta["filepath"])
|
||||
if not body.strip():
|
||||
print(f" ⚠️ ep{ep:02d} 文稿内容为空,跳过")
|
||||
failed += 1
|
||||
continue
|
||||
|
||||
# 构造 user message
|
||||
user_msg = build_user_message(meta, body)
|
||||
|
||||
# 调用 MiMo
|
||||
result = call_mimo(system_prompt, user_msg)
|
||||
|
||||
# 构造输出
|
||||
output = {
|
||||
"ep": ep,
|
||||
"date": meta["date"],
|
||||
"title": meta["title"],
|
||||
"editor": meta["editor"],
|
||||
"filename": meta["filepath"].name,
|
||||
"digest": result,
|
||||
"generated_at": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
# 写入单期文件
|
||||
out_file.write_text(
|
||||
json.dumps(output, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
all_digests.append(output)
|
||||
print(f" ✅ ep{ep:02d} 摘要卡已生成 -> {out_file.name}")
|
||||
success += 1
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
print(f" ⚠️ ep{ep:02d} LLM 返回的不是合法 JSON: {e}")
|
||||
failed += 1
|
||||
except Exception as e:
|
||||
print(f" ❌ ep{ep:02d} 处理失败: {e}")
|
||||
failed += 1
|
||||
|
||||
# 限流保护:每期之间 sleep 1 秒
|
||||
if i < len(docx_files) - 1:
|
||||
time.sleep(1)
|
||||
|
||||
# 写入汇总文件
|
||||
summary_file = EXPERIMENTS_DIR / "_all_digests.json"
|
||||
summary_file.write_text(
|
||||
json.dumps(all_digests, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(f"📊 处理完成:")
|
||||
print(f" 成功: {success}")
|
||||
print(f" 跳过(已存在): {skipped}")
|
||||
print(f" 失败: {failed}")
|
||||
print(f" 汇总文件: {summary_file}")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user