fix: 完成 doco src layout 重构,文件移至 src/doco/ 子目录
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
# Doco 文稿整理子项目 Brief
|
||||
|
||||
> 主项目 → 子项目的"交接宪法":红线、技术栈、出入口接口
|
||||
> 起草日期:2026-06-12
|
||||
> 状态:主项目签发,子项目内部不修改
|
||||
|
||||
---
|
||||
|
||||
## 一、为什么做
|
||||
|
||||
**痛点**:《军事科技》每期节目播出后,产出一份贴近实际播出的终版文稿,过去靠人工对照 A 稿(编导定稿)+ 录音转写 + OCR 字幕反复核对,**单期 4-6 小时**,且容易遗漏错字、整段重写、编导笔误。
|
||||
|
||||
**升级目标**:把"三方文本 → 终版文稿 + 差异报告"做成可自动化的流水线模块。
|
||||
|
||||
**现状起点**:demo 已用《现代防空反导大对决》一期跑通,效果达标。栏目组改变了素材输出方式——直接产出"黑底白字+干净人声"的特殊视频——使物理输入从 3 路(A 稿+B 稿+ASR)收敛到 2 路(A 稿+视频),但**逻辑上仍然三方融合**(A 稿+B 路视频字幕 OCR+音轨 ASR)。
|
||||
|
||||
---
|
||||
|
||||
## 二、做什么(功能边界,只列名)
|
||||
|
||||
| 模块 | 简述 |
|
||||
|---|---|
|
||||
| 视频双路拆分 | ffmpeg 抽帧+字幕变化检测+OCR → B 稿;ffmpeg 抽音轨 → 16k WAV |
|
||||
| 讯飞 ASR 适配层 | 复用 demo 跑通的 `xfyun_asr_standard.py`,替换凭证 |
|
||||
| 三方融合引擎 | 规则层(Python)+ AI 层(Claude Sonnet 4.6),demo 已锁 |
|
||||
| 单期处理接口 | CLI + Python API,契约见主项目回复 §Q3 |
|
||||
| Golden test | 用 demo 那期视频做零回归验证 |
|
||||
| 23 期批量调度入口 | 单期接口的批量调用方式,具体编排不在本子项目 |
|
||||
|
||||
**具体怎么做,子项目内部讨论,本 Brief 不预设方案。**
|
||||
|
||||
---
|
||||
|
||||
## 三、怎么用(目标流程)
|
||||
|
||||
**单期处理**:
|
||||
1. 责编/编排层在 episodes 表落库该期,拿到 INT 主键
|
||||
2. 拼出 episode_id:`ep{NNN}_{YYYYMMDD}_{slug}`
|
||||
3. 把视频和 A 稿放进 `programs/{episode_id}/source/`
|
||||
4. 调 doco:`doco.process_episode(episode_id, video_path, a_draft_path, output_dir)`
|
||||
5. 产物落 `programs/{episode_id}/output/`:终版 docx + 差异报告 + needs_review JSON
|
||||
6. 编导/制片人审 needs_review 队列,决定接受 / 拒绝 / 编辑
|
||||
|
||||
**23 期批量**:用同一接口循环调用 23 次,失败重试 + 进度跟踪由调用方负责。
|
||||
|
||||
**降级回退**:若某期视频不符合"黑底白字+干净人声"条件,通过 feature flag 切换到旧流程(独立 mp3 + 上游 OCR)。
|
||||
|
||||
---
|
||||
|
||||
## 四、不做什么(红线)
|
||||
|
||||
子项目设计任何方案,**绝不**触碰以下:
|
||||
|
||||
- ❌ **不动 demo 已锁的算法层**(三方融合优先级 / 时间戳容忍 ±5s / 口语清理三档 / 改动确信度阈值 0.85/0.6 三段)
|
||||
- ❌ **不引入 `ffmpeg-python` 等 wrapper**(用 subprocess 调系统 ffmpeg)
|
||||
- ❌ **不替换 AI 融合层模型**(Claude Sonnet 4.6 已锁,不临时换 DeepSeek 等)
|
||||
- ❌ **不复用主 project 那把 DeepSeek key**(Cline 工具用的 key 跟生产业务分开)
|
||||
- ❌ **不把凭证写进主 project 的 `backend/.env`**(子模块自治,各自 .env)
|
||||
- ❌ **不假设 episode_id 命名规则**(由调用方给定,doco 只当字符串用)
|
||||
- ❌ **不引入主 project 没有的技术栈**(继续 Python 3.x + Claude API + 讯飞 API)
|
||||
- ❌ **不自己实现批量调度**(失败重试 / 并发控制 / 进度跟踪属于编排层职责)
|
||||
- ❌ **不在子项目里写中台密钥服务**(那不属于 doco 范围)
|
||||
|
||||
---
|
||||
|
||||
## 五、技术栈约束(继承主 project)
|
||||
|
||||
- **运行环境**:Python 3.x(版本对齐 backend),Windows / Linux 都可跑
|
||||
- **系统依赖**:ffmpeg ≥ 4.x(子模块文档明确列出)
|
||||
- **AI 融合**:Anthropic Claude API,模型 `claude-sonnet-4-6`
|
||||
- **OCR**:DeepSeek Vision API(主项目已批,Q1)
|
||||
- **ASR**:讯飞开放平台 录音文件转写标准版(**不要用大模型版**——`language` 参数被阉割)
|
||||
- **音频规格**:16kHz / 单声道 / 16bit WAV(讯飞规格)
|
||||
- **数据格式**:句子级时间戳统一 `[Nm Ns] 句子` 格式
|
||||
|
||||
---
|
||||
|
||||
## 六、交付什么(出口接口)
|
||||
|
||||
子项目最终交付物以下面形式回到主 project,**必须满足以下接口**:
|
||||
|
||||
### 6.1 模块交付物
|
||||
- `doco/` Python 包,可被主 project 编排层 import
|
||||
- CLI 入口:`doco process ...`
|
||||
- 单元测试 + golden test(demo 那期零回归)
|
||||
- `README.md`:系统依赖、安装、使用、配置
|
||||
|
||||
### 6.2 配置与凭证
|
||||
- doco 自己的 `.env`(讯飞 + DeepSeek + Claude 三套 key 各自管)
|
||||
- 主项目仓库根目录的 `docs/api_credentials_inventory.md` 登记一行 doco 的凭证元信息(不存真实 key)
|
||||
|
||||
### 6.3 数据资产沉淀(可入 TPS 中台共享)
|
||||
- A 稿术语表(每期处理时从 A 稿提取,可入主 project 数据库的 terms 维度)
|
||||
- OCR 错字映射表(demo 已整理,固定字形混淆对)
|
||||
|
||||
### 6.4 不接受
|
||||
- 子项目自己改主 project 的 backend 代码、schema、迁移
|
||||
- 子项目自己写主 project 的前端 UI(编导确认 UI 属于上层,不在 doco 范围)
|
||||
- 子项目自己定 episode_id 命名规则(由主项目给定)
|
||||
|
||||
---
|
||||
|
||||
## 七、入口接口(子项目要知道的主 project 现状)
|
||||
|
||||
- **主项目没有 OCR 子模块**(已确认,Q1)
|
||||
- **主项目没有视频处理服务**(已确认,Q2)
|
||||
- **主项目没有节目素材批量编排层**(已确认,Q3)
|
||||
- **主项目没有中台密钥服务**(已确认,Q4)
|
||||
- **主项目没有节目素材目录规约**(已确认,Q5,本子项目带头定 `programs/{episode_id}/...`)
|
||||
- **主项目有 episodes 表**(INT 自增主键),doco 用 episode_id 时假定调用方已落库
|
||||
|
||||
---
|
||||
|
||||
## 八、未决项(归子项目自己拍板)
|
||||
|
||||
以下不在本 Brief 内预设,**由子项目和制片人讨论决定**:
|
||||
|
||||
- 视频抽帧的具体策略(关键帧检测算法 / 抽帧密度 / 字幕变化阈值)
|
||||
- DeepSeek Vision 调用的具体 prompt 设计
|
||||
- 规则层 OCR 错字映射表的具体扩展机制
|
||||
- AI 融合层的具体提示词迭代
|
||||
- needs_review JSON 的具体字段(在 demo 基础上演化)
|
||||
- 单元测试覆盖率目标
|
||||
- 子项目内部的 git 分支策略 / 版本号约定
|
||||
|
||||
---
|
||||
|
||||
## 九、决策史指向(不重复内容)
|
||||
|
||||
子项目讨论前必读:
|
||||
- `doco_project_design.md / .docx`(三方融合算法层,demo 沉淀,已锁)
|
||||
- `doco_xfyun_integration_notes.md / .docx`(讯飞 ASR 接入 15 个坑)
|
||||
- `doco_handoff_to_opus_chat.md / .docx`(子项目立项前的 opus 接手提示)
|
||||
- `PRD_doco_文稿整理子项目_v2.md`(当前子项目的请求稿)
|
||||
- `主project对Doco_PRDv2的回复.md`(主项目的批复)
|
||||
|
||||
---
|
||||
|
||||
## 十、本 Brief 自身的修订规则
|
||||
|
||||
- 本 Brief 不在子项目内修改
|
||||
- 红线 / 技术栈如有变更,**主 project 这边发新版**,通过寄存条同步
|
||||
- 子项目可在自己的 chat 里讨论"建议主 project 调整某条红线",调整动作只能在主 project 发生
|
||||
- 子项目每次出新版 PRD,主项目顾问发新版回复;两者形成 PRD vN ↔ 回复 vN 的对照关系
|
||||
@@ -0,0 +1,295 @@
|
||||
# PRD · Doco 文稿整理模块 · 视频源改造方案
|
||||
|
||||
> 版本:v2
|
||||
> 日期:2026-06-12
|
||||
> 作者:子 project Claude(Opus 4.7)
|
||||
> 状态:草稿 / 待主项目审
|
||||
> 上一版:v1(2026-06-12 同日,更早一稿)
|
||||
> 主要变化:
|
||||
> 1. **子模块正式定名为「Doco 文稿整理模块」**(v1 沿用旧称"TPS 三方融合子模块",本版起全部改用 Doco;Q6 已由制片人拍板,不再列为待决问题)
|
||||
> 2. **Q1(OCR 选型)方向更新**:制片人确认"软件离线刚需"已不成立,云端方案可接受。补充查清的关键事实:**讯飞 OCR 与 STT 共享鉴权凭证但套餐/余额各自独立**——按制片人原判据"如果能通用 token 订阅和余额就选讯飞"不成立。**新推荐方案:DeepSeek API**(制片人手上已有),并向主项目确认是否已有 OCR 能力或 LLM 视觉调用基础设施
|
||||
> 3. 其他 Q(Q2/Q3/Q4/Q5)保持不变
|
||||
|
||||
---
|
||||
|
||||
## 〇、一段话摘要(给主 project 顾问的最短摘要)
|
||||
|
||||
Doco 文稿整理模块(前称"TPS 三方融合子模块")此前的物理输入是 A 稿 docx + B 稿 OCR txt + ASR txt(mp3→讯飞)。栏目组现在改了素材输出方式:责编一次性输出 23 期"特殊视频"——画面是黑底白字唱词字幕、声音是无 BGM/音效的干净人声(极少同期声杂音)。**B 稿与 ASR 现在同源派生于一个视频文件**,物理输入从 3 个收敛到 2 个(A 稿 + 视频)。
|
||||
|
||||
**核心判断**:下游"三方融合引擎"(规则层 + AI 层)算法**零侵入**——只要新增的"视频双路拆分"模块输出与旧版 B 稿/ASR txt 格式完全一致(`[Nm Ns] 句子`),下游所有逻辑不动。**逻辑上仍然是三方融合**——A 稿术语权威 + B 路视频字幕 OCR + 音轨 ASR,三路独立验证的关系没破。
|
||||
|
||||
子项目这边需要主 project 在开工前对 5 件影响接口/数据形态的事拍板。详见 §4。
|
||||
|
||||
---
|
||||
|
||||
## 一、变更前后对照
|
||||
|
||||
### 旧设计(demo 已验证)
|
||||
|
||||
```
|
||||
[A 稿 docx] ──────────────────────┐
|
||||
│
|
||||
[上游既有 OCR 流程] ── B 稿 txt ──┤
|
||||
├─► [三方融合引擎] ─► 终版.docx + 差异报告.docx
|
||||
[节目纯净人声 mp3] │
|
||||
│ │
|
||||
└─► [讯飞 ASR] ── ASR txt ─┘
|
||||
```
|
||||
|
||||
### 新设计(本 PRD 提出)
|
||||
|
||||
```
|
||||
[A 稿 docx] ─────────────────────────────────────┐
|
||||
│
|
||||
[节目特殊视频文件] │
|
||||
画面:黑底白字字幕 │
|
||||
声音:干净人声 │
|
||||
│ │
|
||||
├─► [视频双路拆分子模块 · NEW] │
|
||||
│ │ │
|
||||
│ ├─► ffmpeg 抽帧+字幕变化检测 │
|
||||
│ │ │ │
|
||||
│ │ └─► [OCR] ── B 稿 txt ──────┤
|
||||
│ │ (待 Q1 拍板用 DeepSeek) ├─► [三方融合引擎] ─► 终版 + 差异
|
||||
│ └─► ffmpeg 抽音轨 → 16k/mono WAV │ (复用)
|
||||
│ │ │
|
||||
│ └─► [讯飞 ASR · 复用] │
|
||||
│ └────── ASR txt ──────┘
|
||||
│
|
||||
└─ (后续接 23 期批量调度)
|
||||
```
|
||||
|
||||
### 关键变化清单
|
||||
|
||||
| 项 | 旧 | 新 |
|
||||
|---|---|---|
|
||||
| 子模块名称 | TPS 三方融合子模块 | **Doco 文稿整理模块** |
|
||||
| 物理输入 | A 稿 + B 稿 + ASR | A 稿 + 视频 |
|
||||
| 逻辑融合方 | 三方 | **仍然三方**(A / B 路 / ASR 路) |
|
||||
| B 稿来源 | 上游既有 OCR 流程 | 视频抽帧 → OCR |
|
||||
| ASR 来源 | 节目 mp3 | 视频音轨 |
|
||||
| 算法层 | 规则层 + AI 层 | **不变** |
|
||||
| 新增模块 | — | 视频双路拆分(ffmpeg + OCR) |
|
||||
| 处理粒度 | 单期 | 单期接口 + 23 期批量调度 |
|
||||
|
||||
---
|
||||
|
||||
## 二、子项目这边已经拍板的事(不需要主 project 决策,仅告知)
|
||||
|
||||
### 2.1 制片人已明确的决策
|
||||
|
||||
1. **子模块定名**:**Doco 文稿整理模块**("Doco" 取意"Documentation Consolidation"——文稿整合)。本 PRD 起所有引用旧称("TPS 三方融合子模块"、"三方融合")的地方都用新名替换。
|
||||
2. **离线不再是刚需**:原 v1 推荐 PaddleOCR 本地的核心理由(离线刚需)失效;OCR 选型转向云端。
|
||||
|
||||
### 2.2 demo 已验证或设计文档已定义的事
|
||||
|
||||
不变:
|
||||
|
||||
1. **三方融合优先级**(A 稿术语权威 / ASR 口吻 / B 稿验证)—— 见 project_design.md §5.1
|
||||
2. **时间戳容忍度 ±5 秒** —— project_design.md §5.2
|
||||
3. **口语清理三档**(keep_all / medium / clean)—— project_design.md §5.3
|
||||
4. **改动确信度阈值**(0.85 自动 / 0.6–0.85 黄标 / <0.6 待确认)—— project_design.md §5.4
|
||||
5. **AI 融合层模型**:Claude Sonnet 4.6 起步
|
||||
6. **差异报告格式**:demo 黄金对照已落定
|
||||
7. **A 稿术语表数据结构**:project_design.md §9.1
|
||||
8. **OCR 错字映射表**:复用 demo 整理的字形混淆对
|
||||
|
||||
---
|
||||
|
||||
## 三、子项目这边的开发计划草案(待主 project 审)
|
||||
|
||||
| Phase | 内容 | 产物 | 主依赖 |
|
||||
|---|---|---|---|
|
||||
| P1 | 视频双路拆分预处理(单期)| `video_split.py`:视频 → 抽帧+字幕变化检测+OCR → B稿.txt;视频 → 音轨 → 16k WAV | ffmpeg、OCR(待 Q1 拍板,倾向 DeepSeek API) |
|
||||
| P2 | 讯飞 ASR 适配层 | 复用 `xfyun_asr_standard.py`,替换新凭证、做成可被 P1 流水线调用的模块 | 讯飞开发端密钥(待 Q4 拍板) |
|
||||
| P3 | Doco 融合引擎 | 规则层 Python + AI 层 Claude API;输入 A/B/ASR 三方 txt,输出终版 docx + 差异报告 + needs_review JSON | Claude API |
|
||||
| P4 | 单期端到端 golden test | 用 demo 那期《现代防空反导大对决》的视频跑一遍,与 demo 产物比对,确认零回归 | 上面三 phase |
|
||||
| P5 | 23 期批量 | 单期模块包成可被批量调度调用的接口(CLI / Python API 二选一,待 Q3 拍板) | — |
|
||||
|
||||
---
|
||||
|
||||
## 四、需要主项目决策的 5 个问题
|
||||
|
||||
> 每条都给了子项目这边的推荐答案 + 理由。主项目顾问可以照单批,也可以推翻。
|
||||
|
||||
### Q1. OCR 方案选型?
|
||||
|
||||
**问题**:B 路 OCR 用什么?这是新引入的依赖,绕不开。
|
||||
|
||||
#### Q1.A 关键事实(更新自 v1)
|
||||
|
||||
子项目这边查清了几件事,供主项目决策参考:
|
||||
|
||||
1. **讯飞 OCR 与 STT 的共享情况**:
|
||||
- 鉴权凭证(开发者账号、APPID、APIKey、APISecret)**可共享**,一组凭证可同时调用多个服务
|
||||
- 但**套餐/计费/余额各自独立**:STT 是"5 小时免费试用包"独立购买;OCR 是另一套计费体系(参考 SaaS 零售价 4.9 元/10 张、9.9 元/50 张)。**不存在用 STT 的余额跑 OCR 这种事**
|
||||
- **按制片人原判据("如果能通用 token 订阅和余额就选讯飞")此条件不成立**
|
||||
|
||||
2. **制片人手上已有的 API**:
|
||||
- **DeepSeek**:有专门的 `DeepSeek-OCR` 模型(OCR 2.0 方案,对中文文档识别准确率行业领先)+ DeepSeek Vision 识图模式(官方 API endpoint)。**推荐**
|
||||
- **MiniMax**:有 MiniMax-VL 视觉模型,理论上可 OCR,但 OCR 专项优化不如 DeepSeek-OCR
|
||||
- **小米 MiMo**:主推推理模型,未见明确的视觉/OCR 能力
|
||||
|
||||
3. **本场景的 OCR 难度评估**:
|
||||
- 输入图像是**黑底白字纯字幕画面**——OCR 最简单的子集(高对比度、印刷体、无图案干扰、无版面分析)
|
||||
- LLM 视觉模型在此场景几乎不会出错;用 LLM 做 OCR 在这里不是杀鸡用牛刀,因为成本极低且无新增依赖
|
||||
|
||||
4. **成本估算(23 期总量)**:
|
||||
- 单期约 300 条字幕(demo 数据),23 期 ≈ 6900 张图
|
||||
- DeepSeek Vision API 单张成本约几厘到一分,**总成本预估 50–150 元**
|
||||
- 对比讯飞 OCR SaaS 零售价:6900 张 × 0.198 元/张 ≈ 1370 元
|
||||
- 对比 PaddleOCR 本地:0 元但需要维护本地环境(已不是刚需)
|
||||
|
||||
#### Q1.B 选项与推荐
|
||||
|
||||
**选项**:
|
||||
- A. **DeepSeek API(视觉模型)**——制片人已有 API,无新增订阅
|
||||
- B. **讯飞 OCR**——与 STT 同账号但余额独立
|
||||
- C. **PaddleOCR 本地**——零成本但需本地环境
|
||||
- D. **主 project 已有的 OCR 服务**——如果存在
|
||||
|
||||
**子项目推荐**:A(DeepSeek API)
|
||||
|
||||
**理由**:
|
||||
- 满足制片人"用现有 API 资源"的方向
|
||||
- 黑底白字字幕是 LLM 视觉模型的舒适区,识别质量有保障
|
||||
- 与讯飞 STT 解耦,单点故障范围小
|
||||
- 成本最低(除 PaddleOCR)且无本地环境维护成本
|
||||
|
||||
**请主项目确认**:
|
||||
- 主 project 是否已有 OCR 子模块/能力?(制片人初步判断"主 project 应该没有 OCR 配置",请顾问最终确认)
|
||||
- 主 project 的密钥管理体系是否已经接入 DeepSeek?还是需要 Doco 子模块自行管理 DeepSeek 凭证?
|
||||
- 6900 张图调用 DeepSeek 是否会影响主 project 其他模块的 DeepSeek 配额/限流?
|
||||
|
||||
---
|
||||
|
||||
### Q2. 视频处理依赖(ffmpeg)的入口?
|
||||
|
||||
**问题**:抽帧与抽音轨都依赖 ffmpeg。
|
||||
|
||||
**选项**:
|
||||
- A. Doco 子模块自带 ffmpeg 系统依赖 + Python 封装(`ffmpeg-python` 或 subprocess)
|
||||
- B. 主 project 有统一的视频处理服务/适配层
|
||||
|
||||
**子项目推荐**:A,使用 subprocess 调用系统 ffmpeg
|
||||
|
||||
**理由**:ffmpeg 是标准工具,独立部署足够简单。
|
||||
|
||||
**请主项目确认**:是否已经有视频处理子模块/服务在规划中?如有,应改走中台接口。
|
||||
|
||||
---
|
||||
|
||||
### Q3. 23 期批量 vs 单期接口?
|
||||
|
||||
**问题**:本次实际场景是 23 期,Doco 子模块的接口应该长什么样?
|
||||
|
||||
**选项**:
|
||||
- A. 子模块只负责单期,输入 = 1 视频 + 1 A 稿;批量调度交给主 project 编排层
|
||||
- B. 子模块内部支持批量,输入 = 视频目录 + A 稿目录
|
||||
- C. 两者都提供
|
||||
|
||||
**子项目推荐**:A(子模块只对单期负责)
|
||||
|
||||
**理由**:
|
||||
- 单一职责原则。批量调度涉及失败重试、进度跟踪、并发控制——属于编排层职责
|
||||
- 单期接口在出错时定位简单(哪期失败、失败在哪个 phase 一目了然)
|
||||
- 主 project 未来可能有节目档案库流水线、报题单流水线等,统一在编排层做并发更合理
|
||||
|
||||
**请主项目确认**:
|
||||
- 主 project 是否计划做"节目素材批量处理编排层"?如有,Doco 的单期接口应满足什么样的契约(CLI?Python API?返回值结构?)
|
||||
- 23 期这次是先单期跑通再批量,还是要求一次性 batch?
|
||||
|
||||
---
|
||||
|
||||
### Q4. 讯飞密钥更新/续费策略?
|
||||
|
||||
**问题**:demo 凭证可能已过期(制片人提示"讯飞试用可能也过了"),且需要区分**消费端**(讯飞听见 App)与**开发端**(开放平台)。
|
||||
|
||||
**选项**:
|
||||
- A. Doco 子模块各自申请新凭证、各自管理
|
||||
- B. 接入中台统一密钥服务(如果有)
|
||||
- C. 申请新凭证 + 写入"中台密钥资产文档"作为登记
|
||||
|
||||
**子项目推荐**:C
|
||||
|
||||
**理由**:现阶段中台密钥服务可能未完成,但密钥归属、过期日期、所属子模块应该有统一登记入口。
|
||||
|
||||
**请主项目确认**:
|
||||
- 中台是否已有密钥管理子模块?
|
||||
- 新申请的讯飞开发端密钥(包括 APP_ID、SECRET_KEY、5 小时免费试用包激活状态)应该登记在哪里?
|
||||
- 如果 Q1 拍板用 DeepSeek,DeepSeek API key 是否也按同样规则登记?
|
||||
|
||||
---
|
||||
|
||||
### Q5. 目录结构与命名规约?
|
||||
|
||||
**问题**:23 期素材、抽帧产物、wav、终版 docx 的存储路径需要规约,否则后期归档混乱。
|
||||
|
||||
**子项目草案**:
|
||||
|
||||
```
|
||||
programs/
|
||||
└── {episode_id}/ # 例:20260612_防空反导
|
||||
├── source/
|
||||
│ ├── video.mp4 # 原始特殊视频(黑底白字+干净人声)
|
||||
│ └── a_draft.docx # A 稿(编导定稿)
|
||||
├── work/ # 中间产物(可清理)
|
||||
│ ├── frames/ # 抽出的字幕关键帧
|
||||
│ ├── audio_16k.wav # 抽音轨标准化产物
|
||||
│ ├── b_draft.txt # OCR 产物(带 [Nm Ns] 时间戳)
|
||||
│ ├── asr_result.txt # 讯飞 ASR 产物
|
||||
│ └── asr_result_raw.json # 讯飞原始返回
|
||||
└── output/ # 主产物(入档案库)
|
||||
├── final.docx # 终版文稿
|
||||
├── diff_report.docx # 差异报告
|
||||
└── needs_review.json # 待编导确认队列
|
||||
```
|
||||
|
||||
**请主项目确认**:
|
||||
- TPS 中台是否对节目素材的目录有统一规约?
|
||||
- `episode_id` 命名规则是什么?(日期+栏目+期号?UUID?编导给定?)
|
||||
- `work/` 中间产物保留多久?是否要自动清理?
|
||||
|
||||
---
|
||||
|
||||
## 五、不向主项目提问、但需要告知的事
|
||||
|
||||
1. **demo 的 5 份 fixture 仍然有效**:A 稿 / B 稿 / ASR / 终版 / 差异报告 在新流程下都可作为单元测试与集成测试的黄金对照
|
||||
2. **讯飞 ASR 脚本仍然有效**:`xfyun_asr_standard.py` 只需替换凭证、把"音频文件路径"参数从"独立 mp3"改为"视频抽音轨产物 wav"
|
||||
3. **23 期之后的常态化问题**:栏目组以后是否每期都按"黑底白字+干净人声"特殊视频出素材?还是只这一批 23 期是特例?这影响 Doco 子模块要不要保留旧的 mp3+独立 OCR 流程作为回退路径。**建议主项目顾问直接跟栏目组确认**
|
||||
|
||||
---
|
||||
|
||||
## 六、给主项目顾问的"接手提示词"
|
||||
|
||||
如果主 project 顾问要审这份 PRD,最重要的三件事:
|
||||
|
||||
1. **算法层零侵入是 demo 验证过的硬保证**。任何改动如果会让 P3(Doco 融合引擎)必须改算法,需要警惕——大概率是输入数据形态没对齐。
|
||||
2. **OCR 选型(Q1)和目录规约(Q5)是会广泛影响其他子模块的事**,主项目顾问视野更大,应该是这两条的最终拍板人。如果主 project 有 OCR 能力或 DeepSeek 集成基础设施,Q1 的子项目推荐随时可被覆盖。
|
||||
3. **23 期是一次性还是常态化(§5 第 3 条)是个产品问题,不是技术问题**,建议主项目顾问直接跟栏目组确认后再批 PRD。
|
||||
|
||||
---
|
||||
|
||||
## 七、回复期望
|
||||
|
||||
子项目这边等以下确认才正式开干:
|
||||
- Q1(OCR 选型)—— 必须先答,否则 P1 不能开始
|
||||
- Q4(讯飞密钥)—— 必须先答,否则 P2 不能开始
|
||||
- Q5(目录规约)—— 必须先答,否则 P1 输出无处可放
|
||||
- Q3(接口契约)—— P5 之前必须答
|
||||
- Q2 —— Doco 子模块可以先按推荐方案开干,主项目随后批
|
||||
|
||||
收到主项目 Claude 顾问的回复 md 后,Doco 子项目这边出 PRD v3(含元信息块标注"上一版:v2"),再启动 P1。
|
||||
|
||||
---
|
||||
|
||||
## 八、版本变更摘要
|
||||
|
||||
| 版本 | 日期 | 主要变化 |
|
||||
|---|---|---|
|
||||
| v1 | 2026-06-12 | 首版。6 个待决问题(含子模块命名 Q6)。OCR 选型推荐 PaddleOCR 本地 |
|
||||
| **v2** | **2026-06-12** | 子模块定名"Doco 文稿整理模块";Q1 改推荐 DeepSeek API(基于讯飞 OCR/STT 余额独立的事实查清 + 制片人手上已有 API 资源);Q6 已拍板移出待决列表;剩余 5 个待决问题 |
|
||||
|
||||
---
|
||||
|
||||
*v2 由 Doco 子 project Claude(Opus 4.7)于 2026-06-12 拟稿,遵照《跨 project 协作提示单 v1》§三的规范。*
|
||||
@@ -0,0 +1,103 @@
|
||||
# Handoff to opus_chat · TPS三方融合子模块
|
||||
|
||||
> 这份是发给中台 opus_chat 的最短交接说明。完整设计见 `project_design.md`。
|
||||
|
||||
---
|
||||
|
||||
## 这是什么
|
||||
|
||||
《军事科技》TPS(终版文稿生成中台)下的一个子模块,叫"三方融合"。
|
||||
|
||||
**它做的事**:把三份资料合并成一份贴近实际播出的终版 docx 文稿,并生成一份差异报告。
|
||||
|
||||
三份资料是:
|
||||
1. **A 稿** — 编导手写定稿(docx,有【主持人N】【解说N】【专家N】【动画N】结构标签)
|
||||
2. **B 稿** — OCR 扒词(txt,带 `[Nm Ns]` 时间戳,有 OCR 错字)
|
||||
3. **ASR** — 讯飞录音文件转写结果(txt,带 `[Nm Ns]` 时间戳,有同音误识)
|
||||
|
||||
---
|
||||
|
||||
## 你需要记住的 5 件事
|
||||
|
||||
### 1. 三方权威优先级(**最重要,不要搞反**)
|
||||
|
||||
| 看什么 | 信谁 |
|
||||
|---|---|
|
||||
| 术语 / 人名 / 装备型号 / 事实 | **A 稿** |
|
||||
| 标点 / 语序 / 口吻 | **ASR** |
|
||||
| OCR 字形错(肘/时、娈/变) | 用 **ASR 或 A 稿** 覆盖 B 稿 |
|
||||
| 整段是否存在 | **B 稿 + ASR 联合**判定 |
|
||||
|
||||
为什么 A 稿术语优先?demo 中 ASR 把"美以两国"识别成了"美伊两国"——句子通顺、语义荒谬。无 A 稿无法发现。
|
||||
|
||||
### 2. 架构是双层(不是单层 AI)
|
||||
|
||||
```
|
||||
规则层(Python) AI 层(Claude API)
|
||||
├ OCR 错字映射 ├ 段落对齐
|
||||
├ A 稿术语字典覆盖 ├ 改动类型判定
|
||||
├ 时间戳对齐 ├ 整段改写边界
|
||||
├ 数字 / 标点规范化 └ 编导笔误识别
|
||||
└ 口语词清理(可配置)
|
||||
```
|
||||
|
||||
确定性的事走规则层,需要语义判断的事走 AI 层。**不要全塞给 AI**。
|
||||
|
||||
### 3. 输入有严格规范
|
||||
|
||||
- **A 稿**:自然段开头 `【…】` 是结构标签;`隔断:【…】` 是章节分隔
|
||||
- **B 稿 / ASR**:每行格式 `[Nm Ns] 句子内容`
|
||||
- **音频**(如果走完整流程从音频开始):必须 **16kHz / 单声道 / 16bit WAV**,否则讯飞拒收
|
||||
|
||||
### 4. 讯飞 ASR 有坑(开发前必读)
|
||||
|
||||
- 用"**录音文件转写(标准版)**",**不要**用"大模型版"(免费包阉割了 `language` 参数)
|
||||
- 领域参数 `pd=mil`(军事节目专用)
|
||||
- 顺滑参数 `eng_smoothproc=true` + `eng_colloqproc=true`
|
||||
- **热词机制**是关键:每期从 A 稿提 200 字以内的专业术语作为 `hotWord`
|
||||
- 讯飞**消费端**(讯飞听见 App)和**开发端**(开放平台)密钥**不通用**
|
||||
- "5 小时免费试用"必须走 0 元购买流程激活
|
||||
- 签名:`signa = base64(HmacSHA1(MD5(appid + ts), secretKey))`
|
||||
|
||||
详见 demo 中的 `xfyun_asr_standard.py`。
|
||||
|
||||
### 5. 输出有两份,不只一份
|
||||
|
||||
- **终版.docx** — 主产物,给档案库
|
||||
- **差异报告.docx** — 副产物,给编导回顾。**差异报告的长期价值可能比终版更高**(能反向分析编导删改偏好)
|
||||
|
||||
---
|
||||
|
||||
## 我和这个模块的关系
|
||||
|
||||
- **你(opus_chat)**:中台对话入口,可能会被用户问到本模块的边界 / 接口 / 状态
|
||||
- **本模块**:被 TPS 主流程调用,输入三份文本,输出两份 docx + 一份 JSON
|
||||
- **不归我管**:音频转码、OCR 识别本身、档案入库、编导确认 UI
|
||||
|
||||
如果用户问以下内容,**统一口径**:
|
||||
|
||||
| 问题 | 答 |
|
||||
|---|---|
|
||||
| "能不能不用 A 稿?" | 不能。会引入 ASR 的同音事实错误,且术语全乱。 |
|
||||
| "能不能不用 B 稿?" | 可以,但失去独立交叉验证;不推荐。 |
|
||||
| "能不能不用 ASR?" | demo 证明 A+B 也能跑,但效果显著下降;不推荐。 |
|
||||
| "ASR 选讯飞还是别家?" | 当前讯飞。已踩坑、已跑通;阿里云是规划中的备份。 |
|
||||
| "AI 用哪个模型?" | Claude Sonnet 4.6 起步,单期 26 分钟节目约 16k 字上下文。 |
|
||||
| "处理一期要多久?" | 正式版预估 15–30 分钟(含人工确认);demo 实测 2 小时但近一半花在踩讯飞坑上。 |
|
||||
|
||||
---
|
||||
|
||||
## demo 状态
|
||||
|
||||
- 已完整跑通一期《现代防空反导大对决》(26 分钟)
|
||||
- 关键术语识别率接近 100%(开启 35 条热词后)
|
||||
- 主要修复:OCR 错字 10+ 类、ASR 同音误识 4 类、编导笔误 1 段、整段删除 2 段
|
||||
- 复盘文档:`实测复盘.md`
|
||||
- 设计文档:`project_design.md`
|
||||
|
||||
---
|
||||
|
||||
## 不一致时怎么办
|
||||
|
||||
如果你(opus_chat)发现本备忘与 `project_design.md` 不一致,**以 project_design.md 为准**。
|
||||
如果你发现 project_design.md 本身需要更新,请同步刘统制片组。
|
||||
@@ -0,0 +1,451 @@
|
||||
# TPS · 三方融合子模块 设计文档(project_design.md)
|
||||
|
||||
> 归属:《军事科技》中台 TPS · 终版文稿生成(Three-source Pipeline Synthesis)
|
||||
> 文档目的:把 demo 阶段的实测结论沉淀为可被中台其他子模块(含 opus_chat)调用的明确接口与约束
|
||||
> 当前状态:demo 已跑通一期完整节目(《现代防空反导大对决》),进入正式开发阶段
|
||||
> 文档版本:v0.1(2026-05-15)
|
||||
> 维护:刘统制片组 / Claude
|
||||
|
||||
---
|
||||
|
||||
## 0. TL;DR(给 opus_chat 的最短摘要)
|
||||
|
||||
- 本子模块的输入是三份资料,输出是一份贴近实际播出的 docx 文稿 + 一份逐段差异报告。
|
||||
- **三份输入:**
|
||||
1. **A 稿**:编导手写定稿(docx,带【主持人N】【解说N】【专家N】【动画N】【隔断:…】结构标签)
|
||||
2. **B 稿**:OCR 扒词文档(txt,带 `[Nm Ns]` 句级时间戳,存在 OCR 错字)
|
||||
3. **ASR 结果**:讯飞录音文件转写(txt,带 `[Nm Ns]` 句级时间戳,存在同音误识)
|
||||
- **核心信念**:三方各有短板,单源不可信;融合后才是"播出真相"。
|
||||
- A 稿 → 术语 / 人名 / 装备型号 / 事实 的**权威**
|
||||
- ASR → 播出语序与口吻的**骨架**
|
||||
- B 稿 → 独立**交叉验证**(同时验证 A 与 ASR)
|
||||
- **架构两层:**
|
||||
- **规则层**(Python):术语字典纠正、OCR 错字映射、时间戳对齐、口语词清理、数字规范化
|
||||
- **AI 层**(Claude API):段落对齐、改动类型判定、整段改写边界、编导笔误识别
|
||||
- **请 opus_chat 注意:** 本模块依赖讯飞 ASR 与 Claude API,**两套密钥**互不复用;调用本模块前应确认 A 稿术语表已入库。
|
||||
|
||||
---
|
||||
|
||||
## 1. 业务背景与定位
|
||||
|
||||
### 1.1 上游 / 下游
|
||||
|
||||
```
|
||||
[节目编导] ──A稿(docx)──┐
|
||||
│
|
||||
[OCR 服务(旧流程)]──B稿(txt+时间戳)──┐
|
||||
├──► [本模块 三方融合] ──► [终版docx] ──► [节目档案库]
|
||||
│ └► [差异报告docx] ──► [编导回顾 / 训练素材]
|
||||
[节目纯净声音文件]──[讯飞 ASR]──ASR(txt+时间戳)─┘
|
||||
```
|
||||
|
||||
### 1.2 在 TPS 中台的位置
|
||||
|
||||
本子模块是 TPS(终版文稿生成中台)的**核心算法子模块**,位于上游"素材接收"和下游"档案入库 / 编导确认 UI"之间。
|
||||
|
||||
- 它**不**负责音频转码(由 ASR 适配层处理)
|
||||
- 它**不**负责 OCR 识别(B 稿来自既有 OCR 流程)
|
||||
- 它**不**负责最终入库(由档案子模块负责)
|
||||
- 它**只**负责"三方文本 → 一份贴近播出的终版文稿 + 差异报告"
|
||||
|
||||
### 1.3 为什么不能省略某一方?
|
||||
|
||||
| 缺哪一方 | 后果 | 实例 |
|
||||
|---|---|---|
|
||||
| 缺 A 稿 | 术语 / 人名 / 装备型号易错;ASR 同音错难发现 | "美以两国"被 ASR 识别成"美伊两国"——句子通顺、语义荒谬,无 A 稿无法纠正 |
|
||||
| 缺 ASR | 无法知道实际播出语序与口吻;专家口语词全丢 | A 稿用"观众朋友们,你们好",播出实际是"各位观众你们好" |
|
||||
| 缺 B 稿 | 失去独立交叉验证;ASR/A 稿冲突时无第三方仲裁 | A 稿与 ASR 在某段不一致时,B 稿与谁一致就以谁为准 |
|
||||
|
||||
**结论**:B 稿可以视为弱必需(demo 证明 A+ASR 二方融合也能跑),但建议保留 B 稿做 sanity check。
|
||||
|
||||
---
|
||||
|
||||
## 2. 输入规范
|
||||
|
||||
### 2.1 A 稿(编导定稿)
|
||||
|
||||
- **格式**:`.docx`(实际为 markdown 风格的纯文本,也兼容真 docx)
|
||||
- **结构标签**(出现在自然段开头,方括号包裹):
|
||||
- `【导视】` `【主持人N】` `【解说N】` `【动画N】` `【专家N】`
|
||||
- `隔断:【…】` 用于章节分隔
|
||||
- **特点**:
|
||||
- 内容偏书面化;可能存在编导笔误(如本期【专家7】【专家8】内容字符完全相同 = 复制粘贴失误)
|
||||
- 术语严格、人名 / 装备型号完整("爱国者-3"、"霍拉姆沙赫尔-4"、"见证者-136")
|
||||
- 标点 / 顿号 / 引号规范
|
||||
|
||||
### 2.2 B 稿(OCR 扒词)
|
||||
|
||||
- **格式**:`.txt`
|
||||
- **行格式**:`[Nm Ns] 句子内容`,每行一句
|
||||
- **特点**:
|
||||
- 句子时间戳精度到秒
|
||||
- **存在 OCR 错字**:字形相近字混淆(肘↔时、娈↔变、夭↔天、井↔并、末↔未、纺↔防、观↔现、忽↔您、岳↔越、改命↔致命)
|
||||
- 标点弱(多为空白分隔,缺逗号 / 句号)
|
||||
- 句子切分较碎(一句话常被切成 2–3 行)
|
||||
|
||||
### 2.3 ASR(讯飞录音文件转写)
|
||||
|
||||
- **格式**:`.txt`
|
||||
- **行格式**:`[Nm Ns] 句子内容`,每行一句(与 B 稿对齐)
|
||||
- **特点**:
|
||||
- 标点合理(讯飞已自动加句号、逗号、问号)
|
||||
- 经 `eng_smoothproc + eng_colloqproc` 处理,已**部分**清理"嗯/呃"
|
||||
- **存在同音误识**:"中端↔中段"、"末端↔末段"、"中诚↔中程"、"美伊↔美以"、"高超声速↔高超音速"
|
||||
- 数字 / 百分比识别准确
|
||||
- 句子级时间戳精确,可与 B 稿做交叉对齐
|
||||
|
||||
### 2.4 三方时间戳的对齐口径
|
||||
|
||||
A 稿无时间戳;B 稿与 ASR 都有 `[Nm Ns]` 但**不严格一致**(B 稿来自 OCR 字幕显示时点,ASR 来自语音波形起点;常相差 1–3 秒)。融合时**容忍 ±5 秒**的时间漂移作为同段判定的辅助条件。
|
||||
|
||||
---
|
||||
|
||||
## 3. 输出规范
|
||||
|
||||
### 3.1 终版文稿(主产物)
|
||||
|
||||
- **格式**:`.docx`
|
||||
- **结构**:保留 A 稿的段落标签(【导视】【主持人1】…)
|
||||
- **内容来源策略**:
|
||||
- 默认以 ASR 文本为骨架
|
||||
- A 稿术语 / 人名 / 装备型号回填
|
||||
- B 稿 OCR 错字修复(用 ASR / A 稿覆盖)
|
||||
- 标点采用 ASR 的版本(最自然)
|
||||
- 口语词处理强度由"清理档位"参数控制(见 §5.3)
|
||||
|
||||
### 3.2 差异报告(副产物)
|
||||
|
||||
- **格式**:`.docx`
|
||||
- **内容**:
|
||||
1. 改动总览(段落数 / 改动类型分布)
|
||||
2. 逐段对照(A 稿原文 vs 终版,标注改动类型与依据)
|
||||
3. 关键差异专题:整段删除 / 整段改写 / 术语变化 / 编导笔误 / OCR 错字修复 / ASR 错字修复 / 口语词处理
|
||||
4. 依据图例(三方一致 / ASR 为主 / A 稿权威 / …)
|
||||
- **价值**:差异报告本身就是编导工作回顾,长期累积可分析编导删改偏好。
|
||||
|
||||
### 3.3 改动类型枚举
|
||||
|
||||
| 枚举值 | 含义 | 处置 |
|
||||
|---|---|---|
|
||||
| `unchanged` | 三方一致或语义等价 | 直接采用 A 稿 |
|
||||
| `minor_edit` | 标点 / 顿号 / 简称差异 | 自动以 ASR 版本输出 |
|
||||
| `term_normalize` | 术语 / 字形 / 同音修正 | 规则层处理 |
|
||||
| `rewrite_large` | 整段重写(A 稿与播出版差异较大) | AI 层处理 + **建议编导确认** |
|
||||
| `segment_delete` | A 稿有但播出无 | AI 层判定 + **建议编导确认** |
|
||||
| `segment_add` | A 稿无但播出有 | AI 层判定 + **建议编导确认** |
|
||||
| `editor_typo` | A 稿编导笔误(整段重复 / 明显错位) | AI 层提示 + **必须编导确认** |
|
||||
|
||||
---
|
||||
|
||||
## 4. 模块架构
|
||||
|
||||
### 4.1 顶层流程
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────────────┐
|
||||
│ 三方融合引擎(本子模块) │
|
||||
│ │
|
||||
A 稿 ──────►│ 解析层 ──► 段落对齐层 ──► 规则纠错层 ──► AI 融合 │──► 终版docx
|
||||
B 稿 ──────►│ ↓ │──► 差异报告
|
||||
ASR ──────►│ [Claude API] │
|
||||
└───────────────────────────────────────────────────┘
|
||||
▲
|
||||
│
|
||||
[A 稿术语表 / OCR 错字字典 / 口语词字典]
|
||||
```
|
||||
|
||||
### 4.2 各层职责
|
||||
|
||||
#### 解析层(parser)
|
||||
- A 稿:按 `【…】` 切段,识别隔断标签,输出 `[(label, text)]`
|
||||
- B 稿 / ASR:按 `[Nm Ns]` 行切句,输出 `[(start_ms, end_ms, text)]`
|
||||
|
||||
#### 段落对齐层(aligner)
|
||||
- 目标:把 B 稿 / ASR 的句子聚合并归到 A 稿的段落标签下
|
||||
- 算法:
|
||||
- **强信号**:通过 A 稿首末锚点句做语义匹配(demo 用了人工锚点列表,正式版应用 Claude API 做语义匹配)
|
||||
- **弱信号**:时间戳的单调推进
|
||||
- 容错:允许 ±5 秒漂移、允许中间出现专家发言被识别为"解说"
|
||||
|
||||
#### 规则纠错层(rule-based corrector)
|
||||
- **A 稿术语字典**:从 A 稿提取专有名词,扫描 ASR / B 稿,发现字形不同但拼音相同的同音字 → 用 A 稿覆盖
|
||||
- **OCR 错字字典**:维护固定映射表(肘→时、娈→变、夭→天…),扫描 B 稿
|
||||
- **数字规范化**:`百%` → `100%`、`数10` → `数十`、`马赫数10` → `10马赫`
|
||||
- **标点规范化**:去重逗号、补齐句号
|
||||
|
||||
#### AI 融合层(Claude API)
|
||||
- 输入:经规则层处理后的三方对齐结果
|
||||
- 任务:
|
||||
1. 段落对齐的最终校验(解决规则层算不准的边界)
|
||||
2. 改动类型标记(`unchanged` / `minor_edit` / `rewrite_large` / `segment_delete` / `segment_add` / `editor_typo`)
|
||||
3. 高把握改动直接产出终版
|
||||
4. 低把握改动标记原因 + 给编导待确认列表
|
||||
- 提示词要点:见 §6
|
||||
|
||||
### 4.3 双层划分的依据
|
||||
|
||||
| 任务 | 走哪层 | 理由 |
|
||||
|---|---|---|
|
||||
| OCR 错字纠正 | 规则层 | 字形错误是确定性映射 |
|
||||
| 术语统一 | 规则层 | A 稿术语表是封闭集 |
|
||||
| 时间戳对齐 | 规则层 | 数值比较 |
|
||||
| 段落归属 | AI 层 | 需要语义判断("这句专家发言到底归【专家3】还是【解说5】") |
|
||||
| 整段删除判定 | AI 层 | 需要判断"A 稿这一段,是否在 B 稿 + ASR 都找不到对应内容" |
|
||||
| 编导笔误识别 | AI 层 | 需要"【专家7】【专家8】内容相同" + "ASR 第 N 句到第 M 句讲了完全不同的内容"的联合判断 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 关键决策与参数
|
||||
|
||||
### 5.1 三方冲突时的优先级
|
||||
|
||||
| 冲突类型 | 优先采用 | 例 |
|
||||
|---|---|---|
|
||||
| 术语 / 人名 / 装备型号 | **A 稿** | "美以两国"(ASR 错识"美伊") |
|
||||
| 标点 / 句式 / 口吻 | **ASR** | "各位观众你们好"(A 稿"观众朋友们,你们好") |
|
||||
| 字形错字 | **ASR / A 稿** 覆盖 B 稿 | "肘→时""纺空→防空" |
|
||||
| 整段存在性 | **B 稿 + ASR 联合** 优先 | A 稿"雷达被誉为眼睛..."B 稿/ASR 都没有 → 删 |
|
||||
| 数字 / 百分比 | **ASR**(已确认 ASR 100% 准确) | "5%"、"100%" |
|
||||
| 同音 / 字形错 | **A 稿术语字典** | "中端→中段""高超声速→高超音速" |
|
||||
|
||||
### 5.2 时间戳容忍度
|
||||
|
||||
- 同段判定:B 稿 / ASR 时间戳差 ≤ **5 秒** 视为同一播出位置
|
||||
- 锚点匹配:A 稿首句对应的 ASR 时间位置 ± **10 秒** 为搜索窗
|
||||
|
||||
### 5.3 口语词清理档位(可配置)
|
||||
|
||||
| 档位 | 行为 | 适用场景 |
|
||||
|---|---|---|
|
||||
| `keep_all` | 完全保留"那么""呢""啊""这个" | 反映播出真相、用于事后复盘 |
|
||||
| `medium`(**默认**) | 保留专家段口语词,清理解说段 | 平衡可读性与真实性 |
|
||||
| `clean` | 全部清理,按 A 稿书面化 | 用于外部传播 / 文稿出版 |
|
||||
|
||||
### 5.4 改动确信度阈值
|
||||
|
||||
- AI 判定结果带 `confidence ∈ [0,1]`
|
||||
- `confidence ≥ 0.85`:自动应用,记入差异报告
|
||||
- `0.6 ≤ confidence < 0.85`:自动应用 + **黄色标记**,编导可在 UI 中翻看
|
||||
- `confidence < 0.6`:**不自动应用**,进入编导待确认队列
|
||||
|
||||
---
|
||||
|
||||
## 6. AI 融合层提示词设计纲要
|
||||
|
||||
提示词应包含以下要素(正式实现时按需扩展):
|
||||
|
||||
```
|
||||
[系统角色]
|
||||
你是《军事科技》节目终版文稿生成助手。已知:
|
||||
- A 稿是编导手写定稿,术语 / 人名 / 装备型号权威
|
||||
- ASR 是讯飞录音转写,标点与口吻接近播出真相,但存在同音误识
|
||||
- B 稿是 OCR 扒词,存在字形错字,用作交叉验证
|
||||
三方冲突时按"§5.1 三方冲突优先级"裁决。
|
||||
|
||||
[输入]
|
||||
A 稿全文(含 【…】 标签):{a_doc}
|
||||
ASR 结果(带时间戳):{asr_sentences}
|
||||
B 稿 OCR(带时间戳):{b_sentences}
|
||||
A 稿术语表:{terms_dict}
|
||||
OCR 错字字典:{ocr_typos}
|
||||
口语清理档位:{level}
|
||||
|
||||
[任务]
|
||||
1. 把 ASR / B 稿的句子按时间顺序聚合并归到 A 稿的段落标签下
|
||||
2. 对每段标记改动类型(unchanged / minor_edit / rewrite_large / segment_delete / segment_add / editor_typo)
|
||||
3. 对每段给出 confidence ∈ [0,1]
|
||||
4. 对 confidence ≥ 0.85 的段落直接产出终版文本
|
||||
5. 对 confidence < 0.85 的段落产出"建议版本 + 待确认原因"
|
||||
6. 识别 A 稿与 ASR 的术语 / 同音 / 字形差异,自动应用 A 稿权威
|
||||
7. 识别 A 稿的编导笔误(如整段复制粘贴)并标记
|
||||
|
||||
[输出]
|
||||
JSON:
|
||||
{
|
||||
"segments": [
|
||||
{
|
||||
"label": "【主持人1】",
|
||||
"change_type": "rewrite_large",
|
||||
"confidence": 0.92,
|
||||
"final_text": "...",
|
||||
"evidence": {"asr_lines": [...], "b_lines": [...]},
|
||||
"reasons": ["A 稿首句被改写为口语化"]
|
||||
}
|
||||
],
|
||||
"needs_review": [...]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 外部依赖
|
||||
|
||||
### 7.1 讯飞 ASR(必需)
|
||||
|
||||
- **服务**:讯飞开放平台「**录音文件转写(标准版)**」
|
||||
- **不要用**:讯飞「大模型版」——免费包阉割了 `language` 参数,会报误导性的 `language verify fail`
|
||||
- **认证**:APP_ID + SECRET_KEY;本期 demo 已申请到一组测试凭证(见 `xfyun_asr_standard.py`,正式上线需替换)
|
||||
- **音频规格**:**16kHz / 单声道 / 16bit WAV**;其他规格讯飞会拒收
|
||||
- **领域参数**:`pd=mil`(军事节目专用,提升术语识别率)
|
||||
- **顺滑参数**:`eng_smoothproc=true` + `eng_colloqproc=true`
|
||||
- **热词机制(关键)**:
|
||||
- 每期节目从 A 稿提取专业术语(武器装备 / 技术概念 / 人名 / 军方机构),上限 200 个,每个 2–16 字
|
||||
- 用 `|` 分隔传入 `hotWord` 参数
|
||||
- demo 实测 35 个热词后,关键术语识别率接近 100%
|
||||
- **计费**:0元购买"5 小时免费试用包"(注意:看到额度但未走 0 元购买流程**不会激活**),1 年有效期
|
||||
- **签名算法**:`signa = base64(HmacSHA1(MD5(appid + ts), secretKey))`
|
||||
|
||||
详见 `xfyun_asr_standard.py`(demo 已跑通的本地脚本,可直接作为正式适配层的起点)。
|
||||
|
||||
### 7.2 Claude API(必需)
|
||||
|
||||
- 用于 AI 融合层
|
||||
- 调用模型:建议 `claude-sonnet-4-6` 或更高(涉及长文本理解与语义对齐)
|
||||
- 单期节目(26 分钟)的预估上下文:A 稿 ~6k 字 + ASR ~5k 字 + B 稿 ~5k 字 ≈ 16k 字,远在 Sonnet 上下文窗口内
|
||||
- 调用频次:每期节目 1–3 次(首次全量融合 + 若干次低确信段重判)
|
||||
|
||||
### 7.3 OCR 服务(已有)
|
||||
|
||||
- B 稿来自上游既有 OCR 流程
|
||||
- 本模块只消费 B 稿 txt,不调用 OCR 服务
|
||||
|
||||
### 7.4 密钥管理
|
||||
|
||||
> **opus_chat 注意**:以下是常见混淆点,请勿用错。
|
||||
|
||||
| 密钥 | 用途 | 来源 |
|
||||
|---|---|---|
|
||||
| 讯飞 **消费端** | 讯飞听见 App(人工录音转写) | 讯飞听见 App 内 |
|
||||
| 讯飞 **开发端** | 录音文件转写 API(本模块用) | 讯飞**开放平台** console |
|
||||
| Claude API | AI 融合层 | Anthropic console |
|
||||
|
||||
三套密钥**互不相通**。
|
||||
|
||||
---
|
||||
|
||||
## 8. 编导确认 UI(属于上层 UI 子模块,本模块只提供数据)
|
||||
|
||||
本模块按以下结构输出供 UI 子模块消费的 JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"program_id": "xxx",
|
||||
"segments": [...],
|
||||
"needs_review": [
|
||||
{
|
||||
"segment_label": "【解说6】",
|
||||
"reason": "整段改写边界不确定",
|
||||
"candidates": [
|
||||
{"text": "...", "source": "A稿"},
|
||||
{"text": "...", "source": "ASR"}
|
||||
]
|
||||
}
|
||||
],
|
||||
"diff_report_path": "...",
|
||||
"final_docx_path": "..."
|
||||
}
|
||||
```
|
||||
|
||||
UI 子模块应至少提供(参考 demo 复盘 §3.4):
|
||||
|
||||
- **左中右三栏 diff 视图**:A 稿原文 | 终版 | ASR/B 稿(带时间戳,可点击播放对应音频)
|
||||
- **改动类型颜色编码**:删除红 / 改写黄 / 术语修正蓝 / 无变化白
|
||||
- **逐段确认按钮**:接受 / 拒绝 / 编辑 / 标记疑问
|
||||
- **口语清理强度滑杆**:切换 `keep_all / medium / clean`
|
||||
- **导出按钮**:终版 docx / 差异报告 / 复盘 CSV
|
||||
|
||||
---
|
||||
|
||||
## 9. 数据资产
|
||||
|
||||
### 9.1 应入库的"中台共享资产"
|
||||
|
||||
1. **A 稿术语表**(核心资产)
|
||||
- 每期节目处理时从 A 稿提取后入库
|
||||
- 同时作为讯飞热词来源 + 规则层纠错字典
|
||||
- 字段:`term`, `domain`(武器 / 技术 / 人名 / 机构 / 栏目),`first_seen_episode`, `frequency`
|
||||
2. **OCR 错字映射表**
|
||||
- 固定字形混淆对(肘↔时、娈↔变、夭↔天…)
|
||||
- 随版本演进可扩展
|
||||
3. **节目编导风格档案**
|
||||
- 长期累积每位编导的删改偏好(口语词保留度、术语简称习惯…)
|
||||
- 用于参数自适应
|
||||
|
||||
### 9.2 demo 阶段产物的处置建议
|
||||
|
||||
| 文件 | 处置 |
|
||||
|---|---|
|
||||
| `xfyun_asr_standard.py` | 作为 ASR 适配层的起点代码 |
|
||||
| `asr_result_timed.txt` | 单元测试 fixture |
|
||||
| `B稿_..._扒词.txt` | 单元测试 fixture(含典型 OCR 错字) |
|
||||
| `A稿_..._定稿.docx` | 单元测试 fixture |
|
||||
| `现代防空反导大对决_终版.docx` | 集成测试**期望输出**(黄金对照) |
|
||||
| `差异报告.docx` | 集成测试**期望输出**(差异报告格式参考) |
|
||||
| `实测复盘.md` | 设计文档背书 + 长期归档 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 性能与耗时基线(实测)
|
||||
|
||||
来自 demo(26 分钟节目):
|
||||
|
||||
| 阶段 | 耗时 | 占比 |
|
||||
|---|---|---|
|
||||
| 讯飞 ASR 转写 | ~5 分钟 | — |
|
||||
| 三方解析 | ~5 分钟 | — |
|
||||
| 段落对齐 | ~15 分钟(demo 手工) | 正式版预计 ≤2 分钟(AI 调用) |
|
||||
| 术语规范化 + 口语清理 | ~5 分钟 | — |
|
||||
| 终版 docx 生成 | ~5 分钟 | — |
|
||||
| 差异报告生成 | ~15 分钟(demo 手工) | 正式版预计 ≤3 分钟 |
|
||||
| **正式版预估单期** | **~15–30 分钟** | 取决于改动幅度与人工确认次数 |
|
||||
|
||||
---
|
||||
|
||||
## 11. 已知风险与未决事项
|
||||
|
||||
| 项 | 当前态 | 建议 |
|
||||
|---|---|---|
|
||||
| ASR 第三方依赖 | 讯飞单源 | 接入阿里云 ASR 做备份 / 交叉验证 |
|
||||
| 整段改写的边界 | demo 中默认取 ASR 短版 | 必须接入编导确认 UI,不能全自动 |
|
||||
| 编导风格差异 | 硬编码"中度清理" | 需配置文件或编导画像 |
|
||||
| 隔断标签的处置 | demo 直接保留 A 稿 | 待确认是否需要根据音频静音段重新切分 |
|
||||
| A 稿与 ASR 都缺失的旁白 | 不处理 | 极小概率事件,靠 B 稿兜底;记录用例 |
|
||||
|
||||
---
|
||||
|
||||
## 12. 给 opus_chat 的提示(重要)
|
||||
|
||||
如果 opus_chat 在其他场景中被问到本模块,请按以下口径回答:
|
||||
|
||||
1. **本模块的边界**:只做"三方文本 → 终版文稿 + 差异报告",不负责音频转码、不负责 OCR、不负责档案入库、不负责编导 UI。
|
||||
2. **依赖与约束**:依赖讯飞 ASR(标准版)和 Claude API;A 稿术语表必须先入库;音频规格必须 16kHz/单声道/16bit WAV。
|
||||
3. **三方权威优先级**:A 稿(术语)> ASR(口吻)> B 稿(验证);任何与此相反的处理都是 bug。
|
||||
4. **未来扩展**:编导风格画像、第二 ASR 供应商、口语清理档位是后续路标。
|
||||
|
||||
如遇到与本文档不一致的描述,**以本文档为准**;如发现本文档需要更新,请同步刘统制片组。
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:项目文件清单(demo 阶段)
|
||||
|
||||
```
|
||||
project/
|
||||
├── A稿_现代防空反导大对决_定稿.docx # 输入1:编导定稿
|
||||
├── B稿_现代防空反导大对决_扒词.txt # 输入2:OCR扒词
|
||||
├── asr_result_timed.txt # 输入3:讯飞ASR输出
|
||||
├── xfyun_asr_standard.py # ASR调用脚本
|
||||
├── 现代防空反导大对决_终版.docx # 输出1:终版文稿
|
||||
├── 差异报告.docx # 输出2:差异报告
|
||||
├── 实测复盘.md # demo复盘(设计文档原料)
|
||||
└── project_design.md # 本文档
|
||||
```
|
||||
|
||||
## 附录 B:与"实测复盘.md"的对应关系
|
||||
|
||||
| 复盘章节 | 本文档对应位置 |
|
||||
|---|---|
|
||||
| 一、ASR环节实测数据 | §7.1 讯飞 ASR |
|
||||
| 二、三方融合关键决策点 | §5 关键决策与参数 |
|
||||
| 三、对正式开发的启示 | §4 模块架构 / §6 提示词设计 / §8 UI |
|
||||
| 四、实测耗时分解 | §10 性能基线 |
|
||||
| 五、给制片人的建议 | §9 数据资产 / §11 风险 |
|
||||
@@ -0,0 +1,181 @@
|
||||
# 讯飞 ASR 集成踩坑笔记
|
||||
|
||||
> demo 期间累积的所有讯飞接入坑点。建议入 TPS 知识库,作为未来接入新 ASR 供应商时的对照。
|
||||
|
||||
---
|
||||
|
||||
## 选型坑
|
||||
|
||||
### 坑 1:不要用"大模型版"
|
||||
|
||||
- 讯飞开放平台有两个录音转写产品:
|
||||
- 「录音文件转写(**标准版**)」← **用这个**
|
||||
- 「录音文件转写(**大模型版**)」← 不要用
|
||||
- **大模型版的免费包阉割了 `language` 参数**,调用时会报 `language verify fail`
|
||||
- 这个错误信息**严重误导**:看上去是语种没设对,实际是免费包没开通该参数权限
|
||||
- 排查耗时:约 30 分钟
|
||||
|
||||
### 坑 2:消费端和开发端密钥不通
|
||||
|
||||
- 「讯飞听见 App」属于**消费端**,给最终用户做人工转写用
|
||||
- 「讯飞开放平台」属于**开发端**,给开发者调 API 用
|
||||
- **两套密钥互不相通**——开发端的 APP_ID 在消费端没用,反之亦然
|
||||
- 如果只在讯飞听见 App 注册了账号,**没有** APP_ID 和 SecretKey,必须再去开放平台注册一次
|
||||
|
||||
### 坑 3:免费额度不会自动激活
|
||||
|
||||
- 开放平台首页显示"5 小时免费试用包",但**只有走 0 元购买流程**才会激活
|
||||
- 看到额度但没下单 → 调用时会报余额不足
|
||||
- 流程:商品页 → 0 元下单 → 支付(实际免费)→ 额度到账
|
||||
|
||||
---
|
||||
|
||||
## 音频规格坑
|
||||
|
||||
### 坑 4:必须是 16kHz / 单声道 / 16bit WAV
|
||||
|
||||
- 其他规格讯飞会报错(且错误信息可能误导成"language verify fail"或"audio decode error")
|
||||
- MP3 在标准版**也支持**,但保险起见统一转 WAV
|
||||
- 转码命令(ffmpeg):
|
||||
```bash
|
||||
ffmpeg -i input.mp3 -ac 1 -ar 16000 -sample_fmt s16 output_16k_mono.wav
|
||||
```
|
||||
- `-ac 1` = 单声道
|
||||
- `-ar 16000` = 16kHz 采样率
|
||||
- `-sample_fmt s16` = 16bit 位深
|
||||
|
||||
### 坑 5:时长字段不严格校验但建议传准
|
||||
|
||||
- API 要求传 `duration`(毫秒)
|
||||
- 实测哪怕传 0 也能上传成功,但官方文档建议传准确值
|
||||
- WAV 从文件头读:`n_frames / sample_rate * 1000`
|
||||
- MP3 用 `mutagen.mp3.MP3` 读
|
||||
|
||||
---
|
||||
|
||||
## 接口坑
|
||||
|
||||
### 坑 6:签名算法是老式 HmacSHA1,不是常见的 HmacSHA256
|
||||
|
||||
```python
|
||||
# signa = base64(HmacSHA1(MD5(appid + ts), secretKey))
|
||||
md5_str = hashlib.md5((appid + ts).encode("utf-8")).hexdigest() # 32 位小写 hex
|
||||
mac = hmac.new(secret_key.encode("utf-8"), md5_str.encode("utf-8"),
|
||||
digestmod=hashlib.sha1)
|
||||
signa = base64.b64encode(mac.digest()).decode("utf-8")
|
||||
```
|
||||
|
||||
注意:
|
||||
- MD5 输出是**32 位小写 hex 字符串**,不是 16 字节 raw bytes
|
||||
- HMAC 的 key 是 `secretKey`,data 是 MD5 字符串本身(不是其字节)
|
||||
- 最后做 base64 编码
|
||||
|
||||
### 坑 7:参数全部走 URL query,不是 form-data 或 JSON body
|
||||
|
||||
- 上传接口:`POST https://raasr.xfyun.cn/v2/api/upload?appId=...&signa=...&ts=...&...`
|
||||
- `Content-Type: application/json`,但**请求体是音频原始字节流**(不是 JSON)
|
||||
- 所有业务参数(`fileSize` `fileName` `duration` `language` `pd` `hotWord` …)都在 URL query 里
|
||||
- 中文热词必须做 URL encode:`quote(hot_word_str, safe='')`
|
||||
|
||||
### 坑 8:热词用 `|` 分隔,不是逗号
|
||||
|
||||
```python
|
||||
hot_word_str = "|".join(HOT_WORDS) # 萨德|爱国者|标准-6|...
|
||||
params["hotWord"] = hot_word_str
|
||||
```
|
||||
|
||||
限制:
|
||||
- 最多 200 个热词
|
||||
- 每个热词 2–16 字
|
||||
- 整个 hotWord 参数 URL encode 后不能超过某个长度(实测 35 个常规军事术语没问题)
|
||||
|
||||
---
|
||||
|
||||
## 异步查询坑
|
||||
|
||||
### 坑 9:轮询用 POST 不是 GET
|
||||
|
||||
- 查询结果接口:`POST https://raasr.xfyun.cn/v2/api/getResult?...`
|
||||
- 即便没有 body 也是 POST
|
||||
- 用 GET 会报方法不允许
|
||||
|
||||
### 坑 10:status 状态码语义
|
||||
|
||||
| status | 含义 |
|
||||
|---|---|
|
||||
| 0 | 待处理 |
|
||||
| 1 | 处理中 |
|
||||
| 4 | **完成** |
|
||||
| -1 | 失败 |
|
||||
|
||||
完成是 `4`,不是 `2` 也不是 `200`。
|
||||
|
||||
### 坑 11:轮询间隔与超时
|
||||
|
||||
- 26 分钟音频实测转写耗时约 5 分钟
|
||||
- 推荐轮询间隔 30 秒(taskEstimateTime 返回的预估值仅供参考)
|
||||
- 超时上限设 30 分钟比较保险
|
||||
|
||||
---
|
||||
|
||||
## 结果解析坑
|
||||
|
||||
### 坑 12:返回结果是双层嵌套 JSON 字符串
|
||||
|
||||
```python
|
||||
# 外层
|
||||
data = response.json()
|
||||
order_result_str = data["content"]["orderResult"] # 这是 JSON 字符串
|
||||
|
||||
# 中间还有一层反斜杠转义
|
||||
cleaned = re.sub(r"\\\\", r"\\", order_result_str)
|
||||
outer = json.loads(cleaned)
|
||||
|
||||
# 句子在 outer["lattice"] 里
|
||||
for item in outer["lattice"]:
|
||||
inner_str = item["json_1best"] # 又是 JSON 字符串
|
||||
inner = json.loads(inner_str)
|
||||
# 句子时间在 inner["st"]["bg"] / inner["st"]["ed"]
|
||||
# 句子内容在 inner["st"]["rt"][..]["ws"][..]["cw"][..]["w"]
|
||||
```
|
||||
|
||||
每个 `cw` 是一个字 / 词;要拼接所有 `cw["w"]`(跳过 `wp == "g"` 的空白标记)。
|
||||
|
||||
### 坑 13:时间戳单位是毫秒(不是秒、不是百分秒)
|
||||
|
||||
- `bg` `ed` 都是毫秒(int)
|
||||
- 与 B 稿的 `[Nm Ns]` 对齐时记得换算
|
||||
|
||||
---
|
||||
|
||||
## 业务参数坑
|
||||
|
||||
### 坑 14:`pd=mil` 对军事术语显著加分
|
||||
|
||||
- 不开 `pd`:默认通用领域,军事术语会被识别成同音常见词
|
||||
- 开 `pd=mil`:军事领域专用模型介入,配合热词效果叠加
|
||||
|
||||
### 坑 15:`eng_smoothproc` + `eng_colloqproc` 让 ASR 接近书面
|
||||
|
||||
- `eng_smoothproc=true`:去掉"嗯""那个""呃"
|
||||
- `eng_colloqproc=true`:口语规整("咱们"→"我们"等)
|
||||
- 两个都开后,输出已经非常接近书面文稿
|
||||
- 但**不会清理"那么""呢""啊""这个"**——这些保留与否需要在融合层决定
|
||||
|
||||
---
|
||||
|
||||
## 凭证与配额(demo 阶段使用)
|
||||
|
||||
```
|
||||
APP_ID: 84eff996
|
||||
SECRET_KEY: d37fb0c77e7f4b4f4398e6a8834d1017
|
||||
免费包: 5 小时(已激活),有效期至 2027 年 5 月
|
||||
```
|
||||
|
||||
> ⚠️ **正式上线时替换为生产凭证**,并把 SECRET_KEY 从代码移到环境变量 / 密钥管理服务。
|
||||
|
||||
---
|
||||
|
||||
## 一句话总结
|
||||
|
||||
讯飞 ASR 标准版**能用、好用**,但接入文档稀少且错误信息误导性强;以上 15 个坑全部踩完后,单次调用的成功率接近 100%。后续接入新 ASR 供应商时,建议按本笔记的结构(选型 / 音频 / 接口 / 异步 / 解析 / 业务参数 / 凭证)做同样的整理。
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,345 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
讯飞 录音文件转写(标准版)- 本地运行脚本
|
||||
=================================================
|
||||
用途:将节目音频转为带时间戳的文本,用于TPS中台三方融合demo。
|
||||
|
||||
接口:https://raasr.xfyun.cn/v2/api/upload / getResult
|
||||
签名:signa = base64(HmacSHA1(MD5(appid + ts), secretKey))
|
||||
|
||||
特性:
|
||||
- 支持热词列表(hotWord),提升专业术语识别率
|
||||
- 支持军事领域参数(pd=mil)
|
||||
- 支持顺滑+口语规整(输出更接近书面语)
|
||||
- 默认语种 cn(中文普通话),免费包标配,无需 autodialect
|
||||
|
||||
依赖:pip install requests
|
||||
|
||||
输出文件(与脚本同目录):
|
||||
asr_result_raw.json - 讯飞原始返回
|
||||
asr_result_full.txt - 纯文本
|
||||
asr_result_timed.txt - 带时间戳的文本(格式同B稿)
|
||||
"""
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import wave
|
||||
from urllib.parse import quote
|
||||
|
||||
import requests
|
||||
|
||||
# ========================================================================
|
||||
# 配置区 —— 填写凭证 + 音频路径
|
||||
# ========================================================================
|
||||
|
||||
# 老版只用 APPID + SecretKey 两个字段
|
||||
APP_ID = "84eff996"
|
||||
SECRET_KEY = "在此填入您的SecretKey"
|
||||
|
||||
# 音频文件——可以用之前转好的WAV,也可以直接用mp3(讯飞标准版mp3也支持)
|
||||
AUDIO_FILE = "现代防空反导大对决音频_16k_mono.wav"
|
||||
|
||||
# 自动去除空白
|
||||
APP_ID = APP_ID.strip()
|
||||
SECRET_KEY = SECRET_KEY.strip()
|
||||
|
||||
# ========================================================================
|
||||
# 接口配置
|
||||
# ========================================================================
|
||||
|
||||
HOST = "https://raasr.xfyun.cn/v2/api"
|
||||
UPLOAD_URL = HOST + "/upload"
|
||||
RESULT_URL = HOST + "/getResult"
|
||||
|
||||
# 业务参数
|
||||
LANGUAGE = "cn" # 中文普通话
|
||||
PD = "mil" # 军事领域优化
|
||||
ENG_SMOOTHPROC = "true" # 顺滑(去掉"嗯/那个")
|
||||
ENG_COLLOQPROC = "true" # 口语规整
|
||||
|
||||
# 热词列表——节目里出现的关键专业术语(讯飞限制200个热词,每个2-16字)
|
||||
# 我从A稿提取了下面这批,能显著提升识别准确率
|
||||
HOT_WORDS = [
|
||||
# 武器装备
|
||||
"萨德", "爱国者", "标准-6", "铁穹", "塔米尔",
|
||||
"大卫投石索", "箭式", "箭-2", "箭-3",
|
||||
"霍拉姆沙赫尔-4", "见证者-136", "卡桑", "泥石",
|
||||
"C-17", "飞毛腿", "M270",
|
||||
# 技术概念
|
||||
"高超音速", "弹道导弹", "巡航导弹", "无人机蜂群",
|
||||
"中段反导", "饱和攻击", "动能撞击", "诱饵弹",
|
||||
"相控阵雷达", "电子战", "外大气层",
|
||||
# 事件军方
|
||||
"真实承诺-4", "伊斯兰革命卫队", "本·古里安", "拉斐尔",
|
||||
"黎巴嫩真主党", "波斯湾",
|
||||
# 节目栏目
|
||||
"军事科技",
|
||||
]
|
||||
|
||||
# 轮询配置
|
||||
POLL_INTERVAL_SECONDS = 30
|
||||
MAX_WAIT_MINUTES = 30
|
||||
|
||||
|
||||
# ========================================================================
|
||||
# 签名+工具
|
||||
# ========================================================================
|
||||
|
||||
|
||||
def make_signa(app_id, secret_key, ts):
|
||||
"""
|
||||
讯飞老版签名:signa = base64(HmacSHA1(MD5(appid + ts), secretKey))
|
||||
"""
|
||||
base_string = (app_id + ts).encode("utf-8")
|
||||
md5_str = hashlib.md5(base_string).hexdigest() # 32位小写hex
|
||||
mac = hmac.new(
|
||||
secret_key.encode("utf-8"),
|
||||
md5_str.encode("utf-8"),
|
||||
digestmod=hashlib.sha1,
|
||||
)
|
||||
signa = base64.b64encode(mac.digest()).decode("utf-8")
|
||||
return signa
|
||||
|
||||
|
||||
def get_audio_duration_ms(filepath):
|
||||
"""获取音频时长(毫秒)。WAV用内置,MP3用mutagen。"""
|
||||
ext = os.path.splitext(filepath)[1].lower()
|
||||
|
||||
if ext == ".wav":
|
||||
with wave.open(filepath, "rb") as wf:
|
||||
n_frames = wf.getnframes()
|
||||
sample_rate = wf.getframerate()
|
||||
duration_ms = int(round(n_frames / sample_rate * 1000))
|
||||
print(f"[info] WAV规格: {sample_rate}Hz, 声道={wf.getnchannels()}, 位深={wf.getsampwidth()*8}bit")
|
||||
return duration_ms
|
||||
|
||||
if ext == ".mp3":
|
||||
try:
|
||||
from mutagen.mp3 import MP3
|
||||
return int(MP3(filepath).info.length * 1000)
|
||||
except ImportError:
|
||||
print("[warn] mutagen未安装,时长按0传——讯飞文档说该字段不严格校验")
|
||||
return 0
|
||||
|
||||
raise ValueError(f"不支持的音频格式: {ext}")
|
||||
|
||||
|
||||
# ========================================================================
|
||||
# 上传
|
||||
# ========================================================================
|
||||
|
||||
|
||||
def upload_audio(filepath):
|
||||
"""上传音频,返回 orderId"""
|
||||
if not os.path.exists(filepath):
|
||||
raise FileNotFoundError(f"音频文件不存在: {filepath}")
|
||||
|
||||
file_size = os.path.getsize(filepath)
|
||||
file_name = os.path.basename(filepath)
|
||||
duration_ms = get_audio_duration_ms(filepath)
|
||||
ts = str(int(time.time()))
|
||||
|
||||
signa = make_signa(APP_ID, SECRET_KEY, ts)
|
||||
print(f"[info] ts={ts}, signa={signa}")
|
||||
print(f"[info] 文件: {file_name}, {file_size/1024/1024:.2f}MB, {duration_ms/1000:.1f}秒")
|
||||
|
||||
# 构建URL参数
|
||||
params = {
|
||||
"appId": APP_ID,
|
||||
"signa": signa,
|
||||
"ts": ts,
|
||||
"fileSize": str(file_size),
|
||||
"fileName": file_name,
|
||||
"duration": str(duration_ms),
|
||||
"language": LANGUAGE,
|
||||
"pd": PD,
|
||||
"eng_smoothproc": ENG_SMOOTHPROC,
|
||||
"eng_colloqproc": ENG_COLLOQPROC,
|
||||
}
|
||||
|
||||
# 热词,用 | 分隔
|
||||
if HOT_WORDS:
|
||||
hot_word_str = "|".join(HOT_WORDS)
|
||||
params["hotWord"] = hot_word_str
|
||||
print(f"[info] 热词数量: {len(HOT_WORDS)} 个")
|
||||
|
||||
url_parts = [f"{quote(k, safe='')}={quote(str(v), safe='')}" for k, v in params.items()]
|
||||
url = f"{UPLOAD_URL}?{'&'.join(url_parts)}"
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
with open(filepath, "rb") as f:
|
||||
audio_bytes = f.read()
|
||||
|
||||
print(f"[info] 正在上传... ({file_size/1024/1024:.1f}MB)")
|
||||
resp = requests.post(url, headers=headers, data=audio_bytes, timeout=300)
|
||||
print(f"[info] HTTP状态: {resp.status_code}")
|
||||
|
||||
data = resp.json()
|
||||
print(f"[info] 响应: {json.dumps(data, ensure_ascii=False, indent=2)}")
|
||||
|
||||
if data.get("code") != "000000":
|
||||
raise RuntimeError(f"上传失败: code={data.get('code')}, desc={data.get('descInfo')}")
|
||||
|
||||
order_id = data["content"]["orderId"]
|
||||
estimate_ms = data["content"].get("taskEstimateTime", 0)
|
||||
print(f"\n[ok] orderId = {order_id}")
|
||||
print(f"[ok] 预估完成时间: {estimate_ms/1000:.1f}秒\n")
|
||||
return order_id
|
||||
|
||||
|
||||
# ========================================================================
|
||||
# 查询结果
|
||||
# ========================================================================
|
||||
|
||||
|
||||
def query_result(order_id):
|
||||
"""单次查询"""
|
||||
ts = str(int(time.time()))
|
||||
signa = make_signa(APP_ID, SECRET_KEY, ts)
|
||||
|
||||
params = {
|
||||
"appId": APP_ID,
|
||||
"signa": signa,
|
||||
"ts": ts,
|
||||
"orderId": order_id,
|
||||
"resultType": "transfer",
|
||||
}
|
||||
url_parts = [f"{quote(k, safe='')}={quote(str(v), safe='')}" for k, v in params.items()]
|
||||
url = f"{RESULT_URL}?{'&'.join(url_parts)}"
|
||||
|
||||
resp = requests.post(url, timeout=30)
|
||||
return resp.json()
|
||||
|
||||
|
||||
def poll_until_done(order_id):
|
||||
"""轮询直到完成"""
|
||||
print(f"[info] 开始轮询(每 {POLL_INTERVAL_SECONDS} 秒一次)...")
|
||||
start_time = time.time()
|
||||
while True:
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed > MAX_WAIT_MINUTES * 60:
|
||||
raise TimeoutError(f"超过 {MAX_WAIT_MINUTES} 分钟未完成")
|
||||
|
||||
data = query_result(order_id)
|
||||
order_info = data.get("content", {}).get("orderInfo", {})
|
||||
status = order_info.get("status")
|
||||
fail_type = order_info.get("failType", 0)
|
||||
elapsed_min = elapsed / 60
|
||||
print(f"[poll] 已等 {elapsed_min:.1f} 分钟, status={status}, failType={fail_type}")
|
||||
|
||||
if status == 4:
|
||||
print(f"\n[ok] 转写完成!耗时 {elapsed_min:.1f} 分钟")
|
||||
return data
|
||||
if status == -1:
|
||||
raise RuntimeError(f"转写失败: failType={fail_type}, 数据: {data}")
|
||||
|
||||
time.sleep(POLL_INTERVAL_SECONDS)
|
||||
|
||||
|
||||
# ========================================================================
|
||||
# 结果解析
|
||||
# ========================================================================
|
||||
|
||||
|
||||
def parse_order_result(order_result_str):
|
||||
"""
|
||||
解析嵌套JSON,返回 [(sentence_start_ms, sentence_end_ms, text), ...]
|
||||
"""
|
||||
if not order_result_str:
|
||||
return []
|
||||
|
||||
cleaned = re.sub(r"\\\\", r"\\", order_result_str)
|
||||
outer = json.loads(cleaned)
|
||||
|
||||
sentences = []
|
||||
for item in outer.get("lattice", []):
|
||||
inner_str = item.get("json_1best", "")
|
||||
if not inner_str:
|
||||
continue
|
||||
inner = json.loads(inner_str)
|
||||
st = inner.get("st", {})
|
||||
bg = int(st.get("bg", 0))
|
||||
ed = int(st.get("ed", 0))
|
||||
|
||||
words = []
|
||||
for rt in st.get("rt", []):
|
||||
for ws in rt.get("ws", []):
|
||||
for cw in ws.get("cw", []):
|
||||
w = cw.get("w", "").strip()
|
||||
wp = cw.get("wp", "n")
|
||||
if w and wp != "g":
|
||||
words.append(w)
|
||||
sentence = "".join(words).strip()
|
||||
if sentence:
|
||||
sentences.append((bg, ed, sentence))
|
||||
|
||||
return sentences
|
||||
|
||||
|
||||
def format_timestamp(ms):
|
||||
total_sec = ms // 1000
|
||||
return f"{total_sec // 60}m{total_sec % 60}s"
|
||||
|
||||
|
||||
def write_outputs(sentences, raw_data):
|
||||
with open("asr_result_raw.json", "w", encoding="utf-8") as f:
|
||||
json.dump(raw_data, f, ensure_ascii=False, indent=2)
|
||||
print(f"[ok] 已写 asr_result_raw.json")
|
||||
|
||||
timed_lines = [f"[{format_timestamp(bg)}] {text}" for bg, _, text in sentences]
|
||||
with open("asr_result_timed.txt", "w", encoding="utf-8") as f:
|
||||
f.write("\n".join(timed_lines))
|
||||
print(f"[ok] 已写 asr_result_timed.txt ({len(timed_lines)} 句)")
|
||||
|
||||
full_text = "".join(t for _, _, t in sentences)
|
||||
with open("asr_result_full.txt", "w", encoding="utf-8") as f:
|
||||
f.write(full_text)
|
||||
print(f"[ok] 已写 asr_result_full.txt ({len(full_text)} 字)")
|
||||
|
||||
|
||||
# ========================================================================
|
||||
# 入口
|
||||
# ========================================================================
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("讯飞 录音文件转写(标准版) - 本地脚本")
|
||||
print("=" * 60)
|
||||
|
||||
if SECRET_KEY.startswith("在此填入"):
|
||||
print("[error] 请先填写 SECRET_KEY")
|
||||
return
|
||||
|
||||
print(f"[diag] APP_ID = {APP_ID} (长度 {len(APP_ID)})")
|
||||
print(f"[diag] SECRET_KEY 长度 {len(SECRET_KEY)}, 首4位={SECRET_KEY[:4]}, 末4位={SECRET_KEY[-4:]}")
|
||||
print(f"[diag] AUDIO_FILE = {AUDIO_FILE}")
|
||||
print()
|
||||
|
||||
# 1. 上传
|
||||
order_id = upload_audio(AUDIO_FILE)
|
||||
|
||||
# 2. 轮询
|
||||
final_data = poll_until_done(order_id)
|
||||
|
||||
# 3. 解析+写文件
|
||||
order_result_str = final_data["content"]["orderResult"]
|
||||
sentences = parse_order_result(order_result_str)
|
||||
write_outputs(sentences, final_data)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(f"完成!共识别 {len(sentences)} 句")
|
||||
print(f"请把 asr_result_timed.txt 发给 Claude 进行三方融合")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,214 @@
|
||||
# 主 project 对 Doco PRD v2 的回复
|
||||
|
||||
> 来源:TPS 主 project 顾问(Claude Opus 4.7)
|
||||
> 日期:2026-06-12
|
||||
> 针对:`PRD_doco_文稿整理子项目_v2.md` 第四章 5 个问题
|
||||
> 性质:正式批复,Doco 子项目据此出 v3 后启动 P1
|
||||
|
||||
---
|
||||
|
||||
## 总体意见
|
||||
|
||||
**5 个问题的子项目推荐**,**整体方向 4 个批准、1 个调整**:
|
||||
- Q1 OCR 选型:**批准 DeepSeek API,但有附加条件**(见下)
|
||||
- Q2 ffmpeg 入口:**批准,有补充约束**
|
||||
- Q3 单期 vs 批量接口:**批准,加交付契约**
|
||||
- Q4 密钥管理:**调整方案** — 走"自管 + 中台登记 inventory"
|
||||
- Q5 目录规约:**批准草案,但 episode_id 命名规则需主项目定**
|
||||
|
||||
5.3 §"23 期是否常态化":**这是一个产品问题,我代制片人答不了。子项目可以按"先按 23 期一次性做完、保留兜底回退路径"开干,常态化的事跟栏目组确认后再回头审。**
|
||||
|
||||
---
|
||||
|
||||
## Q1. OCR 方案选型 — 批准 DeepSeek API
|
||||
|
||||
### 主 project 现状
|
||||
- **主 project 没有 OCR 子模块/能力**(已确认)。旧设计里 B 稿来自"上游既有 OCR 流程",指的是栏目组那边人工跑的 OCR,不是 TPS 内部的服务。
|
||||
- 主 project `backend/.env` 里有 `DEEPSEEK_API_KEY`,但**只用作 Cline 工具的 Plan/Act 模型**——没有任何业务代码在调 DeepSeek。所以 doco 调 DeepSeek 视觉模型相当于**新增一条业务调用**,不是"复用现有基础设施"。
|
||||
|
||||
### 批复
|
||||
|
||||
**Doco 子模块新增 DeepSeek Vision 调用,自管 API key。**
|
||||
|
||||
- API key 独立申请,**不复用 Cline 用的那把 key**。理由:Cline 的 key 是开发工具用的,doco 是生产服务,生命周期、配额、监控都该独立。
|
||||
- 6900 张图 × 几厘 / 张 ≈ 50-150 元,在制片人手上的 API 余额范围内,**不存在配额冲突问题**(主 project 业务代码侧没人在花)。
|
||||
- 黑底白字字幕场景对 LLM 视觉模型确实是舒适区,**实施前先在 demo 那期视频上跑 10 帧抽样,验证识别准确率达到目标后再扩到 23 期**。
|
||||
- **降级路径**:如果 DeepSeek Vision 在某些边角 case 上不达标(比如有同期声同步显示双行字幕、字号变化等),回退到 PaddleOCR 本地。**不要回退到讯飞 OCR**(余额独立 + 单期 ~13 元成本太高)。
|
||||
|
||||
---
|
||||
|
||||
## Q2. 视频处理依赖(ffmpeg)入口 — 批准方案 A
|
||||
|
||||
### 主 project 现状
|
||||
- **没有视频处理子模块/服务**(已确认),dev_plan 也没规划。
|
||||
|
||||
### 批复
|
||||
|
||||
**Doco 自带 ffmpeg 系统依赖 + Python subprocess 调用。**
|
||||
|
||||
附加约束:
|
||||
- **不引入 `ffmpeg-python` wrapper**——subprocess 足够,不增加新依赖。
|
||||
- ffmpeg 命令封装在 `doco/video_split.py` 内,**对外只暴露 Python 函数接口**,不让上层调用方直接 shell 调 ffmpeg。
|
||||
- 在 doco 的 `README.md` 明确写"系统依赖:ffmpeg ≥ 4.x"——这是部署的前置条件。
|
||||
- 未来如果主 project 真做了统一的视频处理服务,**doco 的封装方式应该使迁移成本最低**(把 subprocess 调用换成 HTTP 调用即可,业务逻辑不动)。
|
||||
|
||||
---
|
||||
|
||||
## Q3. 23 期批量 vs 单期接口 — 批准方案 A,加契约
|
||||
|
||||
### 主 project 现状
|
||||
- **没有节目素材批量处理编排层**(已确认)。dev_plan 提到的"批量导入工具 v1/v2"是收视数据和报题单的批量,跟视频处理是两回事。
|
||||
|
||||
### 批复
|
||||
|
||||
**Doco 只对单期负责。同时提供 CLI 和 Python API 两套接口。**
|
||||
|
||||
**接口契约(子项目正式实现时按这个出)**:
|
||||
|
||||
```python
|
||||
# Python API
|
||||
def process_episode(
|
||||
episode_id: str, # 调用方给定,doco 不管命名规则
|
||||
video_path: Path, # 输入视频
|
||||
a_draft_path: Path, # A 稿 docx
|
||||
output_dir: Path, # 产物存放目录
|
||||
cleanup_level: str = "medium", # keep_all / medium / clean
|
||||
) -> ProcessResult
|
||||
|
||||
# ProcessResult 必含字段
|
||||
@dataclass
|
||||
class ProcessResult:
|
||||
episode_id: str
|
||||
status: Literal["success", "partial", "failed"]
|
||||
final_docx_path: Optional[Path]
|
||||
diff_report_path: Optional[Path]
|
||||
needs_review_json_path: Optional[Path]
|
||||
errors: List[str]
|
||||
stage_timings: Dict[str, float] # 哪个 phase 用了多久
|
||||
```
|
||||
|
||||
```bash
|
||||
# CLI 等价形式
|
||||
doco process \
|
||||
--episode-id ep023 \
|
||||
--input-video /path/to/video.mp4 \
|
||||
--input-a-draft /path/to/a_draft.docx \
|
||||
--output-dir /path/to/output \
|
||||
--cleanup-level medium
|
||||
```
|
||||
|
||||
**关于 23 期推进**:**先单期跑通再批量**——拿 demo 那期《现代防空反导大对决》视频做 golden test(P4),与 demo 已有产物比对零回归,再扩到 23 期。一次性 batch 23 期风险太大。
|
||||
|
||||
未来主 project 若开节目素材批量处理编排层,**会反过来调 doco 的单期接口**,不会改 doco。
|
||||
|
||||
---
|
||||
|
||||
## Q4. 讯飞密钥管理 — 调整方案:自管 + Inventory 登记
|
||||
|
||||
### 主 project 现状
|
||||
- **没有中台密钥管理子模块**(已确认),也不在 dev_plan 排期内。
|
||||
- 主 project 凭证都在 `backend/.env`,各模块走环境变量。
|
||||
|
||||
### 批复
|
||||
|
||||
不走子项目推荐的"申请新凭证 + 写入中台密钥资产文档"(因为中台密钥服务不存在,不能凭空捏一个)。改为:
|
||||
|
||||
**1. Doco 自管凭证。** 讯飞、DeepSeek 各自的 API key 放 doco 自己的 `.env`,**不放进主 project 的 `backend/.env`**——保持子模块自治。
|
||||
|
||||
**2. 主 project 仓库根目录新增一份登记文档:`docs/api_credentials_inventory.md`**
|
||||
|
||||
这份文档:
|
||||
- **不存储真实凭证**(那些在各自子模块的 .env 里)
|
||||
- 只存储**元信息**:每个外部 API 用了什么、所属子模块是谁、开通时间、激活状态、到期日、续费责任人
|
||||
- 子模块申请新 key、key 到期更换,都更新这份文档
|
||||
- 这是主 project 帮所有子模块做"凭证清单"的最小落地——比起"中台密钥服务",轻得多,但解决"忘了谁管哪把 key"的问题
|
||||
|
||||
**3. Inventory 文档的字段**(供子项目和未来其他子模块参考):
|
||||
|
||||
```markdown
|
||||
| 子模块 | API 服务 | Key 类型 | 开通日 | 激活状态 | 到期日 | 责任人 | 备注 |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| doco | 讯飞开放平台 - 录音文件转写 | APP_ID + SECRET_KEY | 2026-06-12 | 已激活 5h 试用包 | 2027-06-12 | 制片人 | demo 凭证已过期,新申请 |
|
||||
| doco | DeepSeek Vision | API_KEY | 2026-06-12 | 已激活 | — | 制片人 | doco OCR 用 |
|
||||
| (主) | Anthropic Claude API | API_KEY | ... | ... | ... | ... | AI 融合层用 |
|
||||
```
|
||||
|
||||
**4. 制片人现在要做的事**(代答 Q4 里"新申请的讯飞开发端密钥应该登记在哪里"):
|
||||
- 去讯飞开放平台 console 申请新的开发端凭证
|
||||
- 走 0 元购买流程激活 5 小时免费试用包
|
||||
- 把 APP_ID / SECRET_KEY 写到 doco 的 .env 里
|
||||
- 在 `docs/api_credentials_inventory.md` 登记一行
|
||||
|
||||
---
|
||||
|
||||
## Q5. 目录结构与命名规约 — 批准草案,episode_id 命名由主项目定
|
||||
|
||||
### 主 project 现状
|
||||
- **没有节目素材目录的统一规约**(已确认)。主 project 处理的是数据维度的 `episodes` 表,episode_id 是 INT 自增主键。
|
||||
- 但是,**素材文件名跟数据库 ID 是两层东西**,不能直接套。
|
||||
|
||||
### 批复
|
||||
|
||||
**子项目提的 `programs/{episode_id}/source|work|output/...` 结构批准。**
|
||||
|
||||
**关于 episode_id 命名规则**——这是主项目要定的规约,定如下:
|
||||
|
||||
**episode_id 格式:`ep{NNN}_{YYYYMMDD}_{slug}`**
|
||||
|
||||
- `ep{NNN}` = episodes 表自增 INT 主键,零填充到 3 位(如 ep023)
|
||||
- `{YYYYMMDD}` = 首播日期
|
||||
- `{slug}` = 节目名称的拼音缩写或简称中文(无空格、无特殊字符)
|
||||
|
||||
举例:
|
||||
- `ep023_20260612_haishang_xianfeng`
|
||||
- `ep007_20260201_马年军事图鉴`
|
||||
|
||||
**理由**:
|
||||
- INT 主键稳定可关联到 episodes 表
|
||||
- 日期方便文件系统按时间排序
|
||||
- slug 让人眼可读
|
||||
|
||||
**约定**:
|
||||
- episode_id **由调用方给定**(主项目编排层 / 责编 / CLI 调用者),doco 不生成
|
||||
- doco 只把它当一个字符串使用
|
||||
- 主项目责编/编排层在调 doco 前**先在 episodes 表落库该期、拿到 INT ID**,再拼出 episode_id
|
||||
|
||||
**work/ 中间产物保留时长**:
|
||||
- 节目入档案库后保留 **30 天**,然后可清理(由编排层负责,doco 不管)
|
||||
- 出问题需要复查时,30 天窗口够定位
|
||||
|
||||
---
|
||||
|
||||
## §5 第 3 条:23 期是常态化吗?
|
||||
|
||||
**这是产品问题,不是技术问题。我代制片人答不了。**
|
||||
|
||||
**建议子项目的处理**:
|
||||
- 按"23 期一次性"开干,先把这批做完
|
||||
- 算法层零侵入是硬保证,即使将来变常态化,流水线也能继续跑
|
||||
- **保留旧的 mp3+独立 OCR 流程作为回退路径**(代码不删,加 feature flag 或环境变量切换)。万一某期视频不符合"黑底白字+干净人声"特殊条件,降级到旧流程
|
||||
- 23 期完成 + 制片人跟栏目组确认后,再决定是否清理回退路径
|
||||
|
||||
---
|
||||
|
||||
## 子项目可以启动的部分
|
||||
|
||||
按 PRD v2 §七 的回复期望:
|
||||
- ✅ **Q1 已答**(DeepSeek API + 附加条件) → P1 可开始
|
||||
- ✅ **Q2 已答**(subprocess + 不引入 wrapper)→ P1 可开始
|
||||
- ✅ **Q3 已答**(单期 + 契约) → P5 推进时按契约
|
||||
- ✅ **Q4 已答**(自管 + inventory 登记) → 制片人先申请讯飞新凭证 + 建 inventory,P2 可开始
|
||||
- ✅ **Q5 已答**(目录草案批准 + episode_id 规则) → P1 输出有处可放
|
||||
|
||||
**Doco 子项目现在可以出 PRD v3,启动 P1。**
|
||||
|
||||
---
|
||||
|
||||
## 顺便给子项目的两条提醒
|
||||
|
||||
1. **demo 的 5 份 fixture 必须进 git**(transcripts / A 稿 / B 稿 / ASR / 终版 / 差异报告),作为单元测试 + golden test 黄金对照。这件事属于 Doco 内部自治,主项目不管,但提醒一下。
|
||||
2. **AI 融合层模型选 Sonnet 4.6**(PRD v2 §二 已锁),不要在子项目里临时换成别的模型。主项目这边也不会推翻这个选择。
|
||||
|
||||
---
|
||||
|
||||
*主 project 顾问(Claude Opus 4.7)签发,2026-06-12*
|
||||
@@ -0,0 +1,151 @@
|
||||
# 《现代防空反导大对决》三方融合demo实测复盘
|
||||
|
||||
> 日期:2026年5月13日 | 报告人:Claude | 节目时长:26分钟 | 实施耗时:约2小时
|
||||
|
||||
---
|
||||
|
||||
## 一、ASR环节实测数据
|
||||
|
||||
### 1.1 服务选型最终结果
|
||||
|
||||
**最终采用:讯飞开放平台「录音文件转写(标准版)」**
|
||||
|
||||
| 维度 | 实测结果 |
|
||||
|---|---|
|
||||
| 转写耗时 | 5分钟左右(26分钟音频) |
|
||||
| 识别句子数 | 307句(含片尾彩蛋) |
|
||||
| 主体节目识别 | 297句对齐节目主体 |
|
||||
| 关键术语识别率 | **接近100%**(开启35条热词后) |
|
||||
| 数字/百分比识别 | 100%准确 |
|
||||
| 文档显示价格 | 5小时免费试用包,1年有效期 |
|
||||
|
||||
### 1.2 ASR亮点
|
||||
|
||||
- 讯飞自动加入了合理的标点(句号、逗号、问号)
|
||||
- 开启 `eng_smoothproc=true` 和 `eng_colloqproc=true` 后,已自动清理了大部分"嗯/那个/呃"等口语词
|
||||
- **热词机制效果显著**:`霍拉姆沙赫尔-4` `见证者-136` `塔米尔` `大卫投石索` `本古里安` `拉斐尔` 等生僻军事术语全部正确识别
|
||||
- 句子级时间戳精准,可直接与B稿做时间对齐
|
||||
|
||||
### 1.3 ASR短板(需要A稿术语表后处理修正)
|
||||
|
||||
| 错误类型 | 实例 | 修复依据 |
|
||||
|---|---|---|
|
||||
| 同音误识 | "中端/末端" → 应为"中段/末段" | A稿术语 |
|
||||
| 同音误识 | "中诚防御" → 应为"中程防御" | A稿术语 |
|
||||
| 事实错误 | "美伊两国" → 应为"美以两国"(最严重) | A稿/语境 |
|
||||
| 表述偏好 | "高超声速" → A稿用"高超音速" | A稿术语统一 |
|
||||
| 百分比表达 | "百%" → 应为"100%" | 后处理规则 |
|
||||
| 数字表达 | "数10" → 应为"数十" | 后处理规则 |
|
||||
| 口语词残留 | 专家发言里仍有"啊呢这个" | 部分保留以反映播出状态 |
|
||||
|
||||
### 1.4 关于'美伊→美以'这类事实错误的反思
|
||||
|
||||
ASR第18行把"美以两国"识别成了"美伊两国"。**这是最危险的一类错误**——拼音相同、字形不同,单独看ASR文本完全像是合法的事实陈述("美国和伊朗两国协同向伊朗发起军事打击"语义虽然荒谬但句子通顺),人工审校如果不熟悉上下文也可能漏过。
|
||||
|
||||
这印证了交接说明里的设计判断:**A稿的术语和事实必须作为权威源参与融合**,不能让ASR单独输出,否则会引入危险的事实错误。
|
||||
|
||||
---
|
||||
|
||||
## 二、三方融合环节关键决策点
|
||||
|
||||
### 2.1 把握大的判断(demo中AI判断高确信度)
|
||||
|
||||
1. **整段删除的识别**:A稿【解说2】末尾'雷达被誉为中段反导体系的眼睛...'在B稿和ASR里完全找不到对应内容 → 信号非常明确,删除
|
||||
2. **整段重复笔误**:A稿【专家7】和【专家8】内容字符完全相同 → 编导笔误,按ASR的真实内容(讲效费比)替换【专家8】
|
||||
3. **术语事实错误的修复**:ASR'美伊→美以'类同音错误 → A稿权威覆盖
|
||||
4. **OCR错字的批量修复**:'肘→时''娈→变''夭→天'等字形混淆 → ASR提供正确版本
|
||||
|
||||
### 2.2 把握较小的判断(demo中AI判断中等确信度,未来需要编导确认)
|
||||
|
||||
1. **解说6"整段改写"的边界**:A稿80字版本和ASR/B稿40字版本,**究竟保留哪个细节合并哪个细节**——目前demo直接采用了ASR的40字版本,但A稿那80字版本里有"信任危机""推迟采购方案"等A稿独有的内容,编导可能想保留。**未来应该让编导在UI上看diff后人工确认**
|
||||
2. **专家发言的口语词处理**:'呢''啊'保留多少?太多影响阅读,太少违背播出真相。本次demo保留较多,倾向"播出真相"。**未来可做个可调档位("完全保留口语 / 中度清理 / 完全书面化"三档)**
|
||||
3. **A稿首句被改写的情况**:A稿"观众朋友们,你们好!欢迎来到这一期的《军事科技》节目",播出时改为了"各位观众,你们好!欢迎收看《军事科技》"——句式接近但用词全变。demo靠手工锚点列表匹配上了,未来要让Claude做语义对齐而非字符匹配。
|
||||
|
||||
### 2.3 把握很小的判断(demo中AI暂时不处理,必须人工确认)
|
||||
|
||||
1. **句子级的微小增删**:如A稿"在涉及远、中、近程"和ASR"在涉及远中近程"——只是顿号差异。这种差异融合时按ASR走、还是按A稿格式来?**未来UI应该展示句子级diff让编导决定**
|
||||
2. **专有名词的简写问题**:A稿严格用"爱国者-3",播出时多数说"爱国者"。这是术语统一问题还是口语化问题?**取决于该期节目的风格定位**
|
||||
|
||||
---
|
||||
|
||||
## 三、对正式开发TPS子模块的启示
|
||||
|
||||
### 3.1 模块架构建议
|
||||
|
||||
```
|
||||
[音频上传] → [ASR服务(讯飞)] → [OCR处理(B稿)] → [三方融合引擎] → [编导确认UI] → [终版docx]
|
||||
↓
|
||||
[大模型API(Claude)]
|
||||
```
|
||||
|
||||
**核心是"三方融合引擎"应当分两层**:
|
||||
- **规则层**:OCR错字纠正(固定映射表)、术语统一(A稿术语权威)、时间戳对齐 — 走Python代码
|
||||
- **AI层**:段落对齐、整段改写判定、'编导笔误'识别 — 走大模型API
|
||||
|
||||
### 3.2 关于讯飞ASR的具体集成建议
|
||||
|
||||
**应该写进project_design.md【外部依赖】章节的内容**:
|
||||
|
||||
1. 优先使用讯飞「录音文件转写(标准版)」API,**不要**用大模型版(免费包阉割了language参数)
|
||||
2. 音频必须先转码为 **16kHz / 单声道 / 16bit WAV**,否则讯飞会报误导性的`language verify fail`错误
|
||||
3. **热词词库**是关键能力:每期节目从A稿提取专业术语作为热词上传给ASR
|
||||
4. **领域参数** `pd=mil` 对军事节目有显著效果
|
||||
5. 开启 `eng_smoothproc=true` + `eng_colloqproc=true` 让ASR输出更接近书面文稿
|
||||
6. 讯飞密钥分两套:消费端(讯飞听见App)和开发端(讯飞开放平台),**互不相通**
|
||||
7. "免费5小时"需要走0元购买流程才激活,不是看到额度就能用
|
||||
|
||||
### 3.3 关于大模型融合提示词的设计
|
||||
|
||||
未来正式开发时调Claude API做段落对齐,prompt应该包含:
|
||||
|
||||
- **输入1**:A稿全文 + 标签序列
|
||||
- **输入2**:ASR结果(句子+时间戳)
|
||||
- **输入3**:B稿OCR结果(句子+时间戳,作为交叉验证)
|
||||
- **任务1**:把ASR句子按A稿标签序列归类
|
||||
- **任务2**:对每段标记改动类型(无变化/细微改写/整段改写/整段删除/整段新增)
|
||||
- **任务3**:对'高把握'改动直接产出终版;对'低把握'改动标记原因,等编导确认
|
||||
- **任务4**:识别A稿术语和ASR结果的字形/同音差异,用A稿权威修正
|
||||
|
||||
### 3.4 关于编导确认UI的设计
|
||||
|
||||
借鉴本次demo发现的痛点,UI应该至少提供:
|
||||
|
||||
- **左中右三栏diff视图**:A稿原文 | 终版 | ASR/B稿(带时间戳,可点击播放对应音频片段)
|
||||
- **改动类型颜色编码**:删除红色 / 改写黄色 / 术语修正蓝色 / 无变化白色
|
||||
- **逐段确认按钮**:接受 / 拒绝 / 编辑 / 标记疑问
|
||||
- **口语清理强度滑杆**:让编导决定保留多少口语词
|
||||
- **导出按钮**:终版docx / 差异报告 / 复盘CSV
|
||||
|
||||
---
|
||||
|
||||
## 四、实测耗时分解(用于未来效率评估)
|
||||
|
||||
| 阶段 | 实测耗时 | 主要消耗 |
|
||||
|---|---|---|
|
||||
| 讯飞ASR申请+踩坑 | 约45分钟 | 大模型版语种权限问题、音频规格转码 |
|
||||
| 老版讯飞ASR调通 | 约15分钟 | 申请+脚本+调用 |
|
||||
| 三方解析 | 约5分钟 | A稿/B稿/ASR解析为统一结构 |
|
||||
| 段落对齐 | 约15分钟 | 锚点匹配算法+单调推进 |
|
||||
| 术语规范化+口语清理 | 约5分钟 | 字符级规则映射 |
|
||||
| 终版docx生成 | 约5分钟 | python-docx写文档 |
|
||||
| 差异报告+复盘 | 约15分钟 | 这份报告本身 |
|
||||
|
||||
**总耗时约2小时**,其中近一半花在讯飞踩坑上——正式开发后这部分会清零,每期实际处理时间预计15-30分钟(取决于改动幅度)。
|
||||
|
||||
---
|
||||
|
||||
## 五、给您(刘统制片)的几点直接建议
|
||||
|
||||
1. **TPS正式开发时,不必把ASR作为强依赖**。本次demo证明,即便没有ASR,A稿+B稿做二方融合也能产出接近播出版的终版。ASR的价值在于"独立第三方仲裁"——值得做但不应阻塞主流程。
|
||||
|
||||
2. **A稿术语表应该入库管理**,作为TPS的核心知识资产之一。每期节目处理时都从该表查找"权威术语",自动覆盖ASR的同音错误。本次demo手工列出的35个热词清单是个起点。
|
||||
|
||||
3. **节目编导习惯的差异要预留可配置项**。比如有的编导喜欢保留口语词、有的喜欢全部规整化;有的喜欢保留专家发言的原始措辞、有的喜欢编辑过的书面版本。这些应该在配置文件里,而不是硬编码。
|
||||
|
||||
4. **差异报告的价值可能比终版docx更大**。每期节目处理完,差异报告其实是个"编导工作回顾"——能看到自己当初做了哪些删改、哪些是OCR错字。长期累积下来可以分析编导的删改偏好,作为节目优化的素材。
|
||||
|
||||
5. **本次踩的坑值得专门整理一个"讯飞集成踩坑笔记"文档**入TPS知识库。未来下次开发新供应商接入时(比如阿里云ASR),可以走更顺的路。
|
||||
|
||||
---
|
||||
|
||||
*本复盘为demo阶段产物,不代表正式开发的最终决策。所有结论以下次设计文档为准。*
|
||||
Reference in New Issue
Block a user