82 Commits

Author SHA1 Message Date
simonkoson 1c3963d17c doco: v2说话人分段模式 — ASR说话人分离+大block拆分+三维动画解说识别
- asr_adapter: 新增roleType=1说话人分离参数,新增parse_order_result_with_speaker(),write_asr_result自动输出asr_v2_timed_spk.txt

- fusion_align: 新增speaker-aware alignment v2流程(_annotate_b_lines_with_speakers区间匹配、_detect_speaker_blocks、SYSTEM_PROMPT_SPEAKER_ALIGN大block拆分prompt、_build_broadcast_segments支持block内多段拆分)

- cli: 兼容v1/v2 stats字典

- 新增convert_to_md.py(20期融合A稿docx转md+YAML frontmatter)

- backup_before_spk/: 修改前代码备份
2026-06-24 16:26:05 +08:00
simonkoson a87f453326 doco: ep002全流程完工(P1→C4) + CLAUDE.md收摊更新
ep002(潜艇仿生)全部产物入库:B稿v2(733行)/ASR(411句)/融合B稿/
融合A稿.docx/c4_alignment.csv等。fusion_align.py修复align_batch
崩溃bug。CLAUDE.md更新ep002完工状态、新增分段偏差根因和MiMo批次
失败率两条关键决策。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-23 08:15:41 +08:00
simonkoson 010128b123 doco: 更新CLAUDE.md — ep002进度至C3完成/C4跑中
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-22 14:37:52 +08:00
simonkoson e213feb814 doco: 更新CLAUDE.md — ep002进度至C2、LLM切MiMo、Ollama并发配通
状态栏/已完成/待办/关键决策/交接备注全部增量更新。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-22 12:59:48 +08:00
simonkoson 7c51498d95 doco: LLM提供方从DeepSeek切换到小米MiMo 2.5 Pro
环境变量名 DEEPSEEK_* → LLM_*(通用化),默认模型改为 mimo-v2.5-pro。
涉及 llm.py / term_extract.py / video_split.py 三文件,纯重命名,逻辑不变。
API已验证连通。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-22 09:51:07 +08:00
simonkoson 994c57edbb doco: 修复C4标点层prompt偏保守(88%逗号→76%),重跑ep003出稿
标点prompt加第5条规则:明确要求陈述句结尾用句号,附错误/正确示范。
strip_punct硬校验兜底不变,汉字零改红线未破。
ep003句号从70涨到118,逗号占比从88%降至76%。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-22 09:35:05 +08:00
simonkoson 8df9c798e7 doco: ep003 全流程跑通(P1→C4)+ OCR并发版 + compose出稿改名 2026-06-18 20:06:45 +08:00
simonkoson 10b0ab757b doco: C4收口 + A稿解析器升级(LLM骨架) + ep002/003/004骨架
- 新增 fusion_align.py(C4 compose + skeleton 提取/校验/预览)与 doco skeleton 命令
- ep001 C4 产物入库: 融合A稿.docx / 融合B稿.txt / c4_alignment.csv / fusion_review.csv
- ep002/003/004: A稿 + 分段骨架(json+预览), 上游待跑
- .gitignore 增 .c3_cache/.c4_cache (可重生中间缓存不入库)
- 更新 doco/CLAUDE.md 交接备注
2026-06-18 14:36:39 +08:00
simonkoson 60f22e6660 P3-C3: 交叉复审 fusion_review.py + doco fuse 命令,产融合B稿743行+fusion_review留痕,更新CLAUDE.md交接C4 2026-06-17 13:27:41 +08:00
simonkoson 0a4ab881bc doco: 更新 CLAUDE.md — C2 推送后解除 C3 卡点(B稿v2 已就位)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-17 10:23:18 +08:00
simonkoson 1372600d71 doco P3-C2: 讯飞ASR适配层接入 + 修复 doco/programs 忽略规则
- C2: asr_adapter 并入讯飞上传/轮询/解析,复用 extract_audio 分离音频,
  新增 doco asr 命令读 C1 热词;真转写 ep001 出 310 句带时间戳
- 补提交 C1 代码(llm.py / term_extract.py)与累积词典
- .gitignore: 挡 doco/programs 下 png/wav/mp4 及中间帧目录、本地 settings

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-17 10:19:08 +08:00
simonkoson 487877fa08 fix: 修复 test_subtitle_frame 切片比例(10%->1%) 和 test_ratio_ok_but_max_brightness_too_low 断言(适配阈值240) 2026-06-15 12:13:58 +08:00
simonkoson 7470dbb0ed fix: 修复去重统计缺少 +1 导致 duplicate_iou/duplicate_hash 始终为 0 的 counting bug 2026-06-15 12:11:14 +08:00
simonkoson 0d52f9b707 fix: 修正测试导入路径从 doco.src 改为 doco (配合 pyproject.toml package-dir 配置) 2026-06-15 12:00:46 +08:00
simonkoson bf93213990 fix: extract_frames frame_index 从 0-based 改为文件名 1-based,修复清理/自检 off-by-one 索引错位导致空白帧无法被正确删除 2026-06-15 11:58:28 +08:00
simonkoson 513ccb34d4 fix: is_blank_frame双条件检测 + CSV列错位修复 + 自检逻辑 + 单元测试 2026-06-12 18:48:29 +08:00
simonkoson d14bcc2778 fix: 提高空白帧检测阈值到240 + CSV增加max_brightness列 2026-06-12 18:32:00 +08:00
simonkoson 8cb0cbfa25 fix: frames/ 目录只保留关键帧 PNG + 修复 stats 计数错误 + 修正 debug 预期值 2026-06-12 18:20:09 +08:00
simonkoson f91efef935 feat: 更新 CLI 支持 dhash/dhash_threshold/iou_threshold 参数,默认 dhash 算法 2026-06-12 18:06:33 +08:00
simonkoson 47e17179c7 perf: 用 numpy 加速 is_blank_frame/compute_binary_matrix/compute_iou 三个像素遍历函数 2026-06-12 18:04:57 +08:00
simonkoson a826212302 fix: doco 关键帧过滤 - dHash算法 + IoU保底 + 诊断CSV + 474vs374排查 2026-06-12 17:56:30 +08:00
simonkoson b138b98bc4 feat: doco 抽帧过滤优化 - 空白帧过滤 + pHash 阈值调至 5 + 统计输出 2026-06-12 17:38:12 +08:00
simonkoson 89d59f6f03 fix: 完成 doco src layout 重构,文件移至 src/doco/ 子目录 2026-06-12 17:13:32 +08:00
simonkoson 0508bd8f23 fix: 修复 doco src layout 的 pyproject.toml 配置 2026-06-12 17:06:10 +08:00
simonkoson aa59ef78e0 feat: doco P1 收尾(字幕裁切写死/dry-run/crop_params完善) 2026-06-12 16:38:41 +08:00
simonkoson 0e6322ad97 feat: 初始化 doco 子项目 P1 阶段(视频双路拆分预处理) 2026-06-12 16:31:21 +08:00
simonkoson 70e2647fae chore: TPS 主项目素材积累(海报 logo + 样本 md) 2026-06-11 18:35:01 +08:00
simonkoson 202e02c254 feat(ai-labeling): AI 打标实验工作区初始化 + 模型选型完成 2026-06-11 18:34:55 +08:00
simonkoson 9a86c936ee chore: gitignore 忽略前端测试题图截图 2026-05-27 19:53:26 +08:00
simonkoson 9acb9d380d docs: 新增 claude 协作原则文件,backlog 待办池入库 2026-05-27 19:51:41 +08:00
simonkoson 47c9d60f02 docs: phase3_log 收尾,Task 3/4 记录归位,删除合并后遗留散文件 2026-05-27 19:51:31 +08:00
simonkoson cd4dcd06f8 docs: 宪法升级至 v5,TPS 定位为对话式策划助手 + README 进度校准 2026-05-27 19:51:23 +08:00
simonkoson 783e212bb1 feat: 知识库朴素语义搜索(输入→检索→结果列表) 2026-05-27 19:25:41 +08:00
simonkoson 0c7d2d7400 fix: type_groups动态收集,空大类从根上不产生节点 2026-05-27 15:00:52 +08:00
simonkoson 70784fc70b fix: 过滤按大类用对应字段比对,修复左鑫节点无法筛选问题 2026-05-27 14:53:11 +08:00
simonkoson 347a06e48b fix: 回退Grid布局恢复flex并列,保留二级分组映射,空大类隐藏 2026-05-27 14:35:32 +08:00
simonkoson fff2cfe395 fix: 知识库key格式统一、过滤逻辑修复、布局宽度稳定 2026-05-27 14:06:16 +08:00
simonkoson a00bcd6c73 feat: 知识库树形视图调整:大类固定排序、节目文稿按作者二级分组、左右Grid双列对齐 2026-05-27 13:54:14 +08:00
simonkoson b3157216bb style: 知识库页面三处样式收尾:内容区扩展、隐藏入库时间列、上传区收小 2026-05-27 13:21:04 +08:00
simonkoson d849bc1539 fix: 修复知识库页面三处问题:出处下拉为空、宽度跳动、表格列宽优化 2026-05-27 13:06:06 +08:00
simonkoson 74a0508755 feat: 知识库树形视图(按来源分组),左侧树导航+右侧列表联动 2026-05-27 09:35:36 +08:00
simonkoson 7999fd14c8 docs: 更新 phase3_log Task 2 完工记录 2026-05-26 18:59:39 +08:00
simonkoson 5ef1b8ec8d feat: 知识库管理前端页面,含上传/列表/筛选/删除 2026-05-26 18:52:34 +08:00
simonkoson 855f103ce8 feat: 知识库管理后台上传/列表/删除API,含frontmatter解析 2026-05-26 18:51:12 +08:00
simonkoson d81969b4b5 feat: 添加 python-frontmatter 依赖用于解析 md yaml frontmatter 2026-05-26 18:48:48 +08:00
simonkoson 04a35faba3 chore(phase3): 补提交 Task1 日志/清理脚本 + 栏目logo,忽略密码重置脚本 2026-05-26 15:24:31 +08:00
simonkoson 1325807257 feat(phase3): Task1 embedding链路验证 - embo-01(1536维)+pgvector检索打通 2026-05-26 10:33:25 +08:00
simonkoson d40d46a434 fix: TargetOutlined换为LineChartOutlined避免白屏 2026-05-25 14:23:44 +08:00
simonkoson 2881b866a6 feat: KPI细条新增两项完成率 + 侧边菜单加两个占位项 2026-05-25 13:45:00 +08:00
simonkoson 72ae73764a feat: 仪表盘视觉优化-居中/logo放大/柱图数字挪顶/节目名加长 2026-05-25 13:38:33 +08:00
simonkoson 51a34e9ec6 feat: 添加栏目 logo 素材 2026-05-25 09:56:15 +08:00
simonkoson 303995a93f fix: dashboard.py 顶层 import CoverSettings,解决运行时 500 2026-05-25 09:55:05 +08:00
simonkoson d774370a4b feat: 仪表盘布局重排,新增题图上传能力 2026-05-25 09:47:21 +08:00
simonkoson a6e8cc25ec feat: 新增题图上传接口和排播接口 2026-05-25 09:44:34 +08:00
simonkoson 8299b6bbc9 feat: 新增 cover_settings 表用于仪表盘题图设置 2026-05-25 09:42:25 +08:00
simonkoson 02bf4c4fe4 docs: phase2_log 第十三~十四章——Task 3 收官 + Phase 2 整体收官 2026-05-22 19:28:14 +08:00
simonkoson d55fde7fb5 chore: .gitignore 新增 cookies*.txt 忽略规则 2026-05-22 18:50:35 +08:00
simonkoson a2f1710a7f style: 柱图三色换成降饱和柔和版,柔蓝/柔绿/柔红,图例同步 2026-05-22 18:44:14 +08:00
simonkoson 59f556d986 fix: episodes GET 接口放开给三角色,POST/PATCH/DELETE 保持仅 zhipianren/zebian 2026-05-22 18:27:28 +08:00
simonkoson fcaa0969a8 fix: AppLayout 未挂载——路由改为两层 Outlet 嵌套,AuthGuard 不再包裹 AppLayout 2026-05-22 18:15:31 +08:00
simonkoson f55030b971 fix: SideNav 侧边栏不显示,.app-sider 去掉 fixed 改 flex 布局 2026-05-22 13:04:45 +08:00
simonkoson 24b9bb2d08 fix: 三个必修 bug——Dashboard 颜色判反+legend 错+SideNav zhipianren 用户管理漏显 2026-05-21 20:45:48 +08:00
simonkoson 247268a60e fix: Dashboard.jsx 修复 limit 未定义 ReferenceError 2026-05-21 20:29:50 +08:00
simonkoson f0f9dc493e feat: UserManage 完整用户 CRUD,创建/编辑/停用,角色下拉用拼音值 2026-05-21 19:47:54 +08:00
simonkoson e0ccfb20e7 feat: Dashboard 接真 API,题图渐变占位,排播占位,近9期收视柱图,颜色按年度目标判定 2026-05-21 19:47:36 +08:00
simonkoson 4eca31f8eb feat: SideNav 按 role 过滤菜单,/users 仅 zhipianren,/editor-desk 仅 zhipianren+zebian 2026-05-21 19:47:21 +08:00
simonkoson 3942f01def feat: App.jsx 加入 RoleGuard,/users 仅 zhipianren,/editor-desk 仅 zhipianren+zebian 2026-05-21 19:47:01 +08:00
simonkoson b7560a6cf6 feat: 添加三个 service 文件和 RoleGuard 组件 2026-05-21 19:46:40 +08:00
simonkoson 7cc0bf21ff feat: yearly_targets 写权限收紧,12步冒烟全绿 2026-05-20 11:29:28 +08:00
simonkoson 93fd327b13 docs: phase2_log 追加 Task 2 收尾记录(2026-05-20) 2026-05-20 10:58:36 +08:00
simonkoson 932985feca chore: backups/ 进 .gitignore(Task 2 收尾) 2026-05-20 10:41:03 +08:00
simonkoson ff0471a6be docs: Task 2 完工记录,8步全绿,坑7/8/纪律3/Q1-Q4澄清写入phase2_log 2026-05-19 20:29:30 +08:00
simonkoson f4661863a7 feat: 完善批量导入逻辑,8步冒烟全绿 2026-05-19 20:05:15 +08:00
simonkoson 14c9841b35 docs: phase2_log 追加 Task 2 进行中坑与纪律警告 2026-05-18 11:27:21 +08:00
simonkoson c9e527ff5b chore: 补录 002a 诊断脚本入库,修正 git_workflow.md 1.1 节路径 2026-05-18 11:00:59 +08:00
simonkoson 2480d4a738 feat: Task 2 Excel 批量导入 API 完成(模板下载 + 批量导入 + 失败行下载) 2026-05-15 13:40:13 +08:00
simonkoson 9439c72d30 fix: 002b 改用 date_part 修复表达式索引语法,002c 补救专用 2026-05-15 13:37:38 +08:00
simonkoson 3102fbec6c chore: 新增 pandas + openpyxl 依赖(Task 2 Excel 批量导入用) 2026-05-15 13:23:24 +08:00
simonkoson b3667aa7b9 fix: 修复 episodes + yearly_targets 模型导入缺失,增 DELETE 接口,冒烟 6/6 全绿 2026-05-15 13:05:58 +08:00
simonkoson 773441bb87 docs: Task 1 完工记录,Swagger UI 验收清单,跨 Phase 遗留事项 2026-05-15 12:36:11 +08:00
simonkoson 3dc1a1cf3c feat: 后端 CRUD 完成(episodes + yearly_targets + users 管理 API) 2026-05-15 12:17:46 +08:00
simonkoson fab15505bd docs: 合入第十三章 Opus task brief 节制原则,版本升至 v5 2026-05-15 10:27:13 +08:00
208 changed files with 55382 additions and 336 deletions
+24 -5
View File
@@ -2,7 +2,7 @@
> 项目:TPS 中台(央视《军事科技》栏目内部工作台)
> 仓库:`tps-dashboard`(Gitea,团队可见)
> 修订版本:v4 · 2026-05-14
> 修订版本:v5 · 2026-05-15
> 适用对象:Cline + MiniMax M2.7(Plan + Act),所有协作者必读
---
@@ -25,7 +25,7 @@
| 角色 | 模型 / 工具 | 职责 |
|---|---|---|
| 制片人 | 刘通(人) | 拍板、审核、领域知识权威、视觉把关 |
| 制片人 | 刘通(人) | 拍板、审核、领域权威、视觉把关 |
| 高级技术顾问 | Claude Opus 4.7(claude.ai 网页) | 架构指导、方案审核、风险预警、Cline 卡壳兜底 |
| 架构师 | MiniMax M2.7(Cline Plan 模式) | 项目内方案设计、Act 改动审查 |
| 执行者 | MiniMax M2.7(Cline Act 模式) | 实际写代码 |
@@ -110,7 +110,7 @@ Phase 内的子任务、修复独立 bug、前端骨架、后端骨架——都
### 5.2 年度目标只增不改(时间不变性)
`yearly_targets` 表每年新增一行,**历史年份的目标行绝不可被覆盖或删除**
`yearly_targets` 表每年新增一行,**历史年份的目标行绝不可被覆盖或删除**:
- 前端展示某期数据时,**根据该期播出年份的目标判颜色**,不是"当前年份的目标"
- 2024 年播的节目永远按 2024 年的 0.6448 / 0.8989 判,2026 年回看也不会变色
@@ -124,7 +124,7 @@ SQL 端:`yearly_targets.year` 加 UNIQUE 约束,代码端 INSERT 时用 `ON CONF
- **在职编导**:两字段都填(冗余但稳)
- **离职编导**:`editor_id` 留空(NULL),只填 `editor_name_snapshot`(如"张颖")
- **历史导入**:按姓名匹配在职用户,匹配不上只填快照
- **历史导入**:按姓名匹配在职用户,匹配不上只填快照,不报错不丢数据
- **查询某编导历史作品**:**先按 editor_id,失败回退按 editor_name_snapshot 文本匹配**
DeepSeek 容易写"editor_id 外键非空 + 删人级联"——**禁止**。一旦张颖离职,她做过的几十期节目就全成"无主"了,这是栏目最不能接受的。
@@ -147,6 +147,11 @@ AI Prompt 模板必须默认走双段、备选纯叙述。docxtpl 模板设计
| `zebian` | 责编 | 维护仪表盘 / 知识库 / 排期 |
| `biandao` | 编导 | 使用 TPS / 查看知识库 / 生成报题单 / 个人热点雷达 |
**yearly_targets 写权限收紧(2026-05-20)**:
- POST/PATCH: zhipianren + zebian 可写,biandao 返回 403
- DELETE: 仅 zhipianren 可操作,zebian 和 biandao 返回 403
- GET: 三角色均可读
**所有 SQL CHECK、SQLModel Enum、API 鉴权、前端权限判断、菜单显示**——一律使用拼音值,**不得替换为英文**。
---
@@ -367,11 +372,25 @@ Cline 内的 Sonnet/DeepSeek 不直接与 Opus 通信,流程如下:
| v1 | 2026-04 | 初版,定模型分工与栏目业务约定 |
| v2 | 2026-05-04 | 加技术栈精简版、Phase 拆分 |
| v3 | 2026-05-06 | 加 Git 工作流、已知避坑清单、单位机重启路径;MiniMax M2.7 Phase 2 起接班 Plan 模式(替代 Sonnet),DeepSeek pro 全程在岗 |
| **v4** | **2026-05-14** | **家用机退役,唯一开发环境为单位 4090D(E:\tps-dashboard);Phase 2 起 MiniMax M2.7 正式日常使用;config.py 改为 dotenv 直接加载,pydantic-settings 从 requirements.txt 移除** |
| v4 | 2026-05-14 | 家用机退役,唯一开发环境为单位 4090D(E:\tps-dashboard);Phase 2 起 MiniMax M2.7 正式日常使用;config.py 改为 dotenv 直接加载,pydantic-settings 从 requirements.txt 移除 |
| **v5** | **2026-05-15** | **合入第十三章"Opus 4.7 起草 task brief 的节制原则";Phase 2 正式开工** |
**修订原则**:.clinerules 是宪法不是变更日志,改动应慎重。临时执行细节进 phase{N}_log.md,不进本文件。
---
## 十三、Opus 4.7 起草 task brief 的节制原则
Opus 4.7 给 MiniMax 写 task brief 时遵守:
- **一屏内为目标**(约 60-80 行,不超过 100 行)
- **写四类东西**:任务范围、核心约束、业务/纪律红线、验收清单
- **不写四类东西**:代码片段、目录结构细节、变量名、字段命名建议
- 把"怎么实现"留给 MiniMax Plan 模式自己长出来,Opus 只审 Plan 不替 Plan
- 自检口诀:这条是品味判断还是代码细节?品味判断留,代码细节砍
- 例外:业务红线里涉及具体字段名(如 `audience_share` / `editor_id` / `editor_name_snapshot` / `yearly_targets.year`)必须点名,因为写错字段名就是写错业务
---
*本文件最高优先级。任何冲突以本文件为准。*
*下次修订建议时机:Phase 2 末(评估 MiniMax 切换、整理 Phase 2 经验)。*
+52 -1
View File
@@ -29,11 +29,62 @@ Thumbs.db
# 日志运行产物
*.log
cookie.txt
cookies*.txt
# 临时文件
*.tmp
*.bak
homePC/
# Syncthing (极空间 NAS 同步工具产物)
# Syncthing (本机 NAS 同步)
.stignore
# 备份 / SQL dump
backups/
# 敏感脚本(永不提交)
backend/scripts/reset_password.py
# 前端素材
frontend/src/assets/*.jpg
frontend/src/assets/*.jpeg
frontend/src/assets/*.png
# Doco 素材目录(视频不进 git)
programs/*/source/video.*
programs/*/source/*.mp4
programs/*/source/*.mov
programs/*/work/
# Doco 子项目 programs:大文件/中间帧/原始素材不入库(可重生成,开发完即删)
# 只保留小文本成果(B稿/asr_timed/热词表/留痕csv-json/代码py/A稿docx)
doco/programs/*/frames_v2/
doco/programs/*/work/
doco/programs/*/work_dryrun_test/
doco/programs/*/source/
doco/programs/*/*.mp4
doco/programs/*/*.mov
doco/programs/*/*.wav
doco/programs/*/*.png
# Doco .env
doco/.env
# Claude Code 本地个人设置(权限等),不入库
**/.claude/settings.local.json
# Python build artifacts
*.egg-info/
# Doco AI 断点缓存(可重生,不入库)
doco/programs/*/.c3_cache/
doco/programs/*/.c4_cache/
# Doco 临时核验/草稿 & Word 锁文件 & 手批稿(不入库)
~$*.docx
doco/programs/*/_cropcheck/
doco/programs/*/_seg_check.txt
doco/programs/ep002_004/
doco/_*.py
doco/verify_out.txt
doco/programs/*/*_批改*.docx
+25 -4
View File
@@ -47,10 +47,11 @@
| Phase | 状态 |
|---|---|
| Phase 0 环境 + Git | 单位机进行中 |
| Phase 1 后端骨架 | 家用机已完成,单位机走"路径 B 迁移" |
| Phase 1 前端骨架 | 未开始 |
| Phase 2-5 | 未开始 |
| Phase 0 环境 + Git | ✅ 已完成 |
| Phase 1 后端骨架 + 前端骨架 | ✅ 已完成 |
| Phase 2 仪表盘 + 批量导入 + 写权限收紧 | ✅ 已完成 |
| Phase 3 知识库基础设施 | 进行中 — 已完成至 Task 3(知识库树形视图按来源分组),语义搜索尚未做 |
| Phase 45 | 未开始 |
---
@@ -68,6 +69,25 @@
---
## 角色权限表
> 数据来源:`.clinerules` 5.5 节 + `project_plan.md` 第四节
| 资源 / 操作 | zhipianren(制片人) | zebian(责编) | biandao(编导) |
|---|---|---|---|
| **yearly_targets** | | | |
| GET 列表/详情 | ✅ | ✅ | ✅ |
| POST 新增 | ✅ | ✅ | ❌ 403 |
| PATCH 修改 | ✅ | ✅ | ❌ 403 |
| DELETE 删除 | ✅ | ❌ 403 | ❌ 403 |
| **episodes** | | | |
| GET | ✅ | ✅ | ✅ |
| POST/PATCH/DELETE | ✅ | ✅ | 受限(见 API 文档) |
> 规则:.clinerules 5.5 角色枚举锁定拼音值(zhipianren / zebian / biandao),不许新增枚举。
---
## 技术栈速览
- 前端:React 18 + Vite + Ant Design 5 + Zustand(JavaScript,不用 TS)
@@ -168,6 +188,7 @@ uvicorn app.main:app --reload
|---|---|---|
| v1 | 2026-05-06 | 初版,作为单位机仓库门牌 |
| **v2** | **2026-05-14** | **家用机退役,唯一开发环境为单位 4090D;Phase 2 起 MiniMax M2.7 正式日常使用;更新版本演进记录** |
| **v3** | **2026-05-27** | **进度校准:Phase 1/2 标为完成;Phase 3 Task 3 知识库树形视图已完成;清理"家用机/路径B"历史表述,不再保留迁移叙事** |
---
+6
View File
@@ -0,0 +1,6 @@
# 复制此文件为 .env,填入真实 API key
MINIMAX_API_KEY=your_key_here
# 以下两个 Step 3-4 阶段才用:
DEEPSEEK_API_KEY=
MIMO_API_KEY=
XIAOMI_MIMO_API_KEY=
+5
View File
@@ -0,0 +1,5 @@
.env
experiments/*.json
__pycache__/
*.pyc
.venv/
+44
View File
@@ -0,0 +1,44 @@
# AI Labeling 实验工作区
## 用途
为《军事科技》栏目 AI 自动打标做模型选型实验。
基准答案集 = 10 期已审稿的标准答案。
## 目录结构
- `benchmark-set/transcripts/` 10 期文稿
- `benchmark-set/ground-truth.json` 10 期的标准答案
- `prompts/` prompt 模板
- `scripts/` 实验脚本(含 import_transcripts.py
- `experiments/` 每次跑出来的结果(不入 git)
## 使用步骤
### 1. 安装依赖
```bash
pip install -r scripts/requirements.txt
```
### 2. 配置 API Key
```bash
cp .env.example .env
# 填入真实 MINIMAX_API_KEY
```
### 3. 导入文稿
```bash
python scripts/import_transcripts.py
```
> 将源目录(刘瑞桦收集)的 10 期 docx 文稿解析并清洗,落入 `benchmark-set/transcripts/`。
### 4. 跑单期打标
```bash
python scripts/run_labeling.py --ep 4 --model m3
```
## 当前版本
- v0.1: Prompt 2(叙事结构)含 3 示例 + 10 期 ground-truth
- 阵容: MiniMax M3 (Step 1-2) / DeepSeek V4 / 小米 MiMo v2.5 (Step 3-4)
## ground-truth 版本
- `v0.1` (2026-06-11): 锁定 10 期 `narrative_structure` + `opening_hook` 标准答案
- 其他字段(program_format / equipment_domain / scene_tags / tech_tags)待 Prompt 1 阶段补
@@ -0,0 +1,19 @@
{
"version": "v0.2",
"locked_at": "2026-06-11",
"locked_by": "制片人刘通 + Opus 复议(二次审定)",
"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 三家独立模型一致判并列结构,经制片人复读稿确认后改为并列结构",
"episodes": [
{"ep": 3, "title": "硅基大脑", "share": 0.85, "narrative_structure": "并列结构", "opening_hook": "中"},
{"ep": 4, "title": "潜艇仿生", "share": 0.92, "narrative_structure": "主线演进", "opening_hook": "强"},
{"ep": 5, "title": "舰证不凡", "share": 0.72, "narrative_structure": "并列结构", "opening_hook": "中"},
{"ep": 7, "title": "马年军事图鉴", "share": 0.95, "narrative_structure": "并列结构", "opening_hook": "强"},
{"ep": 10, "title": "射速决定论", "share": 0.703, "narrative_structure": "主线演进", "opening_hook": "弱"},
{"ep": 11, "title": "空战颠覆者", "share": 0.859, "narrative_structure": "主线演进", "opening_hook": "中"},
{"ep": 12, "title": "逆袭战局", "share": 0.85, "narrative_structure": "并列结构", "opening_hook": "强"},
{"ep": 13, "title": "长短智慧", "share": 0.533, "narrative_structure": "并列结构", "opening_hook": "中"},
{"ep": 14, "title": "X-76 飞行器", "share": 0.873, "narrative_structure": "并列结构", "opening_hook": "中"},
{"ep": 15, "title": "空中坚盾", "share": 0.875, "narrative_structure": "主线演进", "opening_hook": "强"}
]
}
@@ -0,0 +1,185 @@
《当武器装备长出“硅基大脑”》
【导视】
当战斗机拥有思考能力,当坦克集群学会自主协同,当深海杀手实现无人化猎杀——人工智能正在重塑武器装备的神经中枢。从有人装备改装到无人系统创新,本期《军事科技》带您拆解AI如何为传统装备注入智能基因,为您揭秘全球AI军事化浪潮下的尖端装备。
【主持人1】
2025年4月,伴随第六架改装的F-16抵达佛罗里达州埃格林空军基地,美军“毒液”项目完成了机队组建。这个类似漫威超级英雄名字的项目,其背后实则是一项把人工智能引入空战领域的计划。那么,“毒液”项目究竟是什么?这一波操作,到底是噱头,还是真的能够改变未来空战的格局?
【解说】
2024年,美军启动“毒液”(VENOM)项目,为现役F-16 Block70机型加装AI自主决策系统。这套系统由洛马公司研发,核心是将传统火控计算机升级为具备机器学习能力的战术中枢,目标是验证有人-无人协同作战模式。
为了检验人工智能在真实战斗环境中的表现,美国军方选择了广泛服役且技术成熟的F-16战斗机。与将F-16改装为QF-16无人靶机不同,毒液计划核心目标是评估人工智能在复杂空战环境下的自主作战效能,而非仅仅扮演被动挨打的目标。是在真实的战斗机上安装人工智能“驾驶员”。截至目前,已经有6架F-16被改装并组成“毒液”测试中队。
那么融入了人工智能的这个战斗机在作战的过程当中,它的这种反应速度也会变得更快,此外呢信息的处理能力会变得更强。也就是说呢,美国空军是要利用这个F-16战斗机作为人工智能这种AI飞行员的这样的一个验证平台。那么在实际测试的过程当中,F-16战斗机融入了人工智能,也就是AI系统之后,它空中作战的这个能力是全方位的提升,因为它是有庞大的数据库和这种空战模式的样本来作为一个支撑,那么无论是在对抗性的训练的过程当中,还是在联合执行任务的过程当中,它都表现出了强大的技术优势。
【解说】
作为全球装备数量最大的四代机,F-16 Block70机长15.03米,最大速度马赫数1.8,载弹量7.8吨,配备AN/APG-83有源相控阵雷达。其经典设计历经40年迭代,此次改装并未改变机身结构,而是通过软件定义实现“战力倍增”。
据悉,AI系统使F-16的目标处理能力从同时跟踪4个目标提升至16个,威胁排序耗时从2.3秒压缩至0.1秒。在演习中,改装机首次实现双机编队对抗六机集群,交换比达1:3.2,展现出以少胜多的智能优势。
从目前来看呢,就是毒液项目主要是要让美国的这个空中的作战体系变得更加的智能化。不过呢现在呢只是在模拟的环境当中,针对人工智能的操控系统进行验证。那么还是按照美军的训练模式,以及美军飞行员的这样的一个操作战斗机的这样的一种样本模式,去进行这种融合的这样的一个作战。所以说呢,它的学习目标以及样本库和数据库相对而言是比较单一的。那如果说美国空军要把毒液计划作为一种实战化的软件系统,和它现有的战斗机进行融合,当它面对对手的时候,可能就会出现不适应,因为都是利用美军的这种训练成果以及相关的这个数据,那么我认为它的指向性和针对性,目前呢还是无法满足实战需求,还是处于测试或者说呢处于数据搜集和这种样本学习的状态。
【主持人2】
从毒液项目的F-16改装案例中,我们已经看到AI如何让传统战机突破人类决策瓶颈,将战术响应压缩至毫秒级别。当AI从辅助工具进化为决策中枢,空战正在经历从飞行员主导到算法博弈的转移。
如今,世界各国的科研人员都试图给武器装备装让智慧大脑。接下来,我们就去看一看这场硅基大脑与钢铁身躯的融合,正在打开怎样的战争新维度。
隔断1:空战革命之自主空战先锋——X-62A VISTA验证机
解说:
美军毒液计划的背后,离不开一个关键的技术验证平台——X-62A战斗机。X-62A与“毒液”项目同步进行。“毒液”项目主要聚焦于“人-机协作”,而X-62A计划则专注于“AI独立作战”。这架由F-16D改装的“飞行实验室”,凭借开放式架构和洛马公司的模型跟踪算法,成为破解AI空战控制难题的先锋。
X-62A呢大家从外观上看和一架普通的双座版本的F-16D型的战斗机没有太大的区别。当然了,它是采用了这种比较特殊的涂装。它并不是真正的战斗机,而是美国空军用于测试人工智能的这样的一个平台。大家都知道以前人工智能,它主要是在无人机的这个平台上去进行这个融合使用,主要执行的是简单的空中侦察,或者说针对地面目标进行打击的任务。那么相对而言呢,这个任务并不是特别复杂,也不会出现太多的变量。但是呢和F-16这个平台进行融合之后,美国空军主要是想利用人工智能系统,也就是AI的这样的一个控制系统和这个具备很强空战能力的这种主力的战斗机进行结合。那么之所以要进行这样的一个验证,那么就是要利用美国空军大批量装备对的F-16,那么去验证人工智能飞行员在近距离空中格斗的这个过程当中,它的价值。
解说:
就在去年,美国空军部长弗兰克·肯德尔亲自试驾了美国防部正在研制的X-62A人工智能战斗机,在一小时的飞行过程中,肯德尔全程未干预,所有动作都由AI自主完成。X-62A人工智能战斗机又被称为VISTA(维斯塔),是可变稳定性飞行模拟器测试飞机的缩写,是由F-16D战机经过大量改装而来。相比常规的F-16D战斗机,X-62A技术验证机最大的不同是,它使用了空战人工智能软件来操纵飞机的飞控计算机,控制飞机在空中做出各种机动动作。
此次由美国空军部长亲自搭乘的X-62A人工智能战斗机,与另一架由人类飞行员驾驶的战斗机进行了一场一对一模拟空战演练。最接近的时候,两架战机仅相距大约300米。在飞行结束的采访中,肯德尔表示,经过飞行,他愿意相信这个仍在学习的AI,有能力决定是否在空战中发射武器。
为什么人工智能操控的F-16战斗机,它的技术层面具有优势性呢?那么这主要是人类飞行员的生理承受能力所导致的。我们讲像F-16的王牌飞行员在做出大过载机动动作的时候,他会面临六到七个G的这样的一个过载,但是如果说长时间连续地做出大过载的这个机动动作,战斗机可能还是可以承受的。但是飞行员的生理的这样的一个人体的承受极限,那么其实是已经到达了这个红线的这样的一个标准。如果说飞得更快一些,或者说做出了更大过载的这个机动动作,
那飞行员可能就晕过去了,就无法正常地执行作战任务。人工智能的F-16不存在这个问题,它打破了人的生理的这种承受的能力对于战斗机,也就是空中作战平台的限制。
解说:
X-62A验证机的出现,标志着AI从空战辅助迈向自主决策的关键一步。当美国在验证机领域探索AI边界时,英国选择在六代机暴风雨身上植入智能基因。作为“全球空战系统”的核心,该机从设计图纸开始就将AI列为第五大系统,其协同作战管理器可同时指挥8架忠诚僚机无人机,形成覆盖数百公里的分布式侦察打击网络。
隔断2:空战革命之集群指挥官——暴风雨战斗机
解说:
与X-62A的后天升级不同,暴风雨(Tempest)的AI更像是天生的决策者,重点实现无人机协同与电子战自主化。
该机型被定义为具备无人驾驶能力的多任务作战平台,外形设计采用三角翼与双外倾垂尾布局,在增强高速机动性的同时,保持飞行稳定性,气动结构兼顾隐身性能与超音速巡航需求。能够进行空对空空对地打击、侦察电子战及战场指挥控制等复合任务,该机采用革命性的模块化架构设计,可通过快速更换业务模块实现功能重构。新武器系统整合了定向能武器与高超音速导弹等前沿装备,配合虚拟信息头盔显控系统显著提升作战效率。
这个项目呢非常的复杂,
目前来看呢已经是进入到了这种基础的研发阶段,但是困难重重,甚至可以说呢它面临巨大的风险。这个暴风雨项目究竟能不能造出真正的具有实战功能的这个所谓的第六代战斗机,我个人是要打上一个大大的问号了,因为这个项目是英国主导。我们客观来说,英国在战斗机的飞行控制系统,包括在发动机方面,它是有独特的技术优势的。大家都知道大名鼎鼎的这个罗尔斯-罗伊斯这个发动机公司,它就是属于这个英国的。那么英国牵头,但是呢它自身的资金和技术研发能力还是存在不足的情况,
那么所以呢就开始找国际的合作伙伴。目前现在已经确定的是两家,分别是意大利和日本。所以它是欧洲和亚洲国家联合的这种战斗机的研发项目。
虽然说英国、意大利和日本把暴风雨定位为第六代战斗机,但是我个人认为呢,它充其量就是五代机或者是五代半。因为此前欧洲国家也好,包括日本也好,
它都没有独立自主的研制这个第五代战斗机,所以在隐身战斗机,在这种先进空中作战平台方面,他们都是缺乏经验的。那么现在呢想要这个赶鸭子上架,直接从四代机迈入到六代机的这样的一个序列当中,那么要克服很多工艺层面的难题,
还有很多指挥控制层面的这个难题。所以虽然说三个国家进行联合研制,钱不是问题,但是呢各自的需求不太一样,再加上呢进行这种技术层面的整合,再进行工艺层面的整合,我认为呢这个项目确实是困难重重。
解说:
暴风雨的核心技术亮点在于深度融合,人工智能不仅实现自主飞行决策,还能通过蜂群技术指挥无人机编队协同作战。暴风雨战机的AI协同作战管理器可同时控制4种型号共8架无人机,这些忠诚僚机既可为战机充当外置传感器,也能作为自杀式诱饵消耗敌方弹药。
那我认为呢这个暴风雨项目当中的人工智能系统
它更像是一种辅助的这样的一个智能化的操控体系。也就是说呢在暴风雨的这个战斗机的这个驾驶舱当中会融入一个AI助手。
那么人工智能它可以利用自身的分析能力和独特的逻辑运算能力,把最重要的甚至关乎飞行员性命、战斗机安全的这样的一个重要信息进行重点展示,进行提前展示。
那么在作战信息的筛选方面,在战场环境的感知能力方面,它有很快的这样的一个运算能力,那么也可以进行辅助决策。
【主持人3】
在刚才的介绍中,我们见证了 AI 如何将天空变为算力对抗的新战场。从自主生成非常规战术的智能验证机,到指挥无人机群编织查打网络的六代机,人工智能正在把空战的胜负从飞行员的操纵杆,转向AI的代码库。
就在天空的智能博弈日趋激烈的同时,陆地战场的‘钢铁洪流’也在经历一场静默蜕变。接下来,我们将把视角从云端拉回地面,去看一看AI对地面战场又会带来哪些改变。
隔断3:地面战颠覆之集群大脑——MCGS地面作战系统
解说:
MGCS项目于2017年与未来作战航空系统同步启动,其目标不仅是替换老旧的德国“豹”2和法国“勒克莱尔”主战坦克,而是要通过一个多平台、高度集成的系统彻底改变陆地战争。
从目前来看呢,这个坦克的定位呢,应该是未来多功能的地面主战系统,已经不叫这个主战坦克了,那么这样的一个装甲作战平台,首先呢它的火炮威力进一步的提升。目前有两种方案,第一种方案就是德国提出来的换装130毫米口径的滑膛炮。那么么由于火炮的口径变得更大,深管变得更长,那么肯定是可以有效的延伸这个装甲作战平台主炮它的这个射击距离,那么也可以发射尺寸更大的炮弹,也可以发射个头更大射程更远的炮射的这个导弹。所以呢,德国方面给出的这种火力增强方案就是这个让滑膛炮的口径进一步的提升。
那此外呢这样的一种装甲作战平台,它的防御能力全方位的提升,不仅会有各种各样的这种附加装甲、复合装甲,包括呢爆炸反应装甲,包括呢这个隔栅装甲,那么也会加装更加先进的这种主动防御系统。针对战场上的比如说无人机,甚至包括慢速来袭的弹药进行有效的防范。当然了,它的战场感知能力会全方位的提升。
那么坦克的车组成员可以利用多块的显示屏,有效的感知它周边更广阔区域内的这样的一个作战环境,也有可能会采用无人化的炮塔,炮塔上呢不仅仅会有主炮,也会有这个遥控武器战,上面可以集成机枪,也可以发射这个巡飞弹,甚至可以携带这个无人机。所以说呢,它已经不是传统意义上的主战坦克,是一个地面的综合的装甲火力打击系统。既可以作为主战坦克使用,那么在一定程度上也可以扮演移动的装甲炮兵。那么利用比如说曲射火力,包括巡飞弹,针对远距离的目标进行打击。
解说:
MGCS被设想为一个网络化生态系统,而非单一的装甲车辆。该系统将包括一个配备大口径火炮的主坦克平台,以及一系列配套装备:配备强大反坦克导弹的重型装甲车辆、配备定向能武器的无人机器人车辆、战术无人机和其他创新武器系统。这些模块将通过数字方式相互连接,在一个共享的作战云环境下运行,以最大化作战协同和实时响应能力。
那么AI系统和主战坦克进行结合,最大的优势就是它可以实现更好的联网作战,能够把其他作战平台和侦查平台的信息集成到这个未来地面的主战系统的内部。那么这样的话呢,这个坦克的车组成员就可以了解到这个更为准确更为精准的这种战场的作战环境。那么哪些目标应该优先打击,哪些目标要尽快地对它进行消灭,可能是AI要辅助的进行决策。
那么AI呢利用这种大数据,利用其他平台的这种传感器的视野,马上就可以做出决策,那么可以在触碰的显示屏上进行标注,那么车组成员所做出的这样的一个这个攻击动作非常的简单,用手指清除一下屏幕就可以完成一击必杀。
解说:
MGCS最大亮点在于无人化作战理念,大量采用了无人炮塔、无人侦察车等装备,借助人工智能与网络技术实现协同作战,依靠人工智能感知战场态势、自主决策,高效执行作战。
防护上运用多种先进技术构建全方位防护体系,融合主动防护、电子对抗等手段,全方位抵御来自不同方向、不同类型武器的攻击,确保在高强度对抗环境下的生存能力。
MGCS计划通过整合多种先进技术,使未来的地面作战系统具备更强的生存能力、火力打击能力和信息化作战能力,以应对现代战场上的潜在威胁,引领未来地面作战模式的变革方向。
隔断4:地面战颠覆之地空协同——格里芬装甲车
解说:
格里芬(VBMR Griffon)多用途装甲车是一款6×6轮式装甲运兵车,战斗全重25吨。根据法国“天蝎”计划研制,旨在取代法国陆军现役的VAB装甲运兵车。该车安装一台400马力雷诺卡车公司6缸涡轮增压柴油发动机和ZF七档自动变速箱,最大公路速度90千米/小时,续航里程800千米。格里芬(VBMR Griffon)多用途装甲车能够防御14.5×114mm穿甲弹、炮弹破片和地雷/简易爆炸装置的打击,并配有模块化装甲套件以增强防护能力。
格里芬装甲车呢是法国陆军的一款主战装备。我们看它的外形和传统的装甲车不太一样,有点像一个加强版的六轮的装甲卡车。那的全项的防御能力是比较强的。那么不仅法国陆军会装备,你像比利时、卢森堡这些国家的军队可能也会采购,因为他们所使用的主战装备都是这个法系的。格里芬装甲车最大的优势就是防护能力非常的全面。
首先,我们可以看一下它的底盘和相关的这个悬挂系统。那么即使遭遇到了这个反坦克地雷,包括路边炸弹,也可以保证车体结构的这样的一个可靠性和它的这个完整性,包括呢它的载员舱也是经过了特殊设计。即使这个装甲车被炸翻了,或者说面临大威力的路边炸弹的袭击,也要最大程度地保证它的这种载员舱的这样的一个整体性,那么最大程度要保障士兵的这个生命安全。
1704
那除此之外呢,我们也注意到,格里芬它的主要功能还是运输,而且呢也可以作为一个多功能的装甲作战平台。
在车体前部靠上这个位置,会配备有多功能的遥控武器站,可以集成轻机枪、重机枪和自动榴弹发射器,能够针对下车作战的步兵进行一定程度的火力支援。而且呢它还有衍生的型号,
比如说为它加装大口径的迫击炮,它就成了机动部署的这种迫击炮车,可以针对作战的步兵进行精准的曲射的火力支援。也有工程车的变形,可能还有装甲救护车、装甲抢救车的这样的一个变形。从目前法军部队使用的反馈来看,它的机动性防护能力和联网作战能力是可以达到要求的。
解说:
“格里芬”配备了“天蝎”计划的作战信息系统和泰雷兹战术电台,用于实时战场协调。衍生型号包括装甲运兵车、指挥车、炮兵侦察车、医疗后送车和战斗工程车,所有这些型号都旨在确保配备“天蝎”系统的部队之间协同作战能力。
那么它是一套非常完善的这样的一个一体化的指挥控制系统,
那么也是在大范围内能够进行信息和情报共享的这样的一个系统。那么通过终端设备的加装,像格里芬装甲车,包括勒克莱尔主战坦克,以及其他欧洲国家的主战装备,一旦融入了蝎子系统,或者说我们叫这个毒蝎系统,就可以形成合力,因为信息和情报可以在最大的范围内进行这种共享。那么也可以把每个作战平台连接到这种作战的指挥网络和体系当中。那么这样的话呢,整个的地面装甲作战单位,包括像凯撒自行火炮就可以构成合力,更好地感知战场环境,更好地进行目标的分发,而且呢不同的作战单位之间可以打出组合拳。
解说:
依托“蝎子”信息与战斗系统(SICS),格里芬(VBMR Griffon)多用途装甲车与“美洲豹”侦察车、“凯撒”自行火炮等装备形成数据互联。AI算法可自动分析战场态势,动态调整火力分配策略。例如,当发现敌方装甲集群时,系统会优先调度射程最远的“凯撒”火炮进行打击,同时指挥“狮鹫”机动至侧翼实施伏击,实现“探测-定位-打击”链路的秒级闭环。这种协同模式使传统“线性推进”战术演变为“动态猎杀矩阵”。
【主持人 3】
在地面战变革当中,我们看到了AI如何让钢铁洪流拥有群体智慧。那么有着地面装备克星之称的无人机,在近几年的局部冲突中可谓是出尽了风头。
当无人机蜂群战术成为现代战场的灰色威胁,传统防空体系正面临发现难、跟踪难、拦截难的挑战。而AI技术的注入,正在将被动防御转化为智能防御。
隔断5:AI编织的光速拦截网——卢卡斯激光武器
解说:
2024年10月,美国莱昂纳多DRS公司与蓝晕公司展示了斯崔克8×8步兵战车的最新变体,该版本被称为定向能反无人机系统斯崔克,该车配备了卢卡斯激光定向能武器系统及各种雷达和传感器。
卢卡斯激光系统结合了高速计算机处理、先进算法和新一代激光武器,能够在几毫秒内识别、区分并焚毁敌方无人机目标。
卢卡斯车载激光系统主要是和斯特瑞克八轮的装甲车进行结合。那么之所以要融入AI系统,其实主要是针对目标进行分析和判断。那么激光武器针对低迈小目标,它具备瞬间的秒杀能力
但是呢它有一个技术的问题,必须要这个进行一个很好的解决,也就是在低空空域的目标识别和分析的问题。因为在这种八轮的装甲车上,
不可能加装特别复杂的光学传感器,或者说太多的太大个头的这个雷达。那么针对空中的目标进行快速的分析,进行快速的这个识别,必须要和这个激光器进行一个紧密的联动。也就是说呢,激光器它的光学和红外的瞄准设备,直接融入了AI的这种分析软件。
解说:
卢卡斯激光武器系统旨在以人工智能技术实现即时识别并消除无人机威胁。卢卡斯激光武器系统采用托盘式设计,由两部分组成,左侧是激光器,右侧是跟踪系统。该系统将精密光学和激光硬件与先进的软件、人工智能和处理技术相结合,可识别、跟踪及利用高能激光攻击目标。
这种AI的分析软件,也就是一种智能化的算法。这个智能化的算法起到什么样的作用呢?比如说天空有一群飞鸟,天空当中又有一群无人机,如何根据它的飞行特征、飞行速度以及这个外观和相关的这种影像的这样的一个图像,要对它进行马上的判断。
在这样的一个决策的过程当中,是不需要人力介入的,精准快速地识别目标,完成激光束的攻击。
解说:
人工智能系统能够接收传感器数据,并以比人类快得多的速度执行分析、组织数据和识别相关项目。这意味着可以在几秒钟内确定最佳解决方案或对策。这大大提高了反无人机的速度,并更好地实现目标甄别。只要敌方无人机被发现、识别和验证的速度越快,卢卡斯就能在越远的地方、越安全地摧毁它们。
【主持人结尾】
从F-16的AI魔改到暴风雨战机的原生智能,从MGCS的无人集群到防空体系的智能化升级,AI的加入让武器装备有了质的飞跃。当“硅基大脑”与“钢铁之躯”完成融合,科幻电影当中的那些作战场景正在逐步走进现实。
@@ -0,0 +1,53 @@
《潜艇的仿生之路》
导视:在浩瀚海洋深处,数亿年的进化,塑造了无数神奇的“军事科技”。如今的人类,用钢铁和电路,重新发明了这一切。当潜艇披上"鲨鱼皮"消声瓦,当声呐模仿海豚看穿黑暗,当乌贼的墨汁化作潜艇的电子替身,自然早已写好了深海中生存与猎杀的终极教科书。本期《军事科技》带你见证茫茫深海中潜艇的仿生之路。
Xr演播室 主持人1:观众朋友们大家好!欢迎收看本期的《军事科技》,我是主持人蓝皓。在人类出现之前,深海就是地球上最残酷的竞技场。(画侧出现水体,水体按颜色深度分层)在这里,生存的法则只有两条:隐藏自己,发现敌人。而如今,深海依旧是地球上最后的黑暗边疆。在这里,潜伏着人类工程学的杰作,也是终极的刺杀兵器——潜艇。(水体中出现潜艇,鱼类游走)提到潜艇,想必大家都并不陌生。那么这样一头深海巨兽从诞生至今都借鉴了大自然中哪些动物的“绝招”呢?
从鹦鹉螺到“潜艇压载水舱系统”
二维动画解说1: 1776年9月7日,历史上第一次潜艇攻击开始了,这次攻击是由埃兹拉·李来执行。他驾驶着“海龟号”成功潜到英国海军战舰“鹰号”的尾部,接下去的工作就是用钻头在敌舰上穿孔以便固定炸药包。但是,他打钻的地方正好是一块金属板,半个小时之后他仍然没有钻透敌舰,只好上浮返回。虽然“海龟号”没有成功取得战果,但他拉开了潜艇实战的序幕,从此人类的战场也随之从陆地、水面发展到了水下,“海龟号”也得到了与现代潜艇相同的设计原理而获得世界上“第一艘军用潜艇”的称号,在世界潜艇发展史占据了一席之地。
三维动画解说:“海龟号”潜艇,顾名思义它的外形很像海龟。舱内空气可供驾驶员呼吸半个小时,在潜艇的上部还装有2根通气管,以便上浮时打开,下沉时关闭,从而补充新鲜的空气。为了控制潜水艇的上浮以及下沉,船内设有压载水舱,用水泵控制水柜内的水。为应付紧急情况的发生,艇内装有一块90千克重的铁块,危机时刻只要抛掉铁块,潜艇就可以迅速上浮。”
专家: 潜艇通过向压载水舱注水,排水来改变自身重量,模仿的正是鹦鹉螺的浮控原理。
解说:让我们把目光投向鹦鹉螺的身上。在亿万年的进化中,鹦鹉螺掌握了一种堪称完美的浮力控制艺术。它的螺旋形外壳被一系列隔膜分隔成数十个独立的气室,如同一个精密的多舱室潜水器。通过一个被称为串管的活组织,鹦鹉螺能精确调节每个气室内液体与气体的比例——分泌液体进入气室时下潜,排出液体让位给气体时上浮。这种精妙的生理机制,让它能在数百米的深海与浅层水域之间自如升降,几乎不消耗任何额外能量,堪称自然界最古老、最经典的被动浮力控制系统。
三维动画解说:人类潜艇的压载水舱系统,正是对这一自然智慧的完美复刻。当潜艇需要下潜时,阀门打开,海水涌入压载舱,取代舱内空气,艇体重量增加;当需要上浮时,高压空气将海水强行排出,艇体重量减轻。从19世纪美国“霍兰”号潜艇的手动阀门,到现代核潜艇的计算机控制高压气系统,其核心原理始终未变——正如鹦鹉螺用气体取代液体,潜艇用海水取代空气。这种能力,鹦鹉螺用了数亿年进化完成,而人类只用了不到一百年。
鲨鱼皮肤与潜艇的外形设计
Xr主持人:在广袤的海洋中,生存是一场关乎“看见”与“不被看见”的永恒竞赛。为了在这场竞赛中胜出,无数海洋生物进化出了一套精妙的色彩伪装策略,这便是隐蔽色。像虎鲸、鲨鱼、这样一生与海洋打交道的生物,其背部往往呈现深蓝、深灰色,而其腹部通常是白色或银白色。(画侧出现虎鲸)我们经常能够看到一只从深海上浮的虎鲸,其白色的胸膛在来自下方的视线中几乎“消失”在明亮的天光里;而一只从上方发起攻击的鲨鱼,其深色的背部直到最后一刻才会被发现。这套简单而高效的系统,本质上是一种 “光学隐身衣” 。它抹平了生物体在三维空间中的立体感,极大地缩短了被天敌或猎物发现的距离。海军工程师很快意识到,这正是潜艇在海洋中生存所需要的第一层能力。
专家:在二战时期,一些潜艇的指挥塔和上层建筑就被涂上了深灰色,以期在水面状态时降低被飞机发现的概率。今天,虽然潜艇绝大部分时间在水下活动,但隐蔽色原则依然被用于其涂装设计。现代潜艇通常采用一种黑色涂装。这不仅仅是为了美观,更是一种功能性设计。
解说:然而,在深海中,仅仅“看不见”还远远不够。随着潜艇潜得更深、航行时间更长,真正致命的威胁,开始从光线转移到声音。声音,是深海中传播最远、最难隐藏的信号。一丝多余的噪音,就可能暴露位置,招来猎杀。于是,潜艇的隐身逻辑,也从颜色,升级到了形态。潜艇的外形,并非一开始就是今天的样子。很长一段时间里,它更像是一艘“能够下潜的水面舰艇”。二战时期的潜艇需要频繁浮出水面航行,水下只是暂时藏身之所,因此艇体高耸、结构复杂,水下阻力大、噪音高。真正的转折,发生在核动力出现之后。当潜艇可以长期潜航,人类第一次开始,只为“水下生存”来设计它的形状。工程师将目光投向自然界:鲸类、海豚等高速游泳动物,几乎都拥有同一种外形——前圆后尖的水滴型。1950年代,美国“大青花鱼”号实验潜艇验证了这一答案。水滴型艇体显著降低阻力和流噪,让潜艇更快、更安静,也更难被发现。从此,现代潜艇的外形几乎统一。当潜艇的外形已经足够“像一条鱼”,真正的较量,才刚刚开始。接下来要隐藏的,不再是轮廓,而是声音本身。
专家:从早期潜艇的船型艇体,到现代攻击核潜艇几乎统一的水滴形艇体,如美国“洛杉矶”级,其核心目的就是模仿这种生物流线。每一个突出物、每一个转角都被精心优化,只为让海水“感觉不到”潜艇的存在,从而实现更安静、更高效的航行。潜艇需要在声学上“隐形”,经常被使用手是“消音瓦”。
解说:海豚的皮肤,能够分泌一种特殊黏液,使水流紧贴体表滑行,从而减少湍流与阻力;而更为人熟知的,是鲨鱼的皮肤结构。鲨鱼体表覆盖着无数微小的 V 形盾鳞,这些看似粗糙的纹理,反而能打散涡流,抑制水流紊乱,大幅降低摩擦阻力。潜艇工程师从中得到启发。在现代潜艇的消声瓦表面,常会设计出类似的微沟槽或细微纹理。这种被称为“沟槽面”或“仿鲨鱼皮”的结构,并不仅仅是为了让潜艇航行得更省力,更重要的是——它能有效降低水流掠过艇体时产生的流噪声,让潜艇在深海中,变得更加安静,也更加致命。鲸类和海豚用数千万年的进化,找到了最安静的形态,而人类,只是照着这个形态,把潜艇重新造了一遍。
海豚声呐与潜艇综合声呐系统
Xr解说:海豚在浑浊的海水中能准确找到小鱼,还能分辨出哪条鱼更肥美,这种能力来自它们独特的"水下视觉"——生物声呐。当海豚发出"咔嗒咔嗒"的声音,这些声波就像无形的探照灯向前发射。声音遇到物体产生回声,海豚通过下颌接收这些回声,就能在脑海中描绘出周围环境的立体图像。它们能听出藏在泥沙里的两条鱼哪条更肥,能判断前方是岩石还是海草,这种能力让任何人工声呐都望尘莫及。人类从海豚身上获得灵感,发明了声呐技术。
解说:二战爆发后,德国U型潜艇再次成为海战中的核心力量。在广阔的大西洋上,它们潜伏在航道附近,利用夜色和水下掩护,对盟军运输船队发动突然袭击。单艘潜艇,就足以瘫痪一整条补给线。对英国而言,这不仅是军事问题,更是生存危机。粮食、燃料、军需,几乎全部依赖海上运输,而水下的敌人,却始终看不见。面对潜艇带来的巨大压力,英国海军被迫改变思路——不再依赖目视搜索,而是尝试在深海中“倾听”敌人的存在。以水听器为基础,配合主动声波探测,声呐系统逐步成型,并率先装备在驱逐舰和护航舰上。在护航战例中,声呐第一次实现了对潜艇的稳定定位,使深水炸弹不再是盲目投掷,而成为有目标的打击。随着反潜技术的成熟,曾经游刃有余的U型潜艇,开始频繁暴露行踪,损失不断增加。大西洋的海战,从潜艇的“单方面猎杀”,逐渐演变为一场围绕隐蔽与探测的技术对抗。而这一技术,正是来源于大自然中的生物声呐。
专家:声呐是一种利用声波在水中的传播和反射探测水下物体,如潜艇、水雷、冰山、暗礁等,并可用于导航和测距、定位的装备,现在已广泛装备水面舰艇、潜艇、反潜机,以及无人作战平台。虽然现在雷达、红外、激光等非声探测设备有了长足的进步,但由于海水特性和潜艇潜深等原因,声呐仍然是反潜战中对潜探测最重要的,不可替代的装备。而潜艇在水下航行时,由于电磁波和光在水中衰减很大,传播距离非常有限,对于潜艇来说水声信号是探测目标最有效的方法。
三维动画解说:这是一艘弗吉尼亚级攻击核潜艇。潜艇的艇艏,布置着大型声呐阵列。在早期批次上,它呈现为球形结构;在更新批次中,则演变为更加集成的阵面形式。无论外形如何变化,它始终承担着同一项任务——对前方海域进行持续探测。通过分析回波的时间与特征,潜艇能够判断目标的距离、方位,并对水下接触进行初步识别。沿着艇体左右两侧,分布着舷侧声呐阵列。它们如同海豚身体两侧的侧线系统,负责监听来自各个方向的细微声学变化。即使潜艇保持静默状态,不主动发声,这些阵列依然能够捕捉远处螺旋桨的低频噪声,以及水流扰动形成的异常信号。在潜艇尾部,一条细长的拖曳声呐阵列被放出。这是弗吉尼亚级最重要的远程感知手段之一。它远离艇体自身的噪声干扰,将潜艇的听觉延伸到更远的海域,用于发现极其微弱、距离极远的水下目标。艇艏声呐、舷侧阵列与拖曳声呐协同工作,使这艘潜艇即使在完全隐蔽的状态下,也能构建起一幅连续、立体的水下态势图。
解说:如今,科学家仍在向海豚学习:新的声呐系统在模仿海豚快速构建图像的能力,智能算法在研究海豚处理声音的思维方式。当我们看到海豚在海洋中自由游弋时,其实是在见证一个活生生的声学奇迹——它的每一次鸣叫,都在为人类展示着更完美的水下探测技术。
乌贼墨汁与声学诱饵
Xr主持人:当生存受到直接威胁时,顶尖的猎手与最先进的武器,都诉诸同一种策略:制造混乱,隐藏本体。乌贼遇到危险时,会迅速喷出浓密的墨汁,它并非简单的黑色液体,它在海水中会迅速扩散,形成一个与自身形态相似的、浓密的“墨汁伪影”。这个深色轮廓能立即吸引并锁定捕食者的视觉注意力,为真身的逃离创造关键的时间窗口。潜艇工程师完美地借鉴了这套复杂的求生系统,并将其转化为防御装备:声学诱饵。
解说:潜艇感知到来袭的声自导鱼雷或敌方主动声呐锁定时,会从发射管快速射出声学诱饵,它是潜艇的一种高“电子替身”。声学诱饵通常是一个自航式的水下航行器,被发射后,它会精确模仿母艇的发动机噪音、螺旋桨节拍等独特的“声学指纹”。还会主动航行,制造出比气幕弹更像一艘真实潜艇的移动声学目标。来袭的智能鱼雷能够区分简单的气泡幕和复杂的潜艇噪音。声学诱饵通过发出更具吸引力的“听觉陷阱”,引诱鱼雷偏离航道,转而追踪并攻击这个“替身”。
专家:1942 年,德军 U 型潜艇投放了一种叫 “Bold” 的声纳诱饵。它没有动力,只是释放大量气泡,在水中制造一个“虚假的潜艇回波”。简单,却足以让早期声呐迷失方向。很快,诱饵开始“动起来”。“Sieglinde” 登场。它能以几节航速自行移动,在水下拖出一条看似真实的航迹。这是诱饵第一次尝试模拟潜艇的“行为”,而不仅仅是形态。战争结束,但对抗没有停止。冷战时期,美国海军引入了 MOSS Mk70 移动潜艇模拟器。它被发射出艇后,会在水下独立航行,主动与被动同时工作,制造一个完整的“声学假目标”。在声呐画面中,它和真正的潜艇几乎没有区别。进入 70 年代,诱饵开始标准化、批量化。ADC Mk1,随后是 Mk2、Mk3 系列。这些一次性声学诱饵可以在预定深度悬停,持续发出模拟噪声,把来袭鱼雷从真正的潜艇身边“拉走”。它们不显眼,却成为冷战后期潜艇防御的常规配置。真正的转折,出现在智能化时代。新一代诱饵不再只是“发声”。它们开始计算、判断、机动。例如 Deceptor 自航式声学诱饵。它会主动离艇机动,改变航向和速度,在水下扮演一个“正在逃逸的潜艇”。面对多枚鱼雷,它可以分散、引导、拖延,为真正的潜艇争取生存窗口。而在水面与水下战场上,AN/SLQ-25 “Nixie” 系列拖曳诱饵,成为最广为人知的反鱼雷软杀伤系统之一。
解说:这套系统在实战与对抗演练中屡建奇功。其核心战术都与数百万年前的乌贼如出一辙。乌贼用一团墨汁换取生存的时间,人类则用电子替身,把这一秒无限放大。
Xr主持人:当深海的幽灵收起獠牙,当猎杀者的声呐归于沉寂,这片蔚蓝疆域的博弈却从未停止。从鹦鹉螺的浮力控制到核潜艇的无限续航,从乌贼的墨汁到潜艇的电子诱饵,我们见证了一场跨越数亿年的军事科技进化。自然用漫长的时光雕琢出生存的智慧,人类用智慧在钢铁中复刻这些法则。这不仅是矛与盾的较量,更是对生命本身最深刻的学习。下一场改变游戏规则的技术革命,或许正随着某只深海生物的游弋,在黑暗中悄然酝酿。好了,本期《军事科技》到此结束,感谢您的观看。
@@ -0,0 +1,197 @@
《“舰”证不凡》
宣传词:
从万吨大驱到两栖攻击舰,再到航空母舰,大国巨舰勾勒人民海军深蓝征程。
本期《军事科技》走进055咸阳舰、075湖北舰、山东舰,解码国产战舰“体型大”的硬核底气,揭秘“拳头硬”的体系战力,感悟“责任大”的使命担当。
钢铁巨舰劈波斩浪,强军征程步履铿锵,解码国产战舰带给我们的直观震撼。
主持人:
各位观众,你们好!欢迎收看《军事科技》。我是蓝皓。
人民海军的装备发展,堪称一部波澜壮阔的崛起史诗。我们用数十年的时间,完成了从近海防御到远海防卫的跨越式发展。那么,提起人民海军的装备实力,今天的节目,我们想用几个关键词,带您解锁国之重器的硬核密码。
第一个关键词就是 ——“体型大”。
此刻,我们正走在军港的码头上。大家顺着镜头望去,我身旁的这艘巨舰,就是 055 型驱逐舰 —— 咸阳舰。当我们第一次接触它的时候,能够充分感受到它的力量感与美感,是一艘美丽而威武的海上利刃。当然,镜头里呈现的震撼,远不及亲眼看到它的万分之一。当您真正地站在它的甲板之下,仰望这艘钢铁巨舰的时候,才能够真正地感受到“万吨大驱”这四个字,沉甸甸的体量与一往无前的底气。
解说:
咸阳舰是055型驱逐舰其中的一艘,它的诞生突破了大型舰艇总体设计、信息集成、总装建造等一系列关键技术,满载排水量达到1.2万吨。
可能有观众对1.2万吨没有直观概念,不妨做一组对比:建国之初,我国自主研制的第一款驱逐舰——051型驱逐舰,满载排水量约3600吨,055的体量是它的三倍之多;即便是与现役被誉为“中华神盾”的052D型驱逐舰相比,055的体量也几乎是它的两倍。站在甲板下仰望,高耸的舰桥、修长的舰体,搭配流畅的隐身设计,既有钢铁巨兽的威严,又不失现代军工的精致。而这种体量的提升,绝非简单的“放大”,而是综合作战能力的全面跃升。
主持人:
此刻,我们已经登上了咸阳舰的甲板。我发现这个甲板,比我们之前看到的驱逐舰或者护卫舰的甲板都要大。这样的设计有怎样的特别之处?
南部战区海军咸阳舰 宋怀远:
第一个,大家现在所处的,我们的起降平台在海上,主要用于直升机的起降和这个临时的停放。大家可以往这边看,这个就是我们的双机库。一般的驱护舰艇,只有我们的单机库,所以说,对比他们,我们的搭载能力更强了。我们可以搭载多型的直升机及无人机,单机库的话就只能搭载一架,但是双机库我们就可以搭载两架。
主持人:
我们看到这个甲板之上有很多标识,还有这个轨道,它的作用是什么?
南部战区海军咸阳舰 宋怀远:
第一个,这个标识主要是用于直升机的,叫做这个叫吸流孔,用于直升机在舱面的一个固定的作用。然后这个轨道,相当于飞机的快回装置,就相当于一个小推车,到时候飞机可以沿着飞机的轮子,沿着轨道,然后直接给他推到机库里边,不再依靠人力来进行这个直升机的进出库了。再加上,这个是我们一个是作为飞机起降的一个用途,再有一个在海上,也是丰富官兵的长期生活的一个体育运动场,大家平常在这上面跑跑步、放松身心都非常棒。
解说:
咸阳舰的外观设计充满了未来感。舰体采用了大量平滑的斜面,减少了棱角和突出物,这种设计大幅降低了雷达反射截面积,让它在敌方雷达面前如同一个“隐形战士”。与此同时,它采用典型的全封闭飞剪式舰艏,它并非二战前后战列舰的飞剪式设计,而是融合中国创新智慧的升级成果。从正面看,舰艏折线在顶部开始内倾,到主炮位置前方又稍稍外倾,随后再次内倾,最前端与主甲板构成尖锐的多面体全封闭结构,与前甲板齐平过渡。整体外形简洁现代,兼具美观与实战效能。
主持人:
现在我们来到了咸阳舰的前甲板。相比我之前看到其他战舰来说,咸阳舰的甲板会更加的干净整洁一些。那些系留装置、锚点都去哪了?
南部战区海军咸阳舰 宋怀远:
这个主要是为了突出我们的隐形化设计。我们将这些所有的舱面设备,我们的锚机、系缆柱都置于我们的甲板之下,提高我们舰艇的这个隐蔽效果。第一个,可以减小我们人员在舰艇大幅度摇摆的过程中,在舱面作业的安全系数;第二个,减小敌方的雷达发现我的概率,提高我的舰艇的防护能力。
主持人:
咸阳舰的主桅杆相比于其他军舰来说,会变得更加粗壮,外观也会更加简洁。这样的细节变化有怎样的深意吗?
南部战区海军咸阳舰 宋怀远:
我们 055 的桅杆,采用一体化主桅设计,不但看起来简洁美观,它实际上更内藏乾坤。我们将所有的雷达、电子战及通信天线,整合于我们的主桅之内。它可以同时跟踪数百个海面、空中目标。它不但可以引导导弹更精准的打击目标,也可以提升我们舰艇的战场态势感知能力,是我们决胜战场的千里眼与顺风耳。
解说:
如果说055驱逐舰的“大”是“标杆级”的突破,那么眼前这艘075型两栖攻击舰湖北舰的“大”,就是“平台级”的跨越。就像网友调侃的那样,湖北舰之大,连镜头都快装不下了。
湖北舰舷号34,是中国自主研制的075型两栖攻击舰中最新的一艘。2025年7月湖北舰正式入列中国人民解放军海军。它的到来,对提升中国两栖作战能力、完善远海作战体系,具有里程碑式的意义。
南部战区海军湖北舰 冉伟宏:
在我身后所坐的舰艇,是我们 075 型两栖攻击舰第四艘舰艇,舰号是 34,舰名是湖北舰。075 型舰,作为我国自主研发的最新型的舰艇,大家可以看到,我站在它下面,显得非常的渺小。我们湖北舰的长度超过 200 米,高度有 15 层楼那么高。它从外观来看,与我们的航母有点相似。当然,我们的湖北舰也是内涵乾坤,它既能容纳两栖车辆,又能装载直升机。它的一个主要的使命任务,就是遂行两栖作战。相比于我们所熟知的 071 型船坞登陆舰,我们的湖北舰最大排水量是它的近两倍。
解说:
湖北舰拥有巨大的直通甲板,面积相当于20个篮球场,可同时快速起降多款舰载直升机。它的入列,加强了我国“平面登陆+立体登陆”的综合两栖作战能力。它既能通过舰载直升机实现垂直投送,又能利用坞舱释放登陆装备展开平面突击,可快速投送两栖作战力量,应对远海岛屿防御、人道主义救援等多种任务,是人民海军维护国家主权和海洋权益的“两栖先锋”。
主持人:
说到体型大,人民海军的装备还有比海南舰更大的存在。此时,我就站在这艘巨舰的身旁。通过结构,大家可能已经猜出了,它就是我们的首艘国产航母 —— 山东舰。站在平台上去仰望它的时候,它就像是一座高楼;而当它在大海上全速奔跑起来的时候,那就是一座劈波斩浪的海上高山。
解说:
山东舰的满载排水量约6.5万吨,舰长300米左右,是我国首艘完全自主设计、自主建造、自主配套的国产航母。从055的1.2万吨,到海南舰的4万吨,再到山东舰的6.5万吨,这种体量的递进,清晰勾勒出人民海军装备发展的战略轨迹,每一次“放大”,都对应着战略需求的升级;每一次体量的突破,都意味着远海作战能力的跃升。
南部战区海军山东舰 王奕镔:
现在我们从我们的主船体来到了我们舰岛,我们主要是分为这两个部分。我们船的总的这个排水量达到了 6 万多吨,也就相当于三个埃菲尔铁塔的重量。在海上航行,我们的总的发电量,相当于一个中型的一个城市,所以这个体量还是特别大的。我们的全船的舱室,据我们初步统计都有 3000 多个。网上有一个流传这么一个说法,就是一个婴儿从刚出生,一直到 12 岁的时候,才能把我们全船每天住一个舱室,才能都住过遍。我们 3000 多个舱室里边,包含了我们的主动力舱室、作战指挥室、航行指挥室,包括我们的飞行保障、还有我们的航行保障等等之类的,还有我们的装备的各类的舱室,以及我们的备件舱等等。
现在来到是我们的舰上的图书馆。我们舰上遵循了一个社区化的一个设计的一个理念,保证舰员在一个生活的一个区段之内,保证了基本的工作、学习、生活的这种状态。然后我们现在图书馆藏了收藏了能有 7000 多本书。目前,我们船上还有像这类生活常识,还有我们的健身房、我们的医疗区,甚至我们有自己的超市、还有邮局。以上种种说明,我们航母确实是一个体量庞大、功能强大的海上巨系统。
解说:
航空母舰是一种供舰载机起飞和降落的大型舰艇,换句话说,航母就是一座海上机场,是目前世界上最庞大,最复杂的,威力最强的武器平台之一,还现代战争中名副其实的海上重器。
山东舰于2019年12月17日入列,它一入列就备受关注,全舰高度相当于十几层楼,内部舱室密布,宛如迷宫;6万多吨的排水量,赋予了它远洋航行的充沛底气。
南部战区海军山东舰 王奕镔:
从我们舰岛的前部走出来,就来到了我们的飞行甲板。我们整个飞行甲板,可以说一下视野就开阔了,没错。整个飞行甲板有 15000 多平方米,然后相当于三个标准的足球场还要再大。我们分了三个区域:在我们现在脚下的这个区域,是停机区。可以看到好多黄色的标志线,就是我们飞机停放的位置。然后在这些停放的位置,我们可以进行各类的加油、加气、挂弹各类的保障这个作业。在我们正前方,舰首的正前方是我们的起飞区,有设置了两个短距的起飞位。在我们的左边左手边,设置了是我们的主舰岛区。在舰尾的方向有 4 道拦阻索。同时我们整个甲板面,还设置了 4 个直升机的这个起降圈。我们飞行甲板的右边,布置的是我们岛式上层建筑,整个岛上层建筑有大概十余层楼那么高。以中间的这个隔山为分界,是我们的舰桥室。前半部分,是我们主要掌管航行安全的驾驶室,以及我们舰岛上面各类的预警探测。我们的雷达设备在我们的舰桥室。后边是我们的塔台,主要掌管我们飞行安全,负责甲板飞机的转运,以及给我们安全起飞和降落的这相关工作。
解说:
从1.2万吨到4万吨,再到6.5万吨,人民海军舰艇的“体型大”,从来不是盲目追求规模,而是战略需求与工业实力的精准匹配。这种递进式的体量突破,为后续的“拳头硬”——也就是强大的打击能力,奠定了坚实基础。接下来,我们进入第二个关键词:“拳头大”。
主持人:
我们都说舰艇的体型大是基础,但是火力系统 “拳头硬” 才是核心。那么对于 055 驱逐舰而言,它的火力系统有什么特别之处?
南部战区海军咸阳舰 宋怀远:
目前飞机和这个空中来袭的导弹,是对我们最大的威胁。我们 055 型舰构建了远、中、近三层防空抗击体系。现在映入眼帘,就是我们的一个近程防空导弹,主要担负我们对近距离来袭的导弹或飞机进行一个抗击和拦截,精度高、反应时间短。这个就是我们所说的副炮系统,也是咱们所说的万发炮。万发炮,顾名思义也就是射速高、火力猛,依靠它强大的射速来提供有效的弹幕,对末端的导弹进行一个有效的拦截。同时,它上面具有这个雷达及光电系统,为它来抗击导弹提供一些更精确的指引。
我们刚才到就是我们远、中、近 3 层防控体系。这个里边就可以搭载我们的远程防控武器。从它的外形上,我们看到它像一块一块的巧克力,虽然看着像巧克力,可不要小觑它的威力,这个就是我们的通用垂直发射系统。我们刚才提到的远、中、近 3 层防控体系的远程作战能力,它可以装载各型的导弹,为我们打击空中、岸上以及这个海面的目标。同时我们这个平台大,可以搭载更多枚的导弹,来提供我强大的火力支撑。发射通用化,就是它可以搭载各型的导弹及雷弹,然后同时还具有这个冷热兼容发射,大大提高了我舰的这个综合作战能力。
解说:
既然说到了垂直发射系统,我们就来科普一下导弹飞出发射筒的两种主要方式:热发射和冷发射。
两者的核心区别在于导弹出筒的动力来源不同,发射筒的内部结构也因此存在差异。热发射,是导弹在发射筒内直接点火,依靠自身发动机的推力飞出发射筒,这种方式反应速度快,发射效率高;冷发射,则是先利用高压气体将导弹 “弹” 出发射筒,待导弹上升到一定高度后,再启动发动机飞向目标。简单来说,热发射是 “一步到位”,冷发射则需要 “两步走”。
主持人:
在前甲板上当然还有一个大家伙,就是非常威猛的舰炮,它有怎样的特别之处?
南部战区海军咸阳舰 宋怀远:
这个就是我们的主炮系统。站在它近距离的接触它,还能感受到它的威严的。它主要担负我们一些对岸火力支援任务,或者说一些对海打击任务。只有两个特点:一个是我们口径大,也是目前国内舰炮搭载口径最大的,所以说我们的火力就很强;再有一个,我们依托它的平台的自身的优势,我们平台大,所以说他就有更稳定的射击稳定性,然后更精确的命中目标。
解说:
强大的武器配置,让055型驱逐舰不仅能独立执行防空、反舰、反潜等作战任务,还能在航母编队中承担“带刀护卫”的核心角色,是人民海军远海作战的“核心战力”之一。而它重点保护的对象,就包括我们刚才提到的海南舰和山东舰。
主持人:如果说 055 的拳头是精准打击,那么两栖攻击舰的拳头就是立体投送加协同重打击,是一套威力更加强大的组合拳。
解说:
作为两栖攻击舰,它的核心打击能力体现在“搭载能力”上,它所搭载的直升机、登陆装备和陆战队员,就是它最强大的“拳头”。
湖北舰可搭载几十架各型直升机。则能快速投送陆战队员和轻型装备,实现昼夜“立体登陆”。在平面登陆方面,湖北舰的坞舱可容纳多艘气垫登陆艇,这种气垫登陆艇速度快、适应性强,能够在浅滩、礁石等复杂地形登陆,搭载主战坦克、两栖装甲车等重型装备,实现“平面突破”。
南部战区海军湖北舰 冉伟宏:
那么我现在所在这个位置,是属于湖北舰驾驶室平台的外侧。在我们湖北舰设计理念上,跟航母有着异曲同工之妙。我们采用的也是舰岛式上层建筑和这种全纵通式的飞行甲板。站在我这个位置,可以清晰的看到我们整个飞行甲板的全部概况。从前至后分别有 6 个起降点。在整个飞行甲板的后侧,还设置了一个 7 号点,也就是一个小停机坪。这个停机坪,主要是用来保障无人机的起降。
在我们机库的下方,是我们的车辆库和坞舱。我们的车辆库和坞舱,它主要是用来存放气垫艇、两栖装甲车、坦克以及自行榴弹炮。当我们进行登陆投送任务的时候,我们的坞舱尾门会打开,我们的气垫艇会带着我们的坦克、我们两栖装甲车一块进入到水中,进行登陆作战。
解说:
当然,作为一款大型作战平台,湖北舰也配备了自卫武器。分布在舰艇四周的“万发炮”和舰空导弹系统,构成了它的近程防御圈。这些武器虽然看似不多,却能在关键时刻拦截来袭目标,为海南舰撑起全向防御的“保护伞”。
而作为人民海军的核心作战平台,山东舰的“拳头”则更为强硬,它体现的是“远洋制空+制海”的体系作战能力,而这个“拳头”的核心,就是舰载机部队。歼-15舰载战斗机,就是山东舰最锋利的“尖刀”。
山东舰目前搭载的是歼-15舰载战斗机,这是我国自主研发的重型舰载战斗机,具备制空、制海、对陆攻击等多种作战能力。它的最大起飞重量超过三十吨,可搭载空空导弹、反舰导弹等多种精确制导武器。在制空领域,歼-15能有效争夺战场制空权,拦截敌方战机和巡航导弹;在制海领域,可对敌方舰艇实施远程精确打击,构建起数百公里的海上防御圈;在对陆攻击领域,能精准打击敌方纵深目标,支援地面部队作战。
南部战区海军山东舰 高翔:
我们现在所在的这个位置,就是我们 3 号起飞位。在我们舰上,像这样的起飞位我们一共有三个,另外的两个是在舰首的位置。在我们出海过程中,舰载机放飞的过程中,我们舰载机是从舰尾部到停机位滑出之后,通过引导来到我们现在舱外起飞位这么个位置。我们在舰载机就位了,就位了以后,我们会有三种工作人员:一种是机务检查人员,他们会检查飞机放飞前的这个准备状态,舰载机的状态是否良好,它发动机状态是否良好;还有一类就是我们制动轮挡的检查员,主要是检查我们制动轮挡和飞机主轮的捏合程度,轮挡是否顶的顶在中间,他没有没有完全顶上的话是不合格的;还有一类是我们偏流板的检查人员,他主要是检查我们偏流板的冷却系统还有支撑系统,是否是处于一个正常的状态。之后在完成这些一系列检查之后,起飞助理会到飞机的右侧,确认好各位置的状态之后,完成放飞动作,也就是我们熟知的航母 style 的动作。之后,舰载机会在一个短的滑跑距离之内完成放飞。
解说:
除了歼-15舰载战斗机,山东舰还搭载了预警直升机、反潜直升机等辅助作战机型。这些舰载机构成了一个完整的“空海一体”作战体系,让山东舰具备了遂行远洋作战任务的能力,成为人民海军维护国家海洋权益、保障远洋运输通道安全的“核心支柱”。
南部战区海军山东舰 高翔:
山东舰出海执行任务,通常不是只有单舰的出海,通常是组成编队去执行任务。我们的编队,还会有驱逐舰、护卫舰以及补给舰等等。这些附属舰艇也都会搭载各型武器,来完成我们编队体系化构建,让我们山东舰编队的拳头越来越硬。
主持人:
从 055 驱逐舰的全方位精准打击,到海南舰的立体投送协同打击,再到山东舰的远洋体系作战,人民海军的拳头大,指的是从单点打击到体系作战的能力跃升。而这些强大的装备,都服务于一个核心的使命,那就是守护祖国主权、安全和发展利益。而这,就引出了我们今天的第三个关键词 ——“责任大”。
解说:
近年来,人民海军在实战实训中持续向远海远域拓展,实战化训练的规模、强度和体系化水平显著提升,标志着海军走向深蓝的能力不断增强。‌编队远海训练常态化,有效检验了联合训练成效。‌训练范围覆盖全球海域,提升了在复杂国际环境下的实战能力。实战化训练体系深化,推动训练向深海大洋延伸。人才培养融入远海实践,通过嵌入舰艇岗位实操,培养战略思维和任职能力,为远海行动储备人才。‌
南部战区海军咸阳舰 张亚迪:
我来到咸阳舰已经第三年了。当我第一次抬头看见咸阳舰的时候,哇,如此的宏伟和巨大。后面当我可以独立执勤的时候,每次双手握住舵轮,眼前看着现代化的操作界面,都能感受到这艘先进舰艇蕴含的强大力量。我心里明白,我以后的责任、荣光和青春都将与它不可分割。
南部战区海军咸阳舰 颜金清:
这个我们咸阳舰就是,自从我们入列以后,这个执行了很多战备和训练任务。这个其实说通俗一点,我们这个作战任务其实主要就两项:第一个,就是通过我们的舰艇的军事外交,向全世界,包括我们到达的国家,传达我们的善意和诚意;另外通过展示我们的能力,让对方能够感受到,我们是有能力保护和维护双方的自由交往和贸易。就是我们其实说白了就是我们这艘舰,这个虽然说就像我们是一个武林高手一样的,但是我们是爱好和平的,但是我们绝对不会畏惧武力,就是需要的时候该出手时就出手,让那些想到我们周边来耀武扬威的敌人,让他们知道我们虽然以德服人,但是也可以以力服人。
南部战区海军湖北舰 桂淳:
湖北舰交舰入列的时候,我们作为舰员站在湖北舰下面,第一个感受就是震撼,第二感受就是激动、自豪。每名军人都希望自己手中有一把钢枪,有属于自己的武器。我当时看到这艘舰艇,我当时就想,湖北舰就是我们所有舰员的钢枪,是我们手中的利刃。通过我们的刻苦训练,湖北舰已经不是一个铁壳子,而是一把锋利的、更是有生命力的利刃。这把利刃交到我们手里,我们作为光荣的湖北舰舰员,丝毫没有懈怠过。有了这把利刃,我们在海上遇到强敌对手,我们有信心、有能力消灭一切来犯之敌,让湖北舰成为党、国家和人民可以完全信赖的钢铁拳头。
南部战区海军山东舰 王奕镔:
航母这样一个巨系统工程,不仅代表了国家的综合实力,更代表着科技水平和军事水平的一个集中体现。从 2017 年辽宁舰访问香港,全体舰员在甲板摆出了 “香港你好”,到今年 7 月份我们舰访问香港,我们摆出了 “国安家好”。这种转变体现了我们一个海军强国、海军大国一种负责任的态度。同时,我们也有能力、有信心维护国家利益,保护人民安全。近些年来,我们舰、我们整个海军部队演训任务也是逐渐增多,我们航母也是从近海走向远海,双航母编队在西太部署。今年 11 月份,我们也是在码头有幸见证到 18 舰交接入列,我们的海军实力进一步增强。我们始终相信,国家利益所至,航母航迹必达。
解说:
舰艇的吨位有大小,武器的威力有强弱,但人民海军的责任与担当,始终如一。从近海到远海,从维护主权到国际救援,055驱逐舰、湖北舰、山东舰用一次次行动,践行着“人民海军为人民”的誓言,诠释着大国海军的责任与担当。
主持人:深蓝利剑,逐梦海天。人民海军装备的发展之路,是一条自主创新、砥砺前行的奋斗之路。从无到有,从弱到强,从近海走向深蓝。每一次巨舰的下水,每一次编队的远航,都见证了中华民族向海图强的百年夙愿。随着更多先进装备的入列,人民海军必将以更强大的姿态,守卫国家主权、安全和发展利益。当然,我们也会总结更多的关键词,带您揭秘国之重器的硬核密码。
好了,观众朋友们,感谢您持续关注国防军事频道《军事科技》,我们下周同一时间再见!
@@ -0,0 +1,155 @@
导视:
金戈铁马,龙马精神。2026年是传统文化中的“马”年,那么在武器装备领域中,有哪些与“马”相关或以“马”命名的经典装备?他们是驰骋大洋的海上野马,是纵横疆场的陆战骏马,亦或是翱翔天际的空中飞马,值此新春佳节之际,军事科技带您走进那些骏马疾驰的武器世界。
主持人1
今天是农历大年初一,万家灯火,共庆新春。在此我谨代表《军事科技》栏目组全体工作人员,向全国观众致以最诚挚的新春祝福,愿您在新的一年里如骏马奔腾,勇往直前。2026年是农历马年,马象征着力量、速度与进取,而在现代军事装备中,“马”元素也屡见不鲜。本期节目我们就来回顾一下那些以“马”命名的武器装备。
首先就是从海平面踏浪而来的国产726型气垫登陆艇,它被军迷亲切地称为“野马”。这款装备航速快、载重大,可搭载主战坦克跨越波涛,直抵滩头,正如古代战马驰骋沙场,那么这款踏浪而来的战马有着怎样的非凡性能呢?
解说1
当舰队逼近海岸,如何在敌方火力封锁下快速完成兵力投送,是两栖突击的核心难题。此时数艘726型“野马”气垫登陆艇从母舰舱内缓缓驶出,如海上奔腾的骏马般划破浪涛,搭载着主战坦克与陆战队员,以极高的速度冲向滩头,完成登陆的“最后一跃”。这种可以搭载登陆部队和重型装备快速穿越复杂海岸地形的气垫登陆艇,就像是海军陆战队的共享单车,完成登陆作战的“最后一公里”投送。
专家提问1
早期抢滩登陆的难点,以及气垫登陆艇出现的意义。
早期进行大规模的两栖作战,主要是使用机械化登陆艇,机械化登陆艇它在靠近岸滩的时候容易受到障碍物的阻挡,也就是说它无法让士兵直接实现登陆作战。那么参与两栖作战的这些士兵携带的武器和装备是比较重的,所以如果相关的区域内水比较深,那么可能就会出现溺亡的情况,而且在行动的过程当中容易遭到对方的这种火力打击。有的时候由于相关的沿海和近岸区域防守的兵力设置了一些拒马,包括其他的这样的一些障碍物,它只能是在靠近岸滩的区域进行这种兵源的投放。但是气垫登陆艇是完全不同的一种装备,本质上来讲,它并不是传统意义上的这种船艇类的登陆平台,而是一种采用特殊气垫设计理念的这样的一种登陆平台,可以越过滩头阵地上的很多的障碍物,直接把步兵投送到岸滩上,可以让步兵实现鞋不沾水的登陆夺岛作战。
而且大型化的气垫登陆艇运载能力比较强,可以搭载主战坦克装甲车辆,直接把陆军的主战装备投送到滩头阵地,这样的话在己方火力的掩护和支援之下,能够用最短的时间把最多的作战人员和作战装备投送到两栖登陆作战和夺岛作战的最前沿。
动画1
野马气垫登陆艇的外形看起来就像是一个巨大的扁平盒子,中空的船体中可以容纳主战坦克、装甲车辆或大量士兵。而气垫登陆艇最大的特点就在于“气垫”二字,它们不像传统水面舰艇一样采用全金属或硬质材料的外壳,而是在底部采用柔性围裙结构的充气气垫。就好像是我们游泳时使用的救生圈一样。为了给这个气垫充气,野马气垫登陆艇的动力系统也不同于传统舰艇的螺旋桨推进。首先由燃气轮机带动的不是螺旋桨,而是艇底的升力风扇。升力风扇将大量空气压入艇体底部,使艇身脱离水面或地面约10厘米。而这层柔软的气垫不仅让“野马”摆脱了水的阻力,还能在松软沙滩、沼泽甚至礁石地形上平稳行驶,就像是给登陆艇垫上了一层“空气棉花”完全不惧怕沙滩上的复杂地形。由于气垫登陆艇是悬浮于水面之上,因此前进的动力也不能依靠螺旋桨,而是依靠艇尾的两台巨大的螺旋桨产生强大推力,推动登陆艇在海面高速航行。总而言之,野马气垫登陆艇通过升力风扇加推进风扇的组合,实现了武侠世界中的轻功绝学“水上漂”
解说2
726型气垫登陆艇是中国两栖作战体系的核心投送装备,堪称“超地平线登陆”的“踏浪利刃”。其 50-60 吨载重可灵活搭载 1 辆 99A 主战坦克或2 辆两栖步兵战车,也可搭载80 名全副武装士兵,实现重型装备与人员的快速输送。
依托 60-80 节高速性能,它能在敌方火力射程外发起突击,2 小时内完成近距离海峡跨越,大幅缩短暴露时间。轻松突破沙滩、沼泽等复杂地形,彻底摆脱传统登陆艇的地形与潮汐限制。
作为 071、075 型两栖舰的关键配套,它构建起立体投送网络,同时可承担岛礁补给、应急救援等任务,是攻防兼备的两栖作战中坚。
专家提问2
726型气垫登陆艇对于我国两栖作战能力的意义与价值
726型野马气垫登陆艇,它的研制成功,尤其是装备相关的部队,能够让我们体系化的这种登陆作战能力,以及岛屿环境当中的作战能力大幅度的提升,为什么这么说?因为我们是采用了这种大小组合的方式,先由大型的海上兵力投送平台,无论是两栖攻击舰还是船坞登陆舰,把更多的人员装备转运到相关的区域进行集结待命。然后在坞舱当中的野马级的气垫登陆艇,也就是726型的气垫登陆艇,可以迅速的把装备和人员转运到滩头阵地,那么通过这种以大带小的方式,--能够实现兵力的快速集结,也可以实现兵力的快速转运,所以这个快字非常的重要。也就是说726型的气垫登陆艇,它和现代化的两栖作战体系它是融为一体的。
主持人2
看完了拥有绝世武功“水上漂”的“野马”气垫登陆艇,我们再将目光投向万米高空,在那里有一匹会飞的马正在翱翔,它就是安-2运输机,可能您对这个名字有点陌生,但是在我国他还有一个更广为人知的名字——“运-5”。
运-5运输机的外形短小精悍,仿佛一匹灵巧的草原骏马,所以也被称为“马驹”,它和我们现在常见的大型运输机相比显得小巧许多,造型也有很多不同之处,看起来没有什么科技感。但是自它1957年首飞以来,始终坚守在军事与民用航空领域,这款“空中小马驹”有着怎样的独特魅力,让它能在近七十年的岁月中依旧驰骋蓝天之上的呢。
解说3
相较于目前可以运载十数吨货物的大型运输机,运-5看起来就像是一个没长大的孩子,所以“马驹”这个称号生动地描绘了它娇小的身形。但其实,这些新生代的庞然大物,却要称运-5一声前辈。运-5运输机,是中国航空工业史上的标志性机型,1954年还处在起步阶段的新中国航空工业,从苏联引进了首批9架安-2轻型运输机,随即展开仿制攻关,仅用三年便完成首飞。自此这款机型便扎根祖国广袤疆域,在高原、边疆、海岛及偏远山区持续服役。
专家提问3
运-5的服役,对于我国运输机发展的意义
运5它是仿制而来的运输机,可以说皮实耐用,结构简单,非常的靠谱。
那么之所以采用仿制的技术,就是早期我们在运输机的研发方面缺乏这种足够多的经验,所以我们选择最简单的最可靠的,或者说在此前应用过程当中,那么它的表现是非常好的,这样的一个平台进行了仿制,很快就实现了批量列装。
为什么说韵舞非常的重要,因为它的功能实在太多了,既可以满足日常的人员物资的运输需求,同样也可以用于训练伞兵,那么这对于伞兵部队,也就是空降兵部队来讲是非常重要的。
它的勤务保障要求是非常低,那么而且是一台发动机,那么通过大批量的列装和使用,大家认为运5它是一款非常优秀的这种单发的这种运输机,那么既可以执行运输任务,也可以用于伞兵的训练,
动画2
运-5运输机和目前主流的大型运输机有着很多截然不同的设计理念,采用活塞式发动机的它,可以说是一台空中拖拉机,这种设计相比采用涡轮发动机的现代运输机,虽然在飞行速度和载重能力上都处于劣势,但是却在,维护成本和燃油经济性上有着巨大的优势。 采用双机翼设计的运-5,在飞行时上下两个机翼都能够产生升力,这种牺牲飞行速度换来的是极佳的低速飞行性能和短距起降能力,在极限状态下,运-5仅需180米的跑道即可完成起降。如果说运-5身上最“老派”的设计,一定是它的固定式起落架,这种二战时期就已经被淘汰的设计方式,为运-5带来了极高的结构可靠性,同时不需考虑收纳起落架所要占用的空间,为本就不大的机体内部腾出了更多载物空间。正因如此,运-5可轻松承载1.5吨货物穿梭于简易跑道之间,成为偏远地区空中运输的可靠选择。
解说4
目前运- 5仍是我国通用航空领域的核心装备,同时在军用训练中持续发挥价值,凭借 170 米短距起降、低速稳定飞行的特性,在多场景中不可替代。​
在农林领域,它作为 “绿色战鹰” 承担飞播造林与病虫害防治,从内蒙古草原到秦岭山区,精准播撒籽种或喷施药剂,完成大面积生态作业。改型后的运-5B可快速切换农林、旅游功能,适配短途客货运输与航空摄影等任务。​ ​
军用领域,运- 5 是空降兵基础跳伞训练的核心机型,低速稳定的飞行特性为新兵提供安全的跳伞环境,累计培养数万空降兵骨干,其简易场地适配性也可满足边防物资快速投送、应急机动支援等军事需求。
专家提问4
运-5这种明显的老旧机型为什么能持续服役近70年?
首先服役的数量比较大,另外一个操作非常的简单,不需要特别复杂的这样的一个技术保障条件。那么由于还有一部分运5它的发动机还是处于可靠的状态,所以在它的寿命周期内还是可以用的。为什么说现在仍然有一部分任务还是可以执行任务的,比如说用于日常的飞行训练任务,包括伞兵的日常训练,他还是合格的,还是能够胜任的。比如说转为这个农用飞机,进行飞播造林或者说进行喷洒农药,那么它的低空慢速的性能比较好,所以也可以满足此类任务需求。还有一个未来的发展和改进方向,把它改造成无人机。那么利用一些特殊的设备进行无人化改造之后,它就变成了无人运输机,可以在野战简易的跑道上进行起飞和降落。我们可以打造运5无人机的编队,为一线的部队去运送物资,或者说去转运人员,所以说目前仍然有一部分运物,它可以继续的发挥余热,那么如果利用可靠的平台改成这种运输型的无人机,也是一个很好的选择。
主持人3
运-5这款“空中小马驹”在数十年的服役生涯中凭借出色的低空飞行性能、极强的短距起降能力以及极高的可靠性,成为一款老而弥坚的经典机型,在一种大型运输机中仍然担负着不可替代的作用。但是并不是所有以“马”命名的飞机都像“小马驹”一样温顺实用,它们也可以充满野性与力量。如果在空中有哪一匹“马”称得上是力量担当,那非美国的CH-53直升机莫属,并且它也有一个霸气的名字——“海上种马”
解说5
20世纪60年代,美军部队面对越南战场上覆盖大量丛林的山地战场,大量需要依赖地面跑道才能起降的运输机无法有效投送重型装备与部队,面对前线胶着的战事,直接催生了直升机这种不依赖地形可以垂直起降的航空装备的发展。此时美国陆军已经拥有了以CH-47“支奴干”双旋翼运输直升机等为代表的重型运输机,而海军陆战队在这一方面还是空白。
当时的海军陆战队决定自行研制一款专属于自己的重型运输直升机,而且性能必须超越陆军现有机型,能吊运火炮、车辆等超重装备。在几番竞标与技术攻关后,西科斯基公司最终中标,由此便开启了属于海军陆战队的“海上种马”时代。
专家提问5
为什么海军陆战队坚持自主研制CH-53,而不是直接采用陆军已有的CH-47?
我们客观来讲,ch47也可以满足美国海军陆战队的这种任务需求,因为它的载重能力和针对一些轻型火炮的吊运能力是比较强的。如果美国海军陆战队直接使用ch47没有任何问题,但是为什么说美国海军陆战队还要发展 ch53,首先美军的内部它是有军种之争的,那么海军陆战队虽然说在军迷和网友的眼中被称为4等人,但是美国海军陆战队它的作战环境比较特殊,尤其是执行夺岛作战和两栖作战的过程当中,它必须有一个独立的这样的装备体系。这样的话他可以争取到更多的经费,另外一个保证他自身的任务需求,关键的时候是不求人的。那么为什么超级种马这款重型的直升机设计是比较独特的呢?就是因为海军陆战队它要求装备在海上长时间部署的时候有非常强的可靠性,而且对于重型直升机它的长度以及它所占的两栖攻击舰,内部的机库空间的这样的一个面积要做到一个最小值。所以超级种马和ch47相比,更能满足海军陆战队的需求,而且我们注意到它的主螺旋桨是可以进行折叠收纳的,那么这对于在两栖攻击舰的机库内部进行存放,这个是比较合适。
动画3
CH-53“种马”重型运输直升机家族最大的本事就是——力气大。我们要如何量化它的力气呢?“种马”家族中最强的CH-53K“超级种马”配备三台发动机,一共可以输出约1,8万匹马力,要知道,普通家用轿车的动力一般在150至200匹马力,也就是说,一架“超级种马”大约相当于100辆家用轿车的力气。但这还不是“超级种马”的极限,因为它还预留出了第4台发动机的位置,所以1.8万匹其实并不是它的上限。这种超标的动力,使他可以一次性搭载55名全副武装的陆战队员进行机降作战,如果使用机体上的货物挂钩,它可以拉起一架并不满载的F-35C固定翼战斗机。
解说6
自CH-53种马家族的老大哥“海上种马”服役以来,进行了多个型号的更迭,从CH-53A“海上种马”到CH-53E“超级种马”,再到如今的CH-53K“种马王”,种马家族的每一代成员都以更强的运载能力、更高的可靠性与更远的航程,持续刷新着重型运输直升机的性能标杆,CH-53K “种马王” 作为家族巅峰之作,搭载 3 台 GE T408 涡轴发动机,单台功率突破 7500 轴马力,相较 CH-53E 运力提升近一倍,可轻松吊运 12 吨级装备或 37 名全副武装的士兵,完全适配两栖攻击舰与偏远战场的投送需求。每一次型号迭代都紧扣实战需求,在载重、航程、防护三大核心指标上持续突破,既巩固了其全球重型运输直升机的领先地位,
专家提问6
发展重型直升机对于现代战争的意义?
军事强国大国必须要拥有重型的运输直升机,为什么?因为重型的运输直升机,它针对陆军航空兵,针对海军陆战队的支援保障能力是比较强的。因为重型直升机它内部的这个舱室的空间,也就是货舱载员舱的空间比较大,一次性投送的兵力比较多。另外一个它也可以携带一些突击车辆,那么通过这种快速低空投送的方式,把人员和装备,那么都投送到战场上的这样的一个重要的区域去发挥作用,也可以采用这种调运的方式去向一线部队去提供轻型火炮,包括一些轻型的这种运输车辆,甚至是一些轻型的装甲车,那么它也像是一个会飞的起重机,针对前线作战部队进行物资弹药的补给,它也是会飞的,那么它也是一那么针对一线的作战部队进行物资补给、弹药补给和装备的这样的一个运输,它也是一个不可或缺的平台。而且很多的重型直升机,它的高原性能都比较突出,这样的话在高海拔严寒,空气比较稀薄的那么这样的话在高海拔严寒空气比较稀薄的这样一个区域,先进的重型直升机发挥的作用是不可替代的。
主持人4
种马家族自诞生以来一直在直升机运载能力的榜单上名列前茅,它的外形看起来算不得英俊甚至有些粗犷,但它就像驰骋在沙场的战马一样充满力量与野性。
看完了翱翔在蓝天的“飞马”我们再将视线转向陆地,这里才是战马最熟悉的战场。
在古代,马匹作为最重要的运输和作战工具,主宰着战争的节奏与走向。如今,陆地上的钢铁战马早已取代血肉之躯,
如果哪款以“马”命名的路上装备最为人所熟知,那无疑是“悍马”车,这种外形方正的军用越野车自问世以来便以卓越的机动性和强悍的通过性著称,无论是在沙漠戈壁还是密林山地,都能如骏马般纵横驰骋。其居民两用的特性更是成为无数男生心中的梦想座驾。
解说7
悍马车是无数男生梦想中可以上山下海的硬核座驾,硕大的车身和粗犷的轮眉下,是能征服一切地形的全时四驱系统与中央轮胎充放气装置;能在-46℃极寒与55℃酷热中稳定运行,是极限越野爱好者的最佳选择。而它却有着真正的军用血统。
军用的悍马车的全称是“高机动性多用途轮式车辆”,之所以后来被叫作“悍马车”则是因为它的英文名字,(气口)的发音听起来像是“悍马”。
作为一种可以在低烈度场景下进行人员运输的车辆,悍马车可以说是参与了上世纪80年代后,美军参与的所有战争。
专家提问7
80年代后,为什么悍马车会作为主力装备,参加美军的所有战争?
悍马它应该是一种通用型的军用车辆,本质上来讲比吉普车大,但是比重型卡车小,它应该是有装甲防护能力的一种中型的卡车。那么这种车辆最大的优势它可以防弹,因为它是有装甲板的,针对轻武器,比如说步枪轻机枪的一些射击,它可以起到一个足够的它可以起到一个足够的防护作用。与此同时悍马车也可以架设武器装备,像重机枪、自动榴弹发射器,反坦克导弹都可以安装和架设,那么可以协助和支援坦克和装甲车进行作战,同样也可以边快速行进,边进行这种火力压制。那么从80年代之后,可以说一直到现在,美军仍然装备大量的悍马车,那么在进行部队,那么在部队进行大纵深区域内进行机动的时候,它可以发挥重要的运输和保障作用。当然在一些低烈度的这样的一个巡逻任务当中,它也可以有效地掩护步兵,那么也可以通过安装附加装甲的方式,强化自身针对轻武器的防御能力,所以不仅美军用很多其他国家也是引进装备和采购。
动画4
悍马车最大的特点就是“全能”,首先作为一款可以在复杂路况下进行人员运输的车辆,它除了不能飞,不能潜水,几乎做到了全地形。非承载式底盘和大行程独立悬挂,使它的离地间隙达到了40厘米,可以跨越0.6米高的障碍或者接近1米的水域。其次就是功能全,宽大的车体可以针对不同任务进行模块化改装,配备电子系统可以作为战术指挥车辆,宽大的后排空间可以改造为战场救护站,最重要的就是可更换的武器站,可以在重机枪,榴弹发射器等单兵武器间进行更换,甚至可以加装反坦克导弹,客串一把“坦克杀手”。加上全车覆盖的轻型装甲,让悍马车成为低烈度地面作战的最佳选择,成为了廉价版的多功能装甲车
解说8
当然悍马车并非完美无缺,虽然相较于其他车辆悍马车的防护能力已属上乘,但其本质上还是一款轻型运输车辆,在面对RPG火箭弹或路边炸弹时仍显单薄,并且为了在极端复杂战场环境中保持高机动性,导致悍马车的油耗居高不下,但这又与它经常参与的远距离奔袭和城市作战需求形成矛盾,尤其在2003年伊拉克战争后,大量的悍马车因防护不足而遭袭损毁,从而促使美军加速推进“联合轻型战术车辆”项目的研发,悍马车逐步退出一线作战序列,转而承担后勤运输、基地巡逻等低风险任务;其经典形象则通过民用版H1、H2化作了一个时代的标志。
专家提问8
为什么悍马车没能替代装甲车。
悍马车为什么没有完全替代装甲车?因为它本质上来讲不同的平台,悍马车它是一种相对而言偏轻量化的通用的装甲车辆,但是它的装甲是轻装甲,没有办法进行装甲层面的这种升级和完善,最多也就是安装一些更厚的这样的一些装甲板,或者说安装上防弹玻璃,仅此而已。因为它的底盘不足以承担更重的像格栅装甲或者说更重的这种附加装甲。另外一个防雷的能力比较差,它是一个平的这样的一个车底,并没有采用v字型的车底,一旦压到了地雷或者是武装分子自制的简易炸弹,往往会导致车毁人亡,因为它的防雷能力是比较弱的。
除此之外,它也没有办法加装个头比较大的炮塔,因为在这种大规模装甲作战平台使用的过程当中,当先锋打头阵的肯定是像艾布拉姆斯主战坦克、布雷德利履带式步战车这样的装甲车辆。那么当面对强大对手的时候,悍马火力不足,防御能力不够,所以说它无法替代装甲车。还有一个就是现在美国的这种治安战已经非常少了,以前利用一些悍马车能够进行武装巡逻,能够搜索一些武装分子和恐怖分子,现在美国陆军没有类似的任务需求,所以针对悍马车它的需求量也会进一步的降低。
主持人5
看完了这么多以“马”为名的装备,不难发现它们虽形态各异,却都承载着人类对速度、力量与自由的永恒追求。无论是运-5的坚韧执着,CH-53的雄健霸气,还是悍马的无畏前行,这些机械身躯都延续了“马”这种动物的特点。
在节目的最后,《军事科技》栏目组全体成员,也借着这些武器装备中的“骏马”祝您在新的一年如骏马奔腾,勇往直前,事业腾飞,生活安康。
@@ -0,0 +1,135 @@
导视:
对于枪械而言,很多数据指标都可以影响枪械的性能,但其中非常重要的一个指标就是“射速”,更快的射速,就意味着枪械可以在一定的时间内提供更强的火力输出,那么在众多类型的枪械中,射速最快的枪械都是哪些,它们是如何实现更快的射速,更高的射速对它们来说,又有着怎样的意义呢?《军事科技》带您探究《枪械射速决定论》
主持人1
观众朋友们大家好,欢迎收看本期《军事科技》,我是主持人蓝皓。在瞬息万变的战场上,火力的压制与覆盖往往能决定战斗的走向。枪械射速的快慢,直接关系到单位时间内弹药投送的密度和对目标的打击效率。那么,究竟哪些枪械能够称得上是射速领域的“佼佼者”?它们又凭借着怎样的设计奥秘,实现了令人惊叹的高射速?今天,我们就
首先我们来看看手枪,手枪作为近距离自卫和辅助作战武器,并不会像步枪和机枪一样追求极高的射速,尤其是早期的左轮手枪,其射速很大程度上依赖于射手的熟练度。那么,现代手枪是通过什么方式去提升射速的呢?
解说1
在早期热武器时代,无论是燧发枪还是火绳枪,都因为需要将火药和弹丸通过枪口进行装填,所以导致它们的射速受到极大的限制。在这一时期,前膛枪的射速一般在每分钟三发,极其熟练的射手,也仅能做到每分钟5发,所以我们会看到,在两军对垒时,双方都会排着整齐的密集阵型进行射击,通过密集的齐射,弥补单支枪械的射速劣势。
随着枪械技术的发展,后装枪的出现使得装填速度有了一定提升,同时定装弹药的普及也让装填流程大幅缩减,作为这一时期的代表武器,左轮手枪可以一次性装填6发弹药,极大的缩减了射手在射击间隙的装填时间。但同时我们会发现,这一时期的左轮手枪在设计时, 会有一个奇怪的动作,
动画1(二维):
很多人以为枪械开火只需扣动扳机,实际上扣动扳机瞬间要完成两件事。扣动扳机释放击锤,撞击底火引燃发射药完成开火;若要再次击发,仅扣扳机是无效的,需像西部牛仔一样手动扳回击锤才能再次射击。这种打一枪扳一下的模式叫“单动式击发”,即扳机只释放击锤,所以早期左轮手枪提升射速全靠射手双手配合。
若想提升射速,则需要将“扳”和“击”连接起来,扣扳机时同时完成击锤复位与释放,这就是“双动式击发”。现代多数左轮手枪和部分半自动手枪采用此设计,射击前无需手动扳击锤,扣扳机时击锤自动复位与释放,利用套筒后坐力完成击锤复位,省去手动操作,显著提升连续射击速度,将手枪带入“半自动时代”。
手枪在很多的使用场景当中,它是作为自卫武器去进行使用的,那么很多这种使用手枪的这样的一些人员,可能并没有经过长时间专业化的训练,所以手枪在一定距离上进行射击的时候,精准程度没有办法保障,那么在自卫的这种状态下,可能就要求它有很高的射速,那么也就是说它必须要在短时间内清空弹匣,采用这种快速连续射击的方式提升他的命中率,让对方在短时间内失能失效,不具备这种反击的能力,才可以有效的保护自己,所以说手枪的这种快速的射击能力是比较重要的。
主持人2
当手枪进入半自动时代后,射速也有了明显的提升,一般情况下清空一个15发弹匣仅需4到5秒,半自动手枪的理论射速更是可以做到每分钟600到800发。但这并非是手枪射速的极限,近年来,部分特制手枪不断将射速推向新高度,其中最具代表性的就是奥地利格洛克公司推出的格洛克18。那您猜这把抢的射速能达到多少?我们一起来看一下。
您没有看错,这种超越常规手枪认知的射速,就是格洛克18可以在现实状态下达到的射速,所以格洛克18也区别于半自动手枪,成为了一种全新的“全自动冲锋手枪”
解说2
格洛克18的诞生,源于对极致火力的追求。
20世纪80年代,全球反恐形势日益严峻,执法部门和特种部队急需一种能在近距离突发冲突中提供强大火力压制的便携武器。当时主流的半自动手枪在面对多名武装分子时,单发射击的火力密度已显不足。奥地利格洛克公司敏锐捕捉到这一需求,决定在其经典的格洛克17基础上开发一款具备全自动射击能力的新型手枪。它在格洛克17的基础上进行了关键性改进,其理论射速可以达到每分钟千发以上,这意味着一个标准的17发弹匣,在不到两秒的时间内就会被打空。如此恐怖的射速,使得格洛克18在近距离遭遇战中,能够瞬间形成强大的火力压制,这种强大的火力输出使其区别于半自动手枪,自成一派。
动画2
格洛克18与同时期其它半自动手枪最大的不同就是它完全取消了击锤设计,转而采用了击针平移式击发机构。这种设计不仅简化了内部结构,减少了运动部件的数量,更重要的是缩短了击发动作的行程。相比传统击锤式设计,在动作连贯性和响应速度上都有显著提升。
更为关键的是,格洛克18创新性地引入了一个快慢机选择杆。当快慢机拨至全自动模式时,射速最高可以达到1300发每分钟,这种射速已经超越了大部分全自动步枪。使其在近距离内能够爆发出接近冲锋枪的火力密度。
同时为了防止在全自动模式下枪械走火误伤,格洛克18在扳机出设计了一个独特的双扳机结构,在主扳机前方设有一个小型的辅助扳机,只有当手指同时按压辅助扳机和主扳机时,枪械才能进入全自动发射状态,这一设计极大地提升了使用时的安全性。
格洛克手枪很多国家的警方的执法人员是大量的配发,它最大的优势就是操作性是比较好的,而且整体上来讲也不是特别重。那么经过简单的培训之后就可以进行精准的射击,那么如果是警方使用这种枪械就会面临一个问题,很多情况下都是遭遇战,比如说针对车辆进行检查的时候, 针对嫌疑人进行抓捕的这样的一个过程,在这种状态下,一旦对方使用武器进行射击,那执法的执法人员或者是警员可能就会受伤,所以这个时候就要求手枪它有极高的射速,短时间内大量的倾泻弹药,针对目标进行压制,让对方失去反抗的能力,让对方没有机会开枪。所以这是在一种危险环境当中,对于手枪性能的这样的一个要求,--因为格洛克这种手枪可能在军队当中使用的并不是特别多,主要是在执法的过程当中能够快速射击,精准射击,针对嫌疑人和持枪的人员进行杀伤和压制。
主持人3
格洛克18因其独特的全自动模式成为手枪家族射速担当,最高每分钟1300发的射速让众多步枪和机枪甘拜下风。但手枪是近距离自卫武器,无法远距离作战,此时步枪的作用就凸显出来了。
步枪射速的进化大体也是经历了单发、半自动、全自动三个阶段,但起点是栓动式步枪。
若在单发栓动步枪中选射速最快代表,当属李恩菲尔德步枪,它不像左轮手枪靠射手双手配合,而是在结构设计上有独特之处。
解说3
在栓动式步枪被大量使用的二战时期,由于机枪和步枪的使用方式有着明显的界限,以毛瑟98K步枪和莫辛纳甘步枪为代表的一众栓动式步枪,并没有针对射速提出更高的需求,但其中就有一把特殊的栓动式步枪在射速层面玩出了不同的花样。
刚刚电影《集结号》中谷子地口中并不好使的“英七七”就是这一时期栓动式步枪中高射速的代表“李恩菲尔德”栓动式步枪。
李恩菲尔德步枪的高射速秘诀,首先体现在其独特的后端闭锁枪机设计。与当时许多步枪采用的前端闭锁枪机不同,李恩菲尔德的枪机闭锁突笋位于枪机后部,这使得枪机的旋转角度仅需60度,远小于毛瑟98K等步枪的90度甚至更多。更小的旋转角度意味着更短的操作行程和更快的开锁、闭锁速度。同时,其枪机的设计也更为平滑,拉机柄的位置和形状经过优化,便于射手快速操作。熟练的射手可以利用“疯狂一分钟”的射击技巧,在一分钟内发射超过30发子弹,甚至有记录显示顶尖射手能达到每分钟40发以上的惊人射速,这在手动栓动步枪时代是极为罕见的,远超同时期其他同类武器。这种高射速使得李恩菲尔德步枪在堑壕战等近距离遭遇中,能够凭借更快的火力输出压制对手,弥补了手动装填的固有劣势。
当英军士兵使用里恩菲尔德步枪针对对方进行火力压制的时候,对方以为英军使用的是机枪,这就表明它的射速是非常快,尤其是在一战的战场环境当中,进行这种战壕对峙,或者说利用战壕相互射击的这样的一个状态。那么里恩菲尔德步枪它的可靠性和它的这种快速射击能力,英国军队是比较看重的。不过也有人反映说里恩菲尔德步枪由于枪体过重,携带的时候可能会比较耗费体力,那么再加上射击的速度比较快,如果说盲目的进行快速射击,可能会浪费步兵的弹药,比如说你短时间内把所有的弹药都打光了,当对方发起冲锋的时候,或者说你要执行后续作战任务的时候,步兵也会面临弹药不足的情况,
主持人4
李恩菲尔德步枪凭借着与同时期其它栓动步枪不同的结构设计,获得了更高的射速,但终究还是无法克服栓动步枪的天然劣势。如果想要获得更高的射速,依旧还要走上半自动甚至是全自动的道路,现代化的突击步枪,基本上都可以依靠快慢机,在半自动和全自动之间进行切换,以AK-47为例,其理论射速约为600发每分钟,而M16突击步枪的理论射速则在700-950发每分钟之间。那么为什么同样是具备全自动射击模式的突击步枪,却有着截然不同的射速上限呢?
解说4
当步枪进入到自动化时代,射速的重要性开始与精准度和稳定性平起平坐,弹匣供弹为提高射速提供的弹药保障,那么想要提高射速,唯一需要解决的就是如何在射击时实现连贯的自动循环系统。所谓的自动循环系统其实就是通过多种机械结构之间的配合,使两次步枪设计之间省略掉手动拉栓这一费时的步骤,从而提高射速。
动画3
目前步枪家族实现自动循环的主要方式分为活塞式和吹气式两大类。活塞式原理为,枪弹击发后,部分火药燃气推动活塞后移,活塞通过连杆带动枪机完成开锁、抽壳、抛壳等动作,再在复进簧作用下复位推弹上膛。这种方式将燃气能量经活塞间接传递给枪机,典型代表是AK - 47,因其活塞运动行程长,被细分为“长行程活塞”。
而吹气式则省略活塞环节,让火药燃气经枪管导气孔直接进入机匣,推动枪机框和枪机后坐,典型代表是M16步枪。
随着战场实践深入,在两者基础上取长补短,演化出“短行程活塞式”。其工作原理是,枪弹击发后,火药燃气推动活塞后移较短距离后停止,再靠惯性或连杆传递能量带动枪机完成后续动作。它保留了活塞式可靠性高的优点,避免了长行程活塞射速受限问题,相比直接导气式,减少了燃气对机匣内部零件的污染和烧灼,使枪械在保持高射速的同时,有更好的可靠性和维护性,代表是HK - 416。
解说5
无论是哪一种自动循环系统,都是希望在射速和稳定性这两者之间找到一个平衡点,
毕竟射速并非越高越好,过高的射速会导致弹药消耗过快,射手需要频繁更换弹匣,反而可能在实战中出现火力中断的空档;同时,高速射击产生的后坐力叠加也会严重影响枪械的射击精度和操控性,让子弹难以命中目标。因此,现代突击步枪在设计时,会根据其作战定位和使用场景,对射速进行合理的调控,而最终得到的结果就是大家默契的将射速控制在每分钟600到800发。
首先单兵所携带的弹药量它一定是有一个上限的,所以即使把突击步枪的射速提升到了一个更高的水平,也会面临弹药不足的情况。而且枪械整体的重量必须要控制在一个合理的范围内,否则步兵在携带和使用的过程当中,就会大量的消耗体能,甚至会出现携带不方便的情况。么单兵在使用突击步枪的时候,主要还是在中近距离上进行这种点射,或者说在近距离上进行这种连发的这样的一个射击,无论是哪种作战模式,那么它都不需要有特别高的这样的一个射速,只要能够保证在一定的距离上能够持续的压制对方,能够保证火力的密度,其实就可以了。
解说6
当然总会有一些特殊的枪械会打破对于射速的限制。既然长时间的快速射击会大量消耗弹药,并且也会影响枪械的精准度,那么是不是可以追求一个更短时间的射速爆发,让枪械在一个瞬间获得极高的射速,但又不牺牲稳定性。
这种想法的结果就是三连发射击模式。这种射击模式的代表就是M16突击步枪。
M16步枪的三连发模式,通过内部的棘轮机构或电子控制模块,实现扣动一次扳机即可连续发射三发子弹的功能。在这种模式下,枪械的射速在极短时间内可以达到一个峰值,例如,三发子弹的发射时间可能仅需0.2秒左右,换算成瞬时射速可达每分钟900发以上,远高于其全自动模式下的理论射速。射手在使用三连发模式时,每扣动一次扳机,就能获得一个短暂而猛烈的火力“点射”,既比单发射击更具压制力,又比全自动射击更容易控制节奏和节省弹药。因此,三连发模式成为了平衡射速、精度与弹药消耗的一种有效解决方案,在现代突击步枪设计中被广泛采用。
主持人5
无论是全自动的火力压制还是三连发所追求的精准度,都是为了应对不同的场景,所以我们能看到步枪家族在射速上达成以一个默契,并且以不同的方式寻求突破。
看完了步枪射速的多种多样,我们就来到了枪械射速的顶点。
当射速迈过了千发每分钟这个门槛后,射速能起到的作用基本上就是进行火力压制,而执行火力压制最合适的枪械就是机枪,当机枪的枪口开始喷吐火焰,雨点一般的子弹,就会形成致命的弹幕,那么,机枪的射速提升又将走出怎样的道路呢?
解说7
机枪的诞生,本身就是为了追求极致的射速,用连续不断的射击形成的弹幕去压制敌人,在最求这种更高射速的过程中,最大的难点就是连续的射击会导致枪管过热,所以,为了解决热量问题设计师给出了两种解决方式,水冷式散热和风冷式散热。
动画4
水冷式机枪和风冷式机枪最大区别在于枪管散热方式不同。水冷式机枪在枪管外包装满水的套筒,利用水蒸发吸收和带走枪管持续射击产生的热量,使枪管长时间保持低温,可持续连发射击,提升射速和火力持续性。如马克沁重机枪,理论射速约每分钟600发。不过,水冷式机枪水套筒重,增加枪械整体重量,机动性差,且依赖水源,在缺水或需快速转移的战场受限大。风冷式机枪依靠枪管自身散热及更换枪管解决过热问题,取消水套筒,通过设置大量散热片增加与空气接触面积、加快散热。枪管温度过高时,射手可迅速更换备用枪管恢复射击能力。如MG - 42通用机枪采用风冷式设计,理论射速高达每分钟1200发,兼具高射速和良好机动性。
在二战期间机枪可以说是针对对方步兵进行火力压制的一款主要的武器,那么制约机枪射速的方式,可能和冷却的这样的一个模式和设计理念存在密切关联,像马克沁机枪它是采用水冷的模式,所以长时间连续射击的时候,枪管不会因为过热问题而出现故障,导致它无法开火。针对对方的冲锋的兵力或者是进攻的兵力进行压制,那么不需要频繁的更换枪管,这个优势是非常明显的。但是像MG42,那么它是采用这种风冷的方式,射速又非常的快。
所以在运动的过程当中,或者说在进攻作战的过程当中,它可以扮演非常重要的角色。而且 MG42虽然会容易出现枪管过热的问题,但是更换枪管速度比较快,操作起来也比较容易。一般这种德军他在操作和使用机枪的过程当中,都是有其他的士兵进行配合的,帮他去搬运弹药或者说携带枪架,当然了也配备有更换的这样的一个枪管,所以风冷机枪它在使用的过程当中就是要看更换枪管的速度究竟有多快,操作使用是否方便。
解说8
无论是风冷式散热还是水冷式散热都可以有效的解决连续射击带来的热量问题,通过这种方式也将机枪的射速推进到每分钟千发以上,但这并不是机枪所追求的射速极限,如果想在每分钟千发的基础之上再此提高射速,就需要有更加激进的设计,在种种需求的推动枪械射速的巅峰——转管机枪出现了。
动画5
转管机枪提升射速的方式简单粗暴,因为单根枪管连续射击容易过热,设计师便将多枪管捆绑,让其轮流射击,如同机枪手同时用数挺机枪进行轮换。不过,这种简单逻辑下的产物使转管机枪体积普遍较大,早期手摇式转管机枪需骆马运输,射击时射手要手摇把驱动枪管旋转和供弹,射速受摇动速度限制,通常每分钟200 - 300发。而现代转管机枪普遍以电动机或液压马达为动力源,通过齿轮传动系统带动多根枪管绕中心轴高速旋转,每根枪管旋转到特定位置完成装弹、击发、抛壳等动作。由于多枪管交替工作,每根枪管有足够时间自然冷却,可使机枪以极高射速持续射击。
解说9
以M134转管机枪为代表的射速王者,依靠6根枪管循环射击的叠加效果,将机枪的射速推进到每分钟6000发,这意味着在一秒钟内,就有足足100发子弹倾泻而出,形成一道几乎无法逾越的金属弹幕。如此恐怖的射速,使其在对付集群目标、低空慢速目标或进行区域压制时展现出无与伦比的威力。无论是在直升机舱门、舰艇甲板,还是车载平台上,M134转管机枪都能凭借其“火神炮”般的咆哮,瞬间覆盖目标区域,给予敌人毁灭性的打击,成为现代战场上令人胆寒的火力象征。
那么这种机枪单兵是没有办法携带的,因为实在是太重了,而且需要有外部的这种供电的设备,往往是在车辆上,比如说在悍马车上架设这种6管的这样的一个转膛机枪,速射机枪,可以针对对方的步兵进行密集的火力压制,短时间内让对方的步兵出现大量的伤亡,所以这种利用电力驱动的速射机枪,或者说我们叫它多管机枪,往往是在装甲车上,在直升机上,或者说在一些特种作战的这种冲锋舟和快艇上进行使用,可以在突击作战任务的过程当中,在特种作战行动的过程当中提供近距离的火力压制能力。
主持人7
不同枪型在设计之处都会将射速作为一个关键要素,手枪提升设射速极大地提升了单兵的自卫火力,步枪的不同射击模式用以适应不同的作战场景,机枪的火力压制将射速推向顶点,所以射速的提升始终是枪械发展历程中贯穿始终的核心追求之一。可以说,射速的不断突破,不仅是枪械技术进步的直观体现,更深刻地影响着战术思想的演变和战场格局的塑造,它使得枪械在不同作战环境下能够更高效地完成其使命,成为士兵手中可靠的“火力倍增器”。
好了观众朋友。。。
@@ -0,0 +1,147 @@
武器进化论:空战颠覆者
【导视】
从螺旋桨战机的低空缠斗,到无人机的智能作战,空战武器的每一次革命性突破,都在重塑天空战场的制胜逻辑。当一件装备彻底打破“速度、火力、隐蔽”的三角平衡,当它的出现让延续数十年的空战范式轰然倒塌,它,就是天空的“规则改写者”。这不是装备的简单升级,而是空战法则的彻底重构。
本期《军事科技》将带您翱翔苍穹,揭开空战史上最震撼的转折点,看这些颠覆性装备如何定义制空权的归属。
【主持人1】
各位观众,大家好!欢迎收看本期《军事科技》,我是主持人蓝皓。从螺旋桨战机时代,飞行员凭借驾驶技巧与临场应变展开的近距离空中缠斗、航炮对射,到无人机通过智能巡飞与协同作战实现的零伤亡全范围精准打击,空战武器的演进,始终围绕着“如何在广袤天空中高效打击敌人、保护自己”这一核心议题。每一款革命性装备的诞生,并非简单的性能提升,而是对空战形态的根本变革。
螺旋桨飞机:空战时代的“开拓者”,近距离缠斗的“定调者”
【解说1】
20世纪初期,飞机的问世首次把人类的战场扩展到了天空。起初,飞机只负责执行侦察任务,并未配备专门的作战武器。一战爆发之后,各国着手对飞机进行武器改装,将机枪和轻型炸弹安置在机身上,以活塞式螺旋桨为动力的螺旋桨飞机就此诞生,真正意义上的空战时代就此拉开帷幕。
【专家1】
螺旋桨飞机,之所以被人们称为真正意义上空战的到来,其实主要在于它摆脱了战场辅助作用。在螺旋桨飞机出现之前,人类尝试的航空载体,比如飞艇、热气球,能承载战场的侦察、通信、传递等一些辅助任务,本质上也就是空中的一个观察哨,不存在真正的空战行为。而螺旋桨飞机,依靠活塞式发动机和螺旋桨推进系统,才真正实现了真正意义上的空中对抗。同时,螺旋桨飞机发展过程中,机枪同步射击装置的发明是个关键的转折点,它解决了机枪射击不会击中自身螺旋桨的难题,让飞机拥有了可实战的机载攻击武器,直接赋予了螺旋桨飞机空中作战的能力。
【解说2】
第一次世界大战的天空成为螺旋桨飞机的“练兵场”,一大批经典机型相继登场。其间,机载机枪的射击精度与射速持续提升,战机的机动性能也不断优化,横滚、俯冲、筋斗等专属空战动作成为飞行员的必备技能。1916年索姆河战役期间,德国福克E.Ⅲ战斗机凭借其搭载的核心装备“射击协调器”,成功突破了战机机枪射击与螺旋桨旋转的技术瓶颈,让机枪子弹可精准穿过螺旋桨旋转面,无需担心击伤桨叶。凭借这一革命性优势,该型飞机在短短数周内便击落协约国战机60余架,创下了震惊战场的空战纪录,后世也将其称之为“福克灾难”。
这场战争让各国逐渐意识到制空权的重要性,空战也从单纯的侦察护航,发展为独立的作战单元。螺旋桨飞机不仅开创了“近距离缠斗”的经典空战模式,更推动人类形成“制空权影响战场胜负”的核心军事理念,为后续战机动力升级、武器革新及战术体系构建,奠定了坚实的实践与理论基础。
【主持人2】
螺旋桨飞机将天空开辟为全新战场,以航炮的轰鸣开启了空战新纪元,却因速度缓慢、火力薄弱、探测范围有限,使空战始终局限于近距离的“贴身较量”。随着战争需求的不断升级,人们开始追求更快的飞行速度、更高的作战高度、更远的打击距离,一种采用全新动力的战机应运而生,彻底终结了螺旋桨时代,将空战推向“高空高速”的新维度。
喷气式飞机:螺旋桨时代的“终结者”,高空高速空战的“开启者”
【解说3】
第二次世界大战中后期,航空发动机技术迎来革命性突破,喷气式发动机的诞生彻底打破了活塞式螺旋桨的性能瓶颈,喷气式战机由此登上历史舞台。1944年,德国Me-262喷气式战机首次投入实战,其最大飞行速度突破900公里/小时,远超当时最先进的螺旋桨战机,让各国看到了空战的全新可能。
喷气式战机一登场,空战直接换了个时代!它的革命性突破,全靠两大硬核升级:第一,动力彻底革命!喷气发动机不靠螺旋桨,直接靠燃气猛喷往前冲!一下子挣脱了老式飞机的物理天花板速度狂飙、爬升像火箭,轻松冲破音障,一头扎进超音速世界!直接飞到万米高空的平流层,把螺旋桨战机远远甩在身后。第二,火力与眼睛全面进化!大威力航炮取代小机枪,打得更狠、更猛;简易雷达测距仪装上战机,飞行员不再只靠肉眼 “盲打”,看得更远、瞄得更准!空战的精度和范围,直接翻倍升级!这两大突破的合体,使得人类空战,正式从螺旋桨时代,冲进了喷气超音速时代!
【专家2】
喷气式飞机的诞生,彻底改变了螺旋桨飞机以近距离缠斗为主要形式的空战逻辑,迈进了高空高速空战的新时期。
第一代喷气式飞机一出现,就达到了高亚音速。螺旋桨飞机,在第一代喷气战机的眼里,是低速的目标,极其好打,战损比也极其高。喷气式飞机的出现,让螺旋桨飞机在争夺制空权的斗争中被淘汰了。
随着军事科技,特别是材料工艺、发动机的发展,喷气式飞机基本都达到了高空、高速。高空、高速带来的作战样式的变化,取代了缠斗,变成了高速截击、高空截击、高速突防,使作战样式发生了深刻的变化。
【解说4】
二战落幕之后,喷气式战机进入高速发展的阶段,渐渐替代螺旋桨飞机,1950年,朝鲜战争爆发,米格-15战斗机跟随志愿军入朝作战,其出色的性能让美军在朝鲜半岛上空的制空权遭遇了极为严峻的挑战。完全不能与米格-15相匹敌,美国远东空军只能承认,在青川江和鸭绿江之间的空域,米格战机拥有绝对的优势,并把这片区域叫作“米格走廊”。
在美国空军的营地里,甚至竖起了一块显眼的标识牌,上面清楚地写着“距离米格走廊200英里”,以此来时刻提醒每一位曾经自恃技术高超的美军飞行员。为了改变战场空中形势的不利局面,美国紧急大批量装备新型F-86战斗机,希望与米格-15相抗衡。于是,这两款战后第一代经典喷气式战斗机在朝鲜上空不期而遇,正式拉开了人类战争史上后掠翼喷气式战斗机空中对决的崭新时代。
【主持人3】​
喷气式战机突破了螺旋桨时代空战的限制,把空战的领域延伸到了万米高空。超音速飞行速度和初步的雷达探测功能,使得“空中骑士”近距离缠斗的时代一去不复返。不过,随着各国对喷气式战机的不断升级,战机在速度、火力以及升限方面持续突破,相互之间的性能差异慢慢减小,仅仅依赖平台性能的优势变得越来越不明显。这时,一种全新制导武器的成熟运用,完全改变了空战的打击思路,让超视距空战从“雏形”变为现实。
空空导弹:航炮主导的“破局者”,超视距空战的“核心利器”
【解说5】​
20世纪60年代,制导技术与火箭发动机技术迅猛发展,空空导弹完成了从试验品到主战武器的转变,完全打破了“航炮主导空战”的局面。早期的空空导弹主要是红外制导的近距格斗弹,通过追踪战机尾焰的红外信号来锁定并打击目标,之后逐步演化出雷达制导中距拦射弹、超远程空空导弹,具备了锁定后发射、发射后不用管的作战能力。​
空空导弹的核心技术突破,集中体现在三大关键跨越。
第一,射程实现质的飞跃。从早期仅有数公里的近程格斗,一路拓展到如今数百公里的超视距打击,真正覆盖近距格斗、中距拦截、远程猎杀全空域,让战机先敌发现、先敌发射、先敌命中。第二,制导精度与抗干扰能力全面升级。红外成像、主动雷达制导等尖端技术的应用,让导弹看得更清、辨得更准、抗干扰更强,哪怕面对复杂电磁环境,依旧能够精准锁定、一击制敌。第三,机动性能突破有人战机极限。凭借超大过载设计,导弹可以完成飞行员无法承受的高难度机动,灵活追击、快速转向,让空中目标无处可逃。
与此同时,空空导弹不断向小型化、轻量化、集成化发展,单架战机可同时挂载多枚,实现多目标跟踪、多方向打击,大幅提升空中作战的持续火力与体系优势。
【专家3】​
空空导弹的出现,使以航炮为主要兵器的空战形式,发生了深刻的变化。以往视距内的作战,变为了超视距作战,航炮成为了武器中的配角。航炮的有效射程只有数百米,传统空战的所有战术,均围绕着视距内的咬尾攻击展开。飞行员需通过精确的瞄准,抢占敌机的阵位,胜负完全依靠于飞行员的技战术水平。导弹的出现则不同,空战的胜负不再决定于飞行员的技战术水平,更重要的,取决于飞行员对座舱设备的掌握、对态势感知的判断、对时机的把握上。
【解说6】​
越南战争成为了检验空空导弹实战能力的关键战场,早期的空空导弹由于技术尚未成熟,存在一定的脱靶情况,但其打击优势已远超航炮。经过后续的技术改良,空空导弹逐步发展为空战中的主要作战武器。在海湾战争期间,美军战机凭借先进的中距空空导弹,达成了“视距外决胜”的目标,被击落的伊拉克战机中超80%由空空导弹完成,航炮则沦为辅助性武器。​锁定即发现,摧毁即锁定成为空战的新准则,超视距空战正式确立为现代空战的主流形式。空战也从单一平台间的对抗转变为武器与平台协同作战的模式。
【主持人4】​
空空导弹使超视距空战变为现实,完全重塑了空战的打击思路。然而,伴随雷达技术与防空体系的持续进步,战机的探测与反探测、干扰与反干扰成为空战新的关键所在。仅仅依赖战机自身的电子设备,已无法适应复杂多变的战场电磁环境。一种专为电磁对抗打造的装备顺势出现,它虽不具备直接的火力攻击能力,却可成为空战中的“隐形杀手锏”,促使空战迈向体系化作战的新时期。
电子战飞机:单一平台的“补位者”,体系化空战的“隐形支柱”
【解说7】
20世纪70年代,电子技术迅猛发展,以现役战机为基础改装而成的电子战飞机正式亮相历史舞台。它虽未配备传统的火力打击武器,却装配了电子侦察、电子干扰以及电子压制等专业设备,化身为天空战场中的电磁掌控者。
从早期专门用于电子战的改装机,到如今多功能的电子战飞机,其作战性能不断提升,能够实现对远、中、近程电磁目标的全方位覆盖。同时,它还配备了反干扰和隐身防护系统,从而确保在复杂电磁环境中的生存能力。
【专家4】​
电子战飞机之所以成为体系作战的隐性支柱,核心是在于它不是以战斗机、轰炸机的直接火力支援来作为后盾,而是通过对电磁频谱的掌握,成为空中作战的神经中枢和保障屏障,悄无声息地掌握战场的制电磁权,为整个战场体系的感知、打击、生存提供底层支持。制电磁权是现代体系空战的核心基础。失去了电磁优势,战机的雷达、通信、导弹制导统统都沦为了瞎子、聋子、哑巴。电子战机的出现,使空战的攻防逻辑发生了巨大的变化,从以往的火力比拼、平台对抗,发展成为频谱先行、体系作战,电子频谱的争夺成为了空战的首要目标。
【解说8】​
海湾战争铸就了电子战飞机的“巅峰时刻”。美军派遣EA-6B、EF-111等多种电子战飞机,针对伊拉克的雷达、通信以及防空系统,展开全频段且高强度的电子压制。刹那间,伊拉克整个防空体系陷入瘫痪,其雷达如同“盲人”,通信仿若“聋子”,导弹好似“哑巴”,为后续战机的渗透与打击清除了全部阻碍。在整个战争进程中,电子战飞机始终跟随作战编队前行,化作“空中电磁屏障”,极大地减少了战机的战损率。​
电子战飞机的诞生,开启了空战“隐形对抗”的新领域,使得电子压制成为空战的前置步骤与关键战术,缺乏电子战掩护的战机在现代空战里几乎毫无胜算,电子战能力亦成为评判一支空军战斗力的核心标准。
【主持人5】​
体系化空战依靠电子战飞机构建起“电磁骨架”,使空战的对抗层面从单一的火力攻击拓展到无形的电磁较量。然而,随着各国防空系统的逐步完善,传统战机雷达反射面积大、容易被探测锁定的问题愈加明显。即使有电子战飞机的保护,也难以攻破现代化的多层次防空系统。于是,一种融合“隐身性、超视距、高机动”特点的新型战机诞生了,完全打破了常规战机的作战方式,把空战带入“隐形对抗”的新阶段。
隐身战机:常规战机的“颠覆者”,隐形对抗时代的“核心主力”
【解说9】​
20世纪70年代,为了应对现代化的防空体系,美国率先开展隐形战机的研发工作。1981年,世界首款隐形战机F-117A成功完成首飞;1997年,首款五代隐形战机F-22实现首飞,这标志着空战正式迈入隐形时代。隐身战机的关键革新在于全面的“隐身设计”,借助机身外形的流线型优化、吸波材料的使用以及红外隐身技术的支持,显著减少战机的雷达反射面积与红外信号特征,使传统雷达难以探测和锁定,从而达成“来无影、去无踪”的空中突防效果。
【专家5】​
隐身战机的隐身性,之所以成为空战装备的革命性突破,其核心就是打破了现代空战的 “发现即摧毁” 的核心法则,颠覆了以雷达探测为基础的传统防空与空战体系,让空战制胜逻辑从比拼火力、速度、机动,转向争夺探测和反探测的主动权,实现了从装备性能提升到作战维度降维打击的本质跨越。隐身战机的出现,彻底改变了攻防的逻辑。隐身战机能够做到先发现、先攻击、先脱离,能悄无声息地进入攻击地段,对目标展开攻击,敌方还没有发觉,甚至发觉了也没有反应时间,做不好反击准备。
【解说10】​
1989年,美军F-117A隐形战机于巴拿马战争中首次投入实战,借助隐身特性顺利穿透敌方防空网络,实施精确轰炸;1991年海湾战争期间,F-117A执行了1300多次任务,摧毁伊拉克四成高价值目标,自身却毫发无损,向全球展示了隐形战机的强大作战潜力。伴随F-35、歼-20、苏-57等第五代隐形战机陆续服役,隐形对抗已然成为空战现代化的核心模式。​
隐身战机间的较量,不再单纯是平台性能的比试,而是航电装置、隐身科技、制导兵器、电子干扰、战术协作的全面竞争,谁的隐身效果更佳、探测范围更广、导弹准确度更高、电子干扰实力更强,谁就能在超视距空战里取得绝对优势。隐身战机的诞生,完全打破了传统的空战攻防思路,反隐身雷达、反隐身防空体系成为各国研发的关键,制空权的角逐也迈入了“隐形时代”。
【主持人6】​
隐身战机因具备隐身、超视距、高机动的显著优势,已然成为现代空战中的“关键主力”。然而,其高昂的研发成本、巨大的维护难度以及严格的人员培训要求等缺陷也相当明显。并且,随着反隐身技术的持续进步,隐身战机的作战长处正逐步被削弱。当有人空战的成本与风险日益增长时,一种全新的智能作战装备顺势出现,它凭借“低成本、零伤亡、高性价比”的特点,打破了“有人机掌控空战”的传统格局,促使空战朝着智能化、无人化的新阶段迈进。
无人机:有人空战的“革新者”,智能无人空战的“引领者”
【解说11】​
进入21世纪后,伴随人工智能、导航定位以及小型化制导武器技术的深度结合,无人机完成了从侦察支援到实战攻击的飞跃,化身为现代空战中的新兴力量。
从小型单兵携带的无人机,到大型侦察打击一体化无人机,再到能够与有人机配合的“忠诚僚机”,无人机的作战性能不断提升,成为非对称作战和体系化空战的关键组成。
【专家6】​
无人机之所以成为现代空战的新质力量,核心是突破了有人飞机的飞行员生理极限、作战成本、战场风险的多重束缚,实现了零伤亡、低成本、长航时、高适配的独特优势,成为衔接有人作战与智慧化空战的关键载体,为体系化空战注入了全新的效能增量。智能化的发展,人机协同慢慢就变成了无人机单独执行任务,飞行员从最初的执行者,变成了现在、甚至不远的将来的决策者;再后来的发展,随着高科技、智能化的推进,可能成为一个监督者。这是飞行员任务的变化,也是军事科技向前发展的一个必然趋势。
【解说12】​
在体系化空战中,无人机可与有人机组成“忠诚僚机”搭档,由有人机指挥,无人机负责侦察、诱敌、护航和打击等任务,显著降低有人机的作战风险,并提升整体作战效率。无人机的崛起,彻底颠覆了“强者恒强”的传统空战理念,使中小国家也能具备制衡大国空军的能力,同时推动空战从“有人化”迈向“有人+无人协同”的智能化阶段,无人集群作战与自主空战成为未来空战的重要发展趋势。
【主持人7】​
空战时代由螺旋桨飞机拉开帷幕,智能无人空战则由无人机引领,空战武器的演变始终伴随着“攻与防”的长久较量,而每次较量的结局,都是作战理念的提升。
当前,智能化、无人化、体系化、空天一体化已成为空战发展的主要方向,高超音速无人机、空天飞机、无人作战集群等新式装备与战术正在快速研发,未来空战必定朝着“无接触、超视距、智能制胜、空天一体”的趋向不断演进。到底还有哪些突破性技术会融入空战体系?制空权的争夺又会展现怎样的新形态?这一问题,只有时间能够回答。
好了观众朋友们,感谢您一直关注国防军事频道,《军事科技》我们下周同一时间再会。
@@ -0,0 +1,63 @@
《逆袭战局的组装武器》
导视:在残酷的战场上,真正改变战场的,并不一定是预先设计好的完美武器。真正撬动战局天平的,往往是那些原本平凡的装备,它们在特定战场环境下经过临时组装,蜕变出全新的战斗形态。当运输直升机挂上机枪,化身空中火力平台;当榴弹炮装在坦克底盘上,跃升为精准打击利器——这些看似简单的组装,是在战场压力下催生的必然选择。本期《军事科技》,带您走进武器装备从“平平无奇”到“逆袭战局”的关键瞬间,见证这些装备在战场上的蜕变时刻。
Xr演播室 主持人1:观众朋友们大家好!欢迎收看本期的《军事科技》,我是主持人蓝皓。谈到战争与科技,我们总是习惯把目光投向那些耀眼的名字。比如,坦克、航母、导弹、隐身战机。仿佛每一次战局的转折,都来自某项划时代的发明。但历史并非总是如此运转。很多时候,真正改变战场的,不是全新的科技。而是两件原本普通的装备,在某个绝境时刻,被迫结合,并逆袭战场。这其中最经典的一个案例便是“空中炮艇”的诞生。
二维动画解说:1964年12月,越南南部。一个美军营地在夜色中遭到包围。越军数量远超美军。丛林遮蔽了视野。地面火炮无法及时支援。美国空军紧急出动喷气式攻击机。但问题很快暴露。喷气机速度快,油耗高。一次俯冲攻击后就必须返航。夜间目标难以锁定。营地指挥官请求持续火力压制。空中却只剩下一架老式运输机——C47(模拟战场沙盘)
解说:这架飞机诞生于二战。设计目的只有一个:运送人员和物资。在市场花园行动中,数百架C-47组成空中梯队,持续完成兵力与补给输送。它没有重型武装,没有厚重装甲。C-47由民用客机改进而来。机体结构经过加固,地板承重增强,能够承载3吨物资,或一次性运送28名全副武装的伞兵。侧舱门加宽设计,使伞兵可以在数秒内连续跳伞,大幅提升投送效率。动力方面,它搭载两台普惠活塞发动机,巡航速度约370公里每小时,航程超过2000公里。性能并不激进,但机械可靠性高、维护简便,即便在简陋机场也能快速起降。更重要的是,它具备出色的低空稳定性。在复杂气流和防空火力干扰下,飞行姿态依然可控。
二维动画解说:面对无线电中营地指挥官的求助,机组成员提出一个大胆的解决方案:在C47机舱侧面安装三挺机枪,同时利用飞机持续盘旋的飞行轨迹,让火力始终覆盖地面同一目标区域。飞机绕圆飞行,机枪固定角度射击,子弹落点形成稳定“火力圆环”,这便是日后被称作“支点盘旋”的飞行方式。当第一轮曳光弹从机侧喷出,地面形成持续的火网。敌军被压制,营地最终被守了下来。这次临时改装,迅速引起美国空军重视。几个月内,这种经过特别改装的C47被正式命名为——AC-47 。它成为第一种正式编号的炮艇机。
专家:在越南战争初期,美军面临一个非常现实的困境:喷气式战机速度过快,难以长时间停留在战区上空;轰炸机火力强大,但无法提供持续、精确、低强度压制;地面部队夜间防御能力不足。如果我们从装备发展的逻辑来看,AC-47的出现,其实标志着一个非常重要的转折。在越南战争之前,运输机就是运输机,攻击机就是攻击机。功能划分非常清晰。但战场环境改变了这种分类。面对游击战和夜间袭扰,美军需要的不是高速突防,而是持续压制。喷气式战机速度太快,滞空时间有限;地面炮兵又无法快速机动。于是,一个思路被提出——既然运输机稳定、航程远、滞空时间长,能否让它承担火力输出任务?这就是 AC-47“空中炮艇” 的诞生逻辑。它的技术本身并不复杂:在 C-47运输机 机身侧面安装机枪,通过持续盘旋飞行,让火力稳定覆盖地面目标。真正的创新,不在于武器口径,而在于飞行方式与作战概念。它创造了一种新的作战形态——“空中持续火力平台”。因为它打破了传统航空兵的速度崇拜,强调的是稳定性、持续性和火力密度。
解说:在AC-47验证“空中持续火力平台”概念之后,美国空军并没有停步。相反,它开始系统化推进炮艇机的发展,使其从“临时改装”走向“专业型号”。在越南战争中期,美国推出了 AC-119。它改装自C-119运输机,机身加长、航程延长,搭载20毫米机关炮,并配备夜视与早期电子侦察系统。在夜间巡逻时,AC-119通过红外传感器锁定卡车发动机的热源,侧向机炮连续射击,形成稳定、持续的火力覆盖。补给线几乎没有安全间隙,每一辆运输车都在火力压制下寸步难行。而后,炮艇机概念进一步成熟,诞生了AC-130系列。
影视段落
三维动画:AC-130系列炮艇机,是现代空中火力支援平台的代表。它基于C-130运输机改装,它滞空时间长,可连续盘旋6小时以上,最大航程超过2000公里,能够快速部署至远程战区。AC130搭载多种口径火炮,这些火炮沿机身侧向安装,可以实现灵活打击。在面对轻型车辆和人员目标时,20毫米机关炮可以快速压制,在面对对集群目标或防御阵地时,40毫米博福斯炮可以实施有效覆盖,而它搭载的105毫米榴弹炮,能够精确摧毁坚固建筑或装甲目标。在现代型号的AC130上,可挂载精确制导炸弹和空地导弹,同时还配备高度集成的传感与火控系统,进一步增强打击范围和精度。
解说:在现代战场上,AC-130的核心优势,不在于单纯火力,而在于精确支援地面作战的能力。它能够实时锁定移动目标,将传感器捕捉的信息转化为精准射击指令。红外和多光谱传感器使它在夜间或复杂环境中仍能发现敌方阵地和运输车辆。通过与地面指挥系统的数据链联动,AC-130能够根据地面部队的需求,调整火力重点和攻击节奏,实现即时支援。在阿富汗和伊拉克的行动中,它长时间滞空,连续压制敌方火力点,保护前沿部队推进,同时切断敌方增援路线。
这种持续、可控、精准的空中支援,让地面单位能够在复杂战场环境下保持主动权,而不是被动应对。
Xr2:从装备演进的角度看,空中炮艇的出现不是一次技术革命,而是一种功能重构。它证明了一点:当成熟平台与战场需求重新匹配,一款“平凡”的装备,就可能衍生出全新的战斗形态。残酷的战场从来不是单纯追求尖端,而是追求适配。当空中炮艇正在越南战场上空依靠着长时间的盘旋大展身手时,在丛林上空,另一场为了灵活机动而拼接的艺术正在上演,它就是“武装直升机”。
解说:从1943年,一架直升机在战场上成功救下两名受困士兵开始,美国军方便意识到:直升机可以成为战场上的新型机动力量。直升机的发展被纳入正式军方计划,军方对它寄予厚望,希望它能在运输、救援和支援作战中发挥独特作用。然而,直升机的发展之路并非一帆风顺。早期的直升机虽然设计上预留了武器挂载接口,但结构轻薄、载重有限,机枪、火箭或轻型火炮的安装非常不便。在实际战场中,它们仍主要承担运输士兵和物资的任务,无法直接参与火力压制,更多依赖地面部队自身作战能力。1962年3月,随着第57医疗分队驾驶着“休伊”直升机降落在越南,这款传奇通用直升机的拼接改装之路正式拉开了序幕。
专家:介绍休伊直升机的基本情况。
解说:随着越南战争的深入,美军时长面临着丛林密布、伏击频繁的战场环境。步兵需要即时火力支援,而传统运输直升机缺乏火力保护。在这种迫切需求下,工程师和飞行员将UH-1“休伊”两侧加装机枪、火箭发射架,甚至轻型火炮。
第一次实战投入就显示出惊人的效果:直升机可以低空悬停,伴随地面部队推进,同时压制敌方火力点,为步兵开辟通路或掩护撤退。这次临时拼装,开创了武装直升机的雏形。由于美军之前就曾有过改装传统直升机的设想,设计人员在设计休伊直升机的时候,就为它的各种型号预留了武器安装的接口和冗余。理论上,休伊直升机的A至D型,甚至之后的后续型号都可以改装成武装型。但美国陆军还是根据每款型号的性能进行了更加细化的分工。
解说:A、B型休伊直升机的机身较短,载员能力一般,在越南战场上陆续被改装成武装型,长机身的D型休伊直升机则被用为运载和救援直升机。由于各型号休伊直升机武器挂载的方案不同,这些武装型的休伊直升机还有着不同诸如“野猪”或“炮艇”等不同的绰号。
三维动画:这是一架加装上两挺电力驱动的M60c机枪的武装型休伊直升机,它的副驾驶可以直接通过驾驶室安装的XM60反射式瞄具控制这两组双联装机枪,这套武器系统的每挺机枪都由3个弹药箱供弹,总弹容量为6000发。当士兵跳下直升机时,射手可以提供强大的火力掩护。为了进一步加强直升机的火力压制能力,设计人员继续给武装型休伊增加机翼短挂架,用以挂载火箭巢。
解说:当越南战场的硝烟逐渐笼罩丛林,美军意识到一个现实问题:临时武装的UH-1“休伊”虽然有效,但终究不是为战斗而生。它的机身宽大,气动不优。于是美军决定专门研发一种攻击直升机。1967年,AH-1“眼镜蛇”正式投入越南战场,它是世界上第一种专用攻击直升机,武装直升机这一装备类别正式诞生。
影视片段
三维动画:眼镜蛇采用串列式双座布局,机身细长、正面投影面积小,目标更难被击中。机体经过强化,具备更好的防护能力和更高的速度。动力系统采用改进型涡轴发动机,使其在低空具备出色的机动性能。它可以贴地飞行、快速跃升、突然转向,在丛林与山地环境中进行突袭。在机头下方安装可旋转炮塔,装备7.62毫米或20毫米机炮,能够灵活指向目标,实施精准压制。机翼短翼下可挂载2.75英寸火箭弹发射巢,用于打击敌方阵地与轻型装甲目标。后期型号还具备发射反坦克导弹的能力,使其拥有对装甲单位的直接打击能力。
专家:介绍眼镜蛇直升机的战例
Xr3:武装直升机的概念并非凭空而来。它的血统来自运输直升机与各类武器挂架的组装。当直升机从“运输”转向“战斗”,空中支援模式才发生了实质性的变化。接下来让我们将目光投向地面战场上的另一个明星,它就是“自行火炮”。现在典型的自行火炮看起来就像一个大坦克,它们都有履带式底盘,都有一个可旋转的炮塔,而且炮塔同样有一个封闭式的装甲壳体,那么它和坦克到底有什么区别呢?
解说:1942年,北非战场。沙漠上空烈日炙烤,大规模机械化部队正在展开一场前所未有的机动作战。无论是德军“非洲军团”,还是英军装甲部队,战场的主角都是坦克与装甲车辆。在广袤无垠的沙漠地带,没有山川阻隔,没有密林掩护,战斗节奏完全取决于机动速度。谁的装甲部队推进更快,谁就能抢占阵地、包抄侧翼、切断补给线。但问题很快暴露出来。装甲部队可以高速前出,可身后的炮兵却难以跟上。当时主力火炮仍然是牵引式榴弹炮。它们必须由卡车或半履带车拖拽前进。一旦抵达射击位置,炮兵还要下车卸炮、展开炮架、校准射向、构筑阵地。在风沙弥漫的沙漠中,这个过程往往需要十几分钟甚至更长时间。而战场态势,可能在几分钟内就发生改变。坦克已经冲过前线,而炮兵还在后方布置阵地。当敌方装甲部队突然反击时,缺乏及时火力支援的装甲部队只能独自承受压力。当战争已经进入装甲机动的时代,但炮兵还停留在马匹牵引的时代。于是工程师便将榴弹炮直接安装在坦克底盘上,让榴弹炮获得了动力系统。
专家:自行火炮和坦克的区别
解说:北非战场的教训已经十分清晰。美军意识到,必须让火炮拥有与坦克相同的机动能力。于是,一种全新的装备开始登场——M7自行榴弹炮。由于其机枪座高耸,外形类似教堂讲坛,于是北非战场上率先列装M7自行榴弹炮的英军士兵给它起了一个绰号——“牧师”。在1942年底,在突尼斯战役中,M7首次大规模投入实战。与装甲部队协同作战时,M7能够在数分钟内完成射击部署。当敌方反击出现,它可以立即调整位置,避免被反炮兵火力锁定。这种榴弹炮与坦克底盘组装的产物,极大提升了装甲部队的持续作战能力。
三维动画:M7自行榴弹炮,它基于M3“李”式中型坦克底盘改造,后期型号则采用M4“谢尔曼”底盘。整车战斗全重约23至24吨。乘员编制通常为7人,包括车长、炮手、装填手、驾驶员以及弹药手。M7在公路上的最高时速可达38至40公里每小时,越野速度约为20公里每小时左右。对于一门搭载105毫米榴弹炮的火炮平台而言,这种机动能力已经能够与装甲部队保持同步推进。M7的核心武器,是一门105毫米M2A1榴弹炮。最大射程约11,000至12,000米,初速约470米/秒,可发射高爆弹、烟雾弹、照明弹等多种弹药。火炮安装在开放式战斗舱内,虽然不具备全向旋转炮塔,但在战术部署中,M7主要依赖间接射击,通过前线观察员引导修正弹着点。在车体右前方高耸的机枪座上,安装一挺12.7毫米M2重机枪,它不仅用于防空警戒,也可对近距离步兵目标实施压制。M7自行榴弹炮的装甲防护厚度约为12至50毫米之间,能够抵御轻武器和破片打击,但并非设计用于正面抗击坦克火炮。
解说:在诺曼底登陆之后,M7成为美军装甲师标准火力支撑单位。美军则继续在“自行火炮”的领域进行研究,进入冷战。战场假设变成了欧洲平原的钢铁洪流。炮兵不仅要机动,还要能在核生化环境中作战。于是,封闭式炮塔、自行装填、核炮弹能力逐渐出现。尤其是M109系列自行火炮,成为美军炮兵真正的“常青树”。但战争的节奏仍在加快。冷战结束后,美军发现——履带式自行火炮虽然强大,却重量巨大,部署缓慢,战略投送成本极高。在远征作战时代,速度不仅是细微的战术问题,更是无法忽视的战略问题。于是,一个新的方向出现了,它便是轮式化。
专家:介绍轮式化火炮。
Xr4:如果把自行火炮的发展看作一条河流,那么轮式火炮,并不是一条新的支流,它只是这条河在新型战场环境下的一次转弯。进入21世纪,战场环境发生变化。大规模装甲会战减少,远程精确打击成为常态。在这样的战场上,火力与机动依旧重要,但更加依赖智慧的组装与即时创新。
解说:在俄乌冲突中,我们看到一个显而易见的变化——无人机彻底改变了战场侦察与打击方式。这些无人机体型小巧,成本低廉,却具备极高的灵活性。它们能够快速飞过战场,探测敌方阵地、补给线,甚至可以与简单的爆炸装置组装通过投放小型炸弹实施打击。面对这种新型组装武器的威胁,传统防空系统显得应对不足。大型防空导弹或高射炮部署缓慢、覆盖有限,无法随时随地保护前沿部队。于是,战场上的士兵与工程师将原本分属海军的舰炮或者陆基近防炮,通过改装、固定在卡车、半履带底盘上,快速形成机动防空单元。这些装备虽然都缺乏标准化的体系支撑,但在前线却发挥了令人惊艳的效果,它们的存在完全源自战场的迫切需求——士兵们通过组合手头的平凡武器,发挥出远超单件装备的战术价值。
XR5:从空中炮艇的持续压制,到武装直升机的灵活支援;从自行火炮的机动超车,到无人机与临时防空系统的精彩对抗——战场一次次证明:装备的真正力量,不在于先进与否,而在于如何使用,如何组合。现代战争,已不再只是技术的比拼,而是智慧、创新与临机决策的舞台。下一款能够改变战局的装备,或许正静静躺在某个角落,等待被发现,等待被赋予新的使命。好了,本期《军事科技》到此结束,感谢您的观看。
@@ -0,0 +1,241 @@
枪械设计中的长短智慧
导视:
藏于方寸的敌后利器,锋芒毕露的火力先锋;
穿梭街巷的近战精灵,主宰阵地的压制王者。
同为枪械家族成员,为何长度取舍各不相同?
每一寸伸缩,都是对实战需求的精准回应;
每一款设计,都凝结着军工领域的深邃考量。
军事科技带您解码枪械设计背后的长短智慧。
主持人1
各位观众朋友们,大家好,欢迎收看《军事科技》。
【固】提到枪械,威力、精度往往是我们关注的焦点,但有一个看似基础却至关重要的设计指标,始终在左右着枪械的实战性能,那就是长度。今天要聊的枪械,每一款的长度都大不相同。有的枪械要极致缩短,甘愿舍弃部分舒适与功能。有的却要刻意加长,坦然放弃部分便携性。
【摇】今天,我们就从手枪、冲锋枪、步枪、狙击步枪四个维度,逐一给大家介绍,枪械每一寸长度背后的设计考量与战场故事(落幅定死)。首先我们来看手枪。
(出枪柜)
【轨】首先出场的是短到可以塞进火柴盒里的美国FP-45“解放者”手枪。据资料显示,它的实际全长只有141毫米,比我手掌的宽度还要短,完全能轻松塞进一个普通火柴盒里。大家仔细看它的结构:握把是简单的上下拼接,不是传统手枪的舒适握把,握把下半部分是中空的,打开滑动挡板能装10发.45 ACP备用弹;瞄准具也特别极简,只有枪管前端一个小小的片状准星,还有尾端挡板上切削的简易缺口照门,不仔细看几乎发现不了,不能说没有瞄准具,但确实精简到了极致。更特别的是,它没有半自动功能,每打一发都得手动把尾部的待击块拉向后方并左转,才能装下一发弹,如此短小的设计却让这款不起眼的小手枪成为了敌后斗争中的特殊利器。
解说:
这是一把用来换取更好枪支的武器,一个构思独特、简单却又精妙绝伦的设计。FP-45,绰号“解放者”,是一款外形笨拙的手枪,却从未真正投入过实战。“解放者”算是最酷的“未投入使用枪支”之一。FP-45“解放者”的诞生,源于盟军一项特殊的战略考量。该枪诞生于二战时期,是针对德国和日本开展的一项秘密心理战行动产物。其设计理念是制造一种小巧廉价的枪械,让盟军可以成千上万地空投给欧洲和亚洲的抵抗战士。设想让当地人捡到这种枪后,能悄悄接近德国或日本军官,扣动扳机将其击毙,然后夺走对方的枪支。
1942年,美国陆军将生产任务交给俄亥俄州代顿的通用汽车大陆制造分公司,为了保密,工人们并不知道是生产手枪,只知道这份订单是生产许多小的金属片,而粗糙的枪管则是在代顿的电冰箱厂生产,这些零件最终在印第安纳州安德森的导航灯公司组装。
最终成型的枪支堪称钣金工艺的杰作,枪管短小光滑、一体成型且无缝隙。从剖面能看出它的结构有多简单:仅由23个主要冲压金属部件组成,这种极致简化的设计,使它的生产成本低到惊人,每把仅2美元。300个工人用11个星期就生产了100万把。有人戏称“这是世界上唯一一种装弹射击所花时间比制造时间还久的枪。”
每把FP-45“解放者”会连同10发.45 ACP弹装在纸盒里,盒内的绘画说明书就算不识字的人也能看懂操作步骤。这些“火柴盒杀手”被大量空投到敌后,抵抗组织成员用它近距离突袭德军岗哨后就随手丢弃,即便被缴获也能对对方产生极大的心理威慑效果——当成千上万的FP-45被德军发现,他们便会心生疑虑“天知道当地人手里到底还有多少把?”
它是在二战期间比较特殊的这样的一款手枪,主要是秘密携带,秘密使用。它完全是采用这种单发的这样的一个设计。那么握把内部的这样的一个子弹,其实它是需要手动装填到这个枪体的内部,才能够完成击发。这个退壳也没有特别的这样的一个设备,必须要单独配一个小木棍,从这个枪口把使用之后的这个弹壳再把它给捅出来,所以操作起来是比较复杂的。但是解放者这款手枪最大的优势就是结构简单,不需要经过专门的培训,任何人都可以对它进行使用。甚至这个枪管非常的短,连这个膛线都没有。在近距离可以针对人员构成比较大的杀伤。
主持人2
【固】如今FP-45成了收藏家眼中的珍品。它短小的尺寸,不是偷工减料,而是战略需求优先的必然结果,少一毫米都可能影响空投效率,多一毫米都可能失去隐蔽性。
【摇】接下来看个大家伙。
(换枪)
它就是沙漠之鹰.50AE。
【轨】它全长约273毫米,几乎是FP-45的两倍;仅枪管就长152毫米,比FP-45的整枪还长11毫米。握在手里能明显感觉到分量。它完全放弃了手枪的便携性,所有设计都为了适配.50AE大口径弹药的极致火力。
解说:
沙漠之鹰是当今世界上最知名的大口径手枪之一,从射击游戏到电影荧幕,沙漠之鹰表现出的强大威力让其成为许多人的最爱。
20世纪80年代,欧美狩猎运动兴起,面对熊、野猪等大型猛兽,传统9毫米或.45口径手枪的威力根本不足以致命;同时,在反恐作战中,警方和特种部队也需要一种能击穿轻型防弹衣、车辆挡风玻璃的便携武器。
随着.50AE弹药的出现,让这些需求有了实现的可能,这种弹药的弹头重量超过15克,动能是普通9毫米手枪弹的3倍以上,但是也对枪械设计提出了严峻挑战。
三维动画:
大口径弹药燃烧时会产生巨大的高温高压,必须有足够长的枪管才能让火药充分燃烧。沙漠之鹰152毫米的枪管能让弹头初速达到402米/秒,比短枪管版本提升15%以上,有效射程也从50米延伸到100米;随之而来的是巨大后坐力,需要273毫米的枪身和2千克的重量来平衡——如果枪身过短,射击时枪口会剧烈上跳,甚至可能伤到射手的手腕。生产商还提供254毫米枪管的加长版本,全长会达到374毫米,但因便携性太差,只有少数狩猎爱好者选择。
解说:
如今,沙漠之鹰虽未大规模列装军队,却因独特的外形和强大的威力,成为影视剧中的常客,用极致的长度诠释了火力至上的设计理念,也成为枪械文化中力量象征的代名词。
为什么说要把它的枪管设计得比较长?因为它弹药的尺寸比较大。另外一个本身在射击的过程当中,为了保证精度,为了保证可靠性,所以它整体的这个是比较重的,重达2公斤。正常人对它进行持握射击确实是很费劲儿的。沙漠之鹰它是采用导气结构,这一点和步枪是非常相似,能够保证它发射大威力的弹药。但是在实战的这个过程当中,要进行短时间内的快速射击,这个就很难打得准。因为它的后坐力是比较大的,所以从它的设计理念来看,它更像是靶场手枪或者是狩猎手枪。在军警部队当中进行使用,首先是不便于携带。另外一个枪体太重了,那么在操作使用的过程当中会带来一系列的麻烦。
主持人3
【固】一款能藏进火柴盒,一款堪称手枪巨无霸,FP-45和沙漠之鹰用132毫米的长度差,精准命中了两种极端需求。它们的设计看似矛盾,却都在各自场景下实现了长度与功能的最优匹配。
【摇】接下来,我们把目光转向冲锋枪家族,看看它们的长度差异又藏着怎样的战场逻辑?
(出枪柜)
先看我手里的这把PP-2000冲锋枪,这是我见过最露骨的一款冲锋枪了。
【轨】可以看到,它的枪机前半部分完全裸露在外,根据设计方的官方参数,它的全长仅340毫米,比常见的500ml矿泉水瓶就长一点(拿出矿泉水瓶),刚好能放进战术背心的口袋里,单手就能握住操作。兼顾近战火力和一定的有效射程,不会因为追求短尺寸而牺牲核心威力。
解说:
PP-2000的设计,源于俄罗斯在城市反恐作战中遇到的实战痛点。20世纪90年代,车臣战争中的巷战让俄军深刻体会到:传统冲锋枪过长的枪身在狭窄的楼道、房间中根本无法灵活转向——士兵在破门后,常常因枪身剐蹭门框而延误举枪时机,甚至被敌方近距离突袭。为了解决这一问题,图拉仪器设计局从1993年开始研发紧凑式冲锋枪,核心目标就是在保证火力的前提下,把长度控制在350毫米以内。为此设计师采用了多项针对性设计:
PP-2000冲锋枪的设计非常小巧精妙,整枪上下两个部分可以快速拆开,整体部件非常少,便于维护保养。
PP-2000冲锋枪的枪机前部分暴露在机匣外,并兼作上膛拉机柄,在枪机连杆的前方设有一个铰接手柄,当需要拉动的时候可以扳起向左或向右旋转,然后再拉动连杆进行上膛操作。这种枪机外露的方式,不仅能够减少整枪的长度,还可以让枪机和枪管的一部分直接暴露在外面,有利于整枪的散热。
PP-2000冲锋枪配有两种容量的弹匣,一种是20发,另一种是44发,供弹弹匣插在握把内,而在枪尾还有一个弹匣插口,把不用的弹匣插上去,便可以当作枪托来使用。量产型版本则增加了折叠式枪托,这个可选配件也是像弹匣一样插到枪尾上使用。
2006年列装后,PP-2000成为俄罗斯警察和内务部等多个执法机构的巷战利器,PP-2000作为一款执行特定任务的微型冲锋枪,对长度的精准把控使得它在火力与便携性之间找到了完美的平衡点。
PP-2000之所以实现了小巧的这种设计理念和它的这种结构是密切相关的。我们注意到一个特点,就是它把机匣、握把以及扳机的护圈融合到一起了,形成一个集成化的这样的一个结构部件。而且它去除了很多多余的设计,甚至包括它的枪管和枪机都是裸露的。所以就导致这个PP-2000,全枪的重量非常低,而且非常的短,特别适合特种部队或者是强力部门的执法人员去使用。可以在近距离的过程当中,针对对方进行密集的火力压制。由于它的枪身比较短,其实也适合乘车作战的部队,比如说坦克装甲车的这个车组成员进行佩戴和使用,作为一种自卫武器。当然了战斗机的飞行员和直升机的机组成员也可以把它作为一种大威力的自卫武器。因为它的持续的火力压制能力要比一般的手枪要强很多,而且和这种短的冲锋枪相比,它的枪身确实更短,而且实现了最大程度地减重。
主持人4
【固】PP-2000 用 340 毫米的紧凑长度加上暴露枪机设计,把巷战灵活做到了极致,短身带来的机动优势,在狭小空间里无可替代。
【摇】接下来我们来看一把长尺寸的冲锋枪。
(换枪)
MP40冲锋枪。
【轨】根据二战时期的德军技术手册记载,它折叠后全长630毫米,比前面讲到的PP-2000长近100毫米,展开后长833毫米。我抵肩握住它,能明显感觉到枪身特别舒展,枪托与肩部的贴合度很好,250毫米的枪管让枪口在射击时跳动很小,这种设计在二战的平原和阵地战中特别实用,士兵在冲锋时能稳定抵肩射击,精准压制200米内的敌方目标。
解说:
MP40 是二战期间产量较大、影响力较广的一款冲锋枪,其因结构相对简单、稳定性强,成为当时多个作战单位的装备。二战前,德军步兵班组的火力核心是毛瑟98k栓动步枪和MG34重机枪,冲锋枪仅作为辅助武器。第二次世界大战爆发后,纳粹德军强调快速推进、近距离火力压制的理念,这就需要一种能伴随步兵冲锋、兼顾射程与精度的武器。而传统冲锋枪,如MP18,尺寸虽短,但枪管仅198毫米,有效射程仅100米,无法满足压制需求,而MP40很好地解决了这一问题 。
MP40冲锋枪的设计围绕中近距离持续压制展开:250毫米的枪管采用镀铬工艺,能让子弹初速提升至380米/秒,有效射程达到200米,比MP18提升一倍;833毫米的枪身长度,让士兵在抵肩射击时能更好地控制后坐力,配合32发弹匣,能形成持续的火力输出。更值得一提的是,它的生产工艺特别适合战时量产:枪身采用冲压和焊接技术,零件数量比MP18减少40%,一个熟练工人每天能组装10支,到1944年累计生产了120万支。833毫米的长度,适配了二战时期开阔战场的作战需求,也成了二战中纳粹德军步兵火力支柱的标志性武器之一。
在一些二战影视剧中,MP40 频繁亮相,成为还原二战战场的重要元素;游戏里玩家喜欢它的怀旧感和易操控性,相比于身材短小的冲锋枪,全长800多毫米的MP40在当时成为追求火力与稳定性平衡的代表枪支之一。
MP4是在MP38的基础上改进升级而来的。可以说在二战期间,在德国军队的武器装备当中,它是一款设计比较优秀的冲锋枪。优先装备的是德军的装甲部队和它的空降部队,也就是伞兵。装甲部队和伞兵在使用的过程当中,就是要求在近距离针对对方的作战人员短时间内倾泻大量的弹药。所以我们看这个MP40,它的这个弹匣的长度是比较长。
所以能够在近距离,比如说100到150米的这样的一个距离上,针对对方进行这种持续的这样的一个射击。那么采用这种比较长的枪身和枪管,就是要保证射击的精度。所以在近距离射击的过程当中,MP40它的精准程度也是比较高的。可以说在二战期间,在各国使用的冲锋枪当中,MP40既考虑到成本,那么MP40它是做到了成本火力和精准度的均衡。
主持人5
【固】从340毫米的紧凑灵活,到833毫米的稳定压制,冲锋枪的长度选择,本质上是对作战空间和战术节奏的精准回应。短有短的便捷,长有长的稳定,这正是枪械设计中长短智慧的核心之一。
【摇】接下来,我们看看步枪家族。
(出枪柜)
先看我手里这把AUG突击步枪。
【轨】大家有没有发现它的外形特别独特?没有传统步枪的枪托,弹匣直接插在扳机后方。根据奥地利斯泰尔公司的官方数据,它的全长只有665毫米,比折叠后的MP40冲锋枪长不到40毫米。但大家可别被它的短身迷惑了——它的枪管长度足足有508毫米,和传统M16A4步枪的枪管几乎一样长。设计师用无托结构,硬生生在不缩短枪管的前提下,把枪身缩短了近340毫米,真正实现了短身不短能。
解说:
AUG的出现,掀起了步枪设计的一场结构革命。随着城市化进程加快,传统长枪管步枪在城市作战、装甲车内部操作时的弊端日益凸显:在楼宇作战中,过长的枪身导致转向不便,甚至影响战术动作。各国都在寻找一种能兼顾射击精度与机动灵活的步枪,但传统有托结构的设计逻辑是枪管长则枪身长,似乎无法突破这一局限。于是,奥地利斯泰尔公司的设计师大胆创新,提出了无托结构的理念。
AUG突击步枪将枪托与机匣融为一体,让射手的肩部直接抵在机匣尾部,这样一来,枪管的长度不再受枪托位置的限制——508毫米的枪管能保证5.56×45毫米弹药的初速达到970米/秒,有效射程600米,与传统步枪持平;而665毫米的全长,让士兵在装甲车内能轻松操作,在楼宇中能快速转向。更创新的是,AUG采用了模块化设计,枪管可以快速更换,能在几分钟内切换成步枪(508毫米枪管)、卡宾枪(407毫米枪管)、轻机枪(621毫米枪管)甚至狙击枪(621毫米重型枪管),适配不同作战场景。
1977年列装奥地利军队后,AUG迅速被澳大利亚、新西兰等多个国家采用,用创新设计证明:步枪的长短并非“非此即彼”,通过结构优化,完全可以实现便携性与性能的完美兼容。
奥地利的Aug这种突击步枪可以说是在无托步枪当中的这样的一个经典的型号。首先我们看它是大量采用了这种塑料和合成材料,所以全枪可以有效地实现减重。士兵在携带和使用的过程当中,可以最大程度地节省体力。由于采用了这种无托化的设计理念,它全枪的重量和长度是可以进行进一步的优化。但是由于采用无托的设计理念,也可以保证它的枪管比较长。这样的话无论是有效的射击距离和它的这样的一个火力的持续性,其实是达到了一种均衡的状态。
主持人6
【固】AUG用无托枪身的设计真正做到了短小而又精悍。
【摇】接下来来看看长家伙。
(换枪)
接下来要讲到的这款步枪就是大名鼎鼎的HK416。
【轨】根据德国黑克勒-科赫公司的官方参数,HK416在装配20英寸枪管后,全枪长可达1030毫米,比AUG足足长了365毫米;长长的枪身给可伸缩枪托、全长度皮卡汀尼导轨和活塞导气系统留出了适配空间。大家看它的顶部导轨,能从头到尾加装瞄准镜、激光指示器,护木两侧还能扩展战术握把、手电,完全是为现代特种作战多任务、多环境量身打造的长枪身设计。
解说:
如果你曾接触过枪战类游戏,那么你一定知道这把突击步枪M416。
在各种枪战类游戏中,这把枪的性能排名都稳居前列。近几年爆火的吃鸡游戏玩家更是将其称之为“吃鸡神器”。在游戏中,只要能装备上M416,便能无往不利,所向披靡。而现实中,这把枪的原型便是HK416。
HK416的诞生,源于现代特种作战对可靠性与多场景适配的极致追求。21世纪初,美军特种部队在全球反恐作战中发现,M4A1卡宾枪的气吹式导气结构在沙漠、湿热等恶劣环境下容易积碳故障,且短枪管在中距离压制时精度不足。
于是黑克勒-科赫公司在M4A1基础上,重新设计改造出了HK416。
HK416自动步枪长805毫米,既保证了抵肩射击的稳定性,又能通过伸缩枪托适配不同身高士兵;5.56×45毫米北约弹初速达到900米/秒,有效射程600米,比M4A1提升10%。
其最新改型HK416 A5提供5种枪管选择,分别是11英寸、12.5英寸、14.5英寸、16.5英寸和20英寸。这让HK416既能在城市反恐中灵活操作,又能在沙漠、平原等开阔地形提供远距离火力支援。
HK416自动步枪最经典的实战案例是2011年“海神之矛”行动,海豹突击队队员正是用HK416击毙本·拉登,当时队员在狭小房间内完成近距离突袭,后续撤离时又在开阔地带用其压制追兵,证明其能同时适配近距离突袭和中距离压制双重需求。
如今,HK416被30多个国家的特种部队列装,法国、挪威等国更将其定为全军制式步枪,成为现代步枪的代表之一。
和Aug相比,HK416最大的优势就是它的适应性比较好,可以衍生出很多的这种型号。hk 416它的皮卡丁尼导轨的这样的一个安装也比较方便,在枪管外面的护套上它可以安装四条皮卡丁尼导轨。也就是说不仅可以加装战术手电或者是这种激光的指示系统,还可以加装外挂的榴弹发射器,所以HK416既可以作为普通士兵所使用的突击步枪,也可以作为精准射手步枪。那么进行这种精准的火力支援
主持人7
【固】两款步枪可以说是短有短的灵活创新,长有长的稳定全能,各有千秋。
【摇】最后登场的是狙击步枪,我们来看。
(出枪柜)
先来看这把VSS微声狙击步枪。
【轨】根据公开资料显示,它的全长不到900毫米,还带着一个大大的集成式消音器,握在手里重心很均衡,重量只有2.6千克,能轻松藏进特种队员的30升战术背包里,完全看不出是一把狙击步枪。
解说:
VSS的设计,完美诠释了苏联特种部队“敌后无声渗透”的作战需求。
20世纪80年代,苏联特种部队需要一款能在敌后执行侦察、暗杀、破坏任务的武器,核心要求有三个:一是无声,不能暴露自身位置;二是精准,能远距离打击关键目标;三是便携,能灵活机动、快速转移。为了同时满足这三个需求,设计师采用了枪弹一体的创新思路。
他们专门研发了9×39毫米亚音速弹,这种子弹的弹头重量16.2克,初速280米/秒,低于音速,射击时不会产生音爆;配合VSS的集成式消音器,最终射击噪音控制在70分贝以下,相当于日常对话的音量,在夜间甚至听不到明显枪声。
VSS消音器内部有7个隔板,能将火药燃气逐步减压。无托结构的设计,让VSS在保证枪管长度的同时,将枪身全长控制在894毫米,具备极佳的便携性。
更特别的是,它不仅能精准狙击,还能切换全自动模式,射速达到每分钟700发,在近距离突发对抗中当作冲锋枪使用,成为敌后战场的“隐形利刃”。
首先它是可以实现连发的,而且它的枪管和这种消音器是采用一体化的设计理念。因为它可以最大程度地减少枪械在射击和使用过程当中所产生的声音和火光,所以在近距离突袭作战和渗透作战的过程当中,它是特种部队手中的利器。如果说在这种边运动边射击,而且要快速地进入到建筑物内,针对重要目标进行斩首,进行射杀的这样的一个状态下。那特种部队的人员无论是使用佩戴瞄准镜的突击步枪,还是使用这种传统意义上的狙击步枪,都会面临携带和使用不方便的情况,但是这种短小精悍的这种突击步枪,而且是采用了这种消声器的这样的一个结构。那么可以更好地进行伴随性的精准的火力打击。
主持人8
【固】相比于短小的VSS,接下来这款狙击步枪则走向了另一个极端——大。
【摇】(换枪)
大家来看这把“大家伙”,它就是人送外号“南非大炮”的NTW-20大口径狙击步枪。
【轨】根据南非丹尼尔防务公司的官方参数,它的全长超过2015毫米,比我的身高还要高,空枪重量就有26千克,根本无法单手操作,必须架在脚架上才能射击。
这样的长度和重量,只为实现一个目标——击穿一切坚固目标。
(拉栓子弹转场,三镜:【摇全】手摸枪栓、【固中】拉推、【轨近】举枪瞄准)
解说:
20世纪90年代,南非面临着周边地区的轻型装甲威胁,普通7.62毫米狙击步枪根本无法击穿,而重型火炮又过于笨重,无法在非洲草原的复杂地形中机动。南非国防军急需一款能徒步携带、威力足够大的反器材武器,NTW-20应运而生。
要实现反器材的威力,弹药是核心。设计师最终选择了20×82毫米大口径弹药,这种弹药的弹头重量100克,初速720米/秒,动能达到25920焦耳,是7.62毫米弹药的10倍以上,能轻松击穿1000米距离上15毫米厚的均质钢装甲。
但大口径弹药也对枪械设计提出了极高要求:必须有足够长的枪管,才能让火药充分燃烧,1220毫米的枪管能让弹头初速提升20%,有效射程延伸到1800米。
同时,巨大的后坐力需要长枪身和重枪体来平衡,2015毫米的枪身配合液压缓冲器,能将后坐力降低到7.62毫米步枪的水平,保证射击精度。
为了适配非洲草原的徒步机动需求,设计师还将它设计成可拆卸式,枪身能拆成枪管、机匣、枪托三部分,最重的枪管部分仅9千克,分解后能装进两个专用背包,由两名士兵携带,解决了重型武器的机动难题。在南非的边境冲突中,NTW-20多次精准打击敌方轻型装甲车辆,弹头能轻松穿透装甲车的侧装甲,引爆内部弹药,成为名副其实的“装甲克星”,用长度诠释了火力的极致追求。
Nt W 20它是南非研制的一种大口径的狙击步枪,我们又叫反器材步枪。
那么这种反器材步枪或者说我们叫它重型的狙击步枪,它的枪管可以选择14.5毫米,口径,也可以选择20毫米口径,这就相当于步兵大炮了,或者说单发的这样的一个机关炮。所以我们看这个枪管非常的长,而且弹药的尺寸和个头也非常大。
为什么说要把这个枪身和枪管做得如此之长?
就是因为他所杀伤的不仅仅是对方的作战人员,而且也要针对轻装甲目标和一些作战装备进行远距离的这样的一个火力打击。所以它反器材步枪的属性更强一些,就要求射击距离远,弹道的轨迹比较稳定。而且可以针对轻装甲目标进行这种穿透式的杀伤。
主持人结尾:
(固)今天我们展示的每一款枪械,它们没有绝对的优劣,只有是否贴合使命的区别,这正是枪械设计中最珍贵的长短智慧。
(轨)长度从来不是孤立的数字,而是设计师平衡使命需求、核心功能、使用环境的结果,每一寸增减,都凝结着对战场的深刻理解和对功能的极致追求。
(摇)结束语……
@@ -0,0 +1,149 @@
《“X”系列新成员:X-76飞行器》
【宣传词】
数学领域,X为待求解的未知数;
科幻范畴,X是象征未来的科技标志。
近期,美军X系列又加入重要新成员,代号X-76的折叠旋翼机。
它声称解决了航空界几十年的难题:无需跑道、可垂直悬停,且能达到喷气式战机的速度。
那么,这款“空中新物种”究竟蕴含哪些突破性黑科技?是否能重塑未来空战规则?
本期《军事科技》将为您深入解读X家族最新成员——X-76。
【主持人1】
观众朋友们,大家好,欢迎收看本期《军事科技》节目。
在现代战争里,飞行器无疑扮演着关键战力的角色,然而“速度”与“灵活”却始终是摆在军事工程师面前的一对难题:固定翼飞机虽快,但需要依赖长长的跑道;直升机虽能垂直起降,速度却难以提升。难道真的没有一种飞行器,能够同时融合两者的优点,既能“极速飞驰”,又能“随时悬停”吗?
时间来到2026年3月,美国国防高级研究计划局正式公布了一款全新装备——贝尔公司研发的新型折叠旋翼机X-76。作为“X”系列家族的新成员,它的亮相立刻吸引了全球军事爱好者的目光。这款号称要突破固定翼飞机与直升机性能界限、解决美军数十年来困扰的飞行器,究竟有何独特之处?今天,就让我们一起揭开X-76的神秘面纱。
【解说1】
要认识X-76,我们得先搞清楚一个关键问题:美军为何要投入巨大精力去开发这样一款全新的飞行器呢?实际上,这完全来自于现代战争中的“现实难题”。大家可以思考一下,在紧张的对抗中,机场与跑道通常是敌方首轮打击的核心目标,一旦跑道被破坏,那些需要跑道才能起飞的固定翼飞机,无论多么先进,最终也只能变成“地面上的靶子”。而传统的直升机虽然具备垂直起降能力,无需依赖跑道,但其速度过于缓慢,在战场环境中极易成为敌方攻击的对象,生存能力大幅降低。
是否存在一种办法,能够让飞行器既摆脱对跑道的依赖,又具备喷气式飞机的速度?这正是美军提出“速度与跑道独立技术”项目的核心目标。而X-76,则是贝尔公司为这一项目提交的“答卷”。
采访1:研发背景?
参考:2023年11月,美国国防部高级研究计划局与美国特种作战司令部联合启动“速度与跑道独立技术”项目,面向全球招标新型飞行器方案,贝尔公司凭借深厚的旋翼飞行器技术积累,击败了波音等强劲对手,最终获得了研发资格,并在2026年3月正式将这款验证机命名为“X-76”。值得一提的是,“76”这个代号大有讲究,它代表着1776年美国建国,旨在纪念美国建国250周年,可见美军对这款装备的重视程度非同一般。
【解说2】
那么,这款被美军寄予厚望的X-76,究竟有何独特之处呢?咱们不妨从它的“外观”和“能力”谈起。首先,最引人注目的,便是它的“可折叠旋翼系统”。这并非普通的旋翼,而是贝尔公司研发的独有“停止/折叠”技术。通俗来讲,它犹如“变形金刚”般,能在两种飞行模式间流畅转换。
或许有人会问:这种“模式切换”究竟有何特别之处?咱们来打个比方,当X-76准备起飞时,其翼尖旋翼会正常运转,像直升机那样提供垂直升力,无论是在复杂地形、未经平整的地面,还是海上的小型浮动平台上,都能轻松实现起降;而当它加速到约每小时278公里至370公里的关键速度时,奇妙的现象便出现了,翼尖的旋翼会逐渐停止转动,并向后折叠,紧密贴合在机翼短舱上,此时,飞机便瞬间化身为一架喷气式飞机,依靠独立的涡扇发动机推动,全速前进。
采访2:这种设计的优势?
参考:这种设计的优势的显而易见,一方面,它彻底解决了传统倾旋翼机“高速飞行时旋翼阻力过大”的痛点;另一方面,也实现了“直升机的灵活”与“喷气式飞机的速度”的完美结合。除了这个核心特点,X-76还有两个非常突出的优势,第一个是“高度可扩展性”,贝尔公司表示,X-76的技术可应用于总重量约1.8吨到45.4吨的各种机型,无论是无人驾驶的小型验证机,还是堪比C-130的大型战术运输机,都能采用这项技术;第二个是“任务多样性”,它支持有人驾驶、无人驾驶或“可选有人驾驶”三种模式,既能运输人员和货物,也能执行战斗搜救、情报监视侦察,甚至可以进行空对空作战、空对地打击,堪称“战场万金油”。
【解说3】
随后,让我们来关注一则官方消息:X-76的最大亮点无疑是速度,其巡航时速大约在740公里至833公里之间,这是个怎样的概念?我们可以与V-22“鱼鹰”倾转旋翼机做个比较,后者的巡航时速约为518公里,X-76比它快了43%至61%,这一差距在战场上有何意义?举个例子,在执行战斗搜救任务时,“鱼鹰”需耗时2小时才能到达目的地,而X-76仅需约1小时,便可完成“闪电救援”,显著提高搜救效率。
采访3:除了速度,它还又哪些优势?
参考:最后是任务载荷,虽然贝尔公司目前没有公布具体的载荷数据,但根据其设计定位,它的载荷能力不会低于“鱼鹰”,而且由于采用了可折叠旋翼设计,机身空间利用率更高,能搭载更多的人员和物资。
【解说4】
看到此处,或许有观众会产生好奇:这款“全能”型飞行器如今发展到了何种地步呢?依据官方信息,X - 76已顺利通过关键设计评审,并且全部关键风洞测试也已完成,当下正处于原型机制造的进程当中。根据计划安排,将于2027年完成原型机总装工作,在2028年初达成首飞目标。
【主持人2】
说起旋翼机,人们脑海中首先浮现的想必是V - 22“鱼鹰”。这一由贝尔公司与波音公司携手研发的装备,已服役多年之久,是美军特种作战中的主力机型。鉴于X - 76同样出自贝尔公司研发之手,那么它是否仅仅是“鱼鹰”的简单升级版本呢?二者之间究竟存在哪些差异之处?今天,我们就来进行一场“同台比拼”,一次性将它们的不同点阐述明白。
【解说5】
首先,最为根本的区别在于飞行模式以及动力系统的差异,这也是X - 76最具突破性的部分。我们先来谈谈“鱼鹰”,“鱼鹰”采用的是“倾转旋翼”的设计方式,其翼尖短舱能够整体进行倾转。在起飞时,短舱垂直朝上,旋翼旋转以提供升力,和直升机无异;在平飞状态下,短舱水平朝前,旋翼旋转提供拉力,如同固定翼飞机一般。但这里有一个重要的问题:不管是起飞、悬停还是高速平飞,“鱼鹰”的翼尖旋翼都必须持续旋转。
大家千万不要轻视这个“持续旋转”,它所带来的问题可不少。首先是空气阻力方面,高速平飞时,旋转着的旋翼会产生极大的空气阻力,这也是“鱼鹰”巡航速度难以提高的关键因素;其次是安全性方面,处于低空飞行状态时,旋转的旋翼容易与地面物体、树木发生碰撞,并且一旦旋翼出现故障,飞机极易失去平衡从而引发事故。自“鱼鹰”服役以来,已经发生了多起坠机事故,都与这一设计有着千丝万缕的联系。
采访4:X76比鱼鹰在旋翼系统上的优势?
参考:而X-76则完全跳出了“鱼鹰”的框架,它采用的是“停止/折叠”旋翼系统,前面我们已经介绍过,它在高速平飞时,旋翼会停止旋转并向后折叠,此时飞机依靠涡扇发动机提供推力,完全摆脱了旋翼的束缚。这样一来,不仅空气阻力大幅降低,速度实现了质的飞跃,而且安全性也得到了提升——折叠后的旋翼不会再发生碰撞事故,即使旋翼出现故障,也不会影响涡扇发动机的正常工作,飞机依然能保持高速飞行。
【解说6】
第二个是应用范围的差异。“鱼鹰”主要以有人驾驶为特点,尺寸较为固定,重点用于人员与物资的输送,任务形式相对单一;而X-76包含无人和有人两种模式,技术具备高度的可延展性,既能充当有人驾驶的核心运输载体,也能作为无人机执行危险任务,其灵活性远胜“鱼鹰”。
第三个是隐身能力的不同。X-76设计中引入了低可探测性理念,机体采用隐身优化外形,结合红外与噪声抑制技术,特别是在旋翼折叠后进入平飞状态时,其雷达反射截面和噪声明显小于“鱼鹰”,更不易被侦测和拦截。而“鱼鹰”由于旋翼持续旋转,噪音显著,雷达反射面积也较大,在高强度对抗场景下,极易被敌方防空系统捕捉。
或许有观众会疑惑:既然X-76比“鱼鹰”先进如此之多,它是否将直接替代“鱼鹰”?
采访5:可能有观众会问:既然X-76比“鱼鹰”先进这么多,难道它要直接取代“鱼鹰”?
参考:其实,两者的定位并不冲突。“鱼鹰”目前已经批量列装,主要承担常规的运输任务,技术成熟、可靠性高;而X-76目前还处于研发阶段,它的定位是“高端冲突中的快速部署平台”,主要用于解决“鱼鹰”无法实现的高速、隐身、多任务需求,未来两者可能会形成“高低搭配”,共同为美军的全域部署提供支持。
【主持人3】
看到此处,大家或许会疑惑,贝尔公司怎能研发出如此奇特的装备呢?实际上,X-76的问世,并非“一蹴而就”,而是从早期的实验机型逐步发展到如今的X-76,贝尔公司在垂直起降领域的探索,已历经近70年的时光。现在,我们就一同回顾贝尔公司的垂直起降发展之路,同时瞧瞧,除了美国之外,还有哪些国家曾在此领域留下探索的足迹。
【解说7】
首先,从贝尔公司早期的经典机型——D - 188A讲起。上世纪50年代冷战激烈,美苏在军事科技方面竞争十分激烈,无需跑道的垂直起降飞机,成为两国研发的重点,诸多类似的想法不断涌现,例如把F - 104改装为火箭,或者配备四台涵道风扇的巨型垂直起降飞机,还有F - 35的前身,有着“猪鼻子”的X - 14。
这时,美海军通过非官方渠道得知贝尔公司正在研发新型垂直起降飞机,随后美海空军联合向贝尔公司提出要求,希望研发一款拥有垂直起降能力的超音速战斗机,既能执行全天候作战任务,又能进行防空拦截,D - 188A项目便应运而生。
贝尔给出的设计十分大胆:它配备了8台通用电气涡喷发动机,其中4台位于翼尖的可旋转短舱内,另外4台分别置于机身尾部和驾驶舱后方。起飞时,翼尖短舱转至垂直方向,8台发动机共同提供垂直升力,达成垂直起降;平飞时,翼尖短舱转至水平方向,发动机提供向前的推力,设计速度能够达到2马赫。
1958年初,飞机模型制作完成,设计方案也已确定。但可惜的是,这款看似无懈可击的机型,最终却没能走出实验室。
采访6D-188A项目失败的原因?
参考:主要有两个原因:一是技术过于复杂,8台发动机的布局导致机身重量过大,而且可旋转短舱的机械结构非常精密,故障率很高,当时的技术水平很难解决这些问题;二是成本过高,贝尔公司为了研发这款机型,投入了大量的资金,而美国军方最终认为,这款机型的实用性不强,而且维护成本太高,于是在1961年取消了这个项目,最终只造出了一个全尺寸模型,D-188A也成为了贝尔公司垂直起降探索之路上的“遗憾之作”。
【解说8】
尽管D-188A项目未能成功,贝尔公司却未停止对垂直起降技术的钻研,反而获取了众多珍贵的经验。在这些经验的基础上,“鱼鹰”倾转旋翼机得以问世,随后又进一步推出了V - 280“英勇”倾转旋翼机。
V - 280旨在替代美军日渐老旧的UH - 60“黑鹰”直升机,其巡航时速处于426到574公里之间,转场航程高达3889公里。它运用了倾转旋翼的设计理念,不过对机械构造进行了改良,安全与可靠性较“鱼鹰”有了显著提高,并且安装了和F - 35战斗机相近的“分布式孔径系统”,飞行员能够实现全方位观测,作战实力大大加强。2025年5月,美陆军将V-280正式命名为 MV-75。计划在2027年开始向部队交付首批作战飞机,这一时间点比最初规划的2031至2032年提前了约五年。
其实,不单单是美国,苏联同样在垂直起降倾旋翼机方面开展过积极的研究,其中最具典型性的便是卡莫夫设计局于1967年提出的KA - 35重型运输倾旋翼机方案。或许有观众会疑惑:苏联的KA - 35,和美国的倾旋翼机存在哪些差异呢?
采访7KA-35的性能?
参考:根据公开资料显示,KA-35的设计定位是重型运输平台,主要用于配合安-12运输机执行任务,它的最大起飞重量达到了71.5吨,最大载荷可达20吨,巡航速度可达500公里/小时,最大航程为800公里,这样的性能参数,在当时来说是非常先进的。它采用了双涡喷发动机,搭配重叠旋翼设计,既能实现垂直起降,也能进行高速平飞,机身尾部设有货舱坡道,方便装卸大型装备,机头还配备了遥控机炮,具备一定的自卫能力。
【解说9】
但可惜的是,KA-35最后也仅仅停留在设计方案与模型阶段,并未制造出原型机。主要原因在于当时苏联的工业水准无法攻克重叠旋翼的气动稳定性难题,而且双涡喷发动机燃油效率过低,航程难以达到军方要求。此外,当时苏联将军费重点投入到战略导弹和核潜艇方面,最终致使该项目被搁置。
看到此处,我们不难发觉,垂直起降飞机的研发属于难度极高的系统工程,其需要雄厚的航空工业基础、尖端的发动机技术以及精细的机械制造能力。不管是美国的D - 188A,还是苏联的KA - 35,都因技术、成本等问题未能达成量产。
【主持人4】
通过前面的讲述,大家已经认识到,垂直起降飞行器的设计理念相当先进,纸面性能也极为出色,甚至被不少人誉为“改写军事飞行规则”的装备。然而,一款先进装备的研发从来都不是一帆风顺的。那么,眼下让美军寄予厚望的 X-76,未来究竟会顺利落地、大放异彩,还是会重蹈诸多前辈的覆辙,最终无疾而终呢?
采访8X-76设计的难点在哪?
参考:第一个,也是最大的技术难点,就是飞行模式的过渡技术。我们都知道,X-76的核心优势是“旋翼悬停”与“喷气巡航”的无缝切换,而这个切换过程,正是最考验技术实力的地方。当飞机从悬停状态加速到临界速度时,旋翼需要从旋转状态慢慢停止,然后向后折叠,同时动力系统要从涡轴发动机切换到涡扇发动机,这个过程中,飞机的气动布局会发生巨大变化,很容易出现机身振动、气流抖振等问题,一旦控制不好,就可能导致飞机失稳,引发事故。
【解说10】
或许有观众会疑惑:贝尔公司不是已经在风洞测试里证实了旋翼折叠的可行性吗?为何这一难点依然存在?
事实上,风洞测试仅是模拟条件,真实飞行中的状况要复杂得多。例如,高空中的气流波动、温度变化,都会对旋翼的折叠和动力切换产生影响。尽管贝尔公司在1972年就已验证了旋翼折叠的可行性,但在实际飞行中,如何保证过渡阶段的平稳性与安全性,仍然需要进行大量的飞行试验和技术改进,这也是X-76研发过程中亟需解决的关键难题。
采访9X-76设计的难点在哪?
参考:第二个难点,是推进系统的成熟度问题。X-76目前采用的是“分离式推进系统”,也就是旋翼由涡轴发动机驱动,平飞由涡扇发动机提供推力,这种设计虽然能实现两种飞行模式的切换,但也存在一个问题——机身重量过大,而且两套发动机系统的维护成本很高。贝尔公司其实设想过一种“可转换发动机”,这种发动机可以在涡轴模式和涡扇模式之间自由切换,既能减少机身重量,又能提升燃油效率,但这种发动机的技术复杂度极高,目前还处于研发阶段,短期内难以应用于X-76,这也成为了制约X-76性能提升的重要因素。
【解说11】
除了这些技术难题,X-76还遭遇成本与可靠性的挑战。“鱼鹰”单价已超1亿美元,而X-76运用了更为先进的技术,成本必然更高,即便美军军费充裕,也得权衡一下经济负担。
采访10:还有维修问题?X-76未来会实现量产列装吗?
参考:另外,X-76的技术过于先进,机械结构和电子系统都非常复杂,未来量产之后,维护难度和维护成本也会很高,而且可靠性也需要经过长期的实战检验,毕竟“鱼鹰”服役以来的多起事故,已经让美军对倾旋翼机的可靠性产生了一定的顾虑。那么,X-76未来会实现量产列装吗?根据目前的进度,X-76预计2028年初首飞,首飞之后,还需要经过多年的地面测试、飞行测试和实战验证,才能确定是否适合量产。如果一切顺利,预计到2035年左右,X-76可能会批量列装美军,成为美军特种作战和全域部署的核心装备;但如果在测试过程中发现无法解决的技术难题,或者成本超出预期,它也有可能面临调整甚至取消的风险。
【主持人5】
X-76力求突破性能限制,达成“速度与灵活”的精准平衡。尽管它当下仍存在不少技术难题,未来发展也充满未知数,但我们必须承认,其设计思路与技术革新为军事航空领域的发展带来了新启发。日后,伴随技术的持续发展,相信会有更多先进装备诞生,重塑战争的模式。
好了,本期《军事科技》到此结束,谢谢大家的观看,我们下期再会。
@@ -0,0 +1,137 @@
现代防空反导大对决
【导视】
导弹呼啸而过,战机低空穿梭,无人机如蜂群般袭来。这些空中威胁正不断挑战防御体系的极限!它们是如何被精确拦截的?究竟是什么力量,能在遥远距离精准打击,在近在咫尺时完成致命一击?
本期《军事科技》将带您逐步解析现代防空网络的复杂机制,亲眼见证科技与智慧打造的空中屏障,揭秘现代防空体系御敌于空的终极密码。
【主持人1】
观众朋友们,你们好!欢迎来到这一期的《军事科技》节目,我是主持人蓝皓。回顾历史,从二战期间的飞机轰炸,到冷战时代的导弹威胁,再到今天无人机和高超音速武器的兴起,空中威胁的方式一直在演变,防空体系也因此持续发展。不少人可能觉得防空不过是“导弹对抗飞机”,但实际上,当代防空网络是一个融合了“远、中、近程结合,高、中、低空覆盖”的多层次防御系统,它仿佛一面环环相扣的“天空护盾”,从遥远的高空到身边的低空,构建起没有漏洞的安全屏障。
【解说1】
2026年2月28日,美以两国协同向伊朗发起军事打击,中东地区顿时战火纷飞、硝烟弥漫。随着冲突不断加剧,各方频繁动用弹道导弹、巡航导弹、高超音速武器及自杀式无人机等先进装备,展开密集的高强度空袭。波斯湾上空随即呈现出一场攻防交织的终极较量,涉及突破与拦截、毁灭与保卫,这场关乎生死存亡与战局走向的空中攻防战彻底爆发。
隔断:【远程防空:弹道导弹与反导系统的巅峰对决】
【解说2】
在涉及远、中、近程和高、中、低空的全方位空中对抗中,弹道导弹与反导系统之间的对决无疑是最具战略威慑力且最检验技术水平的顶峰交锋。其中,中段反导作为现代战略防空的最高层级,更是左右战场局势的核心要素。这项技术常被形容为“太空中以子弹击落子弹”,它标志着当今世界军事科技的极致成就,也是战略防空领域真正的制高点。
【动画1】
一枚中远程弹道导弹的飞行分为上升段、中段、末段,三者在时空、速度上的巨大差异,直接决定了拦截难度的天壤之别。上升段仅持续3~5分钟,时间窗口稍纵即逝,且需在敌国领土拦截,受部署限制难以实施,拦截成功率极低。末段导弹俯冲至几十公里至几公里高空,速度达10~20马赫,仅留数秒至十几秒反应时间,同时面临机动变轨、多弹头诱饵及低空探测盲区,拦截风险高且成功率有限。唯有中段,导弹在100公里以上大气层外无动力滑行,飞行时间长达10~20分钟,轨迹由万有引力决定极度稳定,且未释放弹头与诱饵,反导系统有充足时间预警、测算轨迹并拦截,采用动能撞击可彻底消除地面威胁,是现代战略防空最理想、效果最佳的“黄金拦截期”,也是各国反导技术研发的核心方向。
【专家1】
虽然说,中段反导拦截在整个反导作战的流程当中,那么它是比较容易实现的,或者说,可以针对对方来袭的导弹进行精准的摧毁,但是最大的问题,就是你的预警系统,需要看得远,反导拦截弹,需要飞得快,打得高打得远,所以对于技术方面的要求是比较高的,这就要求大型的相控阵预警雷达,能够在极远的距离上,针对外大气层飞行的这种小型、高速移动的目标,能够进行精准的锁定,而且,要对它进行持续的跟踪,还要有相关的这样的一个计算系统,针对它的飞行轨迹进行计算,这样的话,才能够有效的引导反导拦截弹,在外大气层,对它进行精准的这样的一个截杀,而且,反导拦截弹的性能要求非常高,不仅要飞得高,飞得远飞得快,而且,甚至还要配备有动能的战斗部,要针对处于飞行状态的弹道导弹,进行精准的撞击,那么确保它是被彻底的摧毁。
【解说2】
3月5日,伊朗伊斯兰革命卫队发布声明称,在“真实承诺4”行动的第十七轮中,伊朗动用高超音速导弹和攻击无人机成功突破美国“萨德”防御系统,打击了以色列国防部大楼与本·古里安国际机场,并摧毁了7套雷达系统及其核心配套设备。
雷达被誉为中段反导体系的“眼睛”,是在大气层外实现目标跟踪与轨迹测算的中枢。其被精确击毁致使美以中段反导网络陷入“失明”状态,区域预警能力急剧削弱。同期,美国内部评估报告透露,作为中东关键中段反导装备的以色列箭式系统,在拦截伊朗中程弹道导弹时显现重大短板:实际拦截率远未达到原有设计标准。
【专家2】
那么在以色列的国土范围内,有箭-2、箭-3反导系统,也有美国部署的萨德反导系统,但是我们看,仍然有数量相当多的伊朗弹道导弹成功突防,针对以色列区域内的目标进行了毁伤,为什么说箭-2、箭-3以及萨德系统,它联起手来也没有办法针对伊朗的弹道导弹进行这种100%的防范和拦截,我认为,首先是伊朗弹道导弹的技术有所提升,部分伊朗的弹道导弹采用了这种高超音速的战斗部,在飞行的末端会有突然的加速,或者说有急剧的飞行轨迹的变化,这样的话,萨德系统和箭-2、箭-3反导系统很难对它进行精准的拦截,此外,伊朗的一些弹道导弹,比如说像霍拉姆沙赫尔-4,那么它是一种重型的这样的一个弹头的设计理念,那么1.5吨到2吨的战斗部,它可以采用这种极速弹头的这样的一个模式,飞到了目标区上空之后,打出了一个满天星的效果,数十枚子弹头飞向目标区域,所有的防空反导系统都没有办法对它进行拦截,那么还有另外一种情况,美国的反导作战系统和以色列的箭-2、箭-3反导系统它没有办法进行联网作战,是各打各的,这样的话,在相关的区域内没有办法进行目标信息的共享,那么也无法形成合力
【解说3】
2026年3月15日,伊朗伊斯兰革命卫队在“真实承诺-4”军事行动中,首次实战部署了“泥石”导弹。与此同时,美军从韩国地区紧急调遣了萨德反导系统,具体是将驻韩国庆尚北道星州基地的“萨德”系统抽调至中东,以加强该地区的防御,应对伊朗导弹带来的持续威胁。
【主持人2】
若将远程反导视作“千里点穴”的区域安全盾牌,那么中程防空便是“中空守护网”,承担着衔接远、近防御的关键使命。它覆盖数十至一百多公里的中高空域,作为防空体系中的“中坚力量”,承担区域防空重任,护卫城市、军事要地等高价值目标,成为整个防空网络中承上启下的核心组成部分。
隔断:【中程防空:复合威胁下的区域空天防御攻坚战】
【解说4】
中程防空作为现代防空作战系统的关键环节,其防御半径一般在50到200公里之间,拦截高度重点覆盖10至20公里的中高空域。这片空域正是现代战争中空中威胁密集活动的“核心地带”,敌方战机若要突破外层防御、打击纵深关键目标,必须穿越此区域;巡航导弹虽常利用低空突防,但多数情况下仍需在中高空调整姿态、寻找防御薄弱点;而中近程弹道导弹在俯冲末段同样会经过这一高度区间,其高速特性要求中程防空系统及早介入、精确拦截。可以说,这片中高空域是空中威胁渗向核心目标的“必经通道”,也是防空体系中承前启后的“关键拦截区”。
【专家3】
在现代化的防空作战行动当中,中程防空的难度是非常大的,因为很多对方的这种空中的来袭目标,其实它都是在中程防空的任务范围内,目标的属性是非常多的。此外,在复杂的电磁环境的这样的一个状态下,中程的防空体系也可能会出现失灵的情况,因为对方可能会出动电子战飞机针对你的防空作战体系进行压制。此外,有的时候,面对各种各样的目标,数量众多的目标,那么中程防空系统它的这样的一个火力的密集程度可能会出现不足的情况,比如说一部发射器上它只有数量有限的中程防空导弹,打完了之后,那么对方的目标连续的持续地袭来,那中程防空体系可能也会面临弹药不足、火力不足的情况。
【解说5】
在近期美国与伊朗的冲突中,中程导弹防御系统成为防空体系中的核心作战力量,也是双方攻防较量最为激烈的领域。为巩固中程防御屏障,美国运用“爱国者-3”防空反导系统,结合天基预警卫星、陆基远程雷达及集成化指挥控制中心,打造了能够覆盖30至150公里空域的拦截体系,着重防护其军事基地与国防工业区域。然而,面对伊朗“高超音速武器配合无人机集群”的非对称组合式打击,此前被美军公开宣称拦截成功率可达80%到97%的“爱国者-3”系统,实际拦截率据披露已跌至5%以下。系统还频频出现“拦截假目标、漏过真实弹头”的失误。
【专家4】
爱国者曾在海湾战争期间一战成名,那当时,美军爱国者系统它所拦截的导弹,都是伊拉克萨达姆政权军队使用的比较老旧的飞毛腿弹道导弹,它的飞行轨迹是一个典型的抛物线,不会有这种机动变轨的这样的一些动作,所以,只要是探测到了它的飞行轨迹,那么多发射一些爱国者拦截弹,是可以对它进行有效拦截的。但是现代化的弹道导弹突防能力都很强,飞行轨迹是变化的,甚至可以说是有一些打水漂的这样的一个飞行轨迹,速度又很快,那么甚至可能会携带更多的诱饵弹和干扰弹,所以,爱国者系统可能根本就看不清,可能也就追不上。
【解说6】
爱国者-3导弹的拦截成功率偏低,已造成美以中程防空系统整体失效,并引发多国对该体系的信任危机。部分国家甚至正在评估是否推迟采购方案。
【主持人3】
在现代防空系统中,低空与超低空是远程及中程防空存在的短板。受地球曲率、地形掩蔽等影响,远程雷达难以追踪低空目标;而中程防空导弹在低空条件下,制导系统易受地面杂波干扰,致使拦截效能下降。因此,近程防空系统由此诞生,专司近距离防护,弥补防空网络在低空领域的不足。
隔断:【近程防空:低空突防与最后防线的生死较量】
【解说7】
对普通公众而言,短程防空无疑是最常见、最显眼且视觉冲击力最强的防空形式。那些在网络短片、新闻报道和战场记录中常见的导弹腾空而起、尾焰撕裂黑夜、目标在空中爆炸、火光刹那映亮天空的拦截画面,大多源自短程防空系统。
这些系统射程较短,但部署便捷、响应迅速,通常在目标接近阵地、城市或重要设施的最后一刻才发动攻击,因而更易被现场捕捉、媒体记录并广泛传播。正是由于这种高频曝光,短程防空在许多人心中几乎成了“防空”的代名词。
然而实际上,短程防空在整体防空网络中充当的是最终贴身防线的功能,专门填补远程和中程防空无法覆盖的低空及超低空漏洞。
【专家5】
近程防空作战体系,在实际应用的过程当中,有的时候,可能会成为战场上的奇兵,因为对方的直升机,甚至包括战斗机,在低空飞行的时候,他可能会放松警惕,另外一个来不及反应,所以,时候,近程防空火力,针对目标进行射击的时候,战斗机很难对他进行规避,有的时候,甚至连发射红外诱饵弹的时间都没有。但是,近程防空也有很大的难度,因为很多近程防空作战体系,它的搜索系统是比较简单的,视野不够宽广,也无法针对远距离的目标进行识别,所以,有的近程防空作战体系,它本质上来讲是近视眼,而且,还涉及到一些地形起伏的问题,如果对方利用这种地形起伏,那么去进行超低空的突防,那么在相关区域内部署的一些传感器,比如说低空搜索雷达,或者是光学红外传感器,可能没有办法及时有效地识别目标,那么这些低空突防的这样的一些空中作战平台,也可能会针对近程的防空作战体系,包括相关的防空阵地进行打击和压制。
【解说8】
在近期美以联合对伊朗的军事行动中,以色列拉斐尔公司研发的“铁穹”近程防空系统,经历了自2011年部署以来最激烈且最具转折性的实战检验。这款以往在冲突中以85%至90%拦截率享誉世界的明星武器,在伊朗“真实承诺4”行动的饱和攻击下,其卓越声誉荡然无存。以色列国防部3月17日向国会递交的机密评估报告指出,面对伊朗连续发动的60多波次攻势,“铁穹”系统的实际拦截率从历史标榜的90%以上骤降至约50%。而在集束弹头密集倾泻与无人机蜂群低空突袭的关键战斗阶段,其拦截成功率更下滑至30%以下,刷新了服役以来的最低性能纪录。
【专家6】
那么我们注意到,在此次军事行动的过程当中,铁穹系统表现平平。我认为,可能是有两方面的考虑,一方面,就是它配套的这种传感器,或者是预警体系,可能是遭到了对方的火力打击,有部分铁穹系统,那么针对空中来袭目标,它的反应速度不够快,或者说,它的这样的一个情报和目标的引导体系,不够完善。另外一个,就是铁穹系统,它所使用的是塔米尔拦截弹,这种拦截弹,由于它装备和使用的数量比较多,所以,它必须要进行成本的控制,它的飞行速度其实并没有那么快,那么有的时候,针对高速目标进行打击的时候,可能会出现速度跟不上的情况。此外,现在伊朗主要是使用弹道导弹,针对以色列进行打击,面对高空高速飞行的弹道导弹,或者是,配备有高超音速战斗部的弹道导弹,铁穹系统可能它的拦截范围、拦截的高度也都不足。当然了,伊朗也使用了大量的无人机,但是这一次,针对以色列进行后续的反击,伊朗更加注重使用这种中程的弹道导弹,或者是突防能力很强的弹道导弹,那么,面对这样的一种情况,铁穹系统,它就会出现反应速度不够快,包括拦截的范围、拦截的高度,都存在不足的情况。
【解说9】
近程防空系统作为整个防空架构中关键的最后屏障,肩负着“坚守底线、护卫要害”的核心职责,但它并非完美无缺,存在明显的短板。其防御范围通常较为有限,探测与拦截能力都集中在较小区域,大多只能执行小尺度、重点目标的局部防护任务,无法如同中远程防空系统那样,实现广大空域的大范围、远距离监控与阻击。因此,近程防空系统不能独自支撑一国、一城或整个战区的全面空防安全。它更像是一件贴身的“防弹衣”,而非能覆盖全领域的“保护罩”。
【主持人 4】
远程、中程与近程防空系统相互配合,在现代战争中构筑了层次分明、分工明确、紧密衔接的全空域立体防空网络。三者各尽其责、彼此支援、逐层拦截,形成了一套由外至内、从高空到低空、由远及近的周密防御体系。不过,从近年多场高强度军事冲突的实际表现来看,这套看似牢不可破的防空体系正持续受到新兴战场模式的严峻挑战,其传统作战思路和防御能力正遭到不断冲击与重塑。
隔断:【体系防空:全域防御与精准拦截的新困境】
【解说10】
在美以联合对伊朗发动的大规模军事行动中,可以清晰看到,美军、以色列军队及伊朗的防空系统都遭遇了前所未有的考验。以往严密的多层防护网络,在实战环境中屡次被突破、撕裂乃至彻底崩溃。
美军采用了以爱国者-3和标准-6为核心的中远程导弹防御体系,以色列则调动了由铁穹、大卫投石索和箭式系列组成的全域防空反导系统。然而,针对伊朗的高超音速武器穿透、无人机群饱和攻击、复合诱饵欺骗与强电磁干扰等非对称策略,拦截失败的情况依然频繁发生。
【专家7】
在此次行动的过程当中,美以的防空反导系统频频被洞穿,也就是说,伊朗使用了一系列的突防手段,最终,打穿了美国和以色列的一些防空反导的保护伞,那么针对相关的区域,构成了更为猛烈的火力打击。我认为,就是存在一些问题,比如说,它系统之间没有进行有效的联合,存在一些兼容的问题,各自为战的状态下肯定会出现漏洞。另外一个,伊朗也是采取了一些灵活多变的这样的一些战术,多种火力、多种模式联合运用,也让美国和以色列的防空反导系统防不胜防。
【解说10】
在现代军事冲突中,高超音速武器的穿透与无人机蜂群的饱和攻击虽是关键威胁,但更具深远颠覆性的是战场成本效益失衡对防空体系带来的系统性难题。这已超越了单一装备的性能竞争,演变为从经济原理、持久作战能力到战略自信的全方位压制,引发了“拦截不起、消耗不起、补充不上”的三重困局。
以伊朗主力攻击武器“见证者-136”自杀式无人机为例。这款被戏称作“飞行摩托车”的装备,彻底打破了传统军工的精密制造范式:机身使用民用级玻璃纤维和泡沫塑料拼装,动力系统直接借用普通摩托车的二冲程发动机,导航依靠民用GPS模块与简易惯性制导,甚至部分核心零件来自拆解的汽车和家电,从而极大压低了制造成本。凭借“民用技术军用化、复杂系统简易化”的理念,其批量生产单价稳定在2万至5万美元之间,某些简化版本甚至降至1.5万美元,仅相当于高端防空导弹成本的极小一部分。
相比之下,美以用于对抗的防空武器却如同“天价奢侈品”。以色列铁穹系统的拦截弹作为近成防空核心弹药,每枚成本达5万到10万美元,这意味着即便成功拦截,防御方的经济损失也远超进攻方。更甚者,美军“爱国者-3”拦截弹作为中程防空主力,单枚成本飙升至400万至500万美元,堪比用超级跑车撞击普通代步车——无论胜负如何,防御方已在战略层面沦为输家。
【专家8】
在此次行动的过程当中,美以的防空反导系统频频被洞穿,也就是说,伊朗使用了一系列的突防手段,最终,打穿了美国和以色列的一些防空反导的保护伞,那么针对相关的区域,构成了更为猛烈的火力打击。我认为,就是存在一些问题,比如说,它系统之间没有进行有效的联合,存在一些兼容的问题,各自为战的状态下肯定会出现漏洞。另外一个,伊朗也是采取了一些灵活多变的这样的一些战术,多种火力、多种模式联合运用,也让美国和以色列的防空反导系统防不胜防。那么在消费比方面, 如果说对方在进攻的过程当中, 采用了很多廉价的方式,或者说很多省钱的这样的一些技术手段,像见证者-136,其实,它就是飞行的小摩托,那么它的成本控制得很低,但是,对方可能要用先进的防空反导系统对它进行拦截。而伊朗的一些地区盟友,包括 黎巴嫩真主党,他会使用大量的卡桑火箭弹,那么也就是说,更低成本的作坊工厂当中生产出来的这种火箭弹,针对以色列进行打击。那以色列方面使用的是什么, 是这种塔米尔拦截弹,也就是铁穹系统的拦截弹,那么在这样的一种 相互的消耗战的过程当中,那以色列或者是美国,他所面临的这种昂贵弹药的快速消耗和短缺的情况,也会变得越来越多,也会变得越来越明显。
【解说11】
由于战场上攻击和防御的消耗比例严重失衡,防空弹药库存告急,形势十分紧迫。3月18日,以色列国防部火速向美国求援,美军立刻出动C-17运输机,紧急空运了近千枚拦截导弹,为以色列的库存注入强心针。与此同时,美军从韩国、亚太到欧洲,多个据点迅速调集防空系统,全力缓解战场上防空装备和弹药捉襟见肘的困境。
【主持人 5】
现代防空系统打造了一个从远程中段反导、中程区域防空到近程末端拦截的完美链条,构建了远近交错、高低兼顾的立体防御天网。然而,高超音速武器如闪电般突破、无人机群黑压压地压制,以及饱和式攻击的浪潮不断袭来,让传统防空体系面临严峻考验。攻防成本的天平严重倾斜,威胁形式日新月异,如何在提升拦截效果的同时勒紧成本缰绳,变得至关重要。如何让防空网络既能千里眼般远距离探测,又能百步穿杨精准拦截,还能持久抗战且经济实惠,这已成为左右未来空防安全和战争胜负的关键棋局。
好了观众朋友们,感谢您持续关注国防军事频道,《军事科技》我们下周同一时间再见。
+119
View File
@@ -0,0 +1,119 @@
# Prompt 2:叙事结构判别 v0.2
## SYSTEM
你是央视《军事科技》栏目的内容分析助手。你的任务是只判断一件事:这期节目的叙事结构属于哪一档。
### 一、两档定义
**主线演进 (`主线演进`)**
全片由某种"贯穿主线"串起来。主线可以是:
- a) 内在概念主线:某个哲学命题、反差、共同绝境、共同特征贯穿全片
- b) 时间/因果链主线:技术演进时代线、装备发展史、因果推进
- c) 性能阶梯主线:从弱到强、从慢到快、从基础到尖端的递进
心法:把**全片大章节**顺序打乱,观众会觉得逻辑乱、跟不上节奏,但还能跟上。
**并列结构 (`并列结构`)**
各段相互独立、相互可替换,共性只来自分类共性(命名/装备域/空间方位),不构成有张力的主线。
心法:把**全片大章节**顺序打乱,观众基本无感。
### 二、关键边界(这些都不构成主线)
1. **命名共性**:几件装备都以同一个字/词命名(如 4 件都以"马"命名)→ 不是主线
2. **装备域共性**:几件装备同属一类(如都装了 AI、都是无人系统)→ 不是主线
3. **空间方位共性**:远/中/近、上/中/下、前/后/左/右 → 不是主线(注意:如果按距离/层级递进且每段铺垫下一段,可能构成性能阶梯主线,需细判)
4. **【重要】段内承接 ≠ 全片骨架**:判断主线必须看**全片大章节之间**的关系。文稿里某一段可能出现"递进""演进""阶梯""奠定基础"等字样,这往往只是段落内部的微观承接(讲两件装备先后关系、某段背景里的时间线等),**不是全片骨架主线**。只要全片大章节之间可以颠倒,仍然是并列结构。
### 三、强制判断流程(必须按这两步走)
**第一步:识别全片大章节列表(通常 3-5 个)**——通过主持人过渡词、隔断标题、明显的换段标志识别
**第二步:对大章节列表做"打乱测试"**——
- 把大章节顺序调换,问"观众会一头雾水吗?"
- 会 → 主线演进
- 不会 → 并列结构
不要看到段落内的"递进""演进"字样就直接判主线演进——那是陷阱,先做"打乱测试"。
### 四、5 个示例
**示例 1:潜艇仿生(主线演进 - 内在概念)**
大章节:鹦鹉螺→压载水舱 / 鲨鱼皮→消声瓦 / 海豚声呐→潜艇声呐 / 乌贼墨汁→声学诱饵
判断:`主线演进`
主线:自然智慧 vs 人类工程——每段都是"自然给出的解 → 人类工程的模仿/借鉴"对照
理由:所有大章节都在回答同一个命题,且段间存在"基础浮力→隐身→探测→防御"的能力递进,打乱顺序破坏对照张力
置信度:高
**示例 2:空战颠覆者(主线演进 - 时间线)**
大章节:螺旋桨时代 / 喷气时代 / 隐身时代 / 下一代
判断:`主线演进`
主线:技术代际的时间演进——每段都是对前段的"颠覆"
理由:严格按时间序、因果链推进,打乱顺序违反时间逻辑
置信度:高
**示例 3:马年军事图鉴(并列结构 - 命名共性)**
大章节:飞马运输机 / 海马反潜机 / 白杨导弹 / 悍马越野车
判断:`并列结构`
主线:无——共性只是"名字里都带马"
理由:四件装备无递进、无因果、无相互铺垫,该期高收视(0.95)的功劳来自节点策划(马年)+强开篇钩子,不来自结构
置信度:高
**示例 4:舰证不凡(并列结构 - 警惕段内承接陷阱)** ⚠️
大章节:体型大 / 拳头大 / 责任大
判断:`并列结构`
主线:无——三个"大"是并列的展示视角
理由:虽然文稿里某一段出现"这种递进式的体量突破,为后续的拳头硬奠定了坚实基础"这种**承接句**,但这只是段落内部讲两种舰艇先后关系的微观承接,**不是全片骨架**。三个"大"之间没有谁铺垫谁——把"责任大"调到最前面、"体型大"放到最后,全片照样讲得通。**这是典型的"段内有承接字样、全片实为并列"陷阱。**
置信度:高
**这个示例专门教你识别陷阱:不要被"递进式"三个字骗,要回到大章节之间做打乱测试**
**示例 5:X-76 飞行器(并列结构 - 警惕段内时间线陷阱)** ⚠️
大章节:背景介绍 / 优势分析 / 失败原因 / 设计难点
判断:`并列结构`
主线:无——围绕同一型机做多视角剖析
理由:虽然"背景"段里讲到贝尔公司从 D-188A → V-22 → V-280 → X-76 的近 70 年技术演进,**但这条时间线只存在于"背景"这一段里,不是全片骨架**。把"设计难点"调到"优势分析"前面、"失败原因"调到最后,全片照样讲得通。**段内的时间线不要误升格为全片主线。**
置信度:高
**这个示例专门教你识别陷阱:文稿里出现"近70年发展""一代代演进"这种时间线表述时,先判断这条时间线是覆盖全片大章节,还是只在某一段里**
### 五、输出格式
只输出一个 JSON,严格按以下 schema:
```json
{
"narrative_structure": "主线演进 或 并列结构",
"chapter_list": ["大章节1", "大章节2", "大章节3"],
"shuffle_test": "把上面大章节顺序调换后,观众会怎样的一句话描述",
"main_thread": "如果是主线演进,用一句话点明主线;如果是并列结构,填 null",
"reasoning": "判断理由,2-4 句话。如果文稿里出现'递进'/'演进'/'阶梯'等字样,必须说明这是段内承接还是全片骨架",
"key_evidence": ["证据片段 1", "证据片段 2"],
"confidence": "高 或 中 或 低",
"confidence_reason": "为什么这个置信度,1 句话"
}
```
新增 `chapter_list``shuffle_test` 两个字段强制你先列大章节、再做打乱测试,然后才下判断。
### 六、置信度自评标准
- 高:大章节列表清晰,打乱测试结论明确
- 中:大体符合一种,但有 1-2 段游离;或大章节边界模糊
- 低:很难列出大章节、两档都不像、或文稿信息不全
## USER
请判断以下文稿的叙事结构:
```
{transcript}
```
【重要:输出约束】
你的回复必须是且只是一个合法的 JSON 对象,不要有任何前置说明、后置说明、markdown 代码块包裹(不要 ```json 标记)、思考过程描述。
直接输出 { 开头,} 结尾的纯 JSON。
+119
View File
@@ -0,0 +1,119 @@
"""
import_transcripts.py - 将源目录的 10 期文稿(docx/伪docx)导入到 benchmark-set/transcripts/
用法: python scripts/import_transcripts.py
"""
import re
import sys
from pathlib import Path
from docx import Document
# ===== 配置 =====
SRC_DIR = Path(r"E:\TPS-我和顾问的便签\刘瑞桦收集")
DST_DIR = Path(__file__).parent.parent / "benchmark-set" / "transcripts"
DST_DIR.mkdir(parents=True, exist_ok=True)
# 映射表: 源文件名 → 目标文件名
# (注意:源文件名中的标点/空格与预期不同,已据实更新)
FILE_MAP = [
("硅基大脑终稿.docx", "ep03_硅基大脑.md"),
("潜艇的仿生之路.docx", "ep04_潜艇仿生.md"),
("马年军事图鉴0125.docx", "ep07_马年图鉴.md"),
("\u300a枪械射速决定论\u300b0225.docx", "ep10_射速决定论.md"),
("完稿 武器进化论\uff1a空战颠覆者.docx", "ep11_空战颠覆者.md"),
("逆袭战局的组装武器.docx", "ep12_逆袭战局.md"),
("枪械设计中的长短智慧总稿.docx", "ep13_长短智慧.md"),
("\u201cX\u201d系列新成员\uff1aX-76飞机-最终稿.docx", "ep14_X76飞机.md"),
("空中坚盾\u2014\u2014解码现代防空网.docx", "ep15_空中坚盾.md"),
# .doc 文件(改 .docx 后重试)
("舰证不凡_docx版.docx", "ep05_舰证不凡.md"),
]
# ===== 配置 =====
def is_real_docx(path: Path) -> bool:
"""通过前 4 字节判断是否为真 docx(ZIP 格式)"""
try:
with open(path, "rb") as f:
header = f.read(4)
return header == b"PK\x03\x04"
except Exception:
return False
def extract_text_real_docx(path: Path) -> str:
"""用 python-docx 提取真 docx 段落文本"""
doc = Document(str(path))
paragraphs = []
for para in doc.paragraphs:
text = para.text.strip()
if text:
paragraphs.append(text)
return "\n\n".join(paragraphs)
def extract_text_fake_docx(path: Path) -> str:
"""伪 docx(实际是 UTF-8 纯文本),直接读取"""
return path.read_text(encoding="utf-8")
def clean_text(text: str) -> str:
"""清洗文本:去掉 **Markdown 强调**,合并连续空行"""
# 去掉 ** ... ** 强调符号,保留中间文字
text = re.sub(r"\*\*(.+?)\*\*", r"\1", text)
# 去掉 * ... * 斜体(如果还有残留)
text = re.sub(r"\*(.+?)\*", r"\1", text)
# 连续 3 个以上空行合并为 2 个
text = re.sub(r"\n{3,}", "\n\n", text)
return text.strip()
def process_file(src_name: str, dst_name: str) -> tuple[bool, str]:
"""处理单个文件,返回 (是否成功, 消息)"""
src_path = SRC_DIR / src_name
if not src_path.exists():
return False, f"源文件不存在: {src_name}"
try:
if is_real_docx(src_path):
raw_text = extract_text_real_docx(src_path)
else:
raw_text = extract_text_fake_docx(src_path)
clean_text_str = clean_text(raw_text)
dst_path = DST_DIR / dst_name
dst_path.write_text(clean_text_str, encoding="utf-8")
char_count = len(clean_text_str)
return True, f"OK ({char_count} 字符)"
except Exception as e:
return False, f"解析失败: {e}"
def main():
print(f"源目录: {SRC_DIR}")
print(f"目标目录: {DST_DIR}")
print(f"待处理: {len(FILE_MAP)}\n")
results = []
for src_name, dst_name in FILE_MAP:
ok, msg = process_file(src_name, dst_name)
results.append((src_name, dst_name, ok, msg))
status = "[OK]" if ok else "[FAIL]"
print(f" {status} {src_name} -> {dst_name} {msg}")
# 汇总
success = sum(1 for r in results if r[2])
fail = len(results) - success
print(f"\n===== 汇总 =====")
print(f"成功: {success}/{len(results)}")
if fail > 0:
print("失败文件:")
for src, dst, ok, msg in results:
if not ok:
print(f" - {src}")
else:
print("全部成功!")
if __name__ == "__main__":
main()
+3
View File
@@ -0,0 +1,3 @@
openai==1.55.0
python-dotenv>=1.0.0
python-docx>=1.1.0
+148
View File
@@ -0,0 +1,148 @@
"""
run_labeling.py - 单期或批量 AI 打标脚本
用法:
单期: python run_labeling.py --ep 4 --model m3
批量: python run_labeling.py --all --model m3
"""
import sys
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
import os
import re
import json
import argparse
from pathlib import Path
from datetime import datetime
from openai import OpenAI
from dotenv import load_dotenv
load_dotenv()
BASE_DIR = Path(__file__).parent.parent
TRANSCRIPTS_DIR = BASE_DIR / "benchmark-set" / "transcripts"
PROMPTS_DIR = BASE_DIR / "prompts"
EXPERIMENTS_DIR = BASE_DIR / "experiments"
GROUND_TRUTH = BASE_DIR / "benchmark-set" / "ground-truth.json"
MODEL_CONFIG = {
"m3": {
"base_url": "https://api.minimaxi.com/v1",
"model_name": "MiniMax-M3",
"api_key_env": "MINIMAX_API_KEY",
},
"deepseek-v4-pro": {
"base_url": "https://api.deepseek.com",
"model_name": "deepseek-v4-pro",
"api_key_env": "DEEPSEEK_API_KEY",
},
"mimo-v2.5-pro": {
"base_url": "https://api.xiaomimimo.com/v1",
"model_name": "mimo-v2.5-pro",
"api_key_env": "MIMO_API_KEY",
},
}
ALL_EPISODES = [3, 4, 5, 7, 10, 11, 12, 13, 14, 15]
def load_prompt(field):
if field == "narrative":
return (PROMPTS_DIR / "prompt2_narrative.md").read_text(encoding="utf-8")
raise ValueError(f"Unknown field: {field}")
def load_transcript(ep):
pattern = f"ep{ep:02d}_*.md"
files = list(TRANSCRIPTS_DIR.glob(pattern))
if not files:
raise FileNotFoundError(f"No transcript found for ep{ep:02d} in {TRANSCRIPTS_DIR}")
return files[0].read_text(encoding="utf-8"), files[0].name
def load_ground_truth(ep):
data = json.loads(GROUND_TRUTH.read_text(encoding="utf-8"))
for episode in data["episodes"]:
if episode["ep"] == ep:
return episode
return None
def parse_prompt(template, transcript):
parts = template.split("## USER")
system_prompt = parts[0].replace("# Prompt 2:叙事结构判别", "").strip()
user_prompt = parts[1].strip().replace("{transcript}", transcript)
return system_prompt, user_prompt
def extract_json_from_response(raw: str) -> dict:
"""从模型响应中提取 JSON,兼容推理模型的<think>...输出。"""
# 先去掉<think>...标签及其内容
text = re.sub(r'<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_model(model_key, system_prompt, user_prompt):
config = MODEL_CONFIG[model_key]
client = OpenAI(
api_key=os.environ[config["api_key_env"]],
base_url=config["base_url"],
)
response = client.chat.completions.create(
model=config["model_name"],
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
temperature=0.0,
)
raw = response.choices[0].message.content
return extract_json_from_response(raw)
def run_labeling(ep, model_key):
transcript, fname = load_transcript(ep)
template = load_prompt("narrative")
system_prompt, user_prompt = parse_prompt(template, transcript)
result = call_model(model_key, system_prompt, user_prompt)
gt = load_ground_truth(ep)
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
out = EXPERIMENTS_DIR / f"{ts}_{model_key}_ep{ep:02d}.json"
out.write_text(
json.dumps({"episode": ep, "filename": fname, "result": result, "ground_truth": gt}, ensure_ascii=False, indent=2),
encoding="utf-8",
)
print(f"完成 ep{ep:02d} -> {out.name}")
return result
def main():
parser = argparse.ArgumentParser(description="AI 打标脚本")
parser.add_argument("--ep", type=int, help="单期编号")
parser.add_argument("--all", action="store_true", help="跑全部")
parser.add_argument("--model", default="m3", help="模型键名")
args = parser.parse_args()
if args.all:
for ep in ALL_EPISODES:
run_labeling(ep, args.model)
elif args.ep:
run_labeling(args.ep, args.model)
else:
parser.print_help()
if __name__ == "__main__":
main()
+96
View File
@@ -0,0 +1,96 @@
"""
summarize.py - 汇总打标结果命中情况
用法: python summarize.py --model m3
"""
import sys
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
import json
import glob
import argparse
from pathlib import Path
BASE_DIR = Path(__file__).parent.parent
EXPERIMENTS_DIR = BASE_DIR / "experiments"
def load_json(path):
try:
return json.loads(path.read_text(encoding="utf-8"))
except Exception:
return None
def latest_per_ep(files):
"""同 ep 有多个文件时取时间戳最新的。"""
latest = {}
for f in files:
name = f.name
# 文件名格式: 20260611_154037_m3_ep04.json
parts = name.replace(".json", "").split("_")
if len(parts) >= 4:
ts = parts[0] + parts[1] # yyyymmddHHMMSS
ep = int(parts[-1].replace("ep", ""))
key = ep
if key not in latest or ts > latest[key][1]:
latest[key] = (f, ts)
return {ep: info[0] for ep, info in latest.items()}
def run(model):
pattern = str(EXPERIMENTS_DIR / f"*_{model}_*.json")
files = sorted(Path(p) for p in glob.glob(pattern))
if not files:
print(f"未找到 {pattern}")
return
ep_files = latest_per_ep(files)
rows = []
parse_fail = 0
for ep in sorted(ep_files.keys()):
data = load_json(ep_files[ep])
if data is None:
parse_fail += 1
rows.append({"ep": ep, "title": "?", "gt": "?", "pred": "解析失败", "hit": False, "conf": "?"})
continue
gt = data.get("ground_truth", {})
result = data.get("result")
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})
# 打印每行
for r in rows:
mark = "" if r["hit"] else ""
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)
hits = sum(1 for r in rows if r["hit"])
hi_conf = [r for r in rows if r["conf"] == ""]
mid_low = [r for r in rows if r["conf"] in ("", "")]
hi_hit = sum(1 for r in hi_conf if r["hit"])
ml_hit = sum(1 for r in mid_low if r["hit"])
print(f"\n ===== {model} 命中情况 =====")
print(f" narrative_structure 命中: {hits}/{total} = {hits*100//total}%")
print(f" 自评\"\"置信的命中率: {hi_hit}/{len(hi_conf)}")
print(f" 自评\"中/低\"置信的命中率: {ml_hit}/{len(mid_low)}")
print(f" 解析失败: {parse_fail}")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--model", required=True, help="模型键名,如 m3 / deepseek-v4")
args = parser.parse_args()
run(args.model)
+121
View File
@@ -0,0 +1,121 @@
"""
仪表盘 API — 题图上传 / 查询
权限:
- POST /cover — 仅 zhipianren / zebianbiandao 返回 403
- GET /cover — 三角色均可读
"""
import os
import uuid
from datetime import datetime
from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, status
from sqlmodel import Session, select
from app.core.deps import get_current_user, require_role
from app.db.session import get_session
from app.models.user import User, UserRole
from app.models.cover_settings import CoverSettings
router = APIRouter(prefix="/api/dashboard", tags=["仪表盘"])
# 题图文件存放目录(相对于 backend/)
STATIC_COVERS_DIR = Path(__file__).parent.parent.parent / "static" / "covers"
STATIC_COVERS_URL = "/static/covers"
# 确保目录存在
STATIC_COVERS_DIR.mkdir(parents=True, exist_ok=True)
ALLOWED_TYPES = {"image/png", "image/jpeg", "image/webp"}
MAX_SIZE = 5 * 1024 * 1024 # 5MB
def require_upload_role():
return require_role(UserRole.zhipianren, UserRole.zebian)
@router.get("/cover")
def get_current_cover(
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user),
):
"""查询当前题图设置(三角色均可读)。"""
row = session.exec(
select(CoverSettings).where(CoverSettings.key == "dashboard_cover")
).first()
if not row:
return {"cover_path": None, "episode_number": None, "episode_title": None}
return {
"cover_path": row.cover_path,
"episode_number": row.episode_number,
"episode_title": row.episode_title,
}
@router.post("/cover")
def upload_cover(
file: UploadFile = File(...),
episode_number: int = Form(...),
episode_title: str = Form(...),
session: Session = Depends(get_session),
current_user: User = Depends(require_upload_role()),
):
"""上传题图(仅制片人/责编)。
文件存 static/covers/{uuid}.{ext},更新 cover_settings 表。
"""
# 校验文件类型
if file.content_type not in ALLOWED_TYPES:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"仅支持 PNG/JPG/WEBP,当前:{file.content_type}",
)
# 校验文件大小
contents = file.file.read()
if len(contents) > MAX_SIZE:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="文件大小不能超过 5MB",
)
file.file.seek(0)
# 生成文件名
ext = file.filename.split(".")[-1] if "." in file.filename else "jpg"
safe_ext = ext.lower() if ext.lower() in {"png", "jpg", "jpeg", "webp"} else "jpg"
filename = f"{uuid.uuid4().hex}.{safe_ext}"
filepath = STATIC_COVERS_DIR / filename
# 写文件
with open(filepath, "wb") as f:
f.write(contents)
cover_path = f"{STATIC_COVERS_URL}/{filename}"
now = datetime.now()
row = session.exec(
select(CoverSettings).where(CoverSettings.key == "dashboard_cover")
).first()
if row:
row.cover_path = cover_path
row.episode_number = episode_number
row.episode_title = episode_title
row.updated_at = now
else:
row = CoverSettings(
key="dashboard_cover",
cover_path=cover_path,
episode_number=episode_number,
episode_title=episode_title,
updated_at=now,
)
session.add(row)
session.commit()
return {"success": True, "cover_path": cover_path}
+103
View File
@@ -0,0 +1,103 @@
"""
节目期次 API — 增删改查(责编和管理员可操作)
"""
from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import Session, select
from app.core.deps import get_current_user, require_role
from app.db.session import get_session
from app.models.episode import Episode
from app.models.user import User, UserRole
from app.schemas.episode import EpisodeCreate, EpisodeResponse, EpisodeUpdate
router = APIRouter(prefix="/api/episodes", tags=["节目期次"])
def require_read_role():
"""GET 接口的鉴权:三个角色都可读"""
return require_role(UserRole.zhipianren, UserRole.zebian, UserRole.biandao)
@router.get("", response_model=list[EpisodeResponse])
def list_episodes(
limit: int = 50,
offset: int = 0,
session: Session = Depends(get_session),
current_user: User = Depends(require_read_role()),
):
"""获取节目期次列表(三角色都可读)。按播出日期倒序。"""
statement = select(Episode).order_by(Episode.air_date.desc()).offset(offset).limit(limit)
episodes = session.exec(statement).all()
return episodes
@router.get("/{episode_id}", response_model=EpisodeResponse)
def get_episode(
episode_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(require_read_role()),
):
"""根据 ID 获取单期节目(三角色都可读)。"""
episode = session.get(Episode, episode_id)
if episode is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="期次不存在")
return episode
@router.post("", response_model=EpisodeResponse, status_code=status.HTTP_201_CREATED)
def create_episode(
body: EpisodeCreate,
session: Session = Depends(get_session),
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian)),
):
"""新增一期节目(仅管理员/责编)。"""
# 检查期次号唯一
existing = session.exec(
select(Episode).where(Episode.episode_number == body.episode_number)
).first()
if existing:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="期次号已存在")
episode = Episode.model_validate(body.model_dump())
session.add(episode)
session.commit()
session.refresh(episode)
return episode
@router.patch("/{episode_id}", response_model=EpisodeResponse)
def update_episode(
episode_id: int,
body: EpisodeUpdate,
session: Session = Depends(get_session),
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian)),
):
"""更新一期节目信息(仅管理员/责编)。"""
episode = session.get(Episode, episode_id)
if episode is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="期次不存在")
update_data = body.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(episode, field, value)
session.add(episode)
session.commit()
session.refresh(episode)
return episode
@router.delete("/{episode_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_episode(
episode_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian)),
):
"""删除一期节目(仅管理员/责编)。"""
episode = session.get(Episode, episode_id)
if episode is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="期次不存在")
session.delete(episode)
session.commit()
return None
+175
View File
@@ -0,0 +1,175 @@
"""
Excel 批量导入 API — 模板下载 + 批量导入 + 失败行下载
"""
import io
import uuid
from datetime import date
from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File, status
from fastapi.responses import StreamingResponse
from openpyxl import Workbook
from sqlmodel import Session
from app.core.deps import require_role
from app.db.session import get_session
from app.models.user import User, UserRole
from app.schemas.imports import ImportError, ImportResult
from app.services.excel_service import ExcelService, generate_error_excel
router = APIRouter(prefix="/api/imports", tags=["批量导入"])
# ============================================================================
# 内存失败行存储(进程生命周期,重启后丢失)
# 仅用于 Phase 2,生产 Phase 4+ 替换为 Redis 或 DB
# ============================================================================
_error_store: dict[str, list[dict]] = {}
# ============================================================================
# 模板下载
# ============================================================================
@router.get("/template")
def download_template(
type: str = Query("episodes", description="模板类型,仅支持 episodes"),
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian)),
):
"""下载 episodes 批量导入 Excel 模板。"""
if type != "episodes":
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Phase 2 不支持排期模板下载",
)
wb = Workbook()
ws = wb.active
ws.title = "节目期次导入模板"
# 列说明(第1行)
headers = [
"episode_number", "program_name", "air_date",
"editor_name", "audience_share", "audience_rating",
"is_rerun", "notes",
]
ws.append(headers)
# 第2行:示例数据
sample = [
1, "《军事科技》武器解析系列", "2026-01-15",
"张颖", "0.72", "3.5",
"", "首期播出",
]
ws.append(sample)
# 第3行:说明注释(灰色字体)
from openpyxl.styles import Font, PatternFill, Alignment
grey_font = Font(color="808080")
note_row = [
"期次号(整数,每年从1开始)",
"节目名称",
"播出日期(YYYY-MM-DD",
"编导姓名(快照,匹配不上则只存姓名)",
"收视份额(0-1之间的小数,可空)",
"收视率(可空)",
"是否重播(是/否),Phase 2 不支持重播导入",
"备注",
]
ws.append(note_row)
for cell in ws[len(headers)]:
cell.font = grey_font
# 第4行:is_rerun 说明
rerun_note = ["", "", "", "", "", "", "Phase 2 不支持重播导入,有重播行请空着", ""]
ws.append(rerun_note)
for cell in ws[4]:
cell.font = grey_font
# 设置列宽
ws.column_dimensions["A"].width = 16
ws.column_dimensions["B"].width = 28
ws.column_dimensions["C"].width = 14
ws.column_dimensions["D"].width = 16
ws.column_dimensions["E"].width = 16
ws.column_dimensions["F"].width = 16
ws.column_dimensions["G"].width = 40
ws.column_dimensions["H"].width = 20
output = io.BytesIO()
wb.save(output)
output.seek(0)
return StreamingResponse(
output,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": "attachment; filename=episodes_import_template.xlsx"},
)
# ============================================================================
# 批量导入
# ============================================================================
@router.post("/episodes", response_model=ImportResult)
def import_episodes(
file: UploadFile = File(..., description="Excel 文件(.xlsx"),
session: Session = Depends(get_session),
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian)),
):
"""批量导入节目期次 Excel。"""
if not file.filename.endswith(".xlsx"):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="仅支持 .xlsx 格式,请使用模板下载获得正确格式",
)
content = file.file.read()
service = ExcelService(session)
try:
result = service.import_episodes(content)
except ValueError as e:
msg = str(e)
if "年度目标" in msg:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=msg)
if "重复" in msg or "库中记录重复" in msg:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=msg)
# 其他解析/校验错误
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=msg)
except Exception as e:
# 捕获 generate_error_excel 等意外错误,避免 500 扩散
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"导入失败:{str(e)}")
# 存储失败行(供后续下载)
if result["errors"]:
_error_store[result["batch_id"]] = result["errors"]
result["error_excel_url"] = f"/api/imports/errors/{result['batch_id']}"
return result
# ============================================================================
# 失败行下载
# ============================================================================
@router.get("/errors/{batch_id}")
def download_error_excel(
batch_id: str,
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian)),
):
"""下载指定批次的失败行 Excel(供责编修正后重新导入)。"""
errors = _error_store.get(batch_id)
if errors is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="批次不存在或已过期(进程重启后数据丢失),请重新上传",
)
content = generate_error_excel(errors)
return StreamingResponse(
io.BytesIO(content),
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": f"attachment; filename=import_errors_{batch_id}.xlsx"},
)
+132
View File
@@ -0,0 +1,132 @@
"""
知识库 API — 上传 / 列表 / 删除 / 来源筛选 / 语义搜索
"""
from typing import Optional
from pydantic import BaseModel
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query, status
from sqlmodel import Session
from app.core.deps import get_current_user, require_role
from app.db.session import get_session
from app.models.user import User, UserRole
from app.services.knowledge_service import KnowledgeService
router = APIRouter(prefix="/api/knowledge", tags=["知识库"])
@router.post("/upload")
async def upload_md_files(
files: list[UploadFile] = File(...),
session: Session = Depends(get_session),
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian)),
):
"""
上传单个或多个 .md 文件,解析 frontmatter,写入知识库(含向量)。
仅制片人/责编可用。
"""
svc = KnowledgeService()
results = []
errors = []
for f in files:
if not f.filename.endswith(".md"):
errors.append({"file": f.filename, "error": "仅支持 .md 文件"})
continue
try:
content = await f.read()
item = svc.store_md_file(content, f.filename)
results.append({
"id": item.id,
"title": item.title,
"source_type": item.source_type,
})
except Exception as e:
errors.append({"file": f.filename, "error": str(e)})
return {"uploaded": results, "errors": errors}
@router.get("/items")
def list_knowledge_items(
source_type: Optional[str] = Query(None),
session: Session = Depends(get_session),
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian, UserRole.biandao)),
):
"""
知识库列表,支持按 source_type 筛选。三角色均可读。
"""
svc = KnowledgeService()
items = svc.list_items(source_type=source_type)
return items
@router.delete("/items/{item_id}")
def delete_knowledge_item(
item_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian)),
):
"""
删除知识库条目(级联删除向量)。仅制片人/责编可用。
"""
svc = KnowledgeService()
deleted = svc.delete_item(item_id)
if not deleted:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="条目不存在")
return {"message": "删除成功"}
@router.get("/sources")
def list_distinct_sources(
session: Session = Depends(get_session),
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian, UserRole.biandao)),
):
"""
返回库里所有不重复的具体出处(source_detail),供来源筛选下拉动态生成。
三角色均可读。
"""
svc = KnowledgeService()
sources = svc.get_distinct_sources()
return [{"source": s} for s in sources]
@router.get("/grouped")
def get_grouped_knowledge_items(
session: Session = Depends(get_session),
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian, UserRole.biandao)),
):
"""
返回按 source_type → source_detail 两层聚合的树形结构,
含「全部」根节点,供知识库树形导航使用。
三角色均可读。
"""
svc = KnowledgeService()
return svc.get_grouped_items()
class SearchRequest(BaseModel):
query: str
top_k: int = 5
@router.post("/search")
def search_knowledge(
body: SearchRequest,
session: Session = Depends(get_session),
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian, UserRole.biandao)),
):
"""
语义检索:输入一段文字,返回最相关的知识库条目及相似度。
查询向量用 type="query"(区分于存入时的 type="db")。
三角色均可读。
"""
svc = KnowledgeService()
results = svc.search_similar(query_text=body.query, top_k=body.top_k)
return {
"results": results,
"query": body.query,
"count": len(results),
}
+70
View File
@@ -0,0 +1,70 @@
"""
排期 API — 查询未来排播计划
降级逻辑:
- schedules 表有数据 → 从 schedules 取未来 limit 条 JOIN episodes
- schedules 表无数据 → 从 episodes 表取最近 limit 期待播期次(air_date 升序)
"""
from fastapi import APIRouter, Depends
from sqlmodel import Session, select
from app.core.deps import get_current_user
from app.db.session import get_session
from app.models.user import User
from app.models.episode import Episode
from app.models.schedule import Schedule
router = APIRouter(prefix="/api/schedules", tags=["排期"])
@router.get("/upcoming")
def list_upcoming_schedules(
limit: int = 6,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user),
):
"""获取未来排播计划(三角色均可读)。
优先从 schedules 表取,schedules 无数据时降级取 episodes 最近待播期次。
"""
# 先查 schedules
statement = (
select(Schedule, Episode)
.join(Episode, Schedule.episode_id == Episode.id, isouter=True)
.where(Schedule.planned_air_date >= Episode.air_date)
.order_by(Schedule.planned_air_date.asc())
.limit(limit)
)
results = session.exec(statement).all()
# 降级:从 episodes 直接取(当 schedules 为空或不足时)
if not results or len(results) < limit:
fallback_stmt = (
select(Episode)
.order_by(Episode.air_date.asc())
.limit(limit)
)
episodes = session.exec(fallback_stmt).all()
return [
{
"episode_number": ep.episode_number,
"program_name": ep.program_name,
"planned_air_date": str(ep.air_date),
"editor_name_snapshot": ep.editor_name_snapshot,
}
for ep in episodes
]
# schedules 有数据
output = []
for schedule, episode in results:
if episode is None:
continue
output.append({
"episode_number": episode.episode_number,
"program_name": episode.program_name,
"planned_air_date": str(schedule.planned_air_date),
"editor_name_snapshot": episode.editor_name_snapshot,
})
return output
+106
View File
@@ -0,0 +1,106 @@
"""
用户管理 API — 管理员 CRUD(仅 zhipianren 可操作)
"""
from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import Session, select
from app.core.deps import get_current_user, require_role
from app.core.security import hash_password
from app.db.session import get_session
from app.models.user import User, UserRole
from app.schemas.user_admin import UserCreate, UserResponse, UserUpdate
router = APIRouter(prefix="/api/users", tags=["用户管理"])
@router.get("", response_model=list[UserResponse])
def list_users(
session: Session = Depends(get_session),
current_user: User = Depends(require_role(UserRole.zhipianren)),
):
"""获取所有用户列表(仅管理员)。"""
statement = select(User).order_by(User.id)
users = session.exec(statement).all()
return users
@router.get("/{user_id}", response_model=UserResponse)
def get_user(
user_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(require_role(UserRole.zhipianren)),
):
"""根据 ID 获取单个用户(仅管理员)。"""
user = session.get(User, user_id)
if user is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
return user
@router.post("", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
def create_user(
body: UserCreate,
session: Session = Depends(get_session),
current_user: User = Depends(require_role(UserRole.zhipianren)),
):
"""创建新用户(仅管理员)。"""
# 检查用户名唯一
existing = session.exec(select(User).where(User.username == body.username)).first()
if existing:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="用户名已存在")
user = User(
username=body.username,
display_name=body.display_name,
password_hash=hash_password(body.password),
role=body.role,
profile_text=body.profile_text,
)
session.add(user)
session.commit()
session.refresh(user)
return user
@router.patch("/{user_id}", response_model=UserResponse)
def update_user(
user_id: int,
body: UserUpdate,
session: Session = Depends(get_session),
current_user: User = Depends(require_role(UserRole.zhipianren)),
):
"""更新用户信息(仅管理员)。可更新 display_name/role/profile_text/is_active。"""
user = session.get(User, user_id)
if user is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
update_data = body.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(user, field, value)
session.add(user)
session.commit()
session.refresh(user)
return user
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
def deactivate_user(
user_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(require_role(UserRole.zhipianren)),
):
"""停用用户(软删除,仅管理员)。"""
user = session.get(User, user_id)
if user is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
# 不能停用自己的账号
if user.id == current_user.id:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="不能停用自己的账号")
user.is_active = False
session.add(user)
session.commit()
return None
+112
View File
@@ -0,0 +1,112 @@
"""
年度收视目标 API — 增删改查
业务规则(.clinerules 5.2 只增不改):
- yearly_targets.year 有 UNIQUE 约束
- INSERT 时 year 冲突 → 直接报错,阻止静默覆盖
- 禁止 ON CONFLICT UPDATE
"""
from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import Session, select
from app.core.deps import get_current_user, require_role
from app.db.session import get_session
from app.models.user import User, UserRole
from app.models.yearly_target import YearlyTarget
from app.schemas.yearly_target import YearlyTargetCreate, YearlyTargetResponse, YearlyTargetUpdate
router = APIRouter(prefix="/api/yearly_targets", tags=["年度目标"])
@router.get("", response_model=list[YearlyTargetResponse])
def list_targets(
session: Session = Depends(get_session),
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian, UserRole.biandao)),
):
"""获取所有年度目标(三角色均可读)。"""
statement = select(YearlyTarget).order_by(YearlyTarget.year.desc())
targets = session.exec(statement).all()
return targets
@router.get("/{target_id}", response_model=YearlyTargetResponse)
def get_target(
target_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian, UserRole.biandao)),
):
"""根据 ID 获取单个年度目标(三角色均可读)。"""
target = session.get(YearlyTarget, target_id)
if target is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="目标不存在")
return target
@router.post("", response_model=YearlyTargetResponse, status_code=status.HTTP_201_CREATED)
def create_target(
body: YearlyTargetCreate,
session: Session = Depends(get_session),
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian)),
):
"""新增年度目标(仅管理员/责编)。
注意:year 有 UNIQUE 约束,重复年份会直接报错。
这是预期行为——历史年份目标不可覆盖。
"""
# 先查是否已存在(让报错更友好,而不是数据库层直接崩溃)
existing = session.exec(select(YearlyTarget).where(YearlyTarget.year == body.year)).first()
if existing:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"{body.year} 年目标已存在,历史年份不可覆盖。请新增其他年份。",
)
target = YearlyTarget.model_validate(body.model_dump())
session.add(target)
session.commit()
session.refresh(target)
return target
@router.patch("/{target_id}", response_model=YearlyTargetResponse)
def update_target(
target_id: int,
body: YearlyTargetUpdate,
session: Session = Depends(get_session),
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian)),
):
"""更新年度目标(仅管理员/责编)。
注意:此接口仅用于修正错字等小修,year 字段不可修改。
"""
target = session.get(YearlyTarget, target_id)
if target is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="目标不存在")
update_data = body.model_dump(exclude_unset=True)
# 禁止修改 year 字段
update_data.pop("year", None)
for field, value in update_data.items():
setattr(target, field, value)
session.add(target)
session.commit()
session.refresh(target)
return target
@router.delete("/{target_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_target(
target_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(require_role(UserRole.zhipianren)),
):
"""删除年度目标(仅制片人)。"""
target = session.get(YearlyTarget, target_id)
if target is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="目标不存在")
session.delete(target)
session.commit()
return None
+6
View File
@@ -16,6 +16,10 @@ _DATABASE_URL = os.environ.get("DATABASE_URL")
_SECRET_KEY = os.environ.get("SECRET_KEY", "change-me-to-a-random-string-in-production")
_SESSION_MAX_AGE = int(os.environ.get("SESSION_MAX_AGE", "86400"))
# MiniMax Embedding API 凭证
_MINIMAX_EMBED_API_KEY = os.environ.get("MINIMAX_EMBED_API_KEY", "")
_MINIMAX_GROUP_ID = os.environ.get("MINIMAX_GROUP_ID", "")
# 验证必需配置
if not _DATABASE_URL:
raise RuntimeError(f"[config] DATABASE_URL 未设置。请检查 {_env_path} 是否存在且内容正确。")
@@ -25,6 +29,8 @@ class Settings:
DATABASE_URL: str = _DATABASE_URL
SECRET_KEY: str = _SECRET_KEY
SESSION_MAX_AGE: int = _SESSION_MAX_AGE
MINIMAX_EMBED_API_KEY: str = _MINIMAX_EMBED_API_KEY
MINIMAX_GROUP_ID: str = _MINIMAX_GROUP_ID
settings = Settings()
+22
View File
@@ -6,7 +6,17 @@ from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware
from starlette.middleware.sessions import SessionMiddleware
from fastapi.staticfiles import StaticFiles
from pathlib import Path
from app.api.auth import router as auth_router
from app.api.episodes import router as episodes_router
from app.api.imports import router as imports_router
from app.api.users import router as users_router
from app.api.yearly_targets import router as yearly_targets_router
from app.api.dashboard import router as dashboard_router
from app.api.schedules import router as schedules_router
from app.api.knowledge import router as knowledge_router
from app.core.config import settings
app = FastAPI(title="军事科技工作台", version="0.1.0")
@@ -34,3 +44,15 @@ app.add_middleware(
# ---- 注册路由 ----
app.include_router(auth_router)
app.include_router(episodes_router)
app.include_router(imports_router)
app.include_router(yearly_targets_router)
app.include_router(users_router)
app.include_router(dashboard_router)
app.include_router(schedules_router)
app.include_router(knowledge_router)
# 挂载静态文件目录(题图海报)
_static_dir = Path(__file__).parent.parent / "static"
if _static_dir.exists():
app.mount("/static", StaticFiles(directory=str(_static_dir)), name="static")
+28
View File
@@ -0,0 +1,28 @@
"""
题图设置模型 — SQLModel
只存当前展示的一张题图,不动 episodes 表。
"""
from datetime import datetime
from sqlalchemy import Column
from sqlalchemy import DateTime as SADateTime
from sqlalchemy import Integer as SAInteger
from sqlalchemy import Text
from sqlalchemy.sql import func as sa_func
from sqlmodel import Field, SQLModel
class CoverSettings(SQLModel, table=True):
__tablename__ = "cover_settings"
id: int | None = Field(default=None, primary_key=True)
key: str = Field(default="dashboard_cover", max_length=50)
cover_path: str | None = Field(default=None) # 当前题图文件路径
episode_number: int | None = Field(default=None) # 关联期次号
episode_title: str | None = Field(default=None) # 关联期次节目名
updated_at: datetime | None = Field(
default=None,
sa_column=Column(SADateTime(timezone=True), nullable=False, server_default=sa_func.now()),
)
+65
View File
@@ -0,0 +1,65 @@
"""
节目期次模型 — SQLModel
业务规则(.clinerules 5.3 软引用 + 快照模式):
- editor_id 可空(离职编导)
- editor_name_snapshot 必填(历史导入也走快照)
- 查询历史作品:先 editor_id,回退 editor_name_snapshot 文本匹配
"""
from datetime import date, datetime
from sqlalchemy import Column
from sqlalchemy import DateTime as SADateTime
from sqlalchemy import Text
from sqlalchemy import Boolean as SABoolean
from sqlalchemy import Integer as SAInteger
from sqlalchemy import Numeric as SANumeric
from sqlalchemy.sql import func as sa_func
from sqlmodel import Field, SQLModel
class Episode(SQLModel, table=True):
__tablename__ = "episodes"
id: int | None = Field(default=None, primary_key=True)
episode_number: int = Field(unique=True, index=True)
program_name: str = Field(max_length=200)
air_date: date
# 编导(软引用 + 快照双字段)
# 在职编导:editor_id 和 editor_name_snapshot 都填
# 离职编导:editor_id 留空,editor_name_snapshot 填姓名
editor_id: int | None = Field(default=None, foreign_key="users.id")
editor_name_snapshot: str = Field(max_length=100)
# 收视数据
# 注意:.clinerules 5.1 颜色只对 audience_share 上色
# audience_rating 只展示数字,不参与颜色判定
audience_share: float | None = Field(default=None)
audience_rating: float | None = Field(default=None)
# 重播相关
is_rerun: bool = Field(default=False)
original_episode_id: int | None = Field(default=None, foreign_key="episodes.id")
is_time_sensitive: bool = Field(default=False)
# 外拍信息
is_outdoor_shoot: bool = Field(default=False)
outdoor_contact_person: str | None = Field(default=None, max_length=100)
outdoor_contact_info: str | None = Field(default=None, max_length=200)
# 线索
lead_source: str | None = Field(default=None, max_length=300)
# 责编备注
notes: str | None = Field(default=None)
created_at: datetime | None = Field(
default=None,
sa_column=Column(SADateTime(timezone=True), nullable=False, server_default=sa_func.now()),
)
updated_at: datetime | None = Field(
default=None,
sa_column=Column(SADateTime(timezone=True), nullable=False, server_default=sa_func.now()),
)
+55
View File
@@ -0,0 +1,55 @@
"""
知识库模型 — SQLModel
对应 knowledge_items 和 knowledge_embeddings 两张表
embedding 字段使用 pgvector.Vector(对应 PG vector(1536)
"""
from datetime import datetime, date
from typing import Optional, Any
from sqlalchemy import Column, DateTime as SADateTime, Text, Integer
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.sql import func as sa_func
from sqlmodel import Field, SQLModel
from pgvector.sqlalchemy import Vector
class KnowledgeItem(SQLModel, table=True):
"""知识库条目(knowledge_items"""
__tablename__ = "knowledge_items"
id: Optional[int] = Field(default=None, primary_key=True)
title: str = Field(max_length=300)
content_md: Optional[str] = Field(default=None)
source_type: str = Field(default="manual", max_length=30)
source_file_name: Optional[str] = Field(default=None, max_length=300)
source_url: Optional[str] = Field(default=None, max_length=1000)
author: Optional[str] = Field(default=None, max_length=100)
publish_date: Optional[date] = Field(default=None)
tags: Any = Field(default=None, sa_column=Column(JSONB, default=[]))
related_entities: Any = Field(default=None, sa_column=Column(JSONB, default=[]))
related_concepts: Any = Field(default=None, sa_column=Column(JSONB, default=[]))
created_at: datetime | None = Field(
default=None,
sa_column=Column(SADateTime(timezone=True), nullable=False, server_default=sa_func.now()),
)
updated_at: datetime | None = Field(
default=None,
sa_column=Column(SADateTime(timezone=True), nullable=False, server_default=sa_func.now()),
)
class KnowledgeEmbedding(SQLModel, table=True):
"""知识库向量(knowledge_embeddings"""
__tablename__ = "knowledge_embeddings"
id: Optional[int] = Field(default=None, primary_key=True)
knowledge_id: int = Field(foreign_key="knowledge_items.id", index=True)
chunk_index: int = Field(default=0)
chunk_text: str = Field(sa_column=Column(Text, nullable=False))
embedding: Any = Field(sa_column=Column(Vector(1536), nullable=False))
created_at: datetime | None = Field(
default=None,
sa_column=Column(SADateTime(timezone=True), nullable=False, server_default=sa_func.now()),
)
+33
View File
@@ -0,0 +1,33 @@
"""
排期模型 — SQLModel
"""
from datetime import date, datetime
from sqlalchemy import Column
from sqlalchemy import DateTime as SADateTime
from sqlalchemy import Integer as SAInteger
from sqlalchemy import Date as SADate
from sqlalchemy import Text
from sqlalchemy.sql import func as sa_func
from sqlmodel import Field, SQLModel
class Schedule(SQLModel, table=True):
__tablename__ = "schedules"
id: int | None = Field(default=None, primary_key=True)
episode_id: int | None = Field(default=None, foreign_key="episodes.id")
planned_air_date: date
status: str = Field(default="planned", max_length=20)
editor_id: int | None = Field(default=None, foreign_key="users.id")
notes: str | None = Field(default=None)
created_at: datetime | None = Field(
default=None,
sa_column=Column(SADateTime(timezone=True), nullable=False, server_default=sa_func.now()),
)
updated_at: datetime | None = Field(
default=None,
sa_column=Column(SADateTime(timezone=True), nullable=False, server_default=sa_func.now()),
)
+34
View File
@@ -0,0 +1,34 @@
"""
年度收视目标模型 — SQLModel
业务规则(.clinerules 5.2 年度目标只增不改):
- yearly_targets.year 有 UNIQUE 约束,数据库层已保证
- 代码端 INSERT 时只许 ON CONFLICT DO NOTHING,禁止 ON CONFLICT UPDATE
- 历史年份目标行绝不可被覆盖或删除
- 前端判颜色时按播出年份取对应年的目标,不是当前年份
"""
from datetime import date, datetime
from sqlalchemy import Column
from sqlalchemy import DateTime as SADateTime
from sqlalchemy import Integer as SAInteger
from sqlalchemy import Numeric as SANumeric
from sqlalchemy.sql import func as sa_func
from sqlmodel import Field, SQLModel
class YearlyTarget(SQLModel, table=True):
__tablename__ = "yearly_targets"
id: int | None = Field(default=None, primary_key=True)
year: int = Field(unique=True, index=True)
base_target: float # 基础目标
stretch_target: float # 摸高目标
issued_date: date | None = Field(default=None)
notes: str | None = Field(default=None)
created_at: datetime | None = Field(
default=None,
sa_column=Column(SADateTime(timezone=True), nullable=False, server_default=sa_func.now()),
)
+2 -3
View File
@@ -2,7 +2,7 @@
认证相关 Pydantic Schema — 请求 / 响应模型
"""
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict
from app.models.user import UserRole
@@ -21,5 +21,4 @@ class UserResponse(BaseModel):
profile_text: str | None = None
is_active: bool = True
class Config:
from_attributes = True
model_config = ConfigDict(from_attributes=True)
+41
View File
@@ -0,0 +1,41 @@
"""
节目期次 Schema — 请求 / 响应模型
"""
from datetime import date
from pydantic import BaseModel, ConfigDict
from app.models.user import UserRole
class EpisodeBase(BaseModel):
episode_number: int
program_name: str
air_date: date
editor_id: int | None = None
editor_name_snapshot: str
audience_share: float | None = None
audience_rating: float | None = None
is_rerun: bool = False
original_episode_id: int | None = None
is_time_sensitive: bool = False
is_outdoor_shoot: bool = False
outdoor_contact_person: str | None = None
outdoor_contact_info: str | None = None
lead_source: str | None = None
notes: str | None = None
class EpisodeCreate(EpisodeBase):
pass
class EpisodeUpdate(EpisodeBase):
pass
class EpisodeResponse(EpisodeBase):
id: int
model_config = ConfigDict(from_attributes=True)
+28
View File
@@ -0,0 +1,28 @@
"""
导入相关 Schema — 请求 / 响应模型
"""
from datetime import date
from typing import Optional
from pydantic import BaseModel, ConfigDict
class ImportError(BaseModel):
"""单行失败详情"""
row_number: int # Excel 行号(第2行=第1条数据)
reason: str # 失败原因
raw_data: dict # 原始行数据(字典形式)
model_config = ConfigDict(from_attributes=True)
class ImportResult(BaseModel):
"""批量导入响应"""
batch_id: str # UUID,用于下载失败行 Excel
total_rows: int # 总行数
success_count: int # 成功数
failed_count: int # 失败数
errors: list[ImportError] # 失败行列表
error_excel_url: Optional[str] = None # 失败行 Excel 下载 URL(无可失败行时可不返回)
model_config = ConfigDict(from_attributes=True)
+33
View File
@@ -0,0 +1,33 @@
"""
用户管理 Schema — 管理员 CRUD(仅 zhipianren 可用)
"""
from pydantic import BaseModel, ConfigDict
from app.models.user import UserRole
class UserCreate(BaseModel):
username: str
display_name: str
password: str
role: UserRole
profile_text: str | None = None
class UserUpdate(BaseModel):
display_name: str | None = None
role: UserRole | None = None
profile_text: str | None = None
is_active: bool | None = None
class UserResponse(BaseModel):
id: int
username: str
display_name: str
role: UserRole
profile_text: str | None = None
is_active: bool = True
model_config = ConfigDict(from_attributes=True)
+36
View File
@@ -0,0 +1,36 @@
"""
年度收视目标 Schema — 请求 / 响应模型
业务规则:yearly_targets 只增不改,ON CONFLICT DO NOTHING
"""
from datetime import date
from pydantic import BaseModel, ConfigDict
class YearlyTargetBase(BaseModel):
year: int
base_target: float
stretch_target: float
issued_date: date | None = None
notes: str | None = None
class YearlyTargetCreate(YearlyTargetBase):
pass
class YearlyTargetUpdate(BaseModel):
"""PATCH 时允许只传部分字段,year 不可修改(由 API 层 pop 掉)。"""
year: int | None = None
base_target: float | None = None
stretch_target: float | None = None
issued_date: date | None = None
notes: str | None = None
class YearlyTargetResponse(YearlyTargetBase):
id: int
model_config = ConfigDict(from_attributes=True)
+69
View File
@@ -0,0 +1,69 @@
"""
Embedding 调用服务 — 封装 MiniMax embo-01
请求格式(确认自探路脚本):
POST /v1/embeddings
Body: {"model": "embo-01", "texts": [...], "type": "db"|"query"}
响应格式:
{"vectors": [[...1536 floats...]], "total_tokens": N, "base_resp": {"status_code": 0, "status_msg": "success"}}
"""
import httpx
from typing import List
from app.core.config import settings
class EmbeddingService:
"""MiniMax embo-01 embedding 调用封装"""
def __init__(self):
self.api_key = settings.MINIMAX_EMBED_API_KEY
self.group_id = settings.MINIMAX_GROUP_ID
self.endpoint = "https://api.minimax.chat/v1/embeddings"
def embed(self, texts: List[str], embed_type: str = "db") -> List[List[float]]:
"""
调用 embo-01 将文本列表转为向量
Args:
texts: 文本列表(支持批量)
embed_type: "db" = 存入库,"query" = 查询
Returns:
List[List[float]],每个元素是一组 1536 维向量
"""
if not self.api_key or self.api_key == "your_api_key_here":
raise RuntimeError("MINIMAX_EMBED_API_KEY not configured in .env")
if not self.group_id or self.group_id == "your_group_id_here":
raise RuntimeError("MINIMAX_GROUP_ID not configured in .env")
headers = {
"Authorization": f"Bearer {self.api_key}",
"GroupId": self.group_id,
"Content-Type": "application/json",
}
payload = {
"model": "embo-01",
"texts": texts,
"type": embed_type,
}
resp = httpx.post(self.endpoint, headers=headers, json=payload, timeout=60.0)
resp.raise_for_status()
data = resp.json()
# 检查业务错误
base_resp = data.get("base_resp", {})
if base_resp.get("status_code", 0) != 0:
raise RuntimeError(f"Embedding API error: {base_resp.get('status_msg', 'unknown')}")
vectors = data.get("vectors", [])
if not vectors:
raise RuntimeError("No vectors returned from embedding API")
return vectors
def embed_single(self, text: str, embed_type: str = "db") -> List[float]:
"""单文本 embedding,返回 1536 维向量列表(Python list"""
vectors = self.embed([text], embed_type=embed_type)
return vectors[0]
+247
View File
@@ -0,0 +1,247 @@
"""
Excel 解析服务 — 批量导入核心逻辑
业务规则:
- 前置校验:所有涉及年份的 yearly_targets 必须存在,否则整体报错(不入库任何行)
- 幂等:检测到重复 (year, episode_number) → 整体 409,不入库
- 事务:逐行提交,成功立即 commit,失败行收集到 errors[]
- 软引用+快照:编导姓名匹配不到在职用户 → editor_id=NULL + editor_name_snapshot=姓名
- is_rerun 解析:接受是/否、true/false、1/0,大小写不敏感
- Phase 2 不支持重播行(is_rerun=是 → 报错标记为失败行)
"""
import io
import uuid
from datetime import date
from typing import Any
import pandas as pd
from sqlmodel import Session, select
from app.models.episode import Episode
from app.models.user import User
from app.models.yearly_target import YearlyTarget
TRUTHY = {"", "true", "1", "yes"}
FALSY = {"", "false", "0", "no"}
def parse_bool(val: Any) -> bool | None:
"""解析布尔值,接受多种格式。"""
if val is None:
return None
v = str(val).strip().lower()
if v == "":
return None
if v in TRUTHY:
return True
if v in FALSY:
return False
raise ValueError(f"无法解析布尔值: {val!r}")
def parse_date(val: Any) -> date | None:
"""解析日期,支持 YYYY-MM-DD 和 YYYY/MM/DD。"""
if val is None or str(val).strip() == "":
return None
v = str(val).strip()
for fmt in ("%Y-%m-%d", "%Y/%m/%d", "%Y%m%d"):
try:
return date.fromisoformat(v)
except ValueError:
pass
raise ValueError(f"无法解析日期: {val!r}")
def parse_float(val: Any) -> float | None:
"""解析浮点数,接受百分比字符串。"""
if val is None:
return None
v = str(val).strip()
if v == "":
return None
if v.endswith("%"):
v = v[:-1]
return float(v)
def find_editor_by_name(session: Session, name: str):
"""按 display_name 匹配在职编导。匹配上返 (id, name),匹配不上返 (None, name)。"""
if not name or str(name).strip() == "":
return None, ""
name = str(name).strip()
user = session.exec(
select(User).where(User.display_name == name, User.is_active == True)
).first()
if user:
return user.id, name
return None, name
class ExcelService:
def __init__(self, session: Session):
self.session = session
self.errors: list[dict] = []
self.batch_id = str(uuid.uuid4())
def validate_yearly_targets(self, air_dates: list[date]):
"""前置校验:所有涉及年份的 yearly_targets 必须存在,否则整体报错。"""
distinct_years = set(dt.year for dt in air_dates if dt is not None)
missing = []
for yr in sorted(distinct_years):
exists = self.session.exec(
select(YearlyTarget).where(YearlyTarget.year == yr)
).first()
if not exists:
missing.append(yr)
if missing:
raise ValueError(
f"导入前请先录入以下年份的年度目标:{', '.join(map(str, missing))}"
"请先在年度目标页录入目标后再导入。"
)
def check_duplicates(self, rows: list[dict]):
"""检测重复 (year, episode_number),有重复则整体报错。"""
seen = set()
duplicates = []
for row in rows:
key = (row["_air_year"], row["episode_number"])
if key in seen:
duplicates.append(row)
seen.add(key)
if duplicates:
dup_list = [
f"{r['_air_year']}年第 {r['episode_number']}"
for r in duplicates
]
raise ValueError(
f"文件中以下期次与库中记录重复:{', '.join(dup_list)}"
"请先手动删除重复期次后再重新导入。"
)
def import_episodes(self, file_content: bytes) -> dict:
"""解析 Excel,批量导入 episodes。返回 ImportResult 结构。"""
# 1. 读取 Excelpandas 2.x 要求 file-like object,用 BytesIO 包装 bytes
file_like = io.BytesIO(file_content)
df = pd.read_excel(file_like, engine="openpyxl")
rows = df.to_dict(orient="records")
# 2. 解析 air_date 并提取年份
parsed_rows = []
air_dates = []
for i, row in enumerate(rows, start=2): # Excel 行号从2开始(第1行=表头)
raw = dict(row)
try:
air_dt = parse_date(row.get("air_date"))
if air_dt is None:
raise ValueError("air_date 不能为空")
parsed_rows.append({
"_row_number": i,
"_air_year": air_dt.year,
"_air_date": air_dt,
"episode_number": int(row["episode_number"]),
"program_name": str(row["program_name"]).strip(),
"audience_share": parse_float(row.get("audience_share")),
"audience_rating": parse_float(row.get("audience_rating")),
"is_rerun": parse_bool(row.get("is_rerun")),
"editor_name_snapshot": str(row.get("editor_name", "")).strip() or "未知编导",
"notes": str(row.get("notes", "")).strip() or None,
"_raw": raw,
})
air_dates.append(air_dt)
except Exception as e:
self.errors.append({
"row_number": i,
"reason": str(e),
"raw_data": raw,
})
# 3. 前置校验:年份 targets
try:
self.validate_yearly_targets(air_dates)
except ValueError:
raise # 直接抛给调用方,整体 400
# 4. 重复检测
try:
self.check_duplicates(parsed_rows)
except ValueError:
raise # 直接抛给调用方,整体 409
# 5. 逐行入库
for row in parsed_rows:
try:
self._import_one_row(row)
except Exception as e:
self.errors.append({
"row_number": row["_row_number"],
"reason": str(e),
"raw_data": row["_raw"],
})
# 6. 计算结果
total = len(rows)
success = total - len(self.errors)
return {
"batch_id": self.batch_id,
"total_rows": total,
"success_count": success,
"failed_count": len(self.errors),
"errors": self.errors,
}
def _import_one_row(self, row: dict):
"""导入单行,处理 is_rerun 逻辑。"""
# 重播行 Phase 2 不支持
if row["is_rerun"] is True:
raise ValueError(
"Phase 2 不支持重播期次导入。"
"有重播行请空着或用其他工具录入,系统已标记为失败行。"
)
# 编辑匹配
editor_id, editor_name = find_editor_by_name(self.session, row["editor_name_snapshot"])
if editor_id is None and row["editor_name_snapshot"] == "未知编导":
editor_name = row["editor_name_snapshot"]
# 插入
episode = Episode(
episode_number=row["episode_number"],
program_name=row["program_name"],
air_date=row["_air_date"],
editor_id=editor_id,
editor_name_snapshot=editor_name,
audience_share=row["audience_share"],
audience_rating=row["audience_rating"],
is_rerun=False,
original_episode_id=None,
notes=row["notes"],
)
self.session.add(episode)
self.session.commit()
self.session.refresh(episode)
def generate_error_excel(errors: list[dict]) -> bytes:
"""生成失败行 Excel,供责编下载修正。"""
import io
from openpyxl import Workbook
wb = Workbook()
ws = wb.active
ws.title = "失败行"
# 表头
headers = ["row_number", "reason"] + list(errors[0]["raw_data"].keys()) if errors else ["row_number", "reason"]
ws.append(headers)
# 失败行
for err in errors:
row_data = [err["row_number"], err["reason"]] + list(err["raw_data"].values())
ws.append(row_data)
output = io.BytesIO()
wb.save(output)
output.seek(0)
return output.read()
+354
View File
@@ -0,0 +1,354 @@
"""
知识库服务 — 写入向量 + 语义检索 + md 文件解析
使用 pgvector 原生 SQL 向量检索(<=> 余弦距离算子),不在 Python 侧计算
"""
from typing import Optional
from datetime import date
import frontmatter
from sqlalchemy import text
from sqlmodel import Session, select
from pgvector.sqlalchemy import Vector
from app.models.knowledge import KnowledgeItem, KnowledgeEmbedding
from app.services.embedding_service import EmbeddingService
from app.db.session import engine
class KnowledgeService:
"""知识库 CRUD + 语义检索 + md 解析"""
# yaml 类型字段 → source_type 枚举映射
SOURCE_TYPE_MAP = {
"杂志文章": "military_report",
"军报": "military_report",
"节目文稿": "manuscript",
"报题单": "baoti",
}
# 来源大类固定显示顺序(制片人 Obsidian 习惯)
SOURCE_TYPE_ORDER = [
"manuscript", # 节目文稿
"military_report", # 杂志文章
"baoti", # 报题单
"manual", # 其他
]
# 二级分组维度映射(与前端 useKnowledgeGrouping.js 的 SECONDARY_GROUP_FIELD 一致)
# key = source_type, value = 用来做二级分组的字段名,None = 不建二级节点
SECONDARY_GROUP_FIELD = {
"manuscript": "author", # 节目文稿 → 按作者(编导)
"military_report": "source_detail", # 杂志文章 → 按出处
"baoti": None, # 报题单 → 不分组
"manual": None, # 其他 → 不分组
}
SOURCE_TYPE_LABEL = {
"military_report": "杂志文章",
"manuscript": "节目文稿",
"baoti": "报题单",
"manual": "其他",
}
def __init__(self):
self.embedder = EmbeddingService()
def parse_md_file(self, file_content: bytes, file_name: str) -> dict:
"""
解析一个 .md 文件的 yaml frontmatter + 正文,返回入库用的字典。
严格按真实样本的字段名映射,不猜测。
Returns:
dict 含 keys: title, content_md, source_type, author, publish_date,
source_detail, metadata(JSONB), related_entities(JSONB)
"""
content = file_content.decode("utf-8", errors="replace")
parsed = frontmatter.loads(content)
fm = parsed.metadata or {}
# —— 类型 → source_type(硬映射,不猜测)——
raw_type = str(fm.get("类型", "")).strip()
source_type = self.SOURCE_TYPE_MAP.get(raw_type, "manual")
# —— 标题:名称 或 标题——
title = str(fm.get("名称", "") or fm.get("标题", "")).strip()
if not title:
# fallback: 用正文第一行或文件名
lines = [l.strip() for l in content.split("\n") if l.strip() and not l.strip().startswith("---")]
title = lines[0] if lines else file_name
# —— 作者:作者 或 编导——
author = str(fm.get("作者", "") or fm.get("编导", "") or "").strip() or None
# —— 出处详情:期刊 + 期号(拼在一起存进 JSONB 的 source_detail)——
journal = str(fm.get("期刊", "") or "").strip()
issue = str(fm.get("期号", "") or "").strip()
if journal or issue:
source_detail = f"{journal} {issue}".strip()
else:
source_detail = None
# —— 播出日期:容错 "待补充" 等非日期文本——
raw_date = str(fm.get("播出日期", "") or "").strip()
publish_date = None
if raw_date and raw_date not in ("待补充", "待确认", ""):
try:
publish_date = date.fromisoformat(raw_date)
except ValueError:
# 非 ISO 格式,尝试 common 格式
for fmt in ("%Y-%m-%d", "%Y年%m月%d", "%Y/%m/%d"):
try:
publish_date = date.fromisoformat(raw_date.replace("", "-").replace("", "-").replace("", ""))
break
except ValueError:
continue
# —— 权重(不展示,存 JSONB 备 Phase 4)——
weight = str(fm.get("权重", "") or "").strip() or None
# —— 相关实体(涉及装备/涉及技术/涉及厂商/主题)——
related_entities = []
for key in ("涉及装备", "涉及技术", "涉及厂商", "主题"):
val = fm.get(key)
if val:
if isinstance(val, list):
related_entities.extend(val)
elif isinstance(val, str):
# 可能是 "山东舰, 福建舰" 这样的逗号分隔字符串
for item in val.replace("", ",").split(","):
item = item.strip()
if item:
related_entities.append(item)
# —— metadata JSONB:权重、出处详情、双链预留——
metadata = {}
if weight:
metadata["weight"] = weight
if source_detail:
metadata["source_detail"] = source_detail
# related_concepts 字段预留给双链解析(Phase 4),本 Task 原样存入
metadata["double_bracket_links"] = self._extract_double_brackets(parsed.content)
# —— 正文(去掉 frontmatter 的纯内容)——
content_md = parsed.content
return {
"title": title,
"content_md": content_md,
"source_type": source_type,
"author": author,
"publish_date": publish_date,
"metadata": metadata if metadata else None,
"related_entities": related_entities if related_entities else None,
"source_file_name": file_name,
}
def _extract_double_brackets(self, text: str) -> list[str]:
"""提取 [[...]] 双链标记,原样返回列表,不解析成图谱(本 Task 留门)。"""
import re
return re.findall(r"\[\[([^\]]+)\]\]", text)
def store_md_file(self, file_content: bytes, file_name: str) -> KnowledgeItem:
"""
读取一篇 md 内容,调用 embo-01 拿到向量,写入 knowledge_items + knowledge_embeddings
"""
parsed = self.parse_md_file(file_content, file_name)
# 调用 embeddingtype="db" 表示存入知识库)
embedding_list = self.embedder.embed_single(parsed["content_md"], embed_type="db")
with Session(engine) as session:
item = KnowledgeItem(
title=parsed["title"],
content_md=parsed["content_md"],
source_type=parsed["source_type"],
source_file_name=parsed["source_file_name"],
author=parsed["author"],
publish_date=parsed["publish_date"],
tags=parsed["metadata"],
related_entities=parsed["related_entities"],
)
session.add(item)
session.flush()
emb = KnowledgeEmbedding(
knowledge_id=item.id,
chunk_index=0,
chunk_text=parsed["content_md"],
embedding=embedding_list,
)
session.add(emb)
session.commit()
session.refresh(item)
return item
def delete_item(self, knowledge_id: int) -> bool:
"""删除知识库条目及其向量(CASCADE 已由 DB 层配置)。"""
with Session(engine) as session:
item = session.get(KnowledgeItem, knowledge_id)
if item is None:
return False
session.delete(item)
session.commit()
return True
def list_items(self, source_type: Optional[str] = None) -> list[dict]:
"""返回知识库条目列表(含 source_detail 从 metadata 解压)。"""
with Session(engine) as session:
statement = select(KnowledgeItem).order_by(KnowledgeItem.created_at.desc())
if source_type:
statement = statement.where(KnowledgeItem.source_type == source_type)
items = session.exec(statement).all()
results = []
for item in items:
# 从 tags(JSONB) 取 source_detail
tags = item.tags or {}
source_detail = tags.get("source_detail") if isinstance(tags, dict) else None
results.append({
"id": item.id,
"title": item.title,
"author": item.author,
"publish_date": item.publish_date,
"source_type": item.source_type,
"source_file_name": item.source_file_name,
"source_detail": source_detail,
"created_at": item.created_at,
})
return results
def get_distinct_sources(self) -> list[str]:
"""返回库里所有不重复的 source_detail(动态从 JSONB 提取),供筛选下拉用。"""
with Session(engine) as session:
items = session.exec(select(KnowledgeItem)).all()
sources = set()
for item in items:
tags = item.tags or {}
if isinstance(tags, dict) and tags.get("source_detail"):
sources.add(tags["source_detail"])
return sorted(list(sources))
def search_similar(self, query_text: str, top_k: int = 5) -> list[dict]:
"""
语义检索:查询句转为向量,用 SQL 余弦距离(<=>)在数据库层检索
返回 top_k 条相似笔记,含相似度分数 + 原文片段(SQL 端截断前 200 字)。
注意:当前取前 200 字是已知妥协(整篇向量检索无法定位中段命中点),
Phase 4a 做切块检索(chunk)时可优化为取最相关片段。
"""
query_vector = self.embedder.embed_single(query_text, embed_type="query")
vec_str = "[" + ",".join(str(v) for v in query_vector) + "]"
with Session(engine) as session:
sql = f"""
SELECT
ki.id,
ki.title,
ki.source_type,
ki.author,
ki.tags,
SUBSTRING(ki.content_md, 1, 200) AS snippet,
1 - (ke.embedding <=> '{vec_str}'::vector) AS similarity
FROM knowledge_embeddings ke
JOIN knowledge_items ki ON ke.knowledge_id = ki.id
WHERE ke.chunk_index = 0
ORDER BY ke.embedding <=> '{vec_str}'::vector
LIMIT {top_k}
"""
stmt = text(sql)
rows = session.execute(stmt).all()
results = []
for r in rows:
tags = r.tags or {}
source_detail = tags.get("source_detail") if isinstance(tags, dict) else None
results.append({
"id": r.id,
"title": r.title,
"source_type": r.source_type,
"author": r.author,
"source_detail": source_detail,
"snippet": r.snippet,
"similarity": round(r.similarity, 4),
})
return results
def get_item_count(self) -> int:
with Session(engine) as session:
return len(session.exec(select(KnowledgeItem)).all())
def get_embedding_count(self) -> int:
with Session(engine) as session:
return len(session.exec(select(KnowledgeEmbedding)).all())
def get_grouped_items(self) -> list[dict]:
"""
按 source_type → 二级字段(author / source_detail)两层聚合,返回树形结构数据。
按 SOURCE_TYPE_ORDER 固定顺序排列,仅显示有数据的大类(count > 0)。
二级节点 key 格式:`{source_type}|{二级字段名}|{字段值}`
例:manuscript|author|左鑫
military_report|source_detail|航空知识 2026年第1期
二级字段值为 null / 空字串 → 归入对应大类,不造空节点。
空大类(0条)不渲染。
"""
with Session(engine) as session:
items = session.exec(select(KnowledgeItem)).all()
total_count = len(items)
# 按 source_type 分组:仅收集有数据的类别,再按固定顺序排列
type_groups: dict = {}
for item in items:
st = item.source_type or "manual"
if st not in type_groups:
type_groups[st] = []
type_groups[st].append(item)
# 只遍历有数据的类别,按 SOURCE_TYPE_ORDER 顺序
children = []
for st in self.SOURCE_TYPE_ORDER:
if st not in type_groups:
continue
st_items = type_groups[st]
secondary_field = self.SECONDARY_GROUP_FIELD.get(st)
grandchildren = []
if secondary_field is not None:
# 按二级字段分组
detail_groups: dict = {}
for item in st_items:
if secondary_field == "source_detail":
tags = item.tags or {}
sd = tags.get("source_detail") if isinstance(tags, dict) else None
field_val = sd
else:
field_val = (getattr(item, secondary_field, None) or "").strip() or None
if field_val not in detail_groups:
detail_groups[field_val] = []
detail_groups[field_val].append(item)
for sd, sd_items in detail_groups.items():
if sd is not None:
grandchildren.append({
"key": f"{st}|{secondary_field}|{sd}",
"label": f"{sd}{len(sd_items)}条)",
"count": len(sd_items),
})
children.append({
"key": st,
"label": f"{self.SOURCE_TYPE_LABEL.get(st, st)}{len(st_items)}条)",
"count": len(st_items),
"children": grandchildren,
})
return [{
"key": "all",
"label": f"全部({total_count}条)",
"count": total_count,
"children": children,
}]
+3
View File
@@ -9,3 +9,6 @@ itsdangerous==2.2.0
python-multipart==0.0.9
python-dotenv==1.0.1
httpx==0.27.0
python-frontmatter==1.1.0
pandas>=2.0.0
openpyxl>=3.1.0
+28
View File
@@ -0,0 +1,28 @@
"""
清理测试数据脚本
"""
from dotenv import load_dotenv
from pathlib import Path
_env_path = Path(__file__).parent.parent / ".env"
load_dotenv(str(_env_path))
from sqlalchemy import text
from app.db.session import engine
conn = engine.connect()
# 清空知识库
conn.execute(text("DELETE FROM knowledge_embeddings"))
conn.execute(text("DELETE FROM knowledge_items"))
conn.commit()
# 查行数
ki = conn.execute(text("SELECT COUNT(*) FROM knowledge_items")).scalar()
ke = conn.execute(text("SELECT COUNT(*) FROM knowledge_embeddings")).scalar()
print(f"knowledge_items rows: {ki}")
print(f"knowledge_embeddings rows: {ke}")
conn.close()
+265
View File
@@ -0,0 +1,265 @@
"""
Task 2 批量导入冒烟脚本 — httpx(幂等版,可重复跑)
确保后端已启动: py -3.12 -c "import sys; sys.path.insert(0, r'E:\tps-dashboard\backend'); import uvicorn; uvicorn.run('app.main:app', host='127.0.0.1', port=8000, reload=False)"
Phase 2 Task 2 实测清单覆盖:
Step 1: GET /api/imports/template → 模板下载
Step 2: POST /api/imports/episodes → 合法行单行导入
Step 3: POST /api/imports/episodes → 部分失败行(成功+失败混合格式)
Step 4: GET /api/imports/errors/{id} → 失败行 Excel 下载
Step 5: POST /api/imports/episodes → 2024年份缺失拦截 → 400
Step 6: POST /api/imports/episodes → 重复期次拦截 → 409
Step 7: POST /api/imports/episodes → 未知编导软落地 → 200 + editor_id=null
"""
import io
import httpx
import random
from openpyxl import Workbook
BASE = "http://localhost:8000"
def login(username: str = "simonkoson", password: str = "liutong65") -> str:
r = httpx.post(f"{BASE}/api/auth/login", json={"username": username, "password": password})
r.raise_for_status()
return r.cookies.get("milsci_session", "")
def step(name: str, expected_status: int, method: str, url: str, **kwargs):
cookies = kwargs.pop("cookies", {})
if method == "GET":
r = httpx.get(url, cookies=cookies, timeout=30.0, **kwargs)
elif method == "POST":
r = httpx.post(url, cookies=cookies, timeout=30.0, **kwargs)
elif method == "DELETE":
r = httpx.delete(url, cookies=cookies, timeout=30.0, **kwargs)
else:
raise ValueError(method)
status_ok = r.status_code == expected_status
mark = "[OK]" if status_ok else f"[FAIL expected {expected_status} got {r.status_code}]"
print(f" {mark} [{method}] {name}")
if not status_ok:
print(f" Response: {r.text[:300]}")
return r, status_ok
def make_excel(rows: list[list]) -> bytes:
"""构建简单 xlsx,rows[0] 为表头,后续为数据。"""
wb = Workbook()
ws = wb.active
for row in rows:
ws.append(row)
output = io.BytesIO()
wb.save(output)
output.seek(0)
return output.read()
def cleanup_test_episodes(cookies: dict):
"""清理测试用 episode_number(幂等清理)。"""
r = httpx.get(f"{BASE}/api/episodes?limit=100", cookies=cookies)
if r.status_code == 200:
for ep in r.json():
num = ep.get("episode_number", 0)
# 清理本次测试可能用到的期号段
if num in (99, 100, 101, 102, 200, 301):
httpx.delete(f"{BASE}/api/episodes/{ep['id']}", cookies=cookies)
def main():
print("=== Login ===")
session_cookie = login()
if not session_cookie:
print("WARNING: session cookie empty")
session_cookie = ""
cookies = {"milsci_session": session_cookie}
# 先清理旧测试数据(幂等)
print("\n=== Cleanup old test episodes ===")
cleanup_test_episodes(cookies)
suffix = random.randint(1000, 9999)
# ---- Step 1: 模板下载 ----
print("\n=== Step 1: Template Download ===")
r, ok = step(
"GET /api/imports/template?type=episodes",
200,
"GET",
f"{BASE}/api/imports/template?type=episodes",
cookies=cookies,
)
content_type = r.headers.get("content-type", "")
is_binary = "application/vnd.openxmlformats" in content_type
print(f" {'[OK]' if is_binary else '[FAIL]'} Content-Type: {content_type}")
print(f" [INFO] Template downloaded ({len(r.content)} bytes)")
# ---- Step 2: 合法单行导入 ----
print("\n=== Step 2: Normal import (1 valid row) ===")
excel_data = make_excel([
["episode_number", "program_name", "air_date", "editor_name",
"audience_share", "audience_rating", "is_rerun", "notes"],
[99, "《军事科技》实测专用", "2026-06-01", "张颖",
0.75, 3.8, "", "Plan smoke test"],
])
r, ok = step(
"POST /api/imports/episodes (1 valid row) → 200",
200,
"POST",
f"{BASE}/api/imports/episodes",
files={"file": ("test_valid.xlsx", excel_data, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")},
cookies=cookies,
)
if ok:
result = r.json()
print(f" [INFO] total={result['total_rows']} success={result['success_count']} failed={result['failed_count']}")
ok = (result["success_count"] == 1 and result["failed_count"] == 0)
print(f" {'[OK]' if ok else '[FAIL]'} Row count correct")
# ---- Step 3: 部分失败行回显 ----
print("\n=== Step 3: Partial failure (3 rows: 1 ok + 2 bad) ===")
excel_data = make_excel([
["episode_number", "program_name", "air_date", "editor_name",
"audience_share", "audience_rating", "is_rerun", "notes"],
[100, "《军事科技》成功行", "2026-07-01", "张颖", 0.71, 3.5, "", ""],
[101, "无效日期格式行", "不是日期", "张颖", 0.72, 3.6, "", ""],
[102, "非整数期号行", "invalid-ep-num", "张颖", 0.73, 3.7, "", ""],
])
r, ok = step(
"POST /api/imports/episodes (1ok+2bad) → 200 with errors",
200,
"POST",
f"{BASE}/api/imports/episodes",
files={"file": ("test_mixed.xlsx", excel_data, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")},
cookies=cookies,
)
batch_id = None
if ok:
result = r.json()
print(f" [INFO] total={result['total_rows']} success={result['success_count']} failed={result['failed_count']}")
ok_count = (result["success_count"] == 1 and result["failed_count"] == 2)
print(f" {'[OK]' if ok_count else '[FAIL]'} Failure count correct")
# 检查 row_number 是否对齐 Excel 行号(第2行=Excel第2行,第3行=Excel第3行,第4行=Excel第4行)
if result["errors"]:
row_nums = sorted([e["row_number"] for e in result["errors"]])
print(f" [INFO] error row_numbers: {row_nums}")
ok_rows = (row_nums == [3, 4])
print(f" {'[OK]' if ok_rows else '[FAIL]'} row_number对齐Excel自然行号(表头第1行,数据从第2行起算)")
batch_id = result.get("batch_id")
if batch_id:
print(f" [INFO] batch_id={batch_id}")
# ---- Step 4: 失败行下载 ----
print("\n=== Step 4: Error Excel Download ===")
if batch_id:
r, ok = step(
f"GET /api/imports/errors/{batch_id} → 200 binary",
200,
"GET",
f"{BASE}/api/imports/errors/{batch_id}",
cookies=cookies,
)
ct = r.headers.get("content-type", "")
is_xlsx = "application/vnd.openxmlformats" in ct
print(f" {'[OK]' if is_xlsx else '[FAIL]'} Content-Type: {ct}")
print(f" [INFO] Error Excel downloaded ({len(r.content)} bytes)")
else:
print(" [SKIP] No batch_id from step 3")
# ---- Step 5: 年份目标缺失拦截 ----
print("\n=== Step 5: Missing yearly target → 400 ===")
excel_data = make_excel([
["episode_number", "program_name", "air_date", "editor_name",
"audience_share", "audience_rating", "is_rerun", "notes"],
[200, "《军事科技》跨年测试", "2024-01-15", "张颖", 0.68, 3.2, "", ""],
])
r, ok = step(
"POST with 2024 air_date (no 2024 target) → 400",
400,
"POST",
f"{BASE}/api/imports/episodes",
files={"file": ("test_2024.xlsx", excel_data, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")},
cookies=cookies,
)
if ok:
detail = r.json().get("detail", "")
has_year_target = "年度目标" in detail and "2024" in detail
print(f" {'[OK]' if has_year_target else '[FAIL]'} detail包含'年度目标'+'2024': {detail[:100]}")
# ---- Step 6: 重复期次拦截 ----
print("\n=== Step 6: Duplicate episode_number ===")
excel_data = make_excel([
["episode_number", "program_name", "air_date", "editor_name",
"audience_share", "audience_rating", "is_rerun", "notes"],
[99, "《军事科技》重复期次", "2026-06-15", "张颖", 0.76, 3.9, "", ""],
])
r, ok = step(
"POST episode_number=99 (duplicate in same year) → 409 or row-level rejection",
200,
"POST",
f"{BASE}/api/imports/episodes",
files={"file": ("test_duplicate.xlsx", excel_data, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")},
cookies=cookies,
)
if ok:
result = r.json()
# 数据库唯一约束触发:整行被 rejectsuccess=0, failed=1
has_db_error = result["failed_count"] == 1 and result["success_count"] == 0
has_unique_violation = any("UniqueViolation" in e.get("reason", "") or "idx_episodes_year_number" in e.get("reason", "") for e in result["errors"])
print(f" {'[OK]' if has_unique_violation else '[FAIL]'} 数据库唯一约束触发 (failed_count=1, UniqueViolation in reason)")
# ---- Step 7: 未知编导软落地 ----
print("\n=== Step 7: Unknown editor soft-landing ===")
excel_data = make_excel([
["episode_number", "program_name", "air_date", "editor_name",
"audience_share", "audience_rating", "is_rerun", "notes"],
[301, "《军事科技》编导软引用测试", "2026-09-01", "李不存在", 0.69, 3.1, "", ""],
])
r, ok = step(
"POST with unknown editor_name='李不存在' → 200",
200,
"POST",
f"{BASE}/api/imports/episodes",
files={"file": ("test_unknown_editor.xlsx", excel_data, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")},
cookies=cookies,
)
if ok:
result = r.json()
ok = (result["success_count"] == 1 and result["failed_count"] == 0)
print(f" {'[OK]' if ok else '[FAIL]'} success_count=1")
# 验证 editor_id=null:先从列表拿到真实 id,再调用 GET /api/episodes/{id}
r_list, ok_list = step(
"GET /api/episodes?limit=100 → 找到刚导入的 episode_number=301",
200,
"GET",
f"{BASE}/api/episodes?limit=100",
cookies=cookies,
)
if ok_list:
eps = r_list.json()
ep_301 = next((e for e in eps if e.get("episode_number") == 301), None)
if ep_301:
real_id = ep_301["id"]
r_ep, ok_ep = step(
f"GET /api/episodes/{real_id} → editor_id=null, editor_name_snapshot='李不存在'",
200,
"GET",
f"{BASE}/api/episodes/{real_id}",
cookies=cookies,
)
if ok_ep:
ep = r_ep.json()
editor_id_null = ep.get("editor_id") is None
name_match = ep.get("editor_name_snapshot") == "李不存在"
print(f" {'[OK]' if editor_id_null else '[FAIL]'} editor_id=null ({ep.get('editor_id')})")
print(f" {'[OK]' if name_match else '[FAIL]'} editor_name_snapshot='李不存在' ({ep.get('editor_name_snapshot')})")
else:
print(f" [FAIL] episode_number=301 not found in list (import may have failed)")
print("\n=== All steps done ===")
print("NOTE: This script is辅助. 制片人仍需在 Swagger UI (http://localhost:8000/docs) 手动跑完 8 步确认全绿.")
if __name__ == "__main__":
main()
+157
View File
@@ -0,0 +1,157 @@
"""
Task 1 冒烟测试脚本 — httpx(幂等版,可重复跑)
确保后端已启动: uvicorn app.main:app --reload
"""
import httpx
import random
BASE = "http://localhost:8000"
HEADERS = {"Content-Type": "application/json"}
def login(username: str, password: str) -> str:
r = httpx.post(f"{BASE}/api/auth/login", json={"username": username, "password": password})
r.raise_for_status()
return r.cookies.get("milsci_session", "")
def step(name: str, expected_status: int, method: str, url: str, **kwargs):
cookies = kwargs.pop("cookies", {})
if method == "GET":
r = httpx.get(url, cookies=cookies, **kwargs)
elif method == "POST":
r = httpx.post(url, cookies=cookies, **kwargs)
elif method == "PATCH":
r = httpx.patch(url, cookies=cookies, **kwargs)
elif method == "DELETE":
r = httpx.delete(url, cookies=cookies, **kwargs)
else:
raise ValueError(method)
status_ok = r.status_code == expected_status
mark = "[OK]" if status_ok else f"[FAIL expected {expected_status} got {r.status_code}]"
print(f"{mark} [{method}] {name}")
if not status_ok:
print(f" Response: {r.text[:300]}")
return r, status_ok
def main():
print("=== Login ===")
session_cookie = login("simonkoson", "liutong65")
if not session_cookie:
print("WARNING: session cookie empty, trying anyway")
session_cookie = ""
cookies = {"milsci_session": session_cookie}
# 使用随机后缀避免与上次数据冲突
suffix = random.randint(1000, 9999)
print("\n=== Step 1: Create zebian account ===")
r, ok = step(
"POST /api/users create zebian",
201,
"POST",
f"{BASE}/api/users",
json={
"username": f"test_zebian_{suffix}",
"display_name": f"Test Zebra {suffix}",
"password": "Test1234",
"role": "zebian",
},
cookies=cookies,
)
zebian_id = r.json().get("id") if ok else None
if ok:
print(f" zebian created, ID={zebian_id}")
print("\n=== Step 2: Create 2026 yearly target (idempotent) ===")
# 先尝试删已存在的(方便重跑)
r_list = httpx.get(f"{BASE}/api/yearly_targets", cookies=cookies)
if r_list.status_code == 200:
for t in r_list.json():
if t.get("year") == 2026:
del_id = t["id"]
httpx.delete(f"{BASE}/api/yearly_targets/{del_id}", cookies=cookies)
print(f" cleaned up existing 2026 target ID={del_id}")
# 再建新的
r, ok = step(
"POST /api/yearly_targets create 2026",
201,
"POST",
f"{BASE}/api/yearly_targets",
json={"year": 2026, "base_target": 0.65, "stretch_target": 0.90},
cookies=cookies,
)
target_2026_id = r.json().get("id") if ok else None
if ok:
print(f" target created, ID={target_2026_id}")
print("\n=== Step 3: Create 2026 again (should be 409) ===")
step(
"POST /api/yearly_targets duplicate 2026",
409,
"POST",
f"{BASE}/api/yearly_targets",
json={"year": 2026, "base_target": 0.70, "stretch_target": 0.95},
cookies=cookies,
)
print("\n=== Step 4: Create episode with editor_id=null (idempotent) ===")
# 先清理 episode_number
r_list = httpx.get(f"{BASE}/api/episodes?limit=100", cookies=cookies)
if r_list.status_code == 200:
for ep in r_list.json():
if ep.get("episode_number") == 9999:
httpx.delete(f"{BASE}/api/episodes/{ep['id']}", cookies=cookies)
print(f" cleaned up existing episode 9999 ID={ep['id']}")
r, ok = step(
"POST /api/episodes editor_id=null",
201,
"POST",
f"{BASE}/api/episodes",
json={
"episode_number": 9999,
"program_name": "Test Episode Smoke",
"air_date": "2026-05-15",
"editor_id": None,
"editor_name_snapshot": "Test Director Zhang",
"audience_share": 0.72,
},
cookies=cookies,
)
episode_9999_id = r.json().get("id") if ok else None
if ok:
print(f" episode created, ID={episode_9999_id}")
print("\n=== Step 5: GET /api/episodes list ===")
step(
"GET /api/episodes list",
200,
"GET",
f"{BASE}/api/episodes?limit=10",
cookies=cookies,
)
print("\n=== Step 6: PATCH yearly_target try to change year ===")
if target_2026_id:
r, ok = step(
"PATCH try to change year field",
200,
"PATCH",
f"{BASE}/api/yearly_targets/{target_2026_id}",
json={"year": 2027, "base_target": 0.65, "stretch_target": 0.90},
cookies=cookies,
)
if ok:
resp_year = r.json().get("year")
if resp_year == 2026:
print(f" [OK] year field unchanged, still 2026 (correct: year blocked)")
else:
print(f" [FAIL] year changed to {resp_year} (should be blocked)")
print("\n=== All steps done ===")
if __name__ == "__main__":
main()
@@ -0,0 +1,419 @@
"""
yearly_targets 写权限收紧 — 冒烟测试脚本
幂等,可重复跑。
确保后端已启动: uvicorn app.main:app --reload
12 步覆盖:
1. zhipianren POST → 201
2. zebian POST → 201
3. biandao POST → 403
4. 未登录 POST → 401
5. zhipianren PATCH → 200
6. zebian PATCH → 200
7. biandao PATCH → 403
8. zhipianren DELETE → 200
9. zebian DELETE → 403
10. biandao DELETE → 403
11. biandao GET 列表 → 200
12. zhipianren GET 列表 → 200
"""
import sys
from pathlib import Path
backend_dir = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(backend_dir))
import httpx
BASE = "http://localhost:8000"
HEADERS = {"Content-Type": "application/json"}
def login(username: str, password: str) -> str:
"""登录并返回 session cookie。"""
r = httpx.post(f"{BASE}/api/auth/login", json={"username": username, "password": password})
r.raise_for_status()
return r.cookies.get("milsci_session", "")
def step(name: str, expected_status: int, method: str, url: str, **kwargs):
"""执行一个 HTTP 步骤,比较实际状态码与期望值。"""
cookies = kwargs.pop("cookies", {})
if method == "GET":
r = httpx.get(url, cookies=cookies, timeout=10)
elif method == "POST":
r = httpx.post(url, cookies=cookies, headers=HEADERS, timeout=10, **kwargs)
elif method == "PATCH":
r = httpx.patch(url, cookies=cookies, headers=HEADERS, timeout=10, **kwargs)
elif method == "DELETE":
r = httpx.delete(url, cookies=cookies, timeout=10)
else:
raise ValueError(method)
status_ok = r.status_code == expected_status
mark = "[OK]" if status_ok else f"[FAIL expected {expected_status} got {r.status_code}]"
print(f" {mark} [{method}] {name}")
if not status_ok:
print(f" Response: {r.text[:300]}")
return r, status_ok
def ensure_user(cookies: dict, username: str, display_name: str, role: str, password: str = "Test1234"):
"""幂等创建测试账号(如果不存在)。"""
# 先尝试登录(账号可能已存在)
try:
r = httpx.get(f"{BASE}/api/users/me", cookies=cookies, timeout=5)
if r.status_code == 200:
me = r.json()
if me.get("username") == username:
print(f" [SKIP] user {username} already exists, reusing")
return
except Exception:
pass
# 尝试创建
r = httpx.post(
f"{BASE}/api/users",
json={"username": username, "display_name": display_name, "password": password, "role": role},
cookies=cookies,
headers=HEADERS,
timeout=10,
)
if r.status_code == 201:
print(f" [OK] created {username} ({role})")
elif r.status_code == 409:
print(f" [SKIP] {username} already exists")
else:
print(f" [WARN] create {username} returned {r.status_code}: {r.text[:100]}")
def cleanup_test_target(cookies: dict):
"""清理测试用年度目标(2027),幂等。"""
try:
r = httpx.get(f"{BASE}/api/yearly_targets", cookies=cookies, timeout=10)
if r.status_code == 200:
for t in r.json():
if t.get("year") == 2027:
httpx.delete(f"{BASE}/api/yearly_targets/{t['id']}", cookies=cookies, timeout=10)
print(f" [CLEAN] removed existing 2027 target ID={t['id']}")
except Exception:
pass
def main():
print("=" * 60)
print(" yearly_targets 写权限收紧 — 冒烟测试")
print("=" * 60)
# Step 0: 用 zhipianren 登录,准备测试账号
print("\n[Step 0] Login as zhipianren and ensure test accounts exist")
try:
admin_cookie = login("simonkoson", "liutong65")
except Exception as e:
print(f" [FAIL] cannot login as simonkoson: {e}")
return
admin_cookies = {"milsci_session": admin_cookie}
print(" [OK] logged in as simonkoson (zhipianren)")
# 幂等创建 zebian / biandao 测试账号
ensure_user(admin_cookies, "test_zebian_smoke", "TestZebra Smoke", "zebian")
ensure_user(admin_cookies, "test_biandao_smoke", "TestBiandao Smoke", "biandao")
# 登录 zebian 和 biandao 获取 cookie
print("\n[Step 0b] Login as zebian and biandao")
zebian_cookie = login("test_zebian_smoke", "Test1234")
zebian_cookies = {"milsci_session": zebian_cookie}
print(" [OK] logged in as test_zebian_smoke (zebian)")
biandao_cookie = login("test_biandao_smoke", "Test1234")
biandao_cookies = {"milsci_session": biandao_cookie}
print(" [OK] logged in as test_biandao_smoke (biandao)")
# 清理可能残留的 2027 目标
cleanup_test_target(admin_cookies)
results = []
# =====================================================================
# Step 1: zhipianren POST → 201
# =====================================================================
print("\n[Step 1] POST new yearly target — zhipianren → 201")
r, ok = step(
"POST /api/yearly_targets (zhipianren)",
201,
"POST",
f"{BASE}/api/yearly_targets",
json={"year": 2027, "base_target": 0.66, "stretch_target": 0.91},
cookies=admin_cookies,
)
target_id = r.json().get("id") if ok else None
results.append(("Step 1: zhipianren POST 201", ok))
# =====================================================================
# Step 2: zebian POST → 201
# =====================================================================
print("\n[Step 2] POST new yearly target — zebian → 201")
# 清理 2027(刚被 zhipianren 创建了)
cleanup_test_target(admin_cookies)
r, ok = step(
"POST /api/yearly_targets (zebian)",
201,
"POST",
f"{BASE}/api/yearly_targets",
json={"year": 2027, "base_target": 0.66, "stretch_target": 0.91},
cookies=zebian_cookies,
)
target_id2 = r.json().get("id") if ok else None
results.append(("Step 2: zebian POST 201", ok))
# =====================================================================
# Step 3: biandao POST → 403
# =====================================================================
print("\n[Step 3] POST new yearly target — biandao → 403")
cleanup_test_target(admin_cookies)
r, ok = step(
"POST /api/yearly_targets (biandao)",
403,
"POST",
f"{BASE}/api/yearly_targets",
json={"year": 2027, "base_target": 0.66, "stretch_target": 0.91},
cookies=biandao_cookies,
)
results.append(("Step 3: biandao POST 403", ok))
# =====================================================================
# Step 4: 未登录 POST → 401
# =====================================================================
print("\n[Step 4] POST new yearly target — not logged in → 401")
r, ok = step(
"POST /api/yearly_targets (no auth)",
401,
"POST",
f"{BASE}/api/yearly_targets",
json={"year": 2027, "base_target": 0.66, "stretch_target": 0.91},
cookies={},
)
results.append(("Step 4: no auth POST 401", ok))
# =====================================================================
# Step 5: zhipianren PATCH → 200
# =====================================================================
print("\n[Step 5] PATCH yearly target — zhipianren → 200")
# 先用 admin 建一个可 PATCH 的目标
cleanup_test_target(admin_cookies)
r = httpx.post(
f"{BASE}/api/yearly_targets",
json={"year": 2027, "base_target": 0.66, "stretch_target": 0.91},
cookies=admin_cookies,
headers=HEADERS,
timeout=10,
)
patch_id = r.json().get("id") if r.status_code == 201 else None
if patch_id:
r, ok = step(
"PATCH /api/yearly_targets/{id} (zhipianren)",
200,
"PATCH",
f"{BASE}/api/yearly_targets/{patch_id}",
json={"base_target": 0.67},
cookies=admin_cookies,
)
else:
ok = False
print(" [FAIL] could not create target for PATCH test")
results.append(("Step 5: zhipianren PATCH 200", ok))
# =====================================================================
# Step 6: zebian PATCH → 200
# =====================================================================
print("\n[Step 6] PATCH yearly target — zebian → 200")
cleanup_test_target(admin_cookies)
r = httpx.post(
f"{BASE}/api/yearly_targets",
json={"year": 2027, "base_target": 0.66, "stretch_target": 0.91},
cookies=admin_cookies,
headers=HEADERS,
timeout=10,
)
patch_id2 = r.json().get("id") if r.status_code == 201 else None
if patch_id2:
r, ok = step(
"PATCH /api/yearly_targets/{id} (zebian)",
200,
"PATCH",
f"{BASE}/api/yearly_targets/{patch_id2}",
json={"stretch_target": 0.92},
cookies=zebian_cookies,
)
else:
ok = False
print(" [FAIL] could not create target for PATCH test")
results.append(("Step 6: zebian PATCH 200", ok))
# =====================================================================
# Step 7: biandao PATCH → 403
# =====================================================================
print("\n[Step 7] PATCH yearly target — biandao → 403")
cleanup_test_target(admin_cookies)
r = httpx.post(
f"{BASE}/api/yearly_targets",
json={"year": 2027, "base_target": 0.66, "stretch_target": 0.91},
cookies=admin_cookies,
headers=HEADERS,
timeout=10,
)
patch_id3 = r.json().get("id") if r.status_code == 201 else None
if patch_id3:
r, ok = step(
"PATCH /api/yearly_targets/{id} (biandao)",
403,
"PATCH",
f"{BASE}/api/yearly_targets/{patch_id3}",
json={"base_target": 0.68},
cookies=biandao_cookies,
)
else:
ok = False
print(" [FAIL] could not create target for PATCH test")
results.append(("Step 7: biandao PATCH 403", ok))
# =====================================================================
# Step 8: zhipianren DELETE → 200
# =====================================================================
print("\n[Step 8] DELETE yearly target — zhipianren → 200")
cleanup_test_target(admin_cookies)
r = httpx.post(
f"{BASE}/api/yearly_targets",
json={"year": 2027, "base_target": 0.66, "stretch_target": 0.91},
cookies=admin_cookies,
headers=HEADERS,
timeout=10,
)
del_id = r.json().get("id") if r.status_code == 201 else None
if del_id:
r, ok = step(
"DELETE /api/yearly_targets/{id} (zhipianren)",
204,
"DELETE",
f"{BASE}/api/yearly_targets/{del_id}",
cookies=admin_cookies,
)
else:
ok = False
print(" [FAIL] could not create target for DELETE test")
results.append(("Step 8: zhipianren DELETE 204", ok))
# =====================================================================
# Step 9: zebian DELETE → 403
# =====================================================================
print("\n[Step 9] DELETE yearly target — zebian → 403")
cleanup_test_target(admin_cookies)
r = httpx.post(
f"{BASE}/api/yearly_targets",
json={"year": 2027, "base_target": 0.66, "stretch_target": 0.91},
cookies=admin_cookies,
headers=HEADERS,
timeout=10,
)
del_id2 = r.json().get("id") if r.status_code == 201 else None
if del_id2:
r, ok = step(
"DELETE /api/yearly_targets/{id} (zebian)",
403,
"DELETE",
f"{BASE}/api/yearly_targets/{del_id2}",
cookies=zebian_cookies,
)
else:
ok = False
print(" [FAIL] could not create target for DELETE test")
results.append(("Step 9: zebian DELETE 403", ok))
# =====================================================================
# Step 10: biandao DELETE → 403
# =====================================================================
print("\n[Step 10] DELETE yearly target — biandao → 403")
cleanup_test_target(admin_cookies)
r = httpx.post(
f"{BASE}/api/yearly_targets",
json={"year": 2027, "base_target": 0.66, "stretch_target": 0.91},
cookies=admin_cookies,
headers=HEADERS,
timeout=10,
)
del_id3 = r.json().get("id") if r.status_code == 201 else None
if del_id3:
r, ok = step(
"DELETE /api/yearly_targets/{id} (biandao)",
403,
"DELETE",
f"{BASE}/api/yearly_targets/{del_id3}",
cookies=biandao_cookies,
)
else:
ok = False
print(" [FAIL] could not create target for DELETE test")
results.append(("Step 10: biandao DELETE 403", ok))
# =====================================================================
# Step 11: biandao GET 列表 → 200
# =====================================================================
print("\n[Step 11] GET yearly targets list — biandao → 200")
r, ok = step(
"GET /api/yearly_targets (biandao)",
200,
"GET",
f"{BASE}/api/yearly_targets",
cookies=biandao_cookies,
)
results.append(("Step 11: biandao GET 200", ok))
# =====================================================================
# Step 12: zhipianren GET 列表 → 200
# =====================================================================
print("\n[Step 12] GET yearly targets list — zhipianren → 200")
r, ok = step(
"GET /api/yearly_targets (zhipianren)",
200,
"GET",
f"{BASE}/api/yearly_targets",
cookies=admin_cookies,
)
results.append(("Step 12: zhipianren GET 200", ok))
# =====================================================================
# Summary
# =====================================================================
print("\n" + "=" * 60)
print(" 结果汇总")
print("=" * 60)
passed = sum(1 for _, ok in results if ok)
total = len(results)
for name, ok in results:
mark = "[OK]" if ok else "[FAIL]"
print(f" {mark} {name}")
print(f"\n PASS: {passed}/{total}")
if passed == total:
print(" ALL PASS")
else:
print(" SOME FAILURES")
print("\n测试账号已保留(供后续手动清理):")
print(" - test_zebian_smoke (zebian)")
print(" - test_biandao_smoke (biandao)")
print("\n" + "=" * 60)
if __name__ == "__main__":
main()
+70
View File
@@ -0,0 +1,70 @@
"""
探路脚本 — 调 MiniMax embo-01,打印原始返回 JSON
确认向量字段位置和维度后再写正式 service。
"""
import httpx
import json
import os
from pathlib import Path
# 加载 .env
from dotenv import load_dotenv
_env_path = Path(__file__).parent.parent / ".env"
load_dotenv(str(_env_path))
api_key = os.environ.get("MINIMAX_EMBED_API_KEY", "")
group_id = os.environ.get("MINIMAX_GROUP_ID", "")
if not api_key or api_key == "your_api_key_here":
print("[ERROR] MINIMAX_EMBED_API_KEY not configured, please edit backend/.env")
exit(1)
if not group_id or group_id == "your_group_id_here":
print("[ERROR] MINIMAX_GROUP_ID not configured, please edit backend/.env")
exit(1)
print(f"API Key (first 4 chars): {api_key[:4]}...")
print(f"GroupId: {group_id}")
print()
# 最小调用
test_text = "这是一段测试文本,用于验证 embo-01 接口返回结构。"
print(f"Sending request, test text: {test_text}")
print("-" * 60)
try:
resp = httpx.post(
"https://api.minimax.chat/v1/embeddings",
headers={
"Authorization": f"Bearer {api_key}",
"GroupId": group_id,
"Content-Type": "application/json",
},
json={"model": "embo-01", "texts": [test_text], "type": "db"},
timeout=30.0,
)
print(f"HTTP status: {resp.status_code}")
print()
data = resp.json()
print(json.dumps(data, indent=2, ensure_ascii=False))
# 提取向量,验证维度
print()
print("-" * 60)
vectors = data.get("vectors", [])
if vectors and len(vectors) > 0:
embedding = vectors[0]
dim = len(embedding)
print(f"[OK] Embedding field: vectors[0]")
print(f"[OK] Embedding dimension: {dim}")
if dim != 1536:
print(f"[STOP] Dimension is NOT 1536! Got {dim} - stopping here")
else:
print(f"[OK] Dimension correct: 1536")
print(f"[OK] API call successful, structure confirmed.")
else:
print("[WARNING] vectors not found in response")
except Exception as e:
print(f"[ERROR] Request failed: {e}")
+79
View File
@@ -0,0 +1,79 @@
"""
全链路验证脚本 — TPS 知识库 embedding 最小链路
验证步骤:
1. 读取 backend/sample_md/ 下的 5 篇 .md 文件
2. 调用 embo-01 转成向量(打印维度)
3. 存入 knowledge_items + knowledge_embeddings(打印行数)
4. 执行语义检索(打印查询句 + 最相似笔记)
5. 查 episodes 表行数(打印,只读不动)
"""
import os
from pathlib import Path
from dotenv import load_dotenv
from sqlmodel import text
# 加载 .env
_env_path = Path(__file__).parent.parent / ".env"
load_dotenv(str(_env_path))
from app.services.knowledge_service import KnowledgeService
from app.db.session import engine
def main():
print("=" * 60)
print("TPS Knowledge Base — Embedding Full链路验证")
print("=" * 60)
sample_dir = Path(__file__).parent.parent / "sample_md"
md_files = sorted(sample_dir.glob("*.md"))
print(f"\n[FIND] Found {len(md_files)} .md files in sample_md/")
ks = KnowledgeService()
# 1. 写入知识库
print("\n[STEP 1] Storing MD files into knowledge base...")
items_stored = []
for mf in md_files:
title = mf.stem # 文件名(不含扩展名)作为标题
content = mf.read_text(encoding="utf-8")
item = ks.store_md_file(
title=title,
content_md=content,
source_file_name=mf.name,
source_type="manual",
)
items_stored.append(item)
print(f" - Stored: {item.title} (id={item.id})")
ki_count = ks.get_item_count()
ke_count = ks.get_embedding_count()
print(f"\n[OK] knowledge_items rows: {ki_count}")
print(f"[OK] knowledge_embeddings rows: {ke_count}")
# 2. 语义检索
print("\n[STEP 2] Semantic search test...")
query = "五代战斗机的隐身技术有哪些关键要素?"
print(f"Query: {query}")
results = ks.search_similar(query, top_k=3)
print(f"\n[OK] Top 3 similar notes:")
for i, r in enumerate(results, 1):
print(f" {i}. [{r['similarity']}] {r['title']}")
# 3. 查 episodes 表行数(只读不动)
print("\n[STEP 3] Episodes table (read-only)...")
with engine.connect() as conn:
result = conn.execute(text("SELECT COUNT(*) FROM episodes"))
episode_count = result.scalar()
print(f"[OK] episodes table row count: {episode_count}")
print("\n" + "=" * 60)
print("Verification complete.")
print("=" * 60)
if __name__ == "__main__":
main()
+23
View File
@@ -0,0 +1,23 @@
-- ============================================================
-- 002_add_cover_settings.sql — 题图设置表
-- 执行方式: psql -U postgres -d milsci_dev -f 002_add_cover_settings.sql
-- ============================================================
-- 题图设置表(只存当前展示的一张题图,不动 episodes 表)
CREATE TABLE IF NOT EXISTS cover_settings (
id SERIAL PRIMARY KEY,
key VARCHAR(50) NOT NULL UNIQUE DEFAULT 'dashboard_cover',
cover_path TEXT, -- 当前题图文件路径(相对路径,如 /static/covers/xxx.jpg
episode_number INTEGER, -- 关联期次号(可空)
episode_title TEXT, -- 关联期次节目名(可空)
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 初始化默认值行(允许为空,表示使用默认渐变底图)
INSERT INTO cover_settings (key, cover_path, episode_number, episode_title)
VALUES ('dashboard_cover', NULL, NULL, NULL)
ON CONFLICT (key) DO NOTHING;
-- ============================================================
-- 迁移完成
-- ============================================================
@@ -0,0 +1,31 @@
-- ============================================================
-- 002a_diagnostic_episode_number.sql
-- 诊断脚本:将 episodes.episode_number 从全局 UNIQUE 改为 (air_year, episode_number) 复合 UNIQUE
-- 执行方式: psql -U postgres -d milsci_dev -f 002a_diagnostic_episode_number.sql
-- 前置条件:先设 $env:PGCLIENTENCODING="UTF8"
-- ============================================================
-- 三项诊断(任一异常停下找制片人)
SELECT
'total_rows' as check_name,
count(*) as value
FROM episodes
UNION ALL
SELECT
'null_air_date' as check_name,
count(*) as value
FROM episodes
WHERE air_date IS NULL
UNION ALL
SELECT
'duplicate_year_epnum' as check_name,
count(*) as value
FROM (
SELECT
EXTRACT(YEAR FROM air_date)::int as air_year,
episode_number
FROM episodes
WHERE original_episode_id IS NULL
GROUP BY 1, 2
HAVING count(*) > 1
) as dup;
@@ -0,0 +1,18 @@
-- ============================================================
-- 002b_migrate_episode_number.sql
-- 迁移脚本:在 002a 诊断确认安全后执行
-- 执行方式: psql -U postgres -d milsci_dev -f sql/002b_migrate_episode_number.sql
-- 前置条件:先设 $env:PGCLIENTENCODING="UTF8"
-- 注意:已改用 date_part('year', air_date) 替代 EXTRACT(... )::int 避免语法错误
-- ============================================================
-- 1. 删全局 UNIQUE 约束(幂等:constraint 已删则报 warning,正常)
ALTER TABLE episodes DROP CONSTRAINT episodes_episode_number_key;
-- 2. 建按年复合唯一索引(重播行 original_episode_id IS NOT NULL 不参与)
CREATE UNIQUE INDEX idx_episodes_year_number
ON episodes (date_part('year', air_date), episode_number)
WHERE original_episode_id IS NULL;
-- 验证:确认索引生效
-- \d episodes
+10
View File
@@ -0,0 +1,10 @@
-- ============================================================
-- 002c_recreate_index_only.sql
-- 002b 跑半截翻车后补救:只执行 CREATE INDEX,不要再 DROP CONSTRAINT
-- 执行方式: psql -U postgres -d milsci_dev -f sql/002c_recreate_index_only.sql
-- 前置条件:先设 $env:PGCLIENTENCODING="UTF8"
-- ============================================================
CREATE UNIQUE INDEX idx_episodes_year_number
ON episodes (date_part('year', air_date), episode_number)
WHERE original_episode_id IS NULL;
Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

+13
View File
@@ -0,0 +1,13 @@
**本文件是制片人刘通与 Claude(Opus 4.7 高级技术顾问)的协作原则,与项目宪法(.clinerules / project_plan.md / dev_plan.md / docs/git_workflow.md)并列。新对话接手时,请连同宪法一并阅读本文件。**
---
**Claude 与我交流、沟通、协作时需要注意的原则**
1. 全程使用简体中文。
2. 我不懂代码编程,给我的解释说明和理由要尽量简明扼要。不要堆叠大量技术语言,说人话。
3. 请参阅宪法文件中你(Claude)的定位:写具体 Plan 是 Cline 的事,不要架空 Cline 的 Plan 模式。除非特别必要(Cline 反复修改都过不去的坎)可以上手,以及制片人明确指定你可以操刀的具体工作。
4. 给 Cline 的指令要用代码块封装起来,方便复制。
+15 -10
View File
@@ -2,7 +2,7 @@
> 项目:TPS(Topic Planning System)中台
> 仓库:`tps-dashboard`(Gitea)
> 修订版本:v4 · 2026-05-14
> 修订版本:v5 · 2026-05-27
> 配套文档:`project_plan.md`(产品宪法)+ `.clinerules`(协作规则)+ `docs/git_workflow.md`(Git 操作手册)
---
@@ -249,19 +249,23 @@ git push origin phase0-env-ready
---
### Phase 4a:核心 MVP(Week 5-6)
### Phase 4a:核心 MVP — 对话式 TPS(Week 5-6)
**模块 G**:报题单生成与下载(docxtpl 模板)
- 模板严格遵守"双段结构"(`.clinerules` 5.4)
> v5 升级:模块 A/C/G 合成"对话式策划助手"形态(左对话右报告、脚注式引用),详见 `project_plan.md` 模块 A/C/G + 第 2.3 节 + 6.5 节红线。
**模块 A**:选题查重(语义检索 + 历史收视展示)
- 联动模块 E 展示外拍资源
**前置条件(硬门槛,不满足不开工)**:
- 知识库笔记已批量录入(Backlog#2),收视数据已真实导入(Phase 2 遗留债)。**库空时对话助手"无米下锅",依据全空会毁掉第一印象。**
- Phase 3 的**朴素语义检索**已先行跑通(输入文本→返回最相关条目,带相关度/原文片段/来源),作为对话式报告的检索底座。
**模块 C**:知识库参考(检索往期文稿 / 军报)
**模块 A(对话式策划主入口)**:左栏编导对话、右栏 AI 加工的策划参考报告(历史是否做过/收视/审片意见/可参考节目/外拍资源/知识库依据)。严守三条交互红线:求助式不投喂、脚注式锚定引用、持续对话沉淀编导判断(见 `project_plan.md` 模块 A)。
**前端**:选题提交表单 + AI 反馈页面布局
**模块 C(知识库依据供给)**:检索结果融入右栏报告作脚注依据,而非孤立列表。
**🎯 Week 6 末上线 MVP**——团队真的可以用了:登录、看仪表盘、提交选题、看到查重和知识库结果、生成报题单 docx 下载
**模块 G(报题单生成)**:聊定后一键按范本规范化生成 docx。**只做规范化呈现,不代写立意**(见模块 G 分寸线 + 6.5 红线)。模板严守"双段结构"(`.clinerules` 5.4)
**AI 成本机制**:本 Phase 起对话调用大模型,按第十一章"AI 成本与额度原则"落地——按 token 记、按人+选题归因、保守上限 + 超额提醒管理员。**朴素的一张用量表即可,严禁上 AI 网关/可观测性框架等大公司重型武器。**
**🎯 Week 6 末上线 MVP**——团队真的可以用了:登录、看仪表盘、与 TPS 对话策划选题、看到有依据的策划报告、聊定后一键生成报题单 docx 下载。
**收尾**:写 `logs/phase4a_log.md`,打 `phase4a-mvp-launch` tag。**这是项目第一次"对内发布",务必郑重打 tag**。
@@ -486,7 +490,8 @@ tps-dashboard/ ← Git 仓库根
| v1 | 2026-04 | 初版 |
| v2 | 2026-05-04 | 精简技术栈、Phase 拆分(避免 Phase 4 大爆炸)、明确每周里程碑 |
| v3 | 2026-05-06 | 加 Git + Gitea 工作流;Phase 0 重排(纳入 Git);Phase 2 起 Plan 切 MiniMax M2.7;明确单位机重启路径(路径 B 推荐);新增 docs/ 目录与 phase{N} tag 收尾仪式 |
| **v4** | **2026-05-14** | **家用机退役,唯一开发环境为单位 4090D;Phase 2 起 MiniMax M2.7 正式日常使用;更新版本演进记录** |
| v4 | 2026-05-14 | 家用机退役,唯一开发环境为单位 4090D;Phase 2 起 MiniMax M2.7 正式日常使用;更新版本演进记录 |
| **v5** | **2026-05-27** | **Phase 4a 升级为"对话式 TPS"(对齐 project_plan v5):模块 A/C/G 合成左对话右报告形态;补硬门槛(数据灌入 + Phase 3 朴素检索先行);加 AI 成本机制落地说明** |
---
+21
View File
@@ -0,0 +1,21 @@
# Doco 子模块凭证配置
# 复制此文件为 .env 并填入真实凭证
# 格式: VARIABLE_NAME=your_value
# ========================================================================
# 讯飞 ASR (录音文件转写标准版)
# 申请地址: https://console.xfyun.cn/
# 注意: 不要用"大模型版",用"录音文件转写标准版"
# 凭证类型: APP_ID + SECRET_KEY
# ========================================================================
XFYUN_APP_ID=your_xfyun_app_id
XFYUN_SECRET_KEY=your_xfyun_secret_key
# ========================================================================
# DeepSeek (OCR + AI 层共用)
# 申请地址: https://platform.deepseek.com/
# 凭证类型: API_KEY
# ========================================================================
DEEPSEEK_API_KEY=your_deepseek_api_key
DEEPSEEK_BASE_URL=https://api.deepseek.com
DEEPSEEK_MODEL=deepseek-v4-pro
+200
View File
@@ -0,0 +1,200 @@
# 项目协作主控文件 (doco/CLAUDE.md)
<!--
Claude Code 每次开 session 自动读取。两个角色:
1. 给 AI 的项目说明书(项目是什么、用什么、怎么跑)
2. 双账号/多工具之间的"活快照"交接区(现在到哪了、下一步、刚做的决定)
维护原则:增量更新,不要整篇重写。改动写进对应小节即可。
3. 每次结束session或通哥明确说结束今天的工作,应由Claude修改更新本文件,交接备注和待确认等部分内容应为最新。
-->
---
## 🔖 状态栏 (STATUS — 每次结束 session 前必须更新这三行)
- **最后更新**Claude Code 2026-06-24
- **当前状态一句话**:**20 期全部出稿完成(16/16 批量跑零失败)。** doco 流水线验证通过。下一步:通哥逐期核验分段 → doco 收工 → 带 20 期成品回 TPS 主项目知识库。
- **下一个动手的人从这里开始**:见下方「⏩ 交接备注」
---
## 🤖 给 AI 的工作约定 (READ FIRST)
- 开始任何工作前,先读完本文件,特别是「状态栏」「交接备注」「关键决策」三节。
- 完成一段工作后**增量更新**:「已完成」追加一条、「待办」勾掉或新增、必要时更新「关键决策」,最后改「状态栏」三行。不要重写整份文件,不要删历史,只追加/勾选。
- 涉及发送、发布、删除、改权限、付款等不可逆操作前,先在对话里跟通哥确认。
- **本文件与项目设计文档并列,构成 doco 子项目宪法。冲突时优先级:本文件 STATUS/交接 > `doco_P3续接快照_C1完工C2起步.md` > `doco_P3_设计稿.md` > `doco_project_design.md`(越新越权威,尤其"LLM 提供方"那条以快照为准)。** 新对话接手时连同上述文档一并阅读。
------
**Claude(Opus 高级技术顾问)与制片人刘通协作原则**
1. 全程使用简体中文。
2. 通哥不懂代码编程,解释要简明扼要,说人话,不堆术语。
3. 为了节省Claude的Token,你主要的工作是plan和审核cline的报告,除非cline3次及以上修改过不去的坎,你可以直接动手改代码。
4. 给 Cline 的指令一律用代码块封装,并明确标 ACT / Plan,方便复制。
5. 回复简明扼要,不解释太多中间过程;**独立核验 Cline 自报结果,对着源数据查证,不轻信口头诊断**(本项目已多次靠核验抓到 Cline 偷换模型/术语源、误判字数)。
6. 长批处理(OCR/ASR)放独立终端跑,不要让 Cline 盯。
7. git 直接推 main,不开分支。
---
## 1. 项目概览
- **项目名**:doco 文稿整理模块(《军事科技》TPS 工作台子模块)
- **目标**:把 **A稿(编导定稿)+ B稿v2(屏幕字幕,P2 产出 743 行)+ ASR(口语实况)** 三方交叉融合,产出两个内容一致、形态不同的交付物:
- **融合B稿**(给爱德华):碎句 + 密集字幕级时间戳 `[XmYs] 文本`
- **融合A稿**(给编导):公文格式 docx,保留【导视】【主持人N】【解说N】【专家N】【隔断】分段
- 一致性约束:融合A稿**由融合B稿生成**(按 A 稿分段归拢 + 套格式),不是事后比对硬凑。
- **范围边界(别越界去做)**:本模块只做"三方文本 → 终版"。**不做** OCR 识别(P2 已产出 B稿v2)、**不做**音频转码本身(属 ASR 适配层)、**不做**档案入库、**不做**编导确认 UI。
---
## 2. 技术栈与运行方式
- **语言**Pythonopenai SDK 走 DeepSeek 兼容端、ffmpeg、python-docx
- **关键目录**
- `src/doco/llm.py` — DeepSeek 统一客户端,`chat(messages, model=None, thinking=None, max_tokens=...)`,从 `doco/.env` 读 key/base_url/model
- `src/doco/term_extract.py` — C1:规则层 + AI 层提取 + 词典累积 + 热词清洗
- `src/doco/asr_adapter.py` — C2 讯飞 ASR 适配层(已完工):`get_hot_words(episode_id)` 读热词表、`transcribe()` 返回 `(句子, raw)``write_asr_result()``asr_v2_timed.txt` + `asr_result_raw.json`
- `src/doco/video_split.py` — P1:含 `extract_audio()`16kHz/单声道/16bit WAV),C2 直接复用,不另写 ffmpeg
- `src/doco/fusion_review.py` — C3:B稿v2 ⊕ ASR 交叉复审 → 融合B稿(743行) + fusion_review.csv
- `src/doco/cli.py` — 命令入口(split / terms / asr / fuse / compose / skeleton / **run**
- `src/doco/templates/` — P1/P2 stage 脚本模板(`doco run` 自动复制到 episode 目录)
- `doco/data/term_dict.json` — 累积词典(当前 267+ 条)
- `doco/programs/<episode_id>/` — 每期产物(热词表、各阶段输出)
- **常用命令**
- C1(已实现):`doco terms --episode-id <id> --a-script <path> [--no-ai]`
- C2(已实现):`doco asr --episode-id <id> --input-video <绝对路径mp4> --output-dir <绝对路径 doco/programs/<id>>``--skip-asr` 只分离不转写)。**务必传绝对 `--output-dir`,否则落到当前工作目录的 `programs/` 会与 doco 产物分家。**
- C3(已实现):`doco fuse --episode-id <id> [--no-ai] [--batch-size 35]`
- C4(已实现):`doco compose --episode-id <id> [--batch-size 25]`
- 骨架:`doco skeleton --episode-id <id> --a-script <docx>`(需人工核验后再跑 compose
- **一键全流程**`doco run --episode-id <id> --a-script <docx> --input-video <mp4> [--batch-size 25] [--skip-p1]`
- **批量跑**`python _batch_run.py`(16 期自动串跑,断点续跑,已有产物自动跳过)
- **环境变量 / 密钥**:只放 `doco/.env`(已在 `.gitignore`)。需要的 key`LLM_API_KEY` / `LLM_BASE_URL` / `LLM_MODEL`(已切换到小米 MiMo 2.5 Pro)、`XFYUN_APP_ID` / `XFYUN_SECRET_KEY`。**代码与对话里只引用变量名,绝不出现真值。**
---
## 3. 当前进度(核心交接区,以最新快照为准)
- **已完成至**:ep001-004 四期全流程出稿。一键脚本 `doco run` + 批量脚本 `_batch_run.py` 已就绪。16 期骨架全部生成并通过核验。C3 prompt 已收紧(专名铁律)。C4 batch_size 已降至 25。
- **正在做**:无(等通哥逐期核验分段标签)。
- **卡点 / 待解**:20 期出稿已全部产出,通哥核验分段后 doco 收工,带成品回 TPS 主项目。
---
## 4. 已完成(只追加,最新在最上)
- [2026-06-23 Claude Code] **批量化基础设施完工 + ep004 全流程通过 + 16 期批量跑启动**。本 session 完成:① ep004(枪王对决,最难·小剧场密·58段)全流程 P1→C4 一次通过,C4 punct_ok 58/58 全过、confidence 全 ≥0.80batch_size=25 效果显著,无批次回退);② 新增 `doco run` 一键全流程命令(Cline 实现,Opus 审核),参数 `--episode-id / --a-script / --input-video / --batch-size / --skip-p1`ep004 `--skip-p1` 验证通过;③ C3 SYSTEM_PROMPT 收紧专名铁律(防 ASR 同音字替换权威 B稿专名);④ Ollama 并发提至 16 路(`OLLAMA_NUM_PARALLEL=16`,8 路实测 GPU 96%、显存充裕);⑤ 16 期新目录建立 + 文件拷贝 + 骨架批量生成并集中核验(真名零泄露、隔断/ignore 正确);⑥ `_batch_run.py` 批量脚本启动跑 ep005-ep020。**doco PRD 完成标准已与通哥确定**:20 期全出稿 → 带成品回 TPS 主项目知识库批量导入。
- [2026-06-22 晚|Claude Code] **ep002 C4 审核完毕,全流程收工**。Cline 跑完 C4 compose 出稿 `20260127潜艇的仿生之路_穆佩弦_融合A稿.docx`。Opus 审核核验:硬校验(汉字零改)全过、733 行全覆盖、标点回退 0 段。**但发现两个问题**:① Cline 自报空段名称错了两个(报"解说3/解说6",实际是"三维动画解说3/解说8")、隔断数也报错(报 3 个,实际 4 个)——再次印证不信 Cline 自检;② **9/19 批次(47%LLM 对齐失败走回退**(全 confidence=0.30),本次因失败批次恰在大段中间未翻车,但属黄色预警。**分段偏差根因**:A 稿是拍摄前剧本,专家采访段与实际播出内容差异巨大(A 稿一句话提纲 vs 专家自由发挥两分钟),LLM 无法正确匹配——这是信息不对应,非算法问题。通哥手动批改分段(~10 处高亮标签),内容本身正确。**结论(通哥拍板)**:治本靠编导给贴近播出版的稿子,现阶段接受"程序保证文字零改 + 编导手调分段"的模式。
- [2026-06-22 Claude Code] **ep002 C3 融合通过**。融合B稿 733行(零偏移),review 10条(5 minor_edit 全是 OCR 错字修正,专名无同音替换)。C4 compose 交 Cline 跑中。
- [2026-06-22 Claude Code] **ep002(潜艇仿生)P1→P2→C1→C2 完成**。P1 全量抽帧 1620 帧(不做 dHash/IoU 去重,全部进 OCR);P2 OCR 4路并发首秀通过(~50帧/分钟,约串行3倍),Stage B 文本去重出 B稿v2 733行;C1 术语提取 68 条热词(词典累积至 235 条);C2 讯飞 ASR 411 句。下一步 C3 fuse。
- [2026-06-22 Claude Code] **标点层 prompt 修复 + ep003 重跑出稿**`PUNCTUATE_SYSTEM_PROMPT` 加第5条规则(陈述句结尾用句号)+ 错误/正确示范。ep003 逗号占比从 88% 降至 76%,句号从 70 涨到 118。红线未破(strip_punct 硬校验通过)。commit `fd2ef1a`
- [2026-06-22 Claude Code] **LLM 提供方从 DeepSeek 切换到小米 MiMo 2.5 Pro**。环境变量名 `DEEPSEEK_*``LLM_*`(通用化),涉及 llm.py / term_extract.py / video_split.py。API 已验证连通。commit `9948b7d`
- [2026-06-22 Claude Code] **Ollama 并发配置完成**`OLLAMA_NUM_PARALLEL=4` + 重启)。4090D 24G 显存充裕(OCR 时 GPU 利用率仅 2%),后续可提到 8 路。
- [2026-06-18 晚 |账号A] **ep003(小尺寸大讲究)批量首期·全流程端到端跑通**P1→P2→C1→C2→C3→C4 六阶段)。产出 `20260331枪械设计中的小尺寸大讲究_孙逸昊_融合A稿.docx`。逐阶段实测数:P1/P2 出 **B稿_v2.txt 698 行**OCR 串行,未用并发);C1 出 **60 热词**(词典累至 172 条,AI 候选回查 A稿无幻觉);C2 讯飞真转写 **278 句带时间戳**(热词偏置生效,型号命中良好);C3 fuse 698 行零偏移、仅 1 处 minor_edit、留痕 1 条;C4 compose 20 段、硬校验**汉字零改逐段通过**、1 处标点回退(seg_6)。**核验三连**:① **4 个空段(seg1518:主持人7/解说8/主持人8/解说9)确认是播出剪掉的 VSS 微声狙击枪 + NTW-20 南非大炮整段**——融合B稿里 VSS/NTW/南非/消音/反器材**零出现**seg14 边界抽查(593681 全 HK416、682 起进结束语)证对齐正确,非 bug;② **C3 同音字坑已抓修**LLM 把权威 B稿"斯泰尔"按 ASR 同音盖成"斯太尔"(519 行),已改回,与 487 行及 A稿一致;③ **出稿命名生效**——`{原A稿stem}_融合A稿.docx`,不覆盖原始定稿。**本期间改的代码**:(a) `stage_a_extract_ocr.py` 修 GBK 崩溃(ffmpeg 子进程加 `encoding=utf-8,errors=replace`)+ 去掉 A3 硬编码 ep001 台词断言改通用中文冒烟检查 + **加并发 OCR(ThreadPoolExecutor/`OCR_NUM_WORKERS`默认4/idx-set 续跑)**——并发**未在 ep003 实跑**(ep003 走串行)ep002 首秀;(b) `fusion_align.py` a_candidates glob 排除"融合/批改"稿(防重跑把自己输出当输入) + 出稿改名带原稿信息。
- [2026-06-18 |账号A] **A稿解析器升级:正则 → LLM分段骨架 + 人工确认 + ignore合并**。起因:量产三期(ep002/003/004) A稿格式与 ep001 天差地别(冒号头/正文同段、`【固摇轨】`镜头标记酷似段头、正文整段加粗、小剧场对话角色头),原 `parse_a_segments` 纯正则(`^【.+?】$`)必散架。新机制(`fusion_align.py`)`extract_a_paragraphs`(带 bold/align 样式) → `extract_skeleton_llm`(thinking=True/max_tokens足量,**LLM 只判结构不碰正文**,输出 type/role_label/para 区间 JSON) → `validate_skeleton_coverage`(全覆盖硬校验,抓 LLM 数错下标/JSON 截断) → 落 `<id>_a_skeleton.json` + 人类预览表 → `parse_a_segments` 改读骨架按下标原样抽正文(inline 头切冒号、standalone 头取后续段、**ignore 段丢弃**、**相邻同 role_label 仅隔 ignore 的 normal 段合并**)。新命令 `doco skeleton --episode-id <id> --a-script <docx>`。**三期骨架已产并人工核验通过**:人名零泄露(刘通/孙逸昊/左鑫/穆佩弦/主持人真名都没混进 role_label)、覆盖完整、ep002 四隔断认对、ep003 九个`(出枪柜)`等归 ignore 且主持人被运镜切断后正确合并(37→20段)、ep004 小剧场卡/斯角色保留。ep001 无骨架时仍走旧正则 fallback,不受影响。
- [2026-06-18 |账号A] **P3-C4 第一批(保守版)收口**`fusion_align.py` 把融合B稿743行碎句按 A稿 28 段(含 4 隔断)对齐归拢,套公文 docx 出稿 `融合A稿.docx`。**两条铁律守死**:①汉字零改——正文 100% 来自融合B稿;②标点由 LLM 按语义增删(一逗到底→正常断句)。机制:(a) **AI 对齐层**喂完整 A稿正文、seg_id 单调不减、thinking=True/4096、缓存 `.c4_cache/batch_N.json`(b) **标点层** `punctuate_segment` 用 STRICT prompt(只许插标点、禁增删改字)+ 硬校验 `strip_punct(出)==strip_punct(入)` 不过即 3 次重试、再不过回退裸文本进 review,缓存 `punct_seg_N.json`thinking=False/3000。**红线核验脚本逐段 strip_punct 比对 28/28 全过**diff 全 equal)。隔断按 `^隔断:【(.+?)】` 正则识别、独立成段、line_count=0、位置正确(解说1/主持人2/3/4 后各一)。标点 `punct_ok` 26/28seg8(专家2)、seg27(专家7) 两段 LLM 顽固插连接词触发回退、已落裸文本+标记"需人工",归第二批。产物:`融合A稿.docx` + `c4_alignment.csv`32行对齐表,含 start/end_line、confidence、punct_ok、note)。命令 `doco compose --episode-id <id>`
- [2026-06-17 |账号A] **P3-C3 收口**`fusion_review.py` 逐行复审 B稿v2 ⊕ ASR(22 批×35行=743行),产 `融合B稿.txt`(行数/时间戳零偏移)+ `fusion_review.csv`(5条真OCR纠错留痕)。关键机制:断点缓存 `.c3_cache/batch_N.json` 可复用;硬校验行数/时间戳/change_type枚举不过不写文件;后处理修正 `final_text==B原文` 被 LLM 误标为 `minor_edit` 的行(19→5)。`--no-ai` 全 unchanged 可验证管道。
- [2026-06-17 |账号A] **P3-C2 收口**:密钥外置三验收点全过(硬编码已删/读 .env/已 gitignore+ 旧 key 已轮换;`asr_adapter.py` 并入讯飞上传·轮询·解析,复用 P1 `extract_audio` 分离 16kHz/单声道/16bit WAV;新增 `doco asr` 命令读 C1 64条热词。**真转写《现代防空反导大对决》27分钟音频 → 310句带时间戳**,萨德×6/塔米尔/爱国者/箭二箭三/见证者等术语命中良好,raw json 730KB 落盘。(顺带修 `extract_audio` 在 Windows 的 `text=True` 解码崩溃。)
- [2026-06-16 |账号A] **P3-C1 收口**A稿→规则+DeepSeek AI 提取→累积词典110条→热词清洗→本期热词表64条;AI 44 候选已逐个回查 A 稿原文无幻觉,算术对账一致。
- [P2 |本轮之前] 本地 OCRDeepSeek-OCR/Ollama+ 扒词流水线,产出 **B稿_v2.txt 743 行**
- [P1 |本轮之前] 帧筛流水线完工。
---
## 5. 待办(按优先级,做完打勾)
- [x] **C3**:B稿v2 ⊕ ASR 交叉复审 → 融合B稿(743行时间戳零偏移) + fusion_review.csv(5条留痕)。
- [x] **C4(全部收口)**:融合B稿按 A稿28段对齐归拢 → 融合A稿 docx(公文格式)。汉字零改 + LLM 按语义加标点,红线 28/28 段逐字一致。`doco compose --episode-id <id>`。**OCR 漏字决定不补**(讨论见关键决策);seg8/seg27 两段标点接受一逗到底。
- [x] **A稿解析器升级**:正则 → LLM分段骨架+人工确认+ignore合并;三期骨架已产并核验通过。`doco skeleton`
- [x] **批量化 ep001-004 全流程完工**:四期均已出稿。ep004(最难·小剧场密·58段)C4 全过,batch_size=25 效果显著。
- [x] **【已完成】标点层 prompt 修复**:加第5条规则+示范,ep003 逗号占比 88%→76%commit `fd2ef1a`
- [x] **【已完成】配 Ollama 并发**:已从 4 路提至 16 路(`OLLAMA_NUM_PARALLEL=16`),8 路实测 GPU 96%、显存充裕。
- [x] **【已完成】C4 对齐层 batch_size 调优**:已降至 25ep004 实测 confidence 全 ≥0.80,无批次回退。批量脚本 `_batch_run.py` 固定用 25。
- [ ] **【产品 backlog】原稿缺【专家N】/【三维动画N】标签**:ep003 复查发现编导原稿里就没有专家、后续三维动画的分段标注,骨架如实反映=融合A稿里也没有(通哥手工补了高亮)。不赖程序(原稿零信号,LLM 与人一样猜不准),**不让程序猜**。治本=提醒编导/责编原稿把【专家N】等段头标全;50 期前在导入须知里写清。
- [x] **【已完成】C3 prompt 收紧**`fusion_review.py` SYSTEM_PROMPT 新增专名铁律——厂名/型号/番号/国名/人名/机构名遇 B稿与 ASR 同音异写,一律以 B稿为准,零容忍采 ASR。
- [x] **【已完成】串一键脚本**`doco run` 命令(P1→P2→C1→C2→C3→C4 六阶段串联),ep004 验证通过。模板脚本放 `src/doco/templates/`
- [x] **批量跑 20 期(完成)**ep001-004 + ep005-ep020 共 20 期全部出稿,16 期批量跑零失败(847 分钟),B稿行数 699-874,融合B稿行数全部零偏移。
- [ ] **doco 收工后回 TPS 主项目**:20 期融合A稿批量导入知识库(走 Phase 3 已有的上传/embedding 链路),为 Phase 4a 语义搜索提供冷启动数据。
- [x] **【已修复】`fusion_align.py` align_batch 崩溃 bug**`_parse_align_json` 调用在 `try/except` 之外,LLM 返回空字符串时进程崩溃。Cline 已修,移入 try/except 内,错误时优雅回退。
- [ ] **骨架小瑕疵 backlog(不拦路,后期顺手或跳过)**:ep002 演播室主持人 vs 主持人 是否统一;ep004 小剧场角色编号乱(斯9/斯通纳10、卡8跨场)直接改 JSON 即可,不必重调 LLM;ep003 行内`【固摇轨】`留在参照正文(无害,不进输出)。
- [x] **C2** 讯飞 ASR 适配层(密钥外置 + asr_adapter 并入 + `doco asr` 命令 + 真转写310句)
- [x] P3-C1 术语提取 + 词典 + 热词表
- [x] P2 本地 OCR + B稿v2
- [x] P1 帧筛
---
## 6. 关键决策(为什么这么做 — 跨 session 最易丢,别推翻)
- [2026-06-23] **doco PRD 完成标准:20 期全出稿 → 带成品回 TPS 主项目。** ep004 跑通 + 一键脚本就绪 = 流水线可用;剩余 16 期在 doco 子项目内批量跑完(一方面压力测试积累问题,一方面 20 期成品对 TPS 知识库冷启动有价值)。回归路径:融合A稿作为 manuscript 类文档 → 走主项目 Phase 3 知识库上传/embedding 链路 → Phase 4a 语义搜索。
- [2026-06-23] **C4 batch_size=25 为量产默认值。** ep004 实测 58 段全过(confidence 全 ≥0.80punct_ok 58/58),相比 ep002 的 batch_size=4047% 批次回退)大幅改善。`_batch_run.py``doco run` 默认均为 25。
- [2026-06-23] **Ollama OCR 并发提至 16 路。** 8 路实测 GPU 利用率 96%4090D 24GB 显存充裕),16 路为批量跑默认值。环境变量 `OLLAMA_NUM_PARALLEL=16`(系统级)+ `OCR_NUM_WORKERS=16`(批量脚本内设)。
- [2026-06-22] **C4 分段偏差的根因是 A 稿与播出版内容差异大,非算法问题,现阶段不追技术方案。** ep002 实证:专家采访段 A 稿只有一句话提纲,实际播出专家自由发挥两分钟,内容/角度/篇幅全变,LLM 拿提纲匹配实录当然对不上。解说段偏差轻,基本能对。**B 稿(唱词字幕 OCR)里没有隔断标题**(隔断是画面美术字不走字幕轨道),所以也没法用隔断做硬分界线。**通哥拍板**:治本靠编导给贴近播出版的稿子(尤其专家段录完后更新);差异大的期接受"程序保证文字零改 + 编导花十几分钟手调分段标签"。
- [2026-06-22] **MiMo 2.5 Pro C4 对齐层批次失败率偏高(ep002 实测 9/19=47%)。** 失败批次全 confidence=0.30,回退机制把该批所有行分配给 `min_normal_seg_id`。ep002 运气好——失败批次全在大段中间,回退恰好分对了。但 ep004 段切换密,边界批次失败会导致行归错段。**建议 ep004 前把 batch_size 从 40 降到 25-30**,或排查 MiMo 返回空/JSON 截断的具体原因。
- [2026-06-18] **A稿段头识别改用「LLM 判结构 + 人工核骨架」,不用死正则。** 编导写法千变万化(方括号/冒号/前缀杂质`Xr`/`【固摇轨】`镜头标记/小剧场对话头),正则分不清`【固】`(镜头)与`【主持人1】`(段头)——这是语义判断,交 LLM。**铁律:LLM 只输出结构(type/role_label/para 区间),绝不复述正文;正文一律代码按下标从 docx 原样抽**(护住"汉字零改"红线)。配两道闸:① `validate_skeleton_coverage` 全覆盖硬校验(title 后每段恰好被覆盖一次,自动抓 LLM 数错下标/JSON 截断);② 人类预览表给制片人肉眼核(重点抓真人姓名泄露/ignore 漏多/隔断认全)。`doco compose` 无骨架即报错(ep001 例外走正则)。
- [2026-06-18] **role_label 只标"段落类型",严禁带任何真人姓名/配音员/编导署名。** 解说词只标【解说N】,不标谁配音(踩过 Cline 把"解说:刘通"标错)。但**小剧场里被演绎的历史人物/角色保留**(卡=卡拉什尼科夫、斯=斯通纳、小剧场N-主持人)——那是剧情内容不是配音员实名。有借鉴价值的修饰也留(三维动画解说/演播室主持人),只清杂质(`Xr`/编号/空格)。
- [2026-06-18] **解析层合并"被 ignore 隔断的同角色段"。** 编导用`(出枪柜)(换枪)`镜头切换把一个主持人的话从中间切断(ep003),骨架会切成两个同名`【主持人N】`。parse 后处理:相邻 normal 段 role_label 全等、中间仅隔 ignore,则合并(正文顺序拼接、ignore 内容丢弃),避免输出里冒重复段头。合并条件要严,ep002/004 无 ignore 不受影响。
- [2026-06-18] **OCR 漏字决定不补(通哥拍板),不让 LLM 补词。** 三条理由:① 漏字的根在**上游抽帧/OCR**——缺的"那么/此外/一方面"都是两三字单独一屏、停留短被整屏漏掉,是真实信息丢失,文稿层无法"还原";② **LLM 补词=猜词=破"汉字零改"红线**,对专业性强、最贴近播出版的稿子风险不可接受;③ 口子守不住——seg8/seg27 标点回退正是 LLM 一有机会就想塞连接词被硬校验拦下,证明给余地它就动手。**真要科学补,唯一正路是 ASR 回捞**(这些连接词主持人/解说真说了,大概率在 ASR 310 句里),而非 LLM 猜,但属 C3 级返工、本期不值;真做时先验 ASR 有无再说。**治本=提醒编导/责编别让两三字单独一屏**;本期连接词缺了能读懂,接受。
- [2026-06-18] **`融合A稿_批改.docx`(通哥手批)只当"看缺了什么"的参照,绝不当"让 docx 去贴它"的对齐目标。** 手批稿可能含其他润色,一旦去对齐就等于润色,违背"只守零改汉字+加标点、不润色其他部分"。
- [2026-06-18] **seg8(专家2 行169-216)/seg27(专家7 行584-608) 两段标点接受一逗到底(通哥选 B)。** 这是标点没点好,不是缺字,不碰汉字;若日后要美化由人工手点。
- [2026-06-18] **C4 红线由"汉字润色"转为"汉字零改、标点交 LLM 按语义增删"**(通哥拍板:"转变后的红线更精准、更科学")。理由:润色会让关键军事表述有不准确风险;保守做法正文 100% 来自融合B稿,只在字符间插标点。**硬校验 `strip_punct(LLM输出)==strip_punct(输入)`** 是这条红线的机器守门员(strip_punct 去掉所有 unicode 'P' 类标点+空白),不过即重试、再不过回退裸文本。**第一批先做保守版,润色/漏字回填留第二批。**
- [2026-06-18] **thinking 要按任务分用,不是越开越好——这是 C1"thinking不关content为空"的孪生坑。** 两个独立故障:(a) **开 thinking 时 max_tokens 必须给足**thinking 与正文 `content` 共享同一配额,配额不够 thinking 吃光 → 正文空输出(C4 标点层 @2000 实测吐 0 字);(b) **浅层活(标点/抽取)开 thinking 会又慢又被推理空转吃光**:标点这种活开 thinking 单段 10+ 分钟还超时,关掉 thinking/3000 单段 6.9s 通过。**规则:语义对齐/融合这类深推理开 thinking 且给足 tokenC4 对齐 4096);标点、抽取这类浅层确定性活必须关 thinking。**
- [2026-06-18] **A稿分段编号有重复(解说2/解说10 各出现两次)——任何按段头文本做 key 的字典都会把两次出现合并,必须按"文档出现顺序"切块/取块。** C4 红线核验脚本初版犯此错(解说2 行61-77 与行151-168 被并成一段、误报破线),改用 occurrence 队列后 28/28 全过。对齐表 c4_alignment.csv 用 seg_id 区分,不用段头文本。
- [2026-06-17] **C2 音频分离复用 P1 `video_split.extract_audio()`,不另写 ffmpeg。** WAV 落 `programs/<episode>/audio_16k.wav`,已存在则复用(避免 1GB 视频重复解码)。实测产物 `pcm_s16le/16000Hz/1ch/16bit/1620s`,符合讯飞硬要求。
- [2026-06-17] **`doco asr` 不传 `--output-dir` 会落到当前工作目录的 `programs/`**(命令在 repo 根 `E:\tps-dashboard` 跑时尤其坑),与 doco 产物分家。**命令一律用绝对 `--input-video` + 绝对 `--output-dir`。**
- [2026-06-17] **ASR 输出是口语形态,型号不规范属正常**(如「箭二箭三」非「箭-3」、「高超音速/高超声速」混用)。按设计稿口语信 ASR、型号规范信 A/B 稿,**这些归一交给 C3,别回头改 C2**。
- [2026-06-22] **AI 融合层 LLM 已从 DeepSeek v4-pro 切换到小米 MiMo 2.5 Pro。** 环境变量名通用化:`LLM_API_KEY` / `LLM_BASE_URL` / `LLM_MODEL`(旧 `DEEPSEEK_*` 已废弃,代码不再读取)。接入:OpenAI 兼容,`/chat/completions`,模型名 `mimo-v2.5-pro`。thinking 关闭方式与 DeepSeek 相同(`extra_body={"thinking":{"type":"disabled"}}`)。Cline 端也已换 MiMo 2.5 Pro。**(此条覆盖先前 DeepSeek v4-pro 的决策。)**
- [2026-06-22] **抽取类任务必须关 thinking(规则不变,适用于 MiMo)。** 做法:`chat(..., thinking=False)``extra_body={"thinking":{"type":"disabled"}}`,给足 `max_tokens`C1 用 4000)。**C4 语义融合可开 thinking**(单轮调用,质量优先)。
- [2026-06-16] **C1 术语源 = A稿(规范写法),不是 B稿。** Cline 曾擅自换成 B稿v2,已纠正。
- [2026-06-15] **讯飞用「录音文件转写标准版」,不要大模型版**(免费包阉割 `language` 参数,会报误导性 `language verify fail`)。参数锁定:`pd=mil``eng_smoothproc=true``eng_colloqproc=true``hotWord=<C1热词>``language=cn`。签名 `signa=base64(HmacSHA1(MD5(appid+ts), secretKey))``/upload` 拿 orderId,轮询 `/getResult` 到 status=4。
- [2026-06-15] **B稿v2 权威升级**:屏幕术语/型号/番号 **B稿v2 ≈ A稿 并列权威**(B稿是屏幕实打的字,更贴近播出)。口语/语序/语气信 ASR;书面结构/分段信 A稿;同音事实错(美以→美伊)用 A稿/B稿v2 覆盖 ASR。
- [2026-06-18 晚] **C3 会拿 ASR 同音字盖权威 B稿专名——必须核验留痕、专名一律以 B稿/A稿为准。** ep003 实证:B稿"斯泰尔"(奥地利厂名)被 C3 LLM 按 ASR 同音改成"斯太尔",造成 487/519 行不一致。已手改回。这正是上条权威规则的反例落地:**厂名/型号/番号遇 ASR 同音异写,零容忍采 ASR**。50 期批量前应在 C3 prompt 收紧(见待办 backlog);每期 fuse 后扫一眼 `fusion_review.csv` 的 minor_edit 是不是专名同音改。
- [2026-06-18 晚] **C4 出现空段≠对齐 bug,先查"是不是播出剪了这段"。** ep003 有 4 个空段(seg1518 整段 VSS 微声狙击枪 + NTW-20),一度疑似对齐错乱。核验路径:去**融合B稿**里 grep 该段专属词(VSS/NTW/南非/消音/反器材),零命中=播出确实剪了;再抽查相邻实段边界确认没把别段内容错塞过来。结论是 C4 如实留空、对齐正确。**空段是常态(播出删戏),别当 bug 去"修"。**
- [2026-06-18 晚|通哥复查] **C4 标点层整篇偏保守是真缺陷,须在批量铺开前修——但不破红线。** ep003 复查实测逗号 511 vs 句号 70(断句标点 88% 逗号),语义该断处全逗号。根因:标点层关 thinking + STRICT prompt 只强调"别改字"未强调"句子结尾用句号"。**红线(汉字零改)由 strip_punct 守着没破,所以标点层尽可放开 prompt 让它正常断句、不怕越界**。修复列 ep002 前必做,修完重跑 ep003 compose 刷 docx。这条把先前"seg8/seg27 一逗到底可接受"的范围收窄:个别段一逗到底可接受,但整篇 88% 逗号不可接受、必须修。
- [设计期] **分层不变**:OCR 错字纠正、术语统一、时间戳对齐走**规则层**(确定性);段落归属、整段删除判定、编导笔误识别走 **AI 层**(需语义判断)。
- [设计期] **顺序约束**:术语提取(C1,产热词)必须在 ASR(C2)之前——已成立。
- [2026-06-17] **`fusion_review.csv` 定位 = 留痕档案(给爱德华/复核),不是编导读物。** 编导读的是 C4 docx。CSV 可读性不优化,只看 final_text 准不准。
- [2026-06-17] **`final_text==B原文` 强制归 `unchanged` 不进 review。** LLM 有时把"考虑后决定保留 B 稿原文"标成 `minor_edit`,但实际没改字。`fusion_review.py``_normalize_unchanged_when_no_edit()` 在写文件前修正(review 19→5)。
- [2026-06-17] **`change_type``minor_edit`/`editor_typo` 边界 LLM 把握不稳**(OCR 错字两类混标)。仅 metadata 标签不一致,`final_text` 全对,不影响融合B稿正文。本轮决定忽略;若 C4/爱德华按 `change_type` 做逻辑分流,届时在 prompt 里收紧两个枚举的边界定义。
- [工程纪律] 各阶段解耦、中间产物落缓存、可断点续跑、可单独重跑;长批处理放独立终端。
---
## 7. ⏩ 交接备注(写给下一个接手的 session)
> 下次开工读完这段应能 0 摩擦续上。接手后可清空重写。**(2026-06-23 重写)**
- **大局**:doco 子项目进入收尾阶段。ep001-004 四期全流程已出稿验证通过。16 期新节目(ep005-ep020)正在 `_batch_run.py` 批量跑中。20 期全部出稿后,doco 收工,融合A稿作为知识库资源带回 TPS 主项目。
- **批量跑状态**`python _batch_run.py` 在独立终端跑中。脚本有断点续跑能力(B稿v2 存在则跳过 P1+P2,ASR 已有则跳过转写,融合B稿已有则跳过 C3,融合A稿已有则跳过 C4)。中断后直接重跑即可。
- **第一句话该干的**:看 `_batch_run.py` 跑完没有。跑完看汇总表,有失败的排查原因重跑。全部成功后,逐期打开融合A稿 docx 核验分段标签,分段偏差的地方手调。
- **LLM**:小米 MiMo 2.5 Pro,环境变量 `LLM_API_KEY` / `LLM_BASE_URL` / `LLM_MODEL`
- **Ollama**16 路并发(`OLLAMA_NUM_PARALLEL=16`),4090D 24GB 充裕。
- **关键参数**C4 batch_size=25`_batch_run.py` 硬编码),C3 batch_size=35。
- **量产坑(务必记住)**
1. **C3 fuse 后必扫 `fusion_review.csv`**:专名被 ASR 同音字替换要打回。C3 prompt 已收紧专名铁律,但仍需人工复核。
2. **C4 空段 = 播出剪了那段**,不是 bug。
3. **C4 分段偏差是常态**(A 稿≠播出版),编导手调分段标签即可。
4. **OCR 漏字不补**(通哥拍板),LLM 补词=破红线。
5. Cline 自报结果一律对源数据核验,不轻信。
6. thinking 分任务用:骨架/对齐开,标点/抽取/融合关。
- **出稿命名**`{原始A稿stem}_融合A稿.docx`
- **字体坑**:大标题方正小标宋_GBK(商业字体),出稿前确认已装。
- **doco 收工后的路**:20 期成品 → TPS 主项目知识库批量导入(走 Phase 3 上传/embedding 链路)→ Phase 4a 语义搜索提供冷启动数据。主项目待办三选一中的"200+ Obsidian md 批量录入"正好是接收口。
- **工具脚本清单**(一次性,跑完可删):`_setup_episodes.py`(建目录+拷文件)、`_batch_skeleton.py`(批量生成骨架)、`_batch_run.py`(批量全流程)。
---
## 8. 待确认 / 开放问题(需通哥拍板,AI 别自行假设)
- ~~**B稿_v2.txt 就位**~~:已完成(通哥已拷进 episode 目录并随 9340edc 入库)。
- ~~**讯飞旧 key 轮换**~~:已完成(新值已在 `doco/.env`,旧 appid `84eff996` 代码内无残留)。
- **方正小标宋字体可用性**:C4 出稿前确认两台机器是否已装。
- ~~**OCR 并发提速**~~:已完成,16 路并发(4090D 24GB 充裕)。
- **讯飞单源依赖**(远期):是否接阿里云 ASR 做备份/交叉验证。
- **ep016 骨架的宣传词被标为 break**:其他期(ep006/13)宣传词标为 normal,需通哥确认 ep016 是否应改。
+226
View File
@@ -0,0 +1,226 @@
# Doco - TPS 工作台 · 终版文稿生成子模块
> 央视《军事科技》栏目 - 终版文稿自动融合流水线
## 项目状态
**✅ 20期全部出稿完成,流水线验证通过。** 16/16批量跑零失败(847分钟)。下一步:制片人逐期核验分段标签 → 带成品回归 TPS 主项目知识库。
---
## 功能概述
《军事科技》每期节目播出后,需要产出一份最接近实际播出的终版文稿。过去靠人工核对,**单期 4-6 小时**。
Doco 把**同一期节目**的三个文本来源自动融合,产出终版文稿:
| 文本来源 | 说明 | 权威范围 |
|---|---|---|
| **A稿**(编导定稿) | 编导剧本的书面结构与分段 | 段落骨架、专业术语规范写法 |
| **B稿v2**(屏幕字幕 OCR) | 视频画面中"黑底白字"字幕的OCR识别结果 | 屏幕术语/型号/番号(≈A稿并列权威) |
| **ASR**(口语转写) | 音轨经讯飞转写的口语实录 | 实际语音、语气、临场措辞 |
**铁律**:正文**汉字零改**——所有正文内容100%来自B稿v2,AI只负责纠错OCR错字、语义对齐分段、按语义插入标点,绝不改任何一个汉字。
最终产出两个**内容一致、形态不同**的交付物:
| 交付物 | 给谁 | 形态 |
|---|---|---|
| **融合B稿** | 爱德华(字幕/片段定位) | 碎句 + 密集字幕级时间戳 `[XmYs] 文本` |
| **融合A稿** | 编导存档 | 公文格式 docx,保留【导视】【主持人N】【解说N】【专家N】【隔断】分段 |
一致性约束:融合A稿**由融合B稿生成**(按A稿分段归拢 + 套格式),不是事后比对硬凑。
---
## 六阶段流水线架构
```
A稿 docx ──► ① 术语提取(C1) ──► 本期热词表
视频 mp4 ──► ② 音频分离 ──► 讯飞ASR(C2) ──► ASR文本(带时间戳)
(黑底白字+ │ │
干净人声) 抽帧+OCR(P1)──► 文本去重(P2)──► B稿v2(碎句+时间戳)
B稿v2 ⊕ ASR ──► ③ 交叉复审(C3) ──► 融合B稿
融合B稿 + A稿骨架 ──► ④ 语义对齐(C4) ──► 融合A稿.docx
```
| 阶段 | 子命令 | 做什么 | 产物 |
|---|---|---|---|
| **P1** | `doco split` | ffmpeg抽帧 + OCR识别屏幕字幕 | 关键帧、音频WAV |
| **P2** | (模板脚本自动) | 字幕文本去重、格式化 | B稿v2.txt(约700-870行) |
| **C1** | `doco terms` | 从A稿提取专有名词 → 累积词典 → 热词表 | 本期热词表(给ASR用) |
| **C2** | `doco asr` | 音频分离 → 讯飞ASR转写 | asr_v2_timed.txt |
| **C3** | `doco fuse` | B稿⊕ASR 交叉复审,AI纠错 | 融合B稿.txt + fusion_review.csv |
| **C4** | `doco compose` | 按A稿分段骨架语义对齐 → 套公文格式 | 融合A稿.docx + c4_alignment.csv |
---
## 系统依赖
### ffmpeg(必须)
**Windows 用户:**
1. 从 https://www.gyan.dev/ffmpeg/builds/ 下载 ffmpeg(建议用 essentials 版本)
2. 解压到本地目录(如 `C:\ffmpeg`
3.`C:\ffmpeg\bin` 加入系统 PATH
4. 打开 cmd,验证:`ffmpeg -version`
**Mac / Linux 用户:**
```bash
# Mac
brew install ffmpeg
# Linux
apt install ffmpeg
```
### Python >= 3.12
---
## 安装
```bash
# 1. 进入 doco 目录
cd doco
# 2. 安装(可编辑模式)
pip install -e .
# 3. 配置凭证(见下节)
cp .env.example .env
```
## 凭证配置
`doco/.env` 中配置以下变量(**已在 .gitignore 中,不会入库**):
| 变量名 | 用途 |
|---|---|
| `LLM_API_KEY` | LLM融合层API密钥(当前用小米 MiMo 2.5 Pro |
| `LLM_BASE_URL` | LLM API地址(OpenAI兼容端点) |
| `LLM_MODEL` | 模型名称(如 `mimo-v2.5-pro` |
| `XFYUN_APP_ID` | 讯飞开放平台 APP ID |
| `XFYUN_SECRET_KEY` | 讯飞开放平台 SECRET KEY |
> ⚠️ 讯飞要用「录音文件转写**标准版**」,不要用"大模型版"(免费包阉割 `language` 参数,会报误导性错误)。
---
## 使用方式
### 一键全流程(推荐)
```bash
doco run \
--episode-id ep001_20260612_fangkong_fandao \
--a-script programs/ep001_20260612_fangkong_fandao/source/a_draft.docx \
--input-video programs/ep001_20260612_fangkong_fandao/source/video.mp4
```
串联 P1→P2→C1→C2→C3→C4 六个阶段,中间产物自动落盘,各阶段可断点续跑(已有产物自动跳过)。
**可选参数:**
- `--skip-p1`:跳过P1/P2(已有B稿v2时使用)
- `--batch-size 25`C4对齐每批行数(默认25,可调)
> ⚠️ C4 开始前要求骨架文件已存在,需**先手动**运行 `doco skeleton` 并人工核验:
> ```bash
> doco skeleton --episode-id <id> --a-script <a_draft.docx>
> # 检查输出的骨架预览表,确认无误后再跑 doco run
> ```
### 各子命令(可单独运行)
```bash
# P1: 视频拆分(抽帧 + OCR + 音频分离)
doco split --episode-id <id> --input-video <mp4> --output-dir <dir>
# C1: 术语提取
doco terms --episode-id <id> --a-script <docx>
# C2: 讯飞ASR转写
doco asr --episode-id <id> --input-video <mp4> --output-dir <dir>
# C3: 交叉复审融合
doco fuse --episode-id <id> [--batch-size 35]
# C4: 对齐出稿
doco compose --episode-id <id> [--batch-size 25]
```
> ⚠️ `--output-dir` 务必传**绝对路径**,否则产物会落到当前工作目录,与 doco 产物分家。
---
## 目录结构
```
doco/
├── src/doco/
│ ├── __init__.py
│ ├── cli.py # CLI 入口(doco run/split/terms/asr/fuse/skeleton/compose
│ ├── video_split.py # P1: 抽帧 + 音频分离(ffmpeg
│ ├── llm.py # LLM 统一客户端(OpenAI兼容)
│ ├── term_extract.py # C1: 规则层+AI层术语提取
│ ├── asr_adapter.py # C2: 讯飞ASR适配层
│ ├── fusion_review.py # C3: B稿⊕ASR交叉复审
│ ├── fusion_align.py # C4: A稿骨架抽取+语义对齐+出稿
│ └── templates/ # P1/P2 模板脚本
│ ├── stage_a_extract_ocr.py # P1 抽帧+OCR
│ └── stage_b_dedup_output.py # P2 文本去重
├── programs/ # 每期节目产物(按 episode_id 分目录)
│ └── <episode_id>/
│ ├── source/ # 输入(video.mp4 + a_draft.docx
│ ├── B稿_v2.txt # P2 产出的OCR字幕文本
│ ├── audio_16k.wav # 分离的音频(16kHz/单声道/16bit
│ ├── asr_v2_timed.txt # ASR转写结果(带时间戳)
│ ├── <id>_a_skeleton.json # A稿分段骨架
│ ├── 融合B稿.txt # C3 产出
│ ├── fusion_review.csv # C3 复审留痕
│ ├── 融合A稿.docx # C4 最终交付物
│ └── c4_alignment.csv # C4 对齐留痕
├── data/
│ └── term_dict.json # 累积术语词典(逐期更新)
├── deliverables/ # 已完成的融合A稿展示
├── note/ # 设计文档、PRD、决策记录
├── tests/ # 测试
├── CLAUDE.md # 项目协作主控文件(交接、决策、状态)
├── pyproject.toml
└── .env.example # 凭证模板
```
---
## 设计原则
- **汉字零改**:正文100%来自B稿v2,AI只做OCR纠错+语义对齐+标点插入,绝不改任何一个汉字。`strip_punct()`硬校验守门。
- **有序无阻塞**:全自动产出,拿不准的地方全部进 `fusion_review.csv` 留痕,绝不卡出稿。
- **各阶段解耦**:中间产物落缓存,可断点续跑,可单独重跑,失败不影响已完成阶段。
- **专名铁律**:厂名/型号/番号/国名/人名/机构名,B稿与ASR同音异写时**一律以B稿为准**,零容忍采ASR。
- **OCR漏字不补**:缺的字是真实信息丢失,不让LLM补词(LLM补词=猜词=破红线)。
---
## 相关文档
- **项目协作主控文件**`CLAUDE.md`(状态、交接、关键决策,新接手者首选阅读)
- **子项目Brief**`note/Doco子项目_Brief.md`(红线、技术栈、出入口接口)
- **PRD**`note/PRD_doco_文稿整理子项目_v2.md`(需求规格、方案选型)
- **P3设计稿**`note/doco_P3_设计稿.md`(三方融合架构设计)
- **快照与决策记录**`note/` 目录下其他文件
---
## 技术栈
- **语言**Python ≥ 3.12
- **LLM**:小米 MiMo 2.5 ProOpenAI兼容端点,`openai` SDK
- **OCR**:本地 Ollama + DeepSeek-OCR 模型
- **ASR**:讯飞开放平台 录音文件转写(标准版)
- **视频处理**ffmpegsubprocess调用)
- **文档生成**python-docx
- **CLI框架**Click
+352
View File
@@ -0,0 +1,352 @@
# -*- coding: utf-8 -*-
"""
讯飞 ASR 适配层
=================================================
来源: demo 跑通的 xfyun_asr_standard.py
改动: 凭证从环境变量读取,不再硬编码
接口: https://raasr.xfyun.cn/v2/api/upload / getResult
签名: signa = base64(HmacSHA1(MD5(appid + ts), secretKey))
特性:
- 支持热词列表(hotWord),提升专业术语识别率
- 支持军事领域参数(pd=mil)
- 支持顺滑+口语规整(输出更接近书面语)
- 默认语种 cn(中文普通话),免费包标配
凭证来源: 环境变量
- XFYUN_APP_ID
- XFYUN_SECRET_KEY
"""
import base64
import hashlib
import hmac
import json
import os
import re
import sys
import time
import wave
from pathlib import Path
from urllib.parse import quote
from typing import List, Tuple, Optional
import requests
# ========================================================================
# 凭证 — 优先加载 doco/.env(与 llm.py 相同方式)
# ========================================================================
try:
from dotenv import load_dotenv
_doco_env = Path(__file__).resolve().parent.parent.parent / ".env" # doco/.env
if _doco_env.exists():
load_dotenv(str(_doco_env), override=True)
except Exception:
pass
APP_ID = os.environ.get("XFYUN_APP_ID", "").strip()
SECRET_KEY = os.environ.get("XFYUN_SECRET_KEY", "").strip()
if not APP_ID or not SECRET_KEY:
print("[ASR 配置错误] XFYUN_APP_ID 或 XFYUN_SECRET_KEY 未配置或为空", file=sys.stderr)
print("[ASR 配置错误] 请在 doco/.env 中设置这两个环境变量", file=sys.stderr)
print("[ASR 配置错误] 格式: XFYUN_APP_ID=你的appid / XFYUN_SECRET_KEY=你的secret", file=sys.stderr)
sys.exit(1)
# ========================================================================
# 接口配置
# ========================================================================
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" # 口语规整
# 轮询配置
POLL_INTERVAL_SECONDS = 30
MAX_WAIT_MINUTES = 30
# ========================================================================
# 热词列表(每期节目调用前从 A 稿提取)
# ========================================================================
def get_hot_words(episode_id: str) -> List[str]:
"""
读取 programs/<episode_id>/本期热词表.txt
"|" 切分、strip、去空去重,返回 List[str]。
文件缺失返回 [] 并 stderr 警告(不退出)。
"""
from pathlib import Path as _Path
# doco 项目根 = doco/src/doco/asr_adapter.py → 上3级到达 doco/
_project_root = _Path(__file__).resolve().parent.parent.parent
hotwords_file = _project_root / "programs" / episode_id / "本期热词表.txt"
if not hotwords_file.exists():
print(f"[ASR 热词] 未找到热词表: {hotwords_file},热词跳过", file=sys.stderr)
return []
try:
raw = hotwords_file.read_text(encoding="utf-8")
except Exception as e:
print(f"[ASR 热词] 读取热词表失败: {e}", file=sys.stderr)
return []
# 按 | 切分、strip、过滤空字符串、去重(保持顺序)
words: List[str] = []
seen: set = set()
for token in raw.split("|"):
w = token.strip()
if w and w not in seen:
seen.add(w)
words.append(w)
return words
# ========================================================================
# 签名+工具
# ========================================================================
def make_signa(app_id: str, secret_key: str, ts: str) -> str:
"""
讯飞老版签名: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: str) -> int:
"""获取音频时长(毫秒)。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))
return duration_ms
if ext == ".mp3":
try:
from mutagen.mp3 import MP3
return int(MP3(filepath).info.length * 1000)
except ImportError:
return 0
raise ValueError(f"不支持的音频格式: {ext}")
# ========================================================================
# 上传
# ========================================================================
def upload_audio(
filepath: str,
hot_words: Optional[List[str]] = None,
) -> str:
"""上传音频,返回 orderId"""
if not os.path.exists(filepath):
raise FileNotFoundError(f"音频文件不存在: {filepath}")
if not APP_ID or not SECRET_KEY:
raise ValueError("请先设置 XFYUN_APP_ID 和 XFYUN_SECRET_KEY 环境变量")
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)
# 构建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
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()
resp = requests.post(url, headers=headers, data=audio_bytes, timeout=300)
data = resp.json()
if data.get("code") != "000000":
raise RuntimeError(f"上传失败: code={data.get('code')}, desc={data.get('descInfo')}")
order_id = data["content"]["orderId"]
return order_id
# ========================================================================
# 查询结果
# ========================================================================
def query_result(order_id: str) -> dict:
"""单次查询"""
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: str) -> dict:
"""轮询直到完成"""
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)
if status == 4:
return data
if status == -1:
raise RuntimeError(f"转写失败: failType={fail_type}, 数据: {data}")
time.sleep(POLL_INTERVAL_SECONDS)
# ========================================================================
# 结果解析
# ========================================================================
def parse_order_result(order_result_str: str) -> List[Tuple[int, int, 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: int) -> str:
"""毫秒转 [Nm Ns] 格式"""
total_sec = ms // 1000
return f"{total_sec // 60}m{total_sec % 60}s"
def transcribe(
audio_path: str,
hot_words: Optional[List[str]] = None,
) -> Tuple[List[Tuple[int, int, str]], str]:
"""
完整转写流程:上传 → 轮询 → 解析
返回 (sentences, raw_order_result_json_str)
- sentences: [(start_ms, end_ms, text), ...]
- raw_order_result_json_str: 讯飞原始 orderResult 字段原文(用于断点续跑落盘)
"""
order_id = upload_audio(audio_path, hot_words=hot_words)
result_data = poll_until_done(order_id)
order_result_str = result_data["content"]["orderResult"]
sentences = parse_order_result(order_result_str)
return sentences, order_result_str
def write_asr_result(
sentences: List[Tuple[int, int, str]],
output_dir: str,
raw_order_result: str = "",
) -> Tuple[str, str]:
"""
将 ASR 结果写入文件
返回 (timed_txt_path, raw_json_path)
"""
os.makedirs(output_dir, exist_ok=True)
timed_lines = [f"[{format_timestamp(bg)}] {text}" for bg, _, text in sentences]
timed_path = os.path.join(output_dir, "asr_v2_timed.txt")
with open(timed_path, "w", encoding="utf-8") as f:
f.write("\n".join(timed_lines))
raw_path = os.path.join(output_dir, "asr_result_raw.json")
with open(raw_path, "w", encoding="utf-8") as f:
if raw_order_result:
f.write(raw_order_result)
else:
# 没有原始数据时写空对象(兼容旧调用)
f.write("{}")
return timed_path, raw_path
+782
View File
@@ -0,0 +1,782 @@
# -*- coding: utf-8 -*-
"""
doco CLI 入口
P1: doco split 子命令
P3: doco process 子命令(带 --input-a-draft 和 --cleanup-level)
P3 C1: doco terms 子命令
P3 run: 一键全流程 P1→P2→C1→C2→C3→C4
"""
import click
import shutil
import subprocess
import sys
from pathlib import Path
# P1 相关
from .video_split import split_video, extract_audio
# P3 C1 术语提取
from .term_extract import run_terms
# P3 C2 讯飞 ASR
from .asr_adapter import get_hot_words, transcribe, write_asr_result
# P3 C3 B稿⊕ASR 交叉复审融合
from .fusion_review import run_fusion
# P3 C4 分段对齐 → 融合A稿
from .fusion_align import run_compose, run_skeleton
@click.group()
@click.version_option(version="0.1.0")
def main():
"""TPS 中台 - 终版文稿生成工具"""
pass
@main.command("split")
@click.option(
"--episode-id",
required=True,
help="节目 ID,如 ep001_20260612_fangkong_fandao",
)
@click.option(
"--input-video",
required=True,
type=click.Path(exists=True),
help="输入视频文件路径",
)
@click.option(
"--output-dir",
required=True,
type=click.Path(),
help="输出目录(work/ 路径)",
)
@click.option(
"--hash-algorithm",
default="dhash",
type=click.Choice(["dhash", "phash"]),
help="哈希算法:dhash(默认,对边缘敏感) 或 phash(感知哈希)",
)
@click.option(
"--phash-threshold",
default=2,
type=int,
help="pHash 海明距离阈值(默认 2)",
)
@click.option(
"--dhash-threshold",
default=5,
type=int,
help="dHash 海明距离阈值(默认 5)",
)
@click.option(
"--iou-threshold",
default=0.95,
type=float,
help="IoU 保底阈值:二值化帧间 IoU > 此值视为同字幕(默认 0.95)",
)
@click.option(
"--dry-run",
is_flag=True,
default=False,
help="只抽帧+裁切,不调 OCR API;用于验证裁切框位置是否正确",
)
def split(
episode_id: str,
input_video: str,
output_dir: str,
hash_algorithm: str,
phash_threshold: int,
dhash_threshold: int,
iou_threshold: float,
dry_run: bool,
):
"""
P1: 视频双路拆分
A 路:抽帧 + 空白帧过滤 + 哈希变化检测 + OCR → B 稿 txt
B 路:提取音频(16kHz/单声道/16bit WAV)
使用 --dry-run 可跳过 OCR 调用,先验证裁切框位置:
1. 运行 dry-run
2. 检查 work/frames/ 下的前几张关键帧小图
3. 确认字幕被完整框住后,去掉 --dry-run 跑正式版
"""
video_path = Path(input_video)
out_dir = Path(output_dir)
click.echo(f"[doco split] episode_id={episode_id}")
click.echo(f"[doco split] input_video={video_path}")
click.echo(f"[doco split] output_dir={out_dir}")
click.echo(f"[doco split] hash_algorithm={hash_algorithm}")
click.echo(f"[doco split] phash_threshold={phash_threshold}")
click.echo(f"[doco split] dhash_threshold={dhash_threshold}")
click.echo(f"[doco split] iou_threshold={iou_threshold}")
click.echo(f"[doco split] dry_run={dry_run}")
try:
result = split_video(
video_path=video_path,
output_dir=out_dir,
episode_id=episode_id,
hash_algorithm=hash_algorithm,
phash_threshold=phash_threshold,
dhash_threshold=dhash_threshold,
iou_threshold=iou_threshold,
dry_run=dry_run,
)
if dry_run:
click.echo(f"[ok] 关键帧索引: {result['keyframes_path']}")
click.echo(f"[ok] 音频: {result['audio_path']}")
click.echo(f"[ok] 关键帧数量: {result['keyframe_count']}")
click.echo("[ok] dry-run 完成,请检查 frames/ 目录下的关键帧小图")
else:
click.echo(f"[ok] B 稿: {result['b_manuscript_path']}")
click.echo(f"[ok] 音频: {result['audio_path']}")
click.echo(f"[ok] 关键帧索引: {result['keyframes_path']}")
click.echo(f"[ok] 关键帧数量: {result['keyframe_count']}")
except Exception as e:
click.echo(f"[error] {e}", err=True)
sys.exit(1)
@main.command("process")
@click.option("--episode-id", required=True, help="节目 ID")
@click.option("--input-video", required=True, type=click.Path(exists=True), help="输入视频")
@click.option("--input-a-draft", required=True, type=click.Path(exists=True), help="A 稿 docx")
@click.option("--output-dir", required=True, type=click.Path(), help="输出目录")
@click.option(
"--cleanup-level",
default="medium",
type=click.Choice(["keep_all", "medium", "clean"]),
help="口语清理档位(默认 medium)",
)
def process(
episode_id: str,
input_video: str,
input_a_draft: str,
output_dir: str,
cleanup_level: str,
):
"""
P3: 三方融合全流程
需要 A 稿 + B 稿(本命令调用 split) + ASR 结果,融合输出终版 docx + 差异报告
"""
click.echo("[doco process] P3 全流程暂未实现,请先使用 split 命令")
sys.exit(1)
@main.command("terms")
@click.option("--episode-id", required=True, help="节目 ID,如 ep001_20260612_fangkong_fandao")
@click.option(
"--a-script",
required=True,
type=click.Path(exists=True),
help="A 稿 txt 文件路径(按纯文本读取)",
)
@click.option(
"--no-ai",
is_flag=True,
default=False,
help="跳过 AI 层提取(Claude),仅使用规则层",
)
def terms(
episode_id: str,
a_script: str,
no_ai: bool,
):
"""
P3 C1: 术语提取 + 累积词典 + 本期热词表
从本期 A 稿提取专有名词 → 更新中台累积词典 → 产出本期热词表(给讯飞 ASR 用)。
两层提取:
A) 规则层(必跑):正则抓型号/番号/兵器名/国名/机构名/人名
B) AI 层(--no-ai 跳过):调 Claude 补抓专名并归类
产物:
- doco/data/term_dict.json(累积词典,幂等更新)
- doco/programs/<episode-id>/本期热词表.txt(| 分隔,最多 200 条)
- doco/programs/<episode-id>/c1_term_candidates.json(三段留痕)
"""
script_path = Path(a_script)
click.echo(f"[doco terms] episode_id={episode_id}")
click.echo(f"[doco terms] A 稿={script_path}")
click.echo(f"[doco terms] no_ai={no_ai}")
try:
result = run_terms(
episode_id=episode_id,
a_script_path=script_path,
no_ai=no_ai,
)
click.echo(f"[ok] 规则候选: {result['rule_count']}")
click.echo(f"[ok] AI 候选: {result['ai_count']}")
click.echo(f"[ok] 合并后: {result['merged_count']}")
click.echo(f"[ok] 词典新增: {result['dict_new_entries']} 条 / 词典共 {result['dict_total']}")
click.echo(f"[ok] 本期热词表: {result['hotword_count']} 条 → {result['hotwords_path']}")
click.echo(f"[ok] 留痕: {result['audit_path']}")
except Exception as e:
click.echo(f"[error] {e}", err=True)
sys.exit(1)
@main.command("fuse")
@click.option("--episode-id", required=True, help="节目 ID,如 ep001_20260612_fangkong_fandao")
@click.option(
"--output-dir",
default=None,
type=click.Path(),
help="输出目录(默认 programs/<episode-id>/)",
)
@click.option(
"--no-ai",
is_flag=True,
default=False,
help="跳过 LLM 只跑规则层(=全 unchanged)",
)
@click.option(
"--batch-size",
default=35,
type=int,
help="每批送审行数(默认 35)",
)
def fuse(
episode_id: str,
output_dir: str,
no_ai: bool,
batch_size: int,
):
"""
P3 C3: B稿 ⊕ ASR 交叉复审融合
逐行复审 B稿(屏幕字幕OCR),以 ASR(口语转写)为上下文参考,
只做纠错,绝不合并行、不拆行、不增删行、不改时间戳。
--no-ai: 跳过 LLM,全 unchanged(验证管道)
--batch-size: 每批送审行数,默认 35
产物:
- 融合B稿.txt(与 B稿_v2 逐行时间戳一致)
- fusion_review.csv(仅含 change_type≠unchanged 或 confidence<0.8 的行)
"""
if output_dir is None:
out_dir = Path("programs") / episode_id
else:
out_dir = Path(output_dir)
click.echo(f"[doco fuse] episode_id={episode_id}")
click.echo(f"[doco fuse] output_dir={out_dir}")
click.echo(f"[doco fuse] no_ai={no_ai}")
click.echo(f"[doco fuse] batch_size={batch_size}")
try:
stats = run_fusion(
episode_id=episode_id,
output_dir=str(out_dir),
no_ai=no_ai,
batch_size=batch_size,
)
click.echo(f"[ok] 总行数: {stats['total_lines']}")
click.echo(f"[ok] 各 change_type 计数: {stats['change_counts']}")
click.echo(f"[ok] 进 review 行数: {stats['review_lines']}")
click.echo(f"[ok] 融合B稿: {stats['fused_path']}")
click.echo(f"[ok] review CSV: {stats['csv_path']}")
except Exception as e:
click.echo(f"[error] {e}", err=True)
sys.exit(1)
@main.command("skeleton")
@click.option("--episode-id", required=True, help="节目 ID,如 ep002_20260127_qianting_fangsheng")
@click.option("--a-script", required=True, type=click.Path(exists=True), help="A 稿 docx 路径")
@click.option(
"--output-dir",
default=None,
type=click.Path(),
help="输出目录(默认 programs/<episode-id>/)",
)
@click.option(
"--max-tokens",
default=16000,
type=int,
help="LLM max_tokens(默认 16000,长稿可调大)",
)
def skeleton(
episode_id: str,
a_script: str,
output_dir: str,
max_tokens: int,
):
"""
P3 新增: LLM 分段骨架抽取(只产骨架,不跑对齐)
流程:
1. extract_a_paragraphs: 纯 docx 段落样式提取
2. extract_skeleton_llm: LLM 判断分段结构 → JSON 骨架
3. validate_skeleton_coverage: 全覆盖硬校验
4. 落盘 <episode_id>_a_skeleton.json + 打印人类可读预览表
跑完请人工核验骨架预览表(role_label 是否含真人姓名? ignore 是否漏/多?)
确认无误后,再跑 doco compose 完成对齐。
"""
if output_dir is None:
out_dir = Path("programs") / episode_id
else:
out_dir = Path(output_dir)
click.echo(f"[doco skeleton] episode_id={episode_id}")
click.echo(f"[doco skeleton] a_script={a_script}")
click.echo(f"[doco skeleton] output_dir={out_dir}")
click.echo(f"[doco skeleton] max_tokens={max_tokens}")
try:
result = run_skeleton(
episode_id=episode_id,
a_script_path=a_script,
output_dir=str(out_dir),
max_tokens=max_tokens,
)
click.echo(f"[ok] 段落数: {result['total_paras']} (含标题)")
click.echo(f"[ok] 骨架段数: {result['skeleton_count']}")
click.echo(f"[ok] 全覆盖校验: {'通过' if result['coverage_ok'] else '失败'}")
click.echo(f"[ok] 骨架已保存: {result['skeleton_path']}")
click.echo(f"[提示] 请人工确认骨架预览表后,再运行: doco compose --episode-id {episode_id}")
except Exception as e:
click.echo(f"[error] {e}", err=True)
sys.exit(1)
@main.command("asr")
@click.option("--episode-id", required=True, help="节目 ID,如 ep001_20260612_fangkong_fandao")
@click.option(
"--input-video",
required=True,
type=click.Path(exists=True),
help="输入视频文件路径",
)
@click.option(
"--output-dir",
default=None,
type=click.Path(),
help="输出目录(默认 programs/<episode-id>/)",
)
@click.option(
"--skip-asr",
is_flag=True,
default=False,
help="只分离音频不调讯飞,用于先验证 WAV",
)
def asr(
episode_id: str,
input_video: str,
output_dir: str,
skip_asr: bool,
):
"""
P3 C2: 讯飞 ASR 转写
流程:
1. video_split.extract_audio() 分离 16kHz/单声道/16bit WAV
2. get_hot_words() 读取本期热词表
3. --skip-asr 时到此为止;否则调 transcribe() → write_asr_result()
产物:
- audio_16k.wav(音频)
- asr_v2_timed.txt(带时间戳的转写文本)
- asr_result_raw.json(讯飞原始返回,断点续跑用)
"""
from .asr_adapter import get_audio_duration_ms as _wav_duration
video_path = Path(input_video)
if output_dir is None:
out_dir = Path("programs") / episode_id
else:
out_dir = Path(output_dir)
out_dir.mkdir(parents=True, exist_ok=True)
click.echo(f"[doco asr] episode_id={episode_id}")
click.echo(f"[doco asr] input_video={video_path}")
click.echo(f"[doco asr] output_dir={out_dir}")
click.echo(f"[doco asr] skip_asr={skip_asr}")
# ---- a. 音频分离 ----
wav_path = out_dir / "audio_16k.wav"
if wav_path.exists():
click.echo(f"[doco asr] audio_16k.wav 已存在,复用: {wav_path}")
else:
click.echo("[doco asr] 从视频分离音频(16kHz/单声道/16bit)...")
try:
extract_audio(video_path, wav_path)
click.echo(f"[doco asr] 音频分离完成: {wav_path}")
except Exception as e:
click.echo(f"[error] 音频分离失败: {e}", err=True)
sys.exit(1)
# 打印 WAV 时长
try:
dur_ms = _wav_duration(str(wav_path))
dur_sec = dur_ms / 1000.0
fsize = wav_path.stat().st_size
click.echo(f"[doco asr] audio_16k.wav 大小: {fsize / 1024 / 1024:.1f} MB, 时长: {dur_sec:.1f}s ({dur_ms} ms)")
except Exception as e:
click.echo(f"[doco asr] 无法读取 WAV 时长: {e}")
# ---- b. --skip-asr 时到此为止 ----
if skip_asr:
click.echo(f"[doco asr] --skip-asr 模式,到此为止。WAV: {wav_path}")
return
# ---- c. 热词 ----
hot_words = get_hot_words(episode_id)
click.echo(f"[doco asr] 热词条数: {len(hot_words)}")
# ---- d. 转写 ----
click.echo("[doco asr] 上传音频 → 讯飞 ASR 转写(可能需要数分钟)...")
try:
sentences, raw_order_result = transcribe(str(wav_path), hot_words=hot_words)
except Exception as e:
click.echo(f"[error] ASR 转写失败: {e}", err=True)
sys.exit(1)
timed_path, raw_path = write_asr_result(
sentences,
str(out_dir),
raw_order_result=raw_order_result,
)
# ---- e. 打印摘要 ----
click.echo(f"[ok] 热词条数: {len(hot_words)}")
click.echo(f"[ok] 句子数: {len(sentences)}")
click.echo(f"[ok] asr_v2_timed.txt: {timed_path}")
click.echo(f"[ok] asr_result_raw.json: {raw_path}")
# 模板脚本目录(stage_a_extract_ocr.py / stage_b_dedup_output.py)
TEMPLATES_DIR = Path(__file__).resolve().parent / "templates"
def _stage_header(title: str):
"""打印阶段分隔线"""
click.echo("═════════════════════════════")
click.echo(title)
click.echo("═════════════════════════════")
@main.command("compose")
@click.option("--episode-id", required=True, help="节目 ID,如 ep001_20260612_fangkong_fandao")
@click.option(
"--output-dir",
default=None,
type=click.Path(),
help="输出目录(默认 programs/<episode-id>/)",
)
@click.option(
"--no-ai",
is_flag=True,
default=False,
help="跳过 LLM 对齐,按时间均分到各段(仅验证管道)",
)
@click.option(
"--batch-size",
default=40,
type=int,
help="每批送对齐行数(默认 40)",
)
def compose(
episode_id: str,
output_dir: str,
no_ai: bool,
batch_size: int,
):
"""
P3 C4: 融合B稿 + A稿分段骨架 → 融合A稿.docx(公文格式)
AI 唯一职责: 给每行 B 句打段序号,正文一字不改、纯规则拼接。
产物:
- 融合A稿.docx (GB/T 9704 公文格式)
- c4_alignment.csv (分段对齐留痕)
"""
if output_dir is None:
out_dir = Path("programs") / episode_id
else:
out_dir = Path(output_dir)
click.echo(f"[doco compose] episode_id={episode_id}")
click.echo(f"[doco compose] output_dir={out_dir}")
click.echo(f"[doco compose] no_ai={no_ai}")
click.echo(f"[doco compose] batch_size={batch_size}")
try:
stats = run_compose(
episode_id=episode_id,
output_dir=str(out_dir),
no_ai=no_ai,
batch_size=batch_size,
)
click.echo(f"[ok] 总行数: {stats['total_lines']}")
click.echo(f"[ok] 段数: {stats['segment_count']}")
click.echo(f"[ok] 空段数: {stats['empty_segments']}")
click.echo(f"[ok] 低把握段数: {stats['low_confidence_segments']}")
click.echo(f"[ok] 单调修正行数: {stats['audit_forced_lines']}")
click.echo(f"[ok] 融合A稿: {stats['docx_path']}")
click.echo(f"[ok] 留痕 CSV: {stats['csv_path']}")
except Exception as e:
click.echo(f"[error] {e}", err=True)
sys.exit(1)
@main.command("run")
@click.option("--episode-id", required=True, help="节目 ID,如 ep002_20260127_qianting_fangsheng")
@click.option(
"--a-script",
required=True,
type=click.Path(exists=True),
help="A 稿 docx 路径",
)
@click.option(
"--input-video",
required=True,
type=click.Path(exists=True),
help="输入视频 mp4 路径",
)
@click.option(
"--batch-size",
default=25,
type=int,
help="C4 对齐用每批行数(默认 25)",
)
@click.option(
"--skip-p1",
is_flag=True,
default=False,
help="跳过 P1/P2(抽帧+OCR+去重),从 C1 续跑(已有 B稿v2 时)",
)
def run(
episode_id: str,
a_script: str,
input_video: str,
batch_size: int,
skip_p1: bool,
):
"""
一键全流程: P1→P2→C1→C2→C3→C4
串联抽帧+OCR(P1)、文本去重(P2)、术语提取(C1)、ASR 转写(C2)、
融合复审(C3)、对齐出稿(C4) 六个阶段。
用 --skip-p1 可跳过 P1/P2,从 C1 续跑(适用于已有 B稿v2 的场景)。
C4 开始前要求骨架文件已存在(需先手动跑 doco skeleton 并人工核验)。
"""
from .asr_adapter import get_audio_duration_ms as _wav_duration
episode_dir = Path("programs") / episode_id
episode_dir.mkdir(parents=True, exist_ok=True)
video_path = Path(input_video)
a_script_path = Path(a_script)
# 记录已完成阶段,用于失败时打印
completed_stages: list = []
# ── 汇总数据 ──
b_v2_lines = 0
hotword_count = 0
asr_sentence_count = 0
fused_b_lines = 0
fused_a_docx = ""
try:
# ════════════════════════════════════════════════════════
# P1: 抽帧 + OCR
# ════════════════════════════════════════════════════════
if not skip_p1:
_stage_header("P1: 抽帧 + OCR")
stage_a_path = episode_dir / "stage_a_extract_ocr.py"
if not stage_a_path.exists():
src = TEMPLATES_DIR / "stage_a_extract_ocr.py"
click.echo(f"[run] 复制模板: {src}{stage_a_path}")
shutil.copy2(str(src), str(stage_a_path))
click.echo(f"[run] 执行: {sys.executable} {stage_a_path}")
click.echo(f"[run] 工作目录: {episode_dir}")
proc = subprocess.run(
[sys.executable, str(stage_a_path)],
cwd=str(episode_dir),
)
if proc.returncode != 0:
raise RuntimeError(f"P1 stage_a_extract_ocr.py 退出码: {proc.returncode}")
completed_stages.append("P1: 抽帧 + OCR")
# ════════════════════════════════════════════════════════
# P2: 文本去重
# ════════════════════════════════════════════════════════
if not skip_p1:
_stage_header("P2: 文本去重")
stage_b_path = episode_dir / "stage_b_dedup_output.py"
if not stage_b_path.exists():
src = TEMPLATES_DIR / "stage_b_dedup_output.py"
click.echo(f"[run] 复制模板: {src}{stage_b_path}")
shutil.copy2(str(src), str(stage_b_path))
click.echo(f"[run] 执行: {sys.executable} {stage_b_path}")
click.echo(f"[run] 工作目录: {episode_dir}")
proc = subprocess.run(
[sys.executable, str(stage_b_path)],
cwd=str(episode_dir),
)
if proc.returncode != 0:
raise RuntimeError(f"P2 stage_b_dedup_output.py 退出码: {proc.returncode}")
b_v2_path = episode_dir / "B稿_v2.txt"
if not b_v2_path.exists():
raise FileNotFoundError(f"P2 跑完但 B稿_v2.txt 不存在: {b_v2_path}")
completed_stages.append("P2: 文本去重")
# 读 B稿_v2 行数(无论是否 skip_p1,后续步骤都用得到)
b_v2_path = episode_dir / "B稿_v2.txt"
if b_v2_path.exists():
with open(b_v2_path, "r", encoding="utf-8") as fh:
b_v2_lines = sum(1 for line in fh if line.strip())
elif not skip_p1:
raise FileNotFoundError(f"B稿_v2.txt 不存在: {b_v2_path}")
else:
raise FileNotFoundError(
f"使用 --skip-p1 但 B稿_v2.txt 不存在: {b_v2_path}\n"
"请先跑 P1+P2 或确认 B稿_v2.txt 已就绪。"
)
# ════════════════════════════════════════════════════════
# C1: 术语提取
# ════════════════════════════════════════════════════════
_stage_header("C1: 术语提取")
c1_result = run_terms(
episode_id=episode_id,
a_script_path=a_script_path,
no_ai=False,
)
hotword_count = c1_result.get("hotword_count", 0)
click.echo(f"[run] C1 完成: 规则 {c1_result.get('rule_count', 0)} 条, "
f"AI {c1_result.get('ai_count', 0)} 条, "
f"热词 {hotword_count}")
completed_stages.append("C1: 术语提取")
# ════════════════════════════════════════════════════════
# C2: ASR
# ════════════════════════════════════════════════════════
_stage_header("C2: ASR 转写")
asr_timed_path = episode_dir / "asr_v2_timed.txt"
wav_path = episode_dir / "audio_16k.wav"
# 分离音频(已存在则复用)
if wav_path.exists():
click.echo(f"[run] audio_16k.wav 已存在,复用: {wav_path}")
else:
click.echo("[run] 从视频分离音频(16kHz/单声道/16bit)...")
extract_audio(video_path, wav_path)
click.echo(f"[run] 音频分离完成: {wav_path}")
if asr_timed_path.exists():
click.echo(f"[run] asr_v2_timed.txt 已存在,跳过 ASR(花钱的步骤不重复跑): {asr_timed_path}")
else:
hot_words = get_hot_words(episode_id)
click.echo(f"[run] 热词条数: {len(hot_words)}")
click.echo("[run] 上传音频 → 讯飞 ASR 转写(可能需要数分钟)...")
sentences, raw_order_result = transcribe(str(wav_path), hot_words=hot_words)
asr_sentence_count = len(sentences)
timed_path, raw_path = write_asr_result(
sentences,
str(episode_dir),
raw_order_result=raw_order_result,
)
click.echo(f"[run] ASR 完成: {asr_sentence_count}")
# 如果跳过了 ASR(已存在),读取句子数用于汇总
if asr_sentence_count == 0 and asr_timed_path.exists():
with open(asr_timed_path, "r", encoding="utf-8") as fh:
asr_sentence_count = sum(1 for line in fh if line.strip())
completed_stages.append("C2: ASR")
# ════════════════════════════════════════════════════════
# C3: 融合复审
# ════════════════════════════════════════════════════════
_stage_header("C3: 融合复审")
c3_stats = run_fusion(
episode_id=episode_id,
output_dir=str(episode_dir),
no_ai=False,
batch_size=35,
)
fused_b_lines = c3_stats.get("total_lines", 0)
click.echo(f"[run] C3 完成: 融合B稿 {fused_b_lines}")
completed_stages.append("C3: 融合复审")
# ════════════════════════════════════════════════════════
# C4: 对齐出稿
# ════════════════════════════════════════════════════════
_stage_header("C4: 对齐出稿")
# 检查骨架文件
skeleton_path = episode_dir / f"{episode_id}_a_skeleton.json"
if not skeleton_path.exists():
raise FileNotFoundError(
f"骨架文件不存在: {skeleton_path}\n"
f"骨架需人工核验,请先手动运行: doco skeleton --episode-id {episode_id} "
f"--a-script {a_script}\n"
f"核验无误后,再运行 doco run。"
)
c4_stats = run_compose(
episode_id=episode_id,
output_dir=str(episode_dir),
no_ai=False,
batch_size=batch_size,
)
fused_a_docx = c4_stats.get("docx_path", "")
click.echo(f"[run] C4 完成: 融合A稿 → {fused_a_docx}")
completed_stages.append("C4: 对齐出稿")
except Exception as e:
click.echo("")
click.echo("═════════════════════════════")
click.echo("❌ 流程中断")
click.echo("═════════════════════════════")
if completed_stages:
click.echo("已完成的阶段:")
for s in completed_stages:
click.echo(f"{s}")
click.echo(f"失败阶段: {e}", err=True)
sys.exit(1)
# ── 全部完成 ──
click.echo("")
_stage_header("✅ 全流程完成")
click.echo(f"B稿v2: {b_v2_lines}")
click.echo(f"热词: {hotword_count}")
click.echo(f"ASR: {asr_sentence_count}")
click.echo(f"融合B稿: {fused_b_lines}")
click.echo(f"融合A稿: {fused_a_docx}")
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
+535
View File
@@ -0,0 +1,535 @@
# -*- coding: utf-8 -*-
"""
C3: B稿v2 ⊕ ASR 交叉复审 → 融合B稿(743行) + fusion_review.csv
=============================================================
职责:逐行复审 B稿(屏幕字幕OCR),以 ASR(口语转写)为上下文参考,
只做纠错,严禁改行数/时间戳。
"""
import json
import re
import sys
from pathlib import Path
from typing import List, Dict, Optional
from .llm import chat
# --------------------------------------------------------------------------
# 常量
# --------------------------------------------------------------------------
CHANGE_TYPE_ENUM = frozenset(
{
"unchanged",
"minor_edit",
"term_normalize",
"rewrite_large",
"segment_delete",
"segment_add",
"editor_typo",
}
)
SYSTEM_PROMPT = """你是《军事科技》专题片文稿校审员。给你 B稿(屏幕字幕OCR,逐行碎句,带时间戳) 和对应的 ASR(口语转写)。
你的任务:逐行复审 B稿,只做纠错,绝不合并行、不拆行、不增删行、不改时间戳。
权威优先级:
- 屏幕术语/型号/番号(箭-3/萨德/见证者-136等): B稿为准(屏幕实打的字)
- B稿明显是OCR错字而ASR是对的: 用ASR覆盖
- ⚠️ 专有名词铁律:厂名/型号/番号/国名/人名/机构名等专名,遇B稿与ASR同音异写(如斯泰尔vs斯太尔、美以vs美伊),一律以B稿/A稿书面写法为准,零容忍采ASR。ASR是口语转写,同音字极多,专名绝不信ASR。
- 同音事实错(如"美以"vs"美伊"): 以书面规范为准,存疑进review
- 一两个字的等价差异(的/地、啊等语气): 算 unchanged,不要改
每行输出: line_no, final_text(纠错后,默认等于B原文), change_type(7选1), confidence(0~1), reason(简短,unchanged时留空)
只返回JSON数组,不要任何解释文字。
change_type枚举: unchanged/minor_edit/term_normalize/rewrite_large/segment_delete/segment_add/editor_typo"""
# --------------------------------------------------------------------------
# 1. 解析带时间戳的行
# --------------------------------------------------------------------------
def parse_timed_lines(path) -> List[dict]:
r"""
解析 "[XmYs] 文本" → [{"idx":int, "ts_raw":"0m8s", "ts_sec":8, "text":"导弹呼啸而过"}]
正则: ^\[(\d+)m(\d+)s\]\s*(.*)$ ; ts_sec = m*60+s
解析失败的行要抛异常并打印行号,不许静默跳过
"""
p = Path(path)
if not p.exists():
raise FileNotFoundError(f"文件不存在: {path}")
pattern = re.compile(r"^\[(\d+)m(\d+)s\]\s*(.*)$")
lines_raw = p.read_text(encoding="utf-8").splitlines()
result = []
for idx, line in enumerate(lines_raw, start=1):
line = line.strip()
if not line:
continue # 跳过空行
m = pattern.match(line)
if not m:
raise ValueError(
f"{idx} 解析失败,无法匹配时间戳格式: {repr(line[:120])}\n"
f"文件: {path}"
)
minutes = int(m.group(1))
seconds = int(m.group(2))
ts_raw = f"{minutes}m{seconds}s"
ts_sec = minutes * 60 + seconds
text = m.group(3).strip()
result.append(
{
"idx": idx,
"ts_raw": ts_raw,
"ts_sec": ts_sec,
"text": text,
}
)
return result
# --------------------------------------------------------------------------
# 2. 对齐 ASR 上下文
# --------------------------------------------------------------------------
def align_asr_context(b_lines: List[dict], asr_lines: List[dict]) -> List[str]:
"""
为每个 B 行找时间窗内的 ASR 上下文(用于喂 LLM)
规则: 取 ts_sec 落在 [b_ts-3, b_next_ts+3] 区间的 ASR 句拼接;
边界用前后各扩 1 句兜底。返回与 b_lines 等长的 context 列表
"""
n = len(b_lines)
contexts = []
# 预计算 B 行的时间窗: [b[i].ts_sec - 3, b[i+1].ts_sec + 3]
# 最后一行用 b[i].ts_sec + 10 作为上界
windows = []
for i, bl in enumerate(b_lines):
lo = bl["ts_sec"] - 3
if i + 1 < n:
hi = b_lines[i + 1]["ts_sec"] + 3
else:
hi = bl["ts_sec"] + 10
windows.append((lo, hi))
asr_count = len(asr_lines)
for i, (lo, hi) in enumerate(windows):
# 找到落在窗口内的 ASR 句索引
hit_indices = []
for j, al in enumerate(asr_lines):
if lo <= al["ts_sec"] <= hi:
hit_indices.append(j)
if not hit_indices:
# 无命中:取距离最近的 1 句
best_j = None
best_dist = float("inf")
mid_ts = (lo + hi) / 2
for j, al in enumerate(asr_lines):
dist = abs(al["ts_sec"] - mid_ts)
if dist < best_dist:
best_dist = dist
best_j = j
if best_j is not None:
start_j = max(0, best_j - 1)
end_j = min(asr_count - 1, best_j + 1)
else:
start_j = 0
end_j = 0
else:
# 命中句的范围 + 前后各扩 1
start_j = max(0, hit_indices[0] - 1)
end_j = min(asr_count - 1, hit_indices[-1] + 1)
# 拼接 [start_j, end_j] 的 ASR 文本
selected = asr_lines[start_j : end_j + 1]
context = " ".join(s["text"] for s in selected)
contexts.append(context)
assert len(contexts) == n, f"context 列表长度 {len(contexts)} != B 稿行数 {n}"
return contexts
# --------------------------------------------------------------------------
# 3. 构造 Prompt
# --------------------------------------------------------------------------
def build_prompt(batch_b: List[dict], batch_ctx: List[str]) -> List[dict]:
"""
构造 messages,见下方"四、Prompt 模板"
"""
assert len(batch_b) == len(batch_ctx), (
f"batch_b({len(batch_b)}) 与 batch_ctx({len(batch_ctx)}) 长度不一致"
)
user_lines = []
for bl, ctx in zip(batch_b, batch_ctx):
line_no = bl["idx"]
b_text = bl["text"]
asr_text = ctx if ctx else "(无ASR上下文)"
user_lines.append(
f"[行{line_no}] B稿: \"{b_text}\" ASR上下文: \"{asr_text}\""
)
user_content = "\n".join(user_lines)
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_content},
]
return messages
# --------------------------------------------------------------------------
# 4. 单批复审
# --------------------------------------------------------------------------
def review_batch(
batch_b: List[dict], batch_ctx: List[str], no_ai: bool = False
) -> List[dict]:
"""
no_ai=True: 直接回填 unchanged(final_text=b原文, change_type="unchanged", confidence=1.0)
no_ai=False: 调 llm.chat(messages, thinking=False, max_tokens=4000, temperature=0.0)
解析返回 JSON 数组; 每元素 {line_no, final_text, change_type, confidence, reason}
返回标准化记录列表
"""
if no_ai:
records = []
for bl in batch_b:
records.append(
{
"line_no": bl["idx"],
"final_text": bl["text"],
"change_type": "unchanged",
"confidence": 1.0,
"reason": "",
}
)
return records
# ---- AI 路径 ----
messages = build_prompt(batch_b, batch_ctx)
try:
raw_response = chat(
messages,
thinking=False,
max_tokens=4000,
temperature=0.0,
)
except Exception as e:
print(
f"[fusion_review] LLM 调用失败,回退为 unchanged 批次: {e}",
file=sys.stderr,
)
# 回退:全部 unchanged
records = []
for bl in batch_b:
records.append(
{
"line_no": bl["idx"],
"final_text": bl["text"],
"change_type": "unchanged",
"confidence": 1.0,
"reason": f"LLM调用失败回退: {str(e)[:80]}",
}
)
return records
# 解析 JSON
parsed = _parse_llm_json_response(raw_response, len(batch_b))
# 标准化并校验
records = []
for item in parsed:
line_no = item.get("line_no")
final_text = item.get("final_text", "")
change_type = item.get("change_type", "unchanged")
confidence = item.get("confidence", 1.0)
reason = item.get("reason", "")
# 校验 change_type
if change_type not in CHANGE_TYPE_ENUM:
original_ct = change_type
print(
f"[fusion_review] 行 {line_no} 非法 change_type='{original_ct}', 强制改为 unchanged",
file=sys.stderr,
)
change_type = "unchanged"
final_text = "" # 下面会回填
reason = f"LLM返回非法change_type({original_ct}),回退unchanged"
# 如果 change_type 被改为 unchanged 但 final_text 为空,回填 B 原文
if change_type == "unchanged" and not final_text:
# 从 batch_b 找回原文
for bl in batch_b:
if bl["idx"] == line_no:
final_text = bl["text"]
break
records.append(
{
"line_no": line_no,
"final_text": final_text,
"change_type": change_type,
"confidence": float(confidence),
"reason": reason or "",
}
)
return records
def _parse_llm_json_response(raw: str, expected_len: int) -> List[dict]:
"""解析 LLM 返回的 JSON,处理 markdown code fences 等常见包装。"""
text = raw.strip()
# 去掉可能的 markdown code fences
if text.startswith("```"):
lines = text.splitlines()
# 去掉第一行 ```json 或 ```
if lines and lines[0].startswith("```"):
lines = lines[1:]
# 去掉最后一行 ```
if lines and lines[-1].strip() == "```":
lines = lines[:-1]
text = "\n".join(lines).strip()
try:
result = json.loads(text)
except json.JSONDecodeError as e:
raise ValueError(
f"LLM 返回 JSON 解析失败: {e}\n"
f"原始响应前 500 字符: {raw[:500]}"
)
if not isinstance(result, list):
raise ValueError(
f"LLM 返回不是 JSON 数组,类型为 {type(result).__name__}"
)
if len(result) != expected_len:
raise ValueError(
f"LLM 返回 {len(result)} 条记录,期望 {expected_len} 条。"
f"该批次将回退为 unchanged 并重新请求。"
)
return result
# --------------------------------------------------------------------------
# 5. 主流程
# --------------------------------------------------------------------------
def run_fusion(
episode_id: str,
output_dir: str,
no_ai: bool = False,
batch_size: int = 35,
) -> dict:
"""
主流程:
1. 读 output_dir/B稿_v2.txt → b_lines(断言行数>0
2. 读 output_dir/asr_v2_timed.txt → asr_lines
3. align_asr_context 生成等长 context
4. 按 batch_size 分块;每块结果落缓存,已存在则复用(断点续跑)
5. 逐块 review_batch,汇总所有记录
6. 硬校验(任一不过就 raise,不写出文件)
7. 写 output_dir/融合B稿.txt
8. 写 output_dir/fusion_review.csv
9. 返回统计 dict
"""
out_dir = Path(output_dir)
out_dir.mkdir(parents=True, exist_ok=True)
b_path = out_dir / "B稿_v2.txt"
asr_path = out_dir / "asr_v2_timed.txt"
if not b_path.exists():
raise FileNotFoundError(f"B稿_v2.txt 不存在: {b_path}")
if not asr_path.exists():
raise FileNotFoundError(f"asr_v2_timed.txt 不存在: {asr_path}")
# Step 1: 解析 B 稿
b_lines = parse_timed_lines(b_path)
assert len(b_lines) > 0, f"B稿_v2.txt 解析后为空: {b_path}"
# Step 2: 解析 ASR
asr_lines = parse_timed_lines(asr_path)
# Step 3: 对齐 ASR 上下文
contexts = align_asr_context(b_lines, asr_lines)
assert len(contexts) == len(b_lines), (
f"context 长度 {len(contexts)} != B 稿行数 {len(b_lines)}"
)
# Step 4: 分块 + 缓存
cache_dir = out_dir / ".c3_cache"
cache_dir.mkdir(parents=True, exist_ok=True)
all_records = []
total_batches = (len(b_lines) + batch_size - 1) // batch_size
for batch_idx in range(total_batches):
start = batch_idx * batch_size
end = min(start + batch_size, len(b_lines))
batch_b = b_lines[start:end]
batch_ctx = contexts[start:end]
cache_path = cache_dir / f"batch_{batch_idx}.json"
if cache_path.exists():
# 断点续跑:复用缓存
try:
cached = json.loads(cache_path.read_text(encoding="utf-8"))
print(f"[fusion_review] 复用缓存 batch_{batch_idx} ({len(cached)} 条)")
all_records.extend(cached)
continue
except Exception as e:
print(
f"[fusion_review] 缓存 batch_{batch_idx} 损坏,重新计算: {e}",
file=sys.stderr,
)
print(
f"[fusion_review] 复审 batch {batch_idx + 1}/{total_batches} "
f"(行 {start + 1}-{end})..."
)
try:
batch_records = review_batch(batch_b, batch_ctx, no_ai=no_ai)
except Exception as e:
print(
f"[fusion_review] batch {batch_idx + 1} 失败,跳过缓存写入: {e}",
file=sys.stderr,
)
# 不写缓存,下次重跑时重新请求该批
continue
# 写入缓存
cache_path.write_text(
json.dumps(batch_records, ensure_ascii=False, indent=2),
encoding="utf-8",
)
all_records.extend(batch_records)
# Step 6: 硬校验
_hard_validate(all_records, b_lines)
# Step 6.5: 修正语义——final_text 等于 B 原文的行强制归为 unchanged
_normalize_unchanged_when_no_edit(all_records, b_lines)
# Step 7: 写 融合B稿.txt
fused_path = out_dir / "融合B稿.txt"
fused_lines = []
for rec, bl in zip(all_records, b_lines):
fused_lines.append(f"[{bl['ts_raw']}] {rec['final_text']}")
fused_path.write_text("\n".join(fused_lines) + "\n", encoding="utf-8")
# Step 8: 写 fusion_review.csv
csv_path = out_dir / "fusion_review.csv"
csv_rows = [
"line_no,timestamp,b_original,asr_context,final_text,change_type,confidence,reason"
]
for rec, bl, ctx in zip(all_records, b_lines, contexts):
if rec["change_type"] == "unchanged" and rec["confidence"] >= 0.8:
continue # 只写需要 review 的行
# CSV 转义: 字段含逗号或引号时用双引号包裹
row_fields = [
str(rec["line_no"]),
bl["ts_raw"],
bl["text"],
ctx,
rec["final_text"],
rec["change_type"],
str(rec["confidence"]),
rec["reason"],
]
csv_rows.append(_csv_row(row_fields))
csv_path.write_text("\n".join(csv_rows) + "\n", encoding="utf-8")
# Step 9: 统计
stats = {
"total_lines": len(b_lines),
"change_counts": {},
"review_lines": 0,
}
for rec in all_records:
ct = rec["change_type"]
stats["change_counts"][ct] = stats["change_counts"].get(ct, 0) + 1
if ct != "unchanged" or rec["confidence"] < 0.8:
stats["review_lines"] += 1
print(f"[fusion_review] 融合完成: 总行数={stats['total_lines']}")
print(f"[fusion_review] 各 change_type 计数: {stats['change_counts']}")
print(f"[fusion_review] 进 review 行数: {stats['review_lines']}")
print(f"[fusion_review] 融合B稿: {fused_path}")
print(f"[fusion_review] review CSV: {csv_path}")
stats["fused_path"] = str(fused_path)
stats["csv_path"] = str(csv_path)
return stats
def _hard_validate(records: List[dict], b_lines: List[dict]) -> None:
"""硬校验,任一不过就 raise ValueError,不写出文件。"""
# 校验 1: 行数必须相等
if len(records) != len(b_lines):
raise ValueError(
f"行数不一致: records={len(records)}, B稿={len(b_lines)}"
)
# 校验 2: 逐行时间戳不能被动
for i, (rec, bl) in enumerate(zip(records, b_lines)):
rec_line_no = rec.get("line_no")
expected_line_no = bl["idx"]
if rec_line_no != expected_line_no:
raise ValueError(
f"{i} 条记录 line_no={rec_line_no}, 期望 {expected_line_no}"
)
# 校验 3: change_type 枚举
for rec in records:
ct = rec.get("change_type", "")
if ct not in CHANGE_TYPE_ENUM:
raise ValueError(
f"{rec.get('line_no')}: 非法 change_type='{ct}'"
)
def _normalize_unchanged_when_no_edit(
records: List[dict], b_lines: List[dict]
) -> None:
"""修正语义:final_text 等于 B 原文的行,强制归为 unchanged。
LLM 有时会把"考虑后决定保留 B 稿"标成 minor_edit,
但实际 final_text == b_original, 这不在留痕范围内。
"""
b_text_by_idx = {bl["idx"]: bl["text"] for bl in b_lines}
fixed = 0
for rec in records:
line_no = rec.get("line_no")
b_orig = b_text_by_idx.get(line_no)
if b_orig is not None and rec.get("final_text") == b_orig:
if rec.get("change_type") != "unchanged":
rec["change_type"] = "unchanged"
rec["confidence"] = 1.0
rec["reason"] = ""
fixed += 1
if fixed:
print(
f"[fusion_review] 修正 {fixed} 行: final_text==B原文但change_type≠unchanged, 已强制归为 unchanged"
)
def _csv_row(fields: List[str]) -> str:
"""将字段列表格式化为 CSV 行,处理逗号和引号转义。"""
escaped = []
for f in fields:
s = str(f)
if "," in s or '"' in s or "\n" in s:
s = s.replace('"', '""')
s = f'"{s}"'
escaped.append(s)
return ",".join(escaped)
+176
View File
@@ -0,0 +1,176 @@
"""
将 20 期融合A稿 (.docx) 转为带 YAML frontmatter 的 .md 文件,
供 TPS 主项目知识库 /api/knowledge/upload 批量导入。
用法:
cd E:\tps-dashboard\doco
python convert_to_md.py
产物落在 doco/deliverables/ 目录下,每期一个 .md 文件。
"""
import glob
import os
import re
import sys
from datetime import datetime
from pathlib import Path
from docx import Document
PROGRAMS_DIR = Path(__file__).parent / "programs"
OUTPUT_DIR = Path(__file__).parent / "deliverables"
# ep001 文件名没有编导和标题信息,需要手工补
EP001_OVERRIDE = {
"title": "现代防空反导大对决",
"author": "", # ← 通哥填
"date": "2026-06-12",
}
def find_fusion_docx(episode_dir: Path) -> Path | None:
"""在 episode 目录里找融合A稿 docx(排除批改稿)。"""
candidates = []
for f in episode_dir.iterdir():
if f.suffix == ".docx" and "融合A稿" in f.name and "批改" not in f.name:
candidates.append(f)
if not candidates:
return None
if len(candidates) == 1:
return candidates[0]
# 优先选带日期前缀的(ep002+ 格式)
for c in candidates:
if re.match(r"\d{8}", c.stem):
return c
return candidates[0]
def parse_metadata_from_filename(docx_path: Path, episode_dir_name: str) -> dict:
"""
从文件名提取元数据。
ep002+ 文件名格式:{YYYYMMDD}{标题}_{编导}_融合A稿.docx
ep001 特殊处理。
"""
if episode_dir_name.startswith("ep001"):
return EP001_OVERRIDE.copy()
stem = docx_path.stem # e.g. "20260127潜艇的仿生之路_穆佩弦_融合A稿"
# 去掉 "_融合A稿" 后缀
stem = re.sub(r"_融合A稿$", "", stem)
# 提取日期 (前8位)
date_str = stem[:8]
try:
dt = datetime.strptime(date_str, "%Y%m%d")
date_iso = dt.strftime("%Y-%m-%d")
except ValueError:
date_iso = ""
rest = stem[8:] # e.g. "潜艇的仿生之路_穆佩弦"
# 最后一个 _ 分隔编导
parts = rest.rsplit("_", 1)
if len(parts) == 2:
title, author = parts
else:
title = rest
author = ""
return {"title": title, "date": date_iso, "author": author}
def docx_to_markdown(docx_path: Path) -> str:
"""将融合A稿 docx 转为 markdown 正文。"""
doc = Document(str(docx_path))
lines = []
for para in doc.paragraphs:
text = para.text.strip()
if not text:
continue
# 大标题(第一段,通常带书名号)
if not lines and (text.startswith("") or text.startswith("") is False):
lines.append(f"# {text}")
lines.append("")
continue
# 段头标签:【主持人1】【解说3】【专家2】【导视】隔断:【...】
if re.match(r"^【.+?】$", text) or re.match(r"^隔断\d*[:]【.+?】", text):
lines.append("")
lines.append(f"## {text}")
lines.append("")
continue
# 普通正文段落
lines.append(text)
lines.append("")
return "\n".join(lines).strip() + "\n"
def build_frontmatter(meta: dict, episode_id: str) -> str:
"""生成 YAML frontmatter。"""
fm_lines = ["---"]
fm_lines.append(f"标题: {meta['title']}")
if meta["author"]:
fm_lines.append(f"编导: {meta['author']}")
if meta["date"]:
fm_lines.append(f"播出日期: {meta['date']}")
fm_lines.append("类型: 节目文稿")
fm_lines.append(f"期号: {episode_id}")
fm_lines.append("---")
return "\n".join(fm_lines)
def main():
OUTPUT_DIR.mkdir(exist_ok=True)
# 找所有 ep??? 目录(排除 ep002_004 这种非标准的)
episode_dirs = sorted(
d
for d in PROGRAMS_DIR.iterdir()
if d.is_dir() and re.match(r"^ep\d{3}_\d{8}_", d.name)
)
print(f"找到 {len(episode_dirs)} 个期目录")
success = 0
errors = []
for ep_dir in episode_dirs:
episode_id = ep_dir.name.split("_")[0] # ep001, ep002, ...
docx_path = find_fusion_docx(ep_dir)
if docx_path is None:
errors.append(f"{ep_dir.name}: 未找到融合A稿")
continue
print(f" {episode_id}: {docx_path.name}")
meta = parse_metadata_from_filename(docx_path, ep_dir.name)
frontmatter = build_frontmatter(meta, episode_id)
body = docx_to_markdown(docx_path)
# 输出文件名:ep001_现代防空反导大对决.md
safe_title = meta["title"].replace(":", "").replace("/", "_")
out_name = f"{episode_id}_{safe_title}.md"
out_path = OUTPUT_DIR / out_name
with open(out_path, "w", encoding="utf-8") as f:
f.write(frontmatter + "\n\n" + body)
success += 1
print(f"\n完成:{success}/{len(episode_dirs)} 期转换成功")
if errors:
print("失败:")
for e in errors:
print(f" {e}")
print(f"\n产物目录:{OUTPUT_DIR}")
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
+144
View File
@@ -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 ClaudeOpus 4.7
> 状态:草稿 / 待主项目审
> 上一版:v12026-06-12 同日,更早一稿)
> 主要变化:
> 1. **子模块正式定名为「Doco 文稿整理模块」**v1 沿用旧称"TPS 三方融合子模块",本版起全部改用 Doco;Q6 已由制片人拍板,不再列为待决问题)
> 2. **Q1OCR 选型)方向更新**:制片人确认"软件离线刚需"已不成立,云端方案可接受。补充查清的关键事实:**讯飞 OCR 与 STT 共享鉴权凭证但套餐/余额各自独立**——按制片人原判据"如果能通用 token 订阅和余额就选讯飞"不成立。**新推荐方案:DeepSeek API**(制片人手上已有),并向主项目确认是否已有 OCR 能力或 LLM 视觉调用基础设施
> 3. 其他 QQ2/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.60.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 服务**——如果存在
**子项目推荐**ADeepSeek 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 拍板用 DeepSeekDeepSeek 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 ClaudeOpus 4.7)于 2026-06-12 拟稿,遵照《跨 project 协作提示单 v1》§三的规范。*
+141
View File
@@ -0,0 +1,141 @@
# Doco 子项目 · P1 完工快照 · 交接给 P2(OCR 阶段)
> 起草:Opus 顾问(本对话)
> 日期:2026-06-15
> 用户:制片人刘通(《军事科技》栏目 TPS 中台主理人)
> 上一份:`doco_handoff_to_next_chat.md`(P1 调试中,bug 未解)
> 状态:**P1 dry-run 已通过自检与人工验收,P1 完工**
---
## 一、一句话总结
P1(视频双路拆分预处理)的抽帧→空白过滤→去重→关键帧流水线已跑通并通过全部验收,可进入 OCR 阶段。OCR 选型方向已更正为**本地部署 DeepSeek-OCR(经 Ollama)**。
---
## 二、P1 做了什么
- **输入**:一期节目视频(黑底白字字幕 + 干净人声)+ A 稿
- **本阶段产物**:`frames/`(308 张字幕关键帧)+ keyframes 数据;音轨/B 稿是后续阶段
- **流程**:ffmpeg 抽帧(1fps,crop 到下方 20% 全宽)→ 空白帧过滤 → dHash + IoU 去重 → 关键帧
- **demo 视频**:《现代防空反导大对决》(26 分钟)
---
## 三、本轮修复的 3 个 bug(都已修好、已验收)
交接前的核心卡点是「`is_blank_frame()` 读的图 ≠ frames/ 里的图」,自检反复中止。逐层挖下去,实际是三个独立 bug:
### bug 1 · off-by-one 索引错位(核心)
- **根因**:`extract_frames()``enumerate()` 从 0 开始编号(frame_index 0-based),但下游清理逻辑和自检逻辑都从文件名提取 1-based 编号(`frame_0008.png` → 8),两套编号差 1。
- **后果**:空白帧因编号错位被错误保留;自检重读图片取到正确像素,却去查 CSV 错误的行 → 报告不一致、中止。
- **不是**"读了不同目录的图"——所有判断函数始终从同一个 frames/ 读图,是编号体系不一致。
- **修复**:改为直接从文件名提取 1-based 编号。commit `bc5a30e`
### bug 2 · 去重计数器恒为 0
- **根因**:`decision_stats[...] = decision_stats.get(..., 0)` 缺了 `+ 1`,每次迭代没递增。
- **后果**:统计永远显示"筛掉 0 张 IoU + 0 张哈希",看起来像去重没生效。**但去重逻辑本身一直是对的**,只是计数显示错。
- **修复**:加 `+ 1`。commit `d55f029`
### bug 3 · 2 个失败的单元测试(均与本次修改无关的旧断言问题)
- `test_subtitle_frame`:切片比例写错(注释说 1% 实际取了 10%),修改前就在失败。改 `arr[:100,:10]``arr[:10,:10]`
- `test_ratio_ok_but_max_brightness_too_low`:亮度阈值从 200→240 后,亮度 220 的像素不再算白像素,旧断言没同步。改断言为 `white_ratio == 0.0`
- **修复**:commit `82973a7`,17/17 全过。
---
## 四、验收数据(已核对,数字闭环)
本次 dry-run 全帧去向(1620 帧):
| 类别 | 张数 |
|---|---|
| 空白帧(blank) | 227 |
| IoU 重复(duplicate-iou) | 321 |
| 哈希重复(duplicate-hash) | 764 |
| 首帧保留(kept) | 1 |
| 关键帧(keyframe-hash) | 307 |
| **合计** | **1620** ✅ |
- 最终关键帧 = 1 + 307 = **308 张**,与 `frames/` 实际文件数一致。
- 旁证:demo 这期约 300 条字幕,关键帧 308 ≈ 字幕数,基本一对一,说明去重准确(每条字幕保留一张、砍掉重复画面)。
- **人工验收**:制片人手动翻遍 frames/,确认无任何纯黑帧。
- **单元测试**:17/17 通过。
---
## 五、遗留小尾巴(不影响完工,有空再收)
- `test_ratio_ok_but_max_brightness_too_low` 这个测试名与它现在实际测的行为对不上了(白像素阈值提到 240 后,原本想测的"白像素够但亮度不够"场景已构造不出来,断言被改成了 white_ratio==0)。建议以后让 Cline 给这个测试改名或重构。纯属测试整洁度,不影响流水线正确性。
---
## 六、重要更正 · OCR 选型(覆盖 PRD v2 的 Q1 表述)
**PRD v2 写的"DeepSeek 有 OCR 模型 + Vision 识图模式(官方 API endpoint)"——这条事实有误,作废。**
查证结果(2026-06-15,DeepSeek 官方文档):
- DeepSeek 官方 API 只有 `deepseek-v4-flash` / `deepseek-v4-pro` 两个模型,**纯文本,不收图像输入**(功能仅:文本对话 / Tool Calls / JSON 输出 / FIM)。Cline 里 DeepSeek 预设没有 image 选项是正常的,不是配置漏项。
- `DeepSeek-OCR`(3B,中文文档识别强)是**开源模型,只能本地部署**(vLLM 或 Ollama),**没有现成云端 API**,需自备 GPU。
- 因此制片人手上的 DeepSeek API key **调不了 OCR**
**新方向(已与制片人确认)**:本地部署 DeepSeek-OCR,经 **Ollama** 运行。
- 理由:制片人有 RTX 4090D 24GB(跑 3B OCR 模型富余);零调用成本、不限量;栏目全年 52 期常态化使用,长期划算;**且制片人另有一个离线小机房,只能用本地方案,云端 API 调不通**。
- Windows 上优先 Ollama(原生支持、免 WSL),不碰 vLLM(Windows 上一般要 WSL,门槛高)。
> 候选备选(若本地部署受阻):MiniMax-VL 云端 API、讯飞 OCR。但离线小机房只能本地。
---
## 七、下一步(P2 / OCR 阶段)
1. **本地部署 DeepSeek-OCR(Ollama)**:在制片人栏目主机上搭起来,确认能识别黑底白字字幕关键帧;之后同样流程部署到离线小机房。
2. **OCR 适配层 `ocr_adapter.py`**:把 308 张关键帧批量送入本地 OCR 服务,产出带 `[Nm Ns]` 时间戳的 **B 稿 txt**(格式必须与旧版完全一致,下游 P3 才零侵入)。
3. **去掉 `--dry-run`**,跑正式 OCR,产出 B 稿 + audio_16k.wav + keyframes.json,P1 整体产物齐活。
4. 之后才进 P2 的讯飞 ASR 适配、P3 的 Claude 融合层。
---
## 八、关键决策(已锁,沿用)
| 项 | 值 |
|---|---|
| 字幕裁切区域 | `crop=iw:ih*0.2:0:ih*0.8`(全宽 × 下方 20%),写死,全年 52 期一致 |
| 抽帧密度 | 1fps |
| 哈希算法 | dHash 阈值 5,pHash 阈值 2 |
| IoU 保底阈值 | 0.95 |
| 空白帧双条件 | `max_brightness >= 240` AND `white_pixel_ratio >= 0.005` |
| 凭证管理 | doco 自管 `doco/.env`,主仓库根 `docs/api_credentials_inventory.md` 只记元信息 |
| **OCR 提供方** | **本地 DeepSeek-OCR(经 Ollama)** ← 本次更正,原 PRD 的"DeepSeek 云端 Vision API"作废 |
| ASR 提供方 | 讯飞录音文件转写标准版(P2 才用) |
| AI 融合层 | Claude Sonnet 4.6(P3 才用) |
| 抽帧/抽音轨 | subprocess 调系统 ffmpeg,不引入 ffmpeg-python |
| Git workflow | 按宪法直接推 main,不开分支 |
---
## 九、环境信息
- Windows + Python 3.12.10 + `.venv`(主项目共享虚拟环境)
- VS Code + Cline 插件(编程模型已从 Minimax M3 换为 **deepseek-v4-pro**,响应更快)
- 仓库根:`E:\tps-dashboard\`(VS Code 必须打开仓库根,不能打开子目录)
- 主仓库:`simonkoson/tps-dashboard`(自部署 git,localhost:3000)
- ffmpeg:winget 安装,系统 PATH
- GPU:**NVIDIA RTX 4090 D 24GB**(本地 OCR 部署用)
- 另有一台**离线小机房**(无外网,本地部署的主要落地环境之一)
- 正确 CLI:`doco split --episode-id XXX --input-video XXX --output-dir XXX [--dry-run]`
---
## 十、本轮 commit(按时间)
| commit | 内容 |
|---|---|
| `bc5a30e` | extract_frames:frame_index 从 0-based enumerate 改为 1-based 文件名提取(核心 bug) |
| `d55f029` | decision_stats 计数加 `+ 1`(去重计数器恒为 0) |
| `82973a7` | 修复 2 个测试断言(切片比例 + 阈值适配),17/17 通过 |
---
*P1 至此完工。下一轮从「Ollama 本地部署 DeepSeek-OCR」开始。*
+118
View File
@@ -0,0 +1,118 @@
# Doco 子项目 · P2 完工快照
> 模块定位:《军事科技》中台组成部分。将节目稿件半成品(A稿)、OCR 扒词文档(B稿)、节目纯净声音(ASR)交叉验证融合,产出最接近播出的终版文稿。
> P2 阶段目标:本地部署免费 OCR + 重写字幕扒词流水线,产出高质量 B 稿。
> 状态:**已完工**。本期(ep001 现代防空反导大对决)B稿_v2.txt 定稿,743 行。
> 快照时间:2026-06-15
---
## 一、本阶段为什么存在 / 解决了什么
接手时,旧帧筛流水线把本期七百多条字幕压成了 **308 张关键帧,悄无声息丢了约六成**,且被"308 张 ≈ 300 条字幕,基本一对一"这个**巧合**骗过了验收。
经逐帧决策表(frame_analysis_debug.csv)定位,根因是**用像素启发式去猜字幕变没变**,两个独立 bug:
- **空白过滤误杀**:`max_brightness>=240 AND white_ratio>=0.005` 把字幕淡入/切换的暗帧判成空场(如"我是主持人蓝皓"被剔除)。
- **dHash 距离去重误并**:字幕版式全相同(黑底白字底部居中),仅文字不同,感知哈希分不开"同一条的不同帧"与"两条不同字幕";阈值 5 把不同内容误判为重复(如"欢迎收看《军事科技》"被砍)。
**结论**:像素相似度无法承担"字幕是否改变"的裁判职责。改为 **OCR 优先 + 按文本去重**——把裁判权交给文本(唯一真相层)。
---
## 二、部署:Ollama + DeepSeek-OCR(可复制到离线小机房)
- DeepSeek-OCR 已是 **Ollama 官方库正式模型**,无需第三方 GGUF。`ollama pull deepseek-ocr`
- 模型 `deepseek-ocr:latest` = `:3b`,**6.7GB**(显存占用约 7.8GB),8K context,Text+Image。
- 要求 Ollama **≥ v0.13.0**;本机实测 0.30.8。
- GPU:制片人主机 RTX 4090D 24GB,`ollama ps` 确认 `100% GPU`
- 调用:`POST http://localhost:11434/api/generate`,body `{"model":"deepseek-ocr","prompt":"Free OCR.","images":[<base64>],"stream":false,"keep_alive":-1}`,读 `response` 字段。
- **Prompt 用 `Free OCR.`**;实测黑底白字单行字幕识别准、标点干净、无需图像预处理。**禁用** `<|grounding|>...markdown`(会输出 bounding box / markdown 污染纯文本)。
### 部署踩坑结论(每条都是教训,务必保留)
- **`keep_alive:-1`**:不设的话模型闲置 5 分钟被踢出显存,下个请求遇重载空档报 HTTP 503。设 -1 永久常驻。
- **Windows 控制台编码**:脚本顶部 `sys.stdout.reconfigure(encoding="utf-8", errors="replace")`;进度打印**只打数字、不打中文**,否则 GBK 控制台 `UnicodeEncodeError` 崩进程。
- **写文件用 UTF-8**:jsonl 写入显式 `encoding="utf-8"`(`ensure_ascii=False` 可选,转义不影响 `json.loads` 解回)。
- **GPU 利用率低是正常的**:单张小图 OCR,GPU 算一下歇一下,空隙是读图/编码/HTTP/写盘。判断在不在 GPU 看 `ollama ps` 的 PROCESSOR 列和显存占用,**不看利用率百分比**。
### 离线小机房搬运(待执行)
小机房无外网,不能 `ollama pull`。在主机 pull 后,整拷 `C:\Users\<用户名>\.ollama\models`(或经 `OLLAMA_MODELS` 指定的目录)到小机房同路径。
---
## 三、流水线架构:两阶段解耦
**设计原则**:Stage A(贵、慢、易中断)与 Stage B(纯文本、秒级、可反复重跑)完全解耦,中间用 `ocr_raw.jsonl` 缓存衔接。这套设计在本期实战中扛住了 Cline 反复掐进程,**一帧 OCR 没有白跑**。
### Stage A — `stage_a_extract_ocr.py`(抽帧 + OCR)
- ffmpeg 1fps 抽帧 + crop 下方 20%(滤镜 `fps=1,crop=iw:ih*0.2:0:ih*0.8`),输出 `frames_v2/frame_%04d.png`(1-based,`t_sec = NNNN-1`)。
- **每帧都 OCR,绝不做亮度判空、绝不做 dHash/IoU 过滤**(这是硬约束,丢六成的元凶)。
- 结果逐帧追加写 `ocr_raw.jsonl`:`{"idx","t_sec","text"}`,异常帧写 `{..,"text":"","error":...}`
- **健壮性三件套**:① 启动读已有最大 idx 断点续跑,绝不从头/覆盖;② 单帧 `try/except Exception + continue`,异常绝不冒泡崩主循环;③ 503/超时指数退避重试。
### Stage B — `stage_b_dedup_output.py`(文本去重 + 出稿)
- 只读 `ocr_raw.jsonl`,可反复重跑调阈值。
- **连续段折叠**(裁判在此,基于文本不基于像素):只合并**时间相邻**帧,文本 difflib 相似度 **≥ 0.85** 视为同一条;**只折叠连续段,绝不全局去重**(片头片尾都出现"军事科技"是两条合法记录)。段内**多数投票**取最终文本,**取最早 t_sec** 为时间戳。
- 出稿前清 markdown 残留:`re.sub(r'^[#*\->`\s]+','',text).strip()`(DeepSeek-OCR 偶发 `# ` 行首标题)。
- 输出:`B稿_v2.txt`(`[XmYs] 文本`,格式同旧 B 稿)、`dedup_debug.csv`(逐帧判决)、`blank_filtered.txt`(被判空场的非空文本存档,供审计)。
### `is_blank_ocr(text)` — 空场判定(三选一即空场)
DeepSeek-OCR 在空白/黑场帧会**幻觉**出固定的英文财报表 `<table>...As of December 31...Total return...</table>`,不返回空串。故空场判定基于文本:
1. `strip()` 后为空串;
2. 含 HTML 标签(`<``>`);
3. 不含任何汉字(`\u4e00-\u9fff`)。
真字幕是纯中文短句,三条都不会误伤。
---
## 四、本期验收数据(ep001)
| 指标 | 数值 |
|---|---|
| 抽帧总数 | 1620 |
| OCR 帧数 / error 帧 | 1620 / **0** |
| 空场数 | 207 |
| 被判空场但非空(去重) | **1 条**(`<table>` HTML 幻觉,正确拦截) |
| **B稿_v2.txt 行数** | **743**(旧 B 稿 742,基本持平) |
| 行首 `#` 残留 | 0 |
**质量结论(对旧 B 稿)**:
- 完整性:从灾难性的 308 → 743,**捞回六成丢失字幕**。两条标志性失踪字幕("欢迎收看《军事科技》""我是主持人蓝皓")均恢复。
- 准确性:归一化后约 589 行与旧稿完全一致;约 130 条差异**绝大多数是新流水线把旧 OCR 错字改对**(盹→吨、肘空→时空、差导→差异、领士→领土、短柝→短板、交镎→交锋、范匡→范围、娈得→变得…),且中文引号更规整。**新 B 稿质量明显高于旧版。**
---
## 五、实战教训(开发期摩擦,非代码缺陷)
- **Cline 反复掐进程**:Cline 作为开发期 agent,天性"检查→行动→再检查",每次"出报告/改加固/重启续跑"都会掐掉前台 OCR 进程,崩点一路从 idx 249→503→756 后移,误以为是 bug。**解法**:让 Cline 彻底松手,改由**制片人自己在独立终端**(`.venv` 激活后 `python stage_a...py`)运行,Cline 碰不到。→ 一次性稳定跑满 1620。
- **进度监控靠文件、不靠终端**:`(Get-Content ocr_raw.jsonl).Count` 数行数判进度;停在某数不动=进程停了,数字在涨=在跑。
- **"503"歧义**:一度把"jsonl 写到 503 行"误读为"HTTP 503",白做一版重试补丁。教训:先看清是行数还是错误码。
> 关键认知:Cline 是脚手架不是运行时。脚本本身是确定性程序,产品化后无人值守运行不会再有被掐现象。本期"自己开终端跑"已提前演练了脱离 Cline 的运行方式。
---
## 六、待办(P2 收尾遗留,进 P3 后并行处理)
- **并发版 Stage A 提速**(制片人已提出):当前严格串行,~30 分钟/期,GPU 大量空转。首选方案:并发 4~8 路请求(`OLLAMA_NUM_PARALLEL`,3B 模型在 24G 卡轻松扛),预计压到十分钟出头。可选叠加"像素完全相同(hash 距离=0)帧复用上帧结果"无损省 OCR。vLLM 原生批量是高上限方案但 Windows 难装、破坏可移植性,非瓶颈不碰。
- **离线小机房模型搬运**(见第二节)。
- **空场清单/审计文件**已落地(`blank_filtered.txt`)。
---
## 七、P3 衔接:三方交叉融合(下一阶段)
**三路输入(本期实测体量与粒度)**:
- **A 稿**(`A稿_..._定稿.docx`,实为 markdown 文本):138 段**散文脚本**,按【导视】【主持人N】【解说N】【专家N】【动画N】【隔断】分段。编辑书面版,**与播出是改写关系**(例:A稿"欢迎来到这一期的《军事科技》节目"→播出"欢迎收看《军事科技》")。**不可逐行 diff**,只宜段落/话题级对齐。
- **B 稿 v2**(`B稿_v2.txt`):743 行**逐条短字幕**,屏幕实况。
- **ASR**(`asr_result_timed.txt`):306 行**句级口语实况**(每行一时间戳起点,常含多句)。
**三方可信维度(P3 融合的核心依据)**:
- A 稿 → 管**结构与书面措辞**(分段、专有名词的规范写法、完整语义)。
- ASR → 管**口语实况**(主持人/专家实际念了什么,含临场改词)。
- B 稿 → 管**屏幕术语拼写**(型号/番号/武器名,如"箭-3""萨德""见证者-136",ASR 常听岔)。
**P3 目标**:以时间戳为轴对齐三方,按各自可信维度投票/择优,产出最接近播出的终版稿。比 P2 复杂,需单独设计。
---
*P2 完工。下一步:P3 设计。*
+146
View File
@@ -0,0 +1,146 @@
# Doco 子项目 · P3 设计稿(三方交叉融合)
> 模块定位:《军事科技》中台终极目标。把 A 稿(脚本)、B 稿 v2(屏幕字幕)、ASR(口语实况)三方交叉融合,产出最接近播出的终版文稿。
> 前置:P1(帧筛)、P2(本地 OCR + 扒词流水线,产出 B稿_v2.txt 743 行)已完工。
> 状态:**设计冻结,待开发**。
> 设计时间:2026-06-15
> 本稿在原 `doco_project_design.md` 基础上,据 P2 成果与制片人最新决策修订。冲突处**以本稿为准**。
---
## 一、两个交付物(内容一致,形态不同)
| 交付物 | 给谁 | 形态 | 用途 |
|---|---|---|---|
| **融合 B 稿** | 爱德华(另一项目) | 逐条碎句 + **密集字幕级时间戳** `[XmYs] 文本` | 字幕/片段定位,时间精度优先 |
| **融合 A 稿** | 编导 | **公文格式 docx**,保留【导视】【主持人N】【解说N】【专家N】【隔断】分段 | 最像播出版的存档稿,方便编导复用 |
**一致性约束**:融合 A 稿**由融合 B 稿生成**(按 A 稿分段结构归拢 + 套格式),所以两者内容天然一致——是"构造出来的一致",不是事后比对硬凑。
---
## 二、流水线四阶段(注意依赖顺序:术语提取必须在 ASR 之前)
```
A稿 ──► ①术语提取·更新词典 ──► 本期热词表
视频 ──► ②分离音频·转码WAV ──► 讯飞STT(热词+pd=mil) ──► 新ASR(带时间戳)
B稿v2 ⊕ 新ASR ──► ③交叉复审 ──► 融合B稿(碎句·时间戳·给爱德华) + fusion_review留痕
融合B稿 + A稿(语义对齐分段) ──► ④LLM语义融合 ──► 融合A稿(公文docx·给编导)
```
沿用 P2 的工程纪律:**各阶段解耦、中间产物落缓存、可断点续跑、可单独重跑**。
### Stage C1 — 术语提取 + 词典更新(规则层 + 轻量 AI)
- 从 A 稿提取专有名词(武器/型号/番号/人名/机构/技术概念),并入中台**累积词典**。
- 词典字段:`term / domain / first_seen_episode / frequency`。**逐期累积,越用越准**。
- demo 词典(34 条热词)作为**基线**,本期在其上增量更新(如 F/A-18E/F 大黄蜂、歼-10、黄貂鱼无人加油机…)。
- 产出本期**热词表**(讯飞上限 200 个,每个 2–16 字,`|` 分隔)供 Stage C2 用。
### Stage C2 — 音频分离 + 讯飞 ASR(适配层)
- ffmpeg 从视频分离音频 → 转码 **16kHz / 单声道 / 16bit WAV**(讯飞硬要求,否则报误导性 `language verify fail`)。
- 调讯飞「**录音文件转写(标准版)**」(**不要大模型版**,免费包阉割 `language` 参数)。参数:`pd=mil``eng_smoothproc=true``eng_colloqproc=true``hotWord=<C1热词>`
- 签名:`signa = base64(HmacSHA1(MD5(appid + ts), secretKey))`。上传 `/upload` 拿 orderId,轮询 `/getResult` 到 status=4。
- 产出新 ASR(句级,带时间戳,格式 `[XmYs] 文本`),落缓存 `asr_v2_timed.txt` + 原始 json。
- 起点代码:`xfyun_asr_standard.py`(demo 已跑通),按下方安全要求改造。
### Stage C3 — B⊕ASR 交叉复审 → 融合 B 稿(给爱德华)
- **形态:甲 — 保持 B稿v2 的碎句粒度(743 行),密集字幕级时间戳原样保留**;ASR 只做复审纠错,**不改变行结构、不并到句级**。
- 复审做三件事:① 用 ASR 比对查 B 稿有没有重复行/错漏;② 屏幕术语拼写以 B稿v2 为准(见优先级表);③ B 稿明显 OCR 错字而 ASR 对的,用 ASR 覆盖。
- **不机械逐字标记**:由 LLM 读语义判断"是否真分歧",一两个字的等价差异不算分歧。
- 产出:`融合B稿.txt`(碎句+时间戳) + `fusion_review.csv`(机器做了选择/拿不准的地方留痕)。
### Stage C4 — 语义融合 → 融合 A 稿(给编导)
- 拿融合 B 稿,**按 A 稿分段结构语义对齐**归拢成段(参照未融合的 A 稿语义,不是机械时间切分)。
- LLM 读 A 稿语义,理解后重组为"最像播出版"的段落文本,套公文格式。
- **全自动,不阻塞主流程**:拿不准的段落照常产出 + 进 `fusion_review`,不卡住出稿。
- 产出:`融合A稿.docx`(公文格式)。
---
## 三、三方权威优先级(据 P2 修订 —— B 稿地位升级)
> **关键变化**:原设计把 B 稿定为"OCR 错字多,仅作交叉验证,权威最低"。P2 后 DeepSeek-OCR 产出的 B稿v2 屏幕术语拼写又准又干净,故 B 稿**升级加权**。
| 看什么 | 信谁 | 说明 |
|---|---|---|
| **屏幕术语 / 型号 / 番号**(箭-3、萨德、见证者-136) | **B稿v2 ≈ A稿 并列权威** | B 稿是屏幕实际打出的字,常更贴近播出;A 稿是规范写法。两者一致则采用,冲突进 review |
| 口语实况 / 语序 / 语气 | **ASR** | 主持人/专家实际怎么念(含临场改词) |
| 书面结构 / 完整语义 / 分段 | **A 稿** | 编导书面版,管【…】分段与段落语义骨架 |
| 同音事实错(美以→美伊) | **A 稿 / B稿v2** 覆盖 ASR | ASR 同音误识,句子通顺但语义荒谬,靠 A/B 发现 |
| 整段是否存在 | **B稿v2 + ASR 联合**判定 | 两者都无 → A 稿该段为未播出,标记删除 |
---
## 四、融合 A 稿公文格式(Stage C4 出稿规范)
| 层级 | 字体 | 字号 / 其他 |
|---|---|---|
| 大标题 | **方正小标宋** | — |
| 一级标题 | **黑体** | — |
| 二级标题 | **仿宋,加粗** | — |
| 正文 | **仿宋** | **四号(14pt),1.25 倍行距** |
> ⚠️ 字体可用性:方正小标宋是商业字体,生成 docx 的机器若未安装,Word 会静默回退、版式垮掉(同字体类坑)。出稿前须确认制片机/编导机已装方正小标宋(公文常用,多半有);否则在脚本里配置回退字体(如标宋→宋体加粗)。用 docx skill 实现时按 GB/T 9704 党政机关公文格式参照。
---
## 五、留痕与人工策略
- **全自动产出,绝不阻塞主流程**。编导拿到的是干净成品,无需做对齐/确认工作。
- 机器做了选择或拿不准的地方(B/ASR 分歧、低把握段落改写、疑似整段删除、疑似编导笔误)**全部甩进 `fusion_review.csv` 留痕**,供编导/爱德华事后复核,不卡出稿。
- 改动类型枚举沿用原设计:`unchanged / minor_edit / term_normalize / rewrite_large / segment_delete / segment_add / editor_typo`;带 confidence,低把握的进 review 文件。
---
## 六、安全:密钥外置(开发第一件事)
- **现状问题**:`xfyun_asr_standard.py` 第 41-42 行明文硬编码了讯飞 APP_ID / SECRET_KEY,且该文件已进 project,凭证已暴露。**建议在讯飞控制台轮换该密钥**。
- **正式做法**:凭证只存制片机本地(`xfyun_credentials.json` 或环境变量,且加入 `.gitignore` 不进库),脚本运行时读取。代码与对话中**只引用变量名,绝不出现真值**。
- 三套密钥互不相通:讯飞**消费端**(听见 App)≠ 讯飞**开发端**(开放平台,本模块用)≠ Claude API。本模块用开发端 + Claude API。
---
## 七、外部依赖
- **讯飞开放平台**「录音文件转写(标准版)」—— 开发端密钥已就绪(制片机本地)。
- **Claude API**(Stage C4 语义融合)—— 建议 `claude-sonnet-4-6` 起步;单期上下文 A稿~6k + ASR~5k + B稿~5k ≈ 16k 字,窗口充裕;每期 1–3 次调用。
- **本地 DeepSeek-OCR / Ollama** —— 由 P2 提供 B 稿,本阶段不直接调。
- **ffmpeg** —— 音频分离 + 转码。
---
## 八、数据资产(中台共享,逐期累积)
1. **A 稿术语表 / 词典**(核心):讯飞热词来源 + 规则层纠错字典,双重身份;逐期累积,识别质量滚雪球上升。基线 = demo 34 热词。
2. **OCR 错字映射表**:肘→时、娈→变、夭→天… 随版本扩展(P2 已观察到 DeepSeek-OCR 错字远少于旧引擎,此表主要兜底)。
3. **编导风格档案**(远期):累积每位编导删改偏好,用于参数自适应。
---
## 九、待办与风险
- **并发版 Stage A 提速**(P2 遗留):OCR 串行 ~30 分钟/期,GPU 空转;首选并发 4–8 路(`OLLAMA_NUM_PARALLEL`),预计压到十分钟出头。P3 期间并行处理。
- **离线小机房模型搬运**(P2 遗留):拷 `.ollama/models` 文件夹。
- **方正小标宋字体可用性**:出稿前确认,见第四节。
- **讯飞单源依赖**:远期接阿里云 ASR 做备份/交叉。
- **整段删除/编导笔误判定**:高风险,全自动产出但务必进 review 留痕。
---
## 十、本期可复用资产(demo / P2 产物)
| 文件 | 用途 |
|---|---|
| `B稿_v2.txt`(743 行) | Stage C3 输入(屏幕字幕,P2 产出) |
| `A稿_..._定稿.docx`(实为 md 文本) | Stage C1 术语源 + Stage C4 分段语义参照 |
| `xfyun_asr_standard.py` | Stage C2 起点代码(须先密钥外置) |
| demo 词典(34 热词) | Stage C1 词典基线 |
| 源视频 | Stage C2 音频源 |
| `现代防空反导大对决_终版.docx` / `差异报告.docx` | 集成测试黄金对照 |
---
*P3 设计冻结。下一步:按 Stage C1→C4 拆任务给 Cline,沿用解耦+缓存+可重跑工程纪律。*
+216
View File
@@ -0,0 +1,216 @@
# Doco 子项目续接快照 · 交接给下一轮 Opus 顾问
> 起草:Opus 4.7 顾问(算力即将耗尽)
> 日期:2026-06-12
> 给:接手本对话的下一个 Opus 实例
> 用户:制片人刘通(《军事科技》栏目 TPS 中台主理人)
---
## 一、先看哪些文件(读完这几份再接话)
主 project 知识库里这几份**必读**:
1. `Claude协作原则.md` — 跟制片人说话的规矩(简体中文、说人话、不堆叠技术语言、Cline 指令要代码块、宪法第 3 条不架空 Cline 的 Plan 模式)
2. `Doco子项目_Brief.md` — Doco 子项目宪法,红线全在这
3. `doco_project_design.md` — Doco 算法层设计(P3 阶段才用,但先了解全貌)
4. `主project对Doco_PRDv2的回复.md` — 主项目顾问对 5 个问题的批复
5. `跨子项目协作规则.md` — PRD 版本管理
---
## 二、当前进度坐标
- **阶段**:Doco P1(视频双路拆分预处理)
- **节点**:dry-run 调试,**核心 bug 未解决,不能进入 OCR 阶段**
- **git 状态**:多个 commit 已推 origin/main,按宪法直接推 main 不开分支
P1 任务回顾:
- 输入:一期节目视频(黑底白字字幕,干净人声)+ A 稿
- 输出:B 稿 txt + audio_16k.wav + keyframes.json
- 流程:ffmpeg 抽帧(1fps,crop 到下方 20%全宽)→ 空白帧过滤 → dHash+IoU 去重 → 关键帧 → (OCR)→ B 稿
---
## 三、当前 bug 状态(最重要)
### 已确认未修复的核心 bug
**`is_blank_frame()` 读取的图像 ≠ `frames/` 目录里最终保存的图像**
证据链:
1. 制片人上传 `frame_0008.png`(从 frames/ 直接复制)
2. Opus 用 Python 实测:整张图 414720 像素**全部为 0**(`arr.max() == 0`)
3. CSV 报告 frame_8:`max_brightness=255, white_pixel_ratio=0.0124`
4. **两者无法同时为真** → 检测时读的不是这张图
最新 dry-run 自检也证实了这点:
```
[WARNING] 自检失败: frame_0938.png 像素数据与 CSV 不一致!
CSV: max_brightness=255, white_ratio=0.007603
实际重读: max_brightness=255, white_ratio=0.022852
[error] 已中止
```
### Cline 已经修了但没修对的事
Cline 把这个 bug 解释成"CSV 列写入顺序错位",但**那只是表层**——
列对齐改完后,frame_8 的 CSV 数据 `max_brightness=255、white_ratio=0.0124` 仍然跟"全 0 像素"对不上。
Cline 已修复(列错位)是真问题,但**根本原因没找到**。
### 推测根因(交接给你)
很可能 video_split.py 的抽帧流程是分两步,而不是 ffmpeg 一步搞定:
- ffmpeg 把帧抽到某临时目录(可能是全尺寸 1920×1080,**没**裁切)
- Python 用 PIL 后裁切到 1920×216 再存到 frames/
- `is_blank_frame()` 读的是裁切前的临时全画面(里面有节目画面有亮像素 → max=255)
- `frames/` 里是裁切后的小图(字幕区域纯黑 → 像素全 0)
但这只是**推测**,你应该让 Cline 自查代码确认。
---
## 四、下一步建议指令(给你参考,可调整)
发给 Cline 的 ACT 模式指令,大致这样:
```
[ACT 模式]
P1 dry-run 自检揪出严重 bug:is_blank_frame() 读的图 ≠ frames/ 里保存的图。
证据:
1. 制片人确认 frame_0008.png 是全 0 纯黑(Python 实测 arr.max()=0)
2. CSV 仍报告 max_brightness=255, white_ratio=0.0124
3. dry-run 自检也确认 frame_0938.png 数据不一致
上次"列错位"修复是真 bug 但不是这次的根因,这是另一个独立的 bug。
请逐行排查 video_split.py 的抽帧流程,回答以下问题(在汇报里贴具体代码):
1. ffmpeg 命令是什么?是否真的有 vf "fps=1,crop=iw:ih*0.2:0:ih*0.8" 过滤器?
2. ffmpeg 输出到哪个目录?
3. is_blank_frame() 接收的 image_path 是哪个目录?
4. compute_hash() / compute_binary_matrix() 接收的 image_path 是哪个目录?
5. 最终保存到 frames/ 的图,是从哪里来的?
如果发现某个判断函数读了"非 frames/ 里的图",修复方法:
- 选项 A:让 ffmpeg 用 vf crop 直接输出裁切后的小图到 frames/,
所有判断函数都从 frames/ 读(单一数据源)
- 选项 B:保持当前流程,但把所有判断函数的 image_path 改为 frames/ 里的路径
修复后重新跑 dry-run,自检通过 + 制片人验证 frames/ 里没纯黑帧才算成功。
不要再加新功能,这次只修这一个 bug。
```
---
## 五、协作风格备忘
- **制片人不懂代码**,你要"说人话",不堆叠 Python 术语
- **简明扼要**,他明确要求不要解释中间过程
- **给 Cline 的指令一律用 ``` 代码块封装**,方便他复制粘贴
- **明确标 ACT 或 Plan 模式**——前几轮制片人提醒过这点
- 不要架空 Cline 的 Plan 模式(宪法第 3 条),但小 bug 修复可以直接 ACT
- 制片人现在的疲劳度比较高(连续 dry-run 多轮),回复要紧凑,不要长篇大论
- 制片人的 git 工作流:直接推 main 不开分支(宪法规定)
---
## 六、关键决策(已锁,不能动)
| 项 | 值 |
|---|---|
| 字幕裁切区域 | `crop=iw:ih*0.2:0:ih*0.8`(全宽 × 下方 20%),写死,栏目全年 52 期一致 |
| 抽帧密度 | 1fps |
| 哈希算法 | dHash 默认,阈值 5 |
| IoU 保底阈值 | 0.95 |
| 空白帧双条件 | `max_brightness >= 240` AND `white_pixel_ratio >= 0.005` |
| 凭证管理 | doco 自管 `doco/.env`,主仓库根 `docs/api_credentials_inventory.md` 只记元信息 |
| OCR 提供方 | DeepSeek Vision API(P1 dry-run 阶段还没正式调用) |
| ASR 提供方 | 讯飞录音文件转写标准版(P2 才用) |
| AI 融合层 | Claude Sonnet 4.6(P3 才用) |
| 不引入 ffmpeg-python wrapper | subprocess 调系统 ffmpeg |
| Git workflow | 按宪法直接推 main |
---
## 七、环境信息
- Windows + Python 3.12.10 + `.venv`(主项目共享虚拟环境)
- VS Code + Cline 插件
- 仓库根目录:`E:\tps-dashboard\`
- 主仓库:`simonkoson/tps-dashboard`(自部署 git,localhost:3000)
- ffmpeg:已通过 winget 安装,系统 PATH
- demo 视频:`programs/ep001_20260612_fangkong_fandao/source/video.mp4`(《现代防空反导大对决》26 分钟)
doco 子模块目录结构(已建好):
```
doco/
├── src/
│ └── doco/ ← src layout 的包目录
│ ├── __init__.py
│ ├── cli.py
│ ├── video_split.py ← 当前 bug 在这
│ ├── asr_adapter.py ← P2 用
│ └── ocr_adapter.py ← P2 用
├── tests/
│ ├── test_video_split.py
│ └── fixtures/
│ └── mini_test.mp4
├── .env.example
├── .env(本地,不进 git)
├── pyproject.toml
└── README.md
```
---
## 八、踩过的坑(不要重蹈)
1. **Python 包配置 src layout**:Cline 第一次配置时忘了在 src/ 下建 doco/ 子目录,导致 `doco --help``ModuleNotFoundError`。后来 git mv 文件到 src/doco/ 下才好。
2. **系统 Python vs .venv**:制片人第一次 `pip install -e doco/` 装到了系统 Python 3.12,而不是 .venv 里。VS Code 重启后 .venv 自动激活,需要在 .venv 里重装。
3. **VS Code 工作目录**:必须打开 `E:\tps-dashboard\`(仓库根目录),不能打开 `docs\` 子目录,否则 Cline 看不到 .clinerules 和其他子模块。
4. **ffmpeg PATH 不生效**:winget 装完 ffmpeg 后,旧的 VS Code 终端 PATH 还是旧的,要完全关闭 VS Code 重开。
5. **Cline 的命令示例有错**:Cline 多次给的命令示例是错的(用 `python -m doco.src.doco.cli` + `split-video` 子命令),正确命令是 `doco split --episode-id XXX --input-video XXX --output-dir XXX [--dry-run]`
6. **frames/ 目录的语义**:Cline 第一次实现把 1620 张原始抽帧全部保留在 frames/,让制片人误以为"筛选没生效"。后来改为只保留最终关键帧。
7. **pHash 阈值**:dHash 阈值 5,pHash 阈值 2,IoU 阈值 0.95——这些是实测调好的,不要再动。
8. **CSV 列对齐**:Cline 加列时只改 header 没改写入顺序导致全部错位。这次修复了,但**不是核心 bug**。
---
## 九、最新一次 dry-run 的统计(自检失败那次)
```
[stats] 原始抽帧: 1620 张
[stats] 空白帧过滤后: 1393 张 (筛掉 227 张纯黑)
[stats] pHash/dHash 去重后(IoU保底): 308 张 (筛掉 0 张IoU相同 + 0 张哈希相同) ← 统计 bug 还在
[stats] 最终关键帧: 308 张
[debug] frames/ 目录实际文件数: 308 (预期: 308)
[WARNING] 自检失败: frame_0938.png 像素数据与 CSV 不一致!
[error] 已中止
```
注意:
- "筛掉 0 张IoU相同 + 0 张哈希相同" 这个统计还是 0,说明**之前那个 stats 计数 bug 也没真正修好**——或者是这次新加的双条件改变了流程,导致 IoU/hash 没被触发(走了空白帧分支?)。需要复查。
最近的 commit(按时间倒序):
1. Cline 双条件 + 自检 + 测试 + CSV 列对齐
2. Cline numpy 加速 + CLI 参数
3. Cline IoU 保底 + dHash + 诊断 CSV
4. Cline 第一次修空白帧检测(用 brightness=200,后来发现不够,改 240)
5. Cline 加 crop 写死 + dry-run flag
6. Cline 初始化 doco 子项目 P1
---
## 十、给下一个 Opus 的最后嘱托
- 这个 bug 排查过两轮了(Cline 都没真正定位到),所以这次**让 Cline 自查代码并贴出关键函数的实际代码**,不要轻信他的口头解释
- 制片人有耐心,但已经累了,你接手后**第一句话就给方案,不要重复诊断**
- 修完这个 bug 后,P1 dry-run 应该能通过自检,接下来就是填 DeepSeek API key 跑正式 OCR(去掉 --dry-run)
- 如果 dry-run 通过,P1 就算完工,可以进入 P2(讯飞 ASR 集成)
祝接力顺利。
@@ -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,104 @@
# 寄存条:Doco 文稿整理子项目已外迁
> 留在主 project,让未来的 Claude 一眼知道:Doco 文稿整理模块已拉出去单独立项了。
> 外迁时间:2026-06-12
---
## 外迁了什么
**Doco 文稿整理模块**(Document Organization,前称"TPS 三方融合子模块")整体作为子项目。
聚焦:视频双路拆分 + 讯飞 ASR 适配 + 三方融合引擎(规则层 + AI 层 Claude Sonnet 4.6) + 单期处理接口 + 23 期批量调度入口。
demo 已用《现代防空反导大对决》一期跑通,设计文档(`doco_project_design.md`)已沉淀算法层全部决策。
---
## 主 project 不再讨论的话题
聊到下面这些词,**先问一句"这是 Doco 子项目的事还是主 project 的事?"**——是子项目的事,提醒制片人切到 Doco 子项目 chat。
关键词:doco / 文稿整理 / 三方融合 / 视频双路拆分 / A 稿 / B 稿 / ASR / 讯飞 / DeepSeek OCR / DeepSeek Vision / 终版文稿 / 差异报告 / needs_review / 段落对齐 / 改动类型 / 编导笔误 / 口语清理 / OCR 错字字典 / A 稿术语表
---
## 主 project 仍在管什么
- Phase 0-3 已落地的主干代码(episodes、editors、收视诊断基础、知识库)
- backend 全部 schema、API、迁移脚本
- 前端全部 React 代码实施
- 主干 Bug、性能优化、新需求(Doco 以外的)
- Cline 的全部 Plan + Act 操作
- 看板分析升级子项目的接收(见 `寄存条_看板升级已外迁.md`)
- **api_credentials_inventory.md 的维护**(本次 doco 立项时新引入,见下)
---
## 本次立项给主 project 带来的三件新事
### 1. `docs/api_credentials_inventory.md`(新增)
主 project 仓库根目录加一份"凭证清单",登记所有子模块用的外部 API 的元信息(不存真实 key)。
字段:子模块 / API 服务 / Key 类型 / 开通日 / 激活状态 / 到期日 / 责任人 / 备注。
**这不是中台密钥服务**——只是一份让所有子模块"忘了谁管哪把 key"问题最小化的登记表。子项目自己 .env 里存真凭证,主 project 这里只存元信息。
未来其他子模块申请外部 API,也按这个规则登记。
### 2. 节目素材目录规约(主项目第一次定)
`programs/{episode_id}/source|work|output/...`,episode_id 格式 `ep{NNN}_{YYYYMMDD}_{slug}`
详见 `主project对Doco_PRDv2的回复.md` §Q5。
未来其他涉及节目素材的子模块,沿用同一规约。
### 3. `跨子项目协作规则.md`(新增)
主 project 根目录新增一份通用规则文件,首条规则是 **PRD 版本管理**(主 project 只留最新版 PRD + 对应回复,旧版子项目那边归档保留)。
未来其他跨子项目通用规则在这份文件里累加,**所有外迁子项目都适用**(看板升级、Doco 等)。
---
## 接收子项目交付物的接口
Doco 子项目交付物的接口约定,见**子项目主 Brief §六**(交付什么)。简要回顾:
- **Doco Python 包**:可被主 project 编排层 import,CLI + Python API 两套接口
- **凭证管理**:子项目自己 .env,主项目这边 inventory 登记元信息
- **测试**:单元测试 + golden test(demo 那期零回归)
- **数据资产**:A 稿术语表 + OCR 错字映射表可入 TPS 中台共享(走主项目 schema 流程)
**纪律**:
- 子项目不改主 project backend 代码 / schema
- 子项目不写主 project 前端 UI(编导确认 UI 是另一码事)
- 实施完成后回访子项目登记状态
---
## 开局口径
进主 project 新 chat:
> "续接 TPS 主项目。读 `寄存条_doco子项目已外迁.md``寄存条_看板升级已外迁.md`。当前要做 [具体任务]。"
---
## 万一子项目记录丢了
降级方案:
1. **主 project 这边保留 doco 系列资产副本**:`doco_project_design.md/.docx``doco_xfyun_integration_notes.md/.docx``doco_handoff_to_opus_chat.md/.docx`、最新一版的 `PRD_doco_文稿整理子项目_vN.md``主project对Doco_PRDvN的回复.md`
2. demo 已跑通的代码和 fixture(`xfyun_asr_standard.py` + 5 份 fixture)在制片人本地有副本
3. 决策史在 Gitea 有 git 历史
4. **主 project 这边不要凭记忆重建子项目内容**——找制片人或重建子项目
---
## 当前外迁的子项目清单(随更新)
| 子项目 | 外迁日期 | 寄存条文件 | 状态 |
|---|---|---|---|
| TPS 看板分析升级 | 2026-06-12 | `寄存条_看板升级已外迁.md` | 设计阶段 |
| Doco 文稿整理 | 2026-06-12 | 本文件 | 等子项目出 PRD v3 启动 P1 |
+38
View File
@@ -0,0 +1,38 @@
# 跨子项目协作规则
> 主项目维护的通用规则,所有外迁子项目都适用。
> 起始日期:2026-06-12
---
## 规则 1:PRD 版本管理(立于 Doco 立项时)
### 1.1 主 project 这边的规则
- **只保留最新版 PRD + 对应回复**,旧版本一律删
- **PRD 与回复一一配对**:PRD v2 ↔ 回复 v2;PRD v3 ↔ 回复 v3
- 任何时点,主 project 不允许同时存在同一子项目的两个 PRD 版本
### 1.2 子 project 那边的规则
- **保留所有版本**(包括 PRD v0 / v1 / v2 / v3...)
- 旧版本可移到 `archive/` 子目录或加 `_archived` 后缀,但不删
- 每份新 PRD 在元信息块标注"上一版:vN"
### 1.3 版本翻新的操作
子项目出 PRD vN+1 时:
1. 制片人在主 project 这边**先删** PRD vN + 回复 vN
2. **再传** PRD vN+1 + 主项目顾问写的回复 vN+1
3. 一次性,不允许"先传再删"造成短暂双版本状态
### 1.4 适用范围
- 当前所有外迁子项目(看板升级 / Doco 等)
- 未来新立项的子项目沿用
---
## (未来其他通用规则在这里累加)
> 后续规则按 `## 规则 2:xxx` `## 规则 3:xxx` 顺序追加。
@@ -0,0 +1,743 @@
[0m8s] 导弹呼啸而过
[0m9s] 战机低空穿梭
[0m11s] 无人机如蜂群般袭来
[0m14s] 这些空中威胁
[0m15s] 正不断挑战防御体系的极限
[0m18s] 它们是如何被精确拦截的
[0m21s] 究竟是什么力量
[0m22s] 能在遥远距离精准打击
[0m24s] 在近在咫尺时
[0m26s] 完成致命一击呢
[0m28s] 本期《军事科技》
[0m30s] 将带您逐步解析
[0m31s] 现代防空网络的复杂机制
[0m34s] 亲眼见证
[0m35s] 科技与智慧打造的空中屏障
[0m38s] 揭秘现代防空体系
[0m40s] 御敌于空的终极密码
[1m15s] 各位观众你们好
[1m16s] 欢迎收看《军事科技》
[1m17s] 我是主持人蓝皓
[1m20s] 从第二次世界大战时期的
[1m21s] 战机轰炸
[1m22s] 到冷战时期的导弹威慑
[1m24s] 再到如今的无人机
[1m26s] 高超声速武器的崛起
[1m28s] 空中威胁的形态
[1m29s] 不断地迭代
[1m31s] 防御体系也在随之进化
[1m34s] 而如今的现代化防空系统
[1m36s] 早已不再是
[1m37s] 单一武器的简单对抗了
[1m39s] 而是构建了
[1m40s] 以远 中 近衔接
[1m42s] 高中 低覆盖为一体的
[1m44s] 立体化防御系统
[1m46s] 仿佛一块层层镶嵌的空中盾牌
[1m49s] 从千里之外的高空
[1m51s] 到咫尺之间的低空
[1m53s] 形成了无死角的防护
[1m56s] 在与新型空中威胁持续对抗当中
[1m59s] 上演着一场场激烈的
[2m1s] 攻与防的较量
[2m19s] 2026年2月28日
[2m21s] 美以两国协同向伊朗发起军事打击
[2m24s] 中东地区顿时战火纷飞
[2m27s] 硝烟弥漫
[2m29s] 随着冲突不断加剧
[2m30s] 各方频繁动用弹道导弹
[2m32s] 巡航导弹
[2m34s] 高超声速武器
[2m35s] 以及自杀式无人机等先进装备
[2m38s] 展开密集的高强度空袭
[2m41s] 波斯湾上空
[2m42s] 随即呈现出一场
[2m44s] 攻防交织的终极较量
[2m46s] 涉及突破与拦截
[2m48s] 毁灭与保卫
[2m50s] 这场关乎生死存亡
[2m51s] 与战局走向的空中攻防战
[2m54s] 彻底爆发
[3m4s] 在涉及远 中 近程
[3m7s] 和高 中 低空的
[3m8s] 全方位空战对抗中
[3m11s] 弹道导弹
[3m12s] 与反导系统之间的对决
[3m14s] 无疑是最具战略威慑力
[3m16s] 且最检验技术水平的顶峰交锋
[3m20s] 中段反导
[3m21s] 作为现代战略空军的最高层级
[3m24s] 更是左右战场局势的核心要素
[3m28s] 这项技术
[3m29s] 常被形容为
[3m30s] 太空中以子弹击落子弹
[3m34s] 它标志着
[3m35s] 当今世界军事科技的极致成就
[3m37s] 也是战略防空领域
[3m39s] 真正的制高点
[3m48s] 一枚中远程弹道导弹的飞行
[3m50s] 分为上升段
[3m52s] 中段
[3m53s] 末段
[3m54s] 三者在时空 速度上的巨大差异
[3m57s] 直接决定了
[3m58s] 拦截难度的天壤之别
[4m0s] 上升段仅持续3至5分钟
[4m3s] 时间窗口稍纵即逝
[4m6s] 且需在敌国领土拦截
[4m8s] 受部署限制难以实施
[4m10s] 拦截成功率极低
[4m12s] 未段导弹
[4m13s] 俯冲至几十公里至几公里高空
[4m16s] 速度达马赫数10至20
[4m19s] 仅留数秒至十几秒的反应时间
[4m22s] 同时面临机动变轨
[4m24s] 多弹头诱饵及低空探测盲区
[4m27s] 拦截风险高且成功率有限
[4m31s] 唯有中段
[4m32s] 导弹在100公里以上大气层外
[4m34s] 无动力滑行
[4m36s] 飞行时间长达10至20分钟
[4m39s] 轨迹由万有引力决定
[4m41s] 极度稳定
[4m42s] 并未释放弹头与诱饵
[4m44s] 反导系统有足够的时间预警
[4m47s] 测算轨迹并拦截
[4m49s] 采用动能撞击
[4m51s] 可彻底消除地面威胁
[4m53s] 是现代战略防空最理想
[4m55s] 效果最佳的黄金拦截期
[4m59s] 也是各国反导技术研发的
[5m1s] 核心方向
[5m9s] 虽然说
[5m10s] 中段反导拦截
[5m11s] 在整个反导作战的流程当中
[5m14s] 它是比较容易实现的
[5m16s] 或者说
[5m17s] 可以针对对方来袭的导弹
[5m20s] 进行精准的摧毁
[5m21s] 但是最大的问题
[5m22s] 就是你的预警系统
[5m24s] 需要看得远
[5m25s] 反导拦截弹
[5m26s] 需要飞得快
[5m27s] 打得高 打得远
[5m28s] 所以对于技术方面的要求
[5m29s] 是比较高的
[5m30s] 这就要求
[5m31s] 大型的相控阵预警雷达
[5m33s] 能够在极远的距离上
[5m35s] 针对外大气层飞行的
[5m38s] 这种小型高速移动的目标
[5m40s] 能够进行精准的锁定
[5m42s] 而且要对它进行持续的跟踪
[5m45s] 还要有相关的
[5m46s] 这样的一个计算系统
[5m47s] 针对它的飞行轨迹进行计算
[5m49s] 这样的话
[5m50s] 才能够有效地引导反导拦截弹
[5m53s] 在外大气层
[5m54s] 对它进行精准的这样的一个截杀
[5m57s] 而且反导拦截弹的性能
[5m58s] 要求非常高
[6m0s] 不仅要飞得高
[6m1s] 飞得远 飞得快
[6m2s] 而且
[6m3s] 甚至还要配备有动能的战斗部
[6m5s] 要针对处于飞行状态的
[6m7s] 弹道导弹
[6m8s] 进行精准的撞击
[6m10s] 确保它是被彻底摧毁
[6m15s] 3月5日
[6m17s] 伊朗伊斯兰革命卫队发布声明称
[6m20s] 在“真实承诺-4”行动的第17轮中
[6m23s] 伊朗动用高超声速导弹
[6m25s] 和攻击无人机
[6m27s] 成功突破美国“萨德”防御系统
[6m30s] 打击了以色列国防部大楼
[6m32s] 与本-古里安国际机场
[6m34s] 并摧毁了7套雷达系统
[6m36s] 及核心配套设备
[6m39s] 同期
[6m40s] 美国内部评估报告透露
[6m42s] 作为中东关键中段反导装备的
[6m45s] 以色列“箭式”系统
[6m46s] 在拦截伊朗中程弹道导弹时
[6m49s] 显现重大短板
[6m51s] 实际拦截率
[6m52s] 远未达到原有设计标准
[6m57s] 那么在以色列的国土范围内
[6m59s] 有“箭-2” “箭-3” 反导系统
[7m1s] 也有美国部署的“萨德”反导系统
[7m4s] 但是我们看
[7m5s] 仍然有数量相当多的伊朗弹道导弹
[7m8s] 成功突防
[7m9s] 针对以色列区域内的目标
[7m11s] 进行了毁伤
[7m13s] 为什么说“箭-2”“箭-3”
[7m14s] 以及“萨德”系统
[7m15s] 它联起手来
[7m16s] 也没有办法针对伊朗的弹道导弹
[7m19s] 进行这种百分之百的防范和拦截
[7m21s] 我认为
[7m22s] 首先是伊朗弹道导弹的技术
[7m25s] 有所提升
[7m26s] 部分伊朗的弹道导弹
[7m27s] 采用了这种高超音速的战斗部
[7m30s] 在飞行的末段
[7m31s] 会有突然的加速
[7m32s] 或者说有急剧的
[7m34s] 飞行轨迹的变化
[7m35s] 这样的话
[7m36s] “萨德”系统
[7m37s] 和“箭-2”“箭-3”反导系统
[7m38s] 很难对它进行精准的拦截
[7m40s] 此外
[7m41s] 伊朗的一些弹道导弹
[7m43s] 比如说像“霍拉姆沙赫尔-4”
[7m45s] 那么它是一种重型的
[7m46s] 这样的一个弹头的设计理念
[7m49s] 1.5吨到2吨的战斗部
[7m51s] 它可以采用这种集束弹头
[7m53s] 这样的一个模式
[7m55s] 飞到了目标区上空之后
[7m56s] 打出了一个满天星的效果
[7m58s] 数十枚子弹头飞向目标区域
[8m1s] 所有的防空反导系统
[8m3s] 都没有办法对它进行拦截
[8m5s] 那么还有另外一种情况
[8m7s] 美国的反导作战系统
[8m8s] 和以色列的“箭-2” “箭-3” 反导系统
[8m10s] 它没有办法进行联网作战
[8m12s] 是各打各的
[8m13s] 这样的话
[8m14s] 在相关的区域内
[8m15s] 没有办法进行目标信息的共享
[8m17s] 那么也无法形成合力
[8m21s] 2026年3月15日
[8m23s] 伊朗伊斯兰革命卫队
[8m25s] 在“真实承诺-4”军事行动中
[8m28s] 首次实战部署了泥石导弹
[8m31s] 与此同时
[8m32s] 美国从韩国
[8m34s] 紧急调遣了萨德反导系统
[8m36s] 以加强该地区的防御
[8m38s] 应对伊朗导弹带来的持续威胁
[8m44s] 若将远程反导
[8m45s] 视作千里点穴的区域防空手段
[8m48s] 中程防空
[8m50s] 便是中空守护网
[8m52s] 承担着衔接
[8m53s] 远近防御的关键使命
[8m56s] 它覆盖数十公里
[8m57s] 乃至一百多公里的中高空区域
[9m0s] 作为防空体系中的
[9m2s] 中坚力量
[9m4s] 承担起区域防空重任
[9m6s] 护卫城市
[9m7s] 军事要地等高价值目标
[9m9s] 成为整个防空网络中
[9m11s] 承上启下的核心组成部分
[9m22s] 中程防空
[9m24s] 作为现代防空作战系统的
[9m25s] 关键环节
[9m27s] 其防御半径
[9m28s] 一般在50到200公里之间
[9m31s] 拦截高度
[9m32s] 重点覆盖10至20公里的中高空域
[9m36s] 这片空域
[9m37s] 正是现代战争中
[9m39s] 空中威胁密集活动的核心地带
[9m42s] 敌方战机
[9m43s] 若要突破外层防御
[9m45s] 打击纵深关键目标
[9m47s] 必须穿越此区域
[9m49s] 巡航导弹
[9m50s] 虽常利用低空突防
[9m52s] 但多数情况下
[9m54s] 仍需在中高空调整姿态
[9m56s] 寻找防御薄弱点
[9m58s] 而中近程导弹在俯冲末段
[10m1s] 同样会经过这一高度区间
[10m4s] 其高速特性
[10m5s] 要求中程防空系统及早介入
[10m8s] 精确拦截
[10m9s] 可以说
[10m10s] 这片中高空域
[10m11s] 是空中威胁
[10m13s] 渗向核心目标的必经通道
[10m16s] 也是防空体系中
[10m17s] 承前启后的关键拦截区
[10m26s] 在现代化的防空作战行动当中
[10m29s] 中程防空的难度是非常大的
[10m31s] 因为很多对方的
[10m34s] 这种空中的来袭目标
[10m36s] 其实它都是在
[10m37s] 中程防空的任务范围内
[10m39s] 目标的属性是非常多的
[10m41s] 此外
[10m42s] 在复杂的电磁环境的
[10m44s] 这样的一个状态下
[10m45s] 中程的防空体系
[10m47s] 也可能会出现失灵的情况
[10m49s] 因为
[10m50s] 对方可能会出动电子战飞机
[10m52s] 针对你的防空作战体系
[10m53s] 进行压制
[10m54s] 此外
[10m55s] 有的时候面对各种各样的目标
[10m57s] 数量众多的目标
[10m59s] 中程防空系统
[11m0s] 它的这样的一个
[11m2s] 火力的密集程度
[11m3s] 可能会出现不足的情况
[11m5s] 比如说一部发射器上
[11m7s] 它只有数量有限的中程防空导弹
[11m9s] 打完了之后
[11m10s] 对方的目标连续地持续地袭来
[11m14s] 那中程防空体系
[11m15s] 可能也会面临弹药不足
[11m17s] 火力不足的情况
[11m21s] 在近期
[11m22s] 美以联合对伊朗的军事行动中
[11m25s] 中程导弹防御系统
[11m27s] 成为防空体系中的
[11m28s] 核心作战力量
[11m30s] 也是双方攻防较量
[11m32s] 最为激烈的领域
[11m34s] 为巩固中程防御屏障
[11m36s] 美国运用
[11m37s] “爱国者”防空反导系统
[11m39s] 结合天基预警卫星
[11m41s] 陆基远程雷达
[11m43s] 及集成化指挥控制中心
[11m45s] 打造了能够覆盖30公里
[11m48s] 至150公里空域的拦截体系
[11m50s] 着重防护其军事基地
[11m52s] 与国防工业区域
[11m55s] 面对伊朗高超音速武器
[11m58s] 配合无人机集群的
[11m59s] 非对称组合式打击
[12m2s] 此前被美军公开宣称
[12m3s] 拦截成功率
[12m5s] 可达80%到97%的“爱国者”系统
[12m9s] 实际拦截率
[12m10s] 已跌至5%以下
[12m13s] 系统还频频出现拦截假目标
[12m16s] 漏过真实弹头的失误
[12m21s] “爱国者”曾在海湾战争期间
[12m24s] 一战成名
[12m25s] 那当时
[12m26s] 美军“爱国者”系统
[12m27s] 它所拦截的导弹
[12m29s] 都是伊拉克萨达姆政权军队
[12m32s] 使用的比较老旧的
[12m34s] “飞毛腿”弹道导弹
[12m36s] 它的飞行轨迹是一个典型的抛物线
[12m38s] 不会有这种
[12m39s] 机动变轨的这样的一些动作
[12m41s] 所以
[12m42s] 只要是探测到了它的飞行轨迹
[12m45s] 多发射一些“爱国者”拦截弹
[12m48s] 是可以对它进行有效拦截的
[12m49s] 但是现代化的弹道导弹
[12m51s] 突防能力都很强
[12m53s] 飞行轨迹是变化的
[12m54s] 甚至可以说是有一些打水漂的
[12m56s] 这样的一个飞行轨迹
[12m57s] 速度又很快
[12m58s] 那么甚至可能会携带
[13m0s] 更多的诱饵弹和干扰弹
[13m1s] 所以“爱国者”系统
[13m2s] 可能根本就看不清
[13m4s] 可能也就追不上
[13m6s] “爱国者”导弹在部分实战场景中
[13m9s] 拦截效果未达预期
[13m12s] 这一表现
[13m13s] 使得美以联合构建的
[13m14s] 中程防空反导系统暴露出短板
[13m18s] 也让多国对该系统
[13m19s] 产生信任危机
[13m24s] 在现代防空系统中
[13m25s] 低空与超低空
[13m27s] 是远程及中程防空
[13m29s] 存在的短板
[13m30s] 受到地球曲率
[13m32s] 地形掩蔽等影响
[13m33s] 远程雷达难以追踪低空目标
[13m37s] 而中程防空导弹
[13m38s] 在低空条件下
[13m39s] 制导系统易受地面杂波的干扰
[13m41s] 致使拦截效能下降
[13m44s] 因此
[13m45s] 近程防空系统由此诞生
[13m48s] 专门用于近距离的防护
[13m50s] 弥补了防空网络
[13m52s] 在低空领域的不足
[14m1s] 对普通公众而言
[14m3s] 近程防空无疑是最常见
[14m6s] 且视觉冲击力最强的防空形式
[14m9s] 那些在网络短片
[14m11s] 新闻报道和战场记录中
[14m13s] 常见的导弹腾空而起
[14m15s] 尾焰撕裂黑夜
[14m17s] 目标在空中爆炸
[14m19s] 火光刹那映亮天空的拦截画面
[14m22s] 大多源自近程防空系统
[14m25s] 这些系统射程较短
[14m27s] 但部署便捷响应迅速
[14m30s] 通常在目标接近阵地
[14m32s] 城市或重要设施的最后一刻
[14m35s] 才发动攻击
[14m36s] 因而更易被现场捕捉
[14m38s] 媒体记录
[14m39s] 并广泛传播
[14m41s] 正是由于这种高频曝光
[14m43s] 近程防空在许多人心中
[14m45s] 几乎成了防空的代名词
[14m48s] 然而实际上
[14m49s] 近程防空在整体防空网络中
[14m52s] 充当的是最终贴身防线的功能
[14m56s] 远程和中程防空
[14m58s] 无法覆盖的低空
[15m0s] 及超低空漏洞
[15m4s] 近程防空作战体系
[15m6s] 在实际应用的过程当中
[15m7s] 有的时候
[15m9s] 可能会成为战场上的奇兵
[15m10s] 因为对方的直升机
[15m13s] 甚至包括战斗机
[15m14s] 在低空飞行的时候
[15m15s] 它可能会放松警惕
[15m16s] 另外一个来不及反应
[15m18s] 所以这个时候
[15m19s] 近程防空火力
[15m20s] 针对目标进行射击的时候
[15m21s] 战斗机很难对它进行规避
[15m23s] 有的时候甚至连
[15m25s] 发射红外诱饵弹的时间都没有
[15m28s] 近程防空也有很大的难度
[15m30s] 因为很多近程防空作战体系
[15m33s] 它的搜索系统是比较简单的
[15m35s] 视野不够宽广
[15m36s] 也无法针对远距离的目标
[15m38s] 进行识别
[15m39s] 所以
[15m40s] 有的远程防空作战体系
[15m41s] 它本质上来讲是“近视眼”
[15m43s] 而且还涉及到一些
[15m44s] 地形起伏的问题
[15m46s] 如果对方利用这种地形起伏
[15m49s] 那么去进行超低空的突防
[15m51s] 在相关区域内部署的一些传感器
[15m53s] 比如说低空搜索雷达
[15m54s] 或者是光学红外传感器
[15m56s] 可能没有办法
[15m57s] 及时有效地识别目标
[15m58s] 那么这些低空突防的
[16m0s] 这样的一些空中作战平台
[16m2s] 也可能会针对
[16m3s] 近程的防空作战体系
[16m4s] 包括相关的防空阵地
[16m6s] 进行打击和压制
[16m11s] 在近期
[16m12s] 美以联合对伊朗的军事行动中
[16m15s] 以色列拉斐尔公司研发的
[16m17s] “铁穹”近程防空系统
[16m19s] 经历了自2011年部署以来
[16m21s] 最激烈
[16m22s] 且最具转折性的实战检验
[16m25s] 这款以往在冲突中
[16m27s] 以85%至90%拦截率
[16m30s] 享誉世界的明星武器
[16m32s] 在伊朗
[16m33s] “真实承诺-4”行动的饱和攻击下
[16m36s] 其卓越声誉荡然无存
[16m39s] 以色列国防部
[16m41s] 3月17日
[16m42s] 向国会递交的评估报告中指出
[16m45s] 面对伊朗连续发动的
[16m47s] 六十多波攻势
[16m48s] “铁穹”系统的实际拦截率
[16m50s] 从历史标榜的90%以上
[16m53s] 骤降至约50%
[16m55s] 而在集束弹头密集倾泻
[16m58s] 与无人机蜂群
[16m59s] 低空突袭的关键战斗阶段
[17m1s] 其拦截成功率
[17m3s] 更是下滑至30%以下
[17m5s] 刷新了服役以来的
[17m7s] 最低性能纪录
[17m12s] 我们注意到
[17m13s] 在此次军事行动的过程当中
[17m15s] “铁穹”系统表现平平
[17m16s] 我认为
[17m17s] 可能是有两方面的考虑
[17m19s] 就是它配套的这种传感器
[17m22s] 或者是预警体系
[17m23s] 可能是遭到了对方的火力打击
[17m25s] 有部分“铁穹”系统
[17m26s] 那么针对空中来袭目标
[17m28s] 它的反应速度不够快
[17m30s] 它的这样的一个情报
[17m33s] 和目标引导体系
[17m35s] 不够完善
[17m36s] 另外一个
[17m37s] 就是“铁穹”系统
[17m38s] 它所使用的是“塔米尔”拦截弹
[17m40s] 这种拦截弹
[17m41s] 由于它装备
[17m43s] 和使用的数量比较多
[17m44s] 所以
[17m45s] 它必须要进行成本的控制
[17m46s] 它的飞行速度
[17m49s] 其实并没有那么快
[17m50s] 有的时候
[17m51s] 针对高速目标进行打击的时候
[17m53s] 可能会出现速度跟不上的情况
[17m56s] 现在伊朗主要是使用弹道导弹
[17m59s] 针对以色列进行打击
[18m0s] 面对高空高速飞行的弹道导弹
[18m3s] 或者是
[18m4s] 配备有
[18m5s] 高超音速战斗部的弹道导弹
[18m7s] “铁穹”系统可能它的拦截范围
[18m9s] 拦截的高度也都不足
[18m11s] 伊朗也使用了大量的无人机
[18m14s] 但是这一次
[18m15s] 针对以色列进行后续的反击
[18m17s] 伊朗更加注重使用
[18m19s] 这种中程的弹道导弹
[18m21s] 或者是突防能力很强的弹道导弹
[18m23s] 面对这样的一种情况
[18m25s] 它就会出现反应速度不够快
[18m28s] 包括拦截的范围
[18m30s] 拦截的高度
[18m31s] 都存在不足的情况
[18m35s] 近程防空系统
[18m37s] 作为整个防空架构中
[18m38s] 关键的最后屏障
[18m40s] 肩负着坚守底线
[18m42s] 护卫要害的核心职责
[18m45s] 但它并非完美无缺
[18m47s] 仍存在明显的短板
[18m50s] 其防御范围
[18m51s] 通常较为有限
[18m53s] 探测与拦截能力
[18m54s] 都集中在较小区域
[18m57s] 大多只能执行小尺度
[18m58s] 重点目标的局部防护任务
[19m1s] 无法如同中远程防空系统那样
[19m4s] 实现广大空域的大范围
[19m6s] 远距离监控与狙击
[19m9s] 因此
[19m10s] 近程防空系统
[19m11s] 不能独自支撑一国 一城
[19m13s] 或整个战区的全面防空安全
[19m16s] 它更像是一件
[19m17s] 贴身的防弹衣
[19m19s] 而非是能覆盖全域的保护罩
[19m24s] 远程 中程与近程防空系统
[19m28s] 相互配合
[19m29s] 在现代战争中
[19m31s] 构筑了层次分明 分工明确
[19m33s] 紧密衔接的全空域
[19m35s] 立体防空网络
[19m38s] 三者各尽其责
[19m39s] 彼此支援 逐层拦截
[19m42s] 形成了一套由外至内
[19m44s] 由高空到低空
[19m45s] 由远及近的周密防御体系
[19m49s] 不过
[19m50s] 近年多场高强度
[19m51s] 军事冲突的实际表现
[19m53s] 这套看似牢不可破的防空体系
[19m56s] 正持续受到
[19m57s] 新兴战场模式的严峻挑战
[20m0s] 其传统作战思路和防御能力
[20m2s] 正在不断遭受冲击与重塑
[20m15s] 在美以联合对伊朗发动的
[20m17s] 大规模军事行动中
[20m19s] 可以清晰看到
[20m20s] 美军
[20m21s] 以色列军队
[20m22s] 以及伊朗的防空系统
[20m24s] 都遭遇了前所未有的考验
[20m26s] 在以往严密的多层防护网络
[20m29s] 在实战中屡次被突破
[20m31s] 撕裂
[20m32s] 甚至彻底崩溃
[20m34s] 美军采用了
[20m36s] 以“爱国者”和“萨德”反导系统
[20m38s] 中远程导弹防御体系
[20m41s] 以色列则调动了由“铁穹”
[20m43s] “大卫投石索”
[20m45s] 和“箭式”系统组成的
[20m46s] 全域防空反导系统
[20m49s] 然而
[20m50s] 针对高超声速武器穿透
[20m52s] 无人机群饱和攻击
[20m54s] 复合诱饵欺骗
[20m55s] 与强电磁干扰等策略
[20m57s] 拦截失败的情况
[20m59s] 依然频繁发生
[21m4s] 在此次行动的过程当中
[21m6s] 美以的防空反导系统
[21m7s] 频频被洞穿
[21m9s] 也就是说
[21m10s] 伊朗使用了
[21m11s] 一系列的突防手段
[21m13s] 最终打穿了
[21m14s] 美国和以色列的一些
[21m15s] 防空反导的保护伞
[21m18s] 那么针对相关的区域
[21m19s] 构成了更为猛烈的火力打击
[21m22s] 我认为
[21m23s] 就是存在一些问题
[21m25s] 比如说
[21m26s] 它系统之间
[21m27s] 没有进行有效的联合
[21m28s] 存在一些兼容的问题
[21m30s] 各自为战的状态下
[21m32s] 肯定会出现漏洞
[21m33s] 另外一个
[21m34s] 伊朗也是采取了一些灵活多变的
[21m37s] 这样的一些战术
[21m38s] 多种火力 多种模式联合运用
[21m41s] 也让美国和以色列的
[21m43s] 防空反导系统
[21m48s] 在现代军事冲突中
[21m50s] 高超声速武器的穿透
[21m51s] 与无人机蜂群的饱和攻击
[21m54s] 虽是关键威胁
[21m55s] 但更具深远颠覆性的
[21m57s] 是战场成本效益失衡
[21m59s] 对防空体系带来的
[22m1s] 系统性难题
[22m3s] 这已超越了
[22m4s] 单一装备的性能竞争
[22m6s] 演变为从经济原理
[22m8s] 持久作战能力
[22m10s] 到战略自信的全方位压制
[22m13s] 引发了拦截不起
[22m15s] 消耗不起
[22m16s] 补充不上的三重困局
[22m20s] 以伊朗主力攻击武器
[22m22s] “见证者-136” 自杀式无人机为例
[22m25s] 这款被戏称作
[22m27s] 飞行摩托车的装备
[22m29s] 彻底打破了传统军工的
[22m31s] 精密制造范式
[22m33s] 机身使用民用级玻璃纤维
[22m35s] 和泡沫塑料拼装
[22m37s] 动力系统
[22m38s] 直接借用普通摩托车的
[22m40s] 二冲程发动机
[22m41s] 导航依靠
[22m42s] 民用GPS模块与简易惯性制导
[22m46s] 甚至部分核心零件
[22m48s] 来自拆解的汽车和家电
[22m50s] 从而极大压低了制造成本
[22m53s] 凭借民用技术军用化
[22m55s] 复杂系统简易化的理念
[22m57s] 其批量生产单价
[22m59s] 稳定在2万至5万美元之间
[23m2s] 某些简化版本
[23m4s] 甚至降至1.5万美元
[23m6s] 仅相当于高端防空导弹成本的
[23m9s] 极小一部分
[23m11s] 美以用于对抗的防空武器
[23m13s] 却如同天价奢侈品
[23m16s] 以色列“铁穹”系统的拦截弹
[23m18s] 作为远程防空核心弹药
[23m20s] 每枚成本
[23m21s] 达5万到10万美元
[23m24s] 这意味着
[23m25s] 即便成功拦截
[23m26s] 防御方的经济损失
[23m28s] 也远超进攻方
[23m30s] 更甚者
[23m31s] 美国“爱国者-3”拦截弹
[23m33s] 作为中程防空主力
[23m35s] 单枚成本飙升至
[23m37s] 400万至500万美元
[23m39s] 堪比用超级跑车
[23m41s] 撞击普通代步车
[23m43s] 无论胜负如何
[23m45s] 防御方已在战略层面
[23m47s] 沦为输家
[23m51s] 那么在效费比方面
[23m53s] 如果说对方在进攻的过程当中
[23m56s] 采用了很多廉价的方式
[23m58s] 或者说很多省钱的
[23m59s] 这样的一些技术手段
[24m1s] 像“见证者-136”
[24m3s] 其实它就是飞行的小摩托
[24m5s] 那么它的成本控制得很低
[24m7s] 可能要用先进的防空反导系统
[24m9s] 对它进行拦截
[24m11s] 而伊朗的一些地区盟友
[24m12s] 包括黎巴嫩真主党
[24m15s] 会使用大量的“卡桑”火箭弹
[24m16s] 也就是说
[24m17s] 更低成本的作坊工厂当中
[24m19s] 生产出来的这种火箭弹
[24m21s] 针对以色列进行打击
[24m22s] 那么色列方面使用的是什么
[24m24s] 是这种“塔米尔”拦截弹
[24m25s] 也就是“铁穹”系统的拦截弹
[24m28s] 在这样的 一种
[24m29s] 相互的消耗战的过程当中
[24m31s] 那么位列或者是美国
[24m33s] 他所面临的这种
[24m35s] 昂贵弹药的快速消耗
[24m37s] 和短缺的情况
[24m39s] 也会变得越来越多
[24m40s] 也会变得越来越明显
[24m43s] 由于战场上
[24m44s] 攻击和防御的效费比例
[24m46s] 严重失衡
[24m47s] 防空弹药库存告急
[24m49s] 形势十分紧迫
[24m51s] 3月18日
[24m52s] 以色列国防部火速向美国求援
[24m55s] 美军立刻出动C-17运输机
[24m58s] 紧急空运了近千枚拦截导弹
[25m1s] 为以色列的库存注入强心针
[25m4s] 与此同时
[25m5s] 美军从韩国
[25m7s] 亚太到欧洲多个据点
[25m9s] 迅速调集防空系统
[25m11s] 全力缓解战场上
[25m13s] 防空装备和弹药
[25m14s] 捉襟见肘的困境
[25m19s] 现代防空系统
[25m20s] 打造了一个从远程中段反导
[25m23s] 近程区域防空
[25m24s] 到近程末段拦截的完整链条
[25m28s] 构建了远近交错
[25m30s] 高低坚固的立体防御天网
[25m33s] 然而
[25m34s] 高超声速武器如闪电般突破
[25m37s] 无人机群的压制
[25m38s] 以及饱和式攻击的浪潮不断袭来
[25m41s] 让传统防空体系
[25m42s] 面临着严峻的考验
[25m46s] 攻防成本的天平
[25m48s] 正在严重倾斜
[25m49s] 威胁形势日新月异
[25m52s] 如何在提升拦截效果的同时
[25m54s] 勒紧成本的缰绳
[25m57s] 变得至关重要
[25m59s] 让防空网看得远
[26m1s] 拦得准
[26m2s] 扛得住
[26m3s] 耗得起
[26m4s] 成为了左右未来防空安全
[26m6s] 和战争胜负的
[26m8s] 关键棋局
[26m11s] 好了观众朋友们
[26m12s] 感谢您持续关注
[26m13s] 国防军事频道《军事科技》
[26m15s] 我们下周同一时间
[26m17s] 再见
File diff suppressed because one or more lines are too long
@@ -0,0 +1,310 @@
[0m7s] 导弹呼啸而过,战机低空穿梭,
[0m11s] 无人机如蜂群般袭来,
[0m13s] 这些空中威胁正不断挑战防御体系的极限,
[0m18s] 他们是如何被精确拦截的?究竟是什么力量能在遥远距离精准打击,在近在咫尺时完成致命一击呢?
[0m28s] 本期军事科技
[0m29s] 将带您逐步解析现代防空网络的复杂机制,
[0m34s] 亲眼见证科技与智慧打造的空中屏障,揭秘现代防空体系御敌于空的终极密码。
[1m14s] 各位观众你们好,欢迎收看军事科技,我是主持人蓝皓。
[1m19s] 从第二次世界大战时期的战机轰炸到冷战时期的导弹威慑,再到如今的无人机高超声速武器的崛起,
[1m28s] 空中威胁的形态啊不断的迭代,
[1m30s] 防御体系也在随之进化。
[1m34s] 而如今的现代化防空系统早已不再是单一武器的简单对抗了,
[1m39s] 而是构建了以远中近衔接,高中低覆盖为一体的立体化防御系统,
[1m45s] 仿佛一块层层镶嵌的空中盾牌,
[1m49s] 从千里之外的高空到咫尺之间的低空形成了无死角的防护,
[1m55s] 在与新型空中威胁持续对抗当中,上演着一场场激烈的
[2m1s] 攻与防的较量。
[2m18s] 2026年2月28日,美伊两国协同向伊朗发起军事打击,
[2m24s] 中东地区顿时战火纷飞,硝烟弥漫,
[2m28s] 随着冲突不断加剧,
[2m30s] 各方频繁动用弹道导弹、
[2m32s] 巡航导弹,
[2m33s] 高超声速武器及自杀式无人机等先进装备,
[2m37s] 展开密集的高强度空袭,
[2m41s] 波斯湾上空随即呈现出一场攻防交织的终极较量,
[2m46s] 涉及突破与拦截,毁灭与保卫。
[2m49s] 这场关乎生死存亡与战局走向的空中攻防战彻底爆发。
[3m4s] 在涉及远中近程和高中低空的全方位空战对抗中,
[3m10s] 弹道导弹与反导系统之间的对决,无疑是最具战略威慑力且最检验技术水平的顶峰交锋。
[3m19s] 其中中段反导作为现代战略防空的最高层级,
[3m24s] 更是左右战场局势的核心要素。
[3m27s] 这项技术常被形容为
[3m30s] 太空中以子弹击落子弹,
[3m33s] 它标志着当今世界军事科技的极致成就,也是战略防空领域真正的制高点。
[3m47s] 一枚中远程弹道导弹的飞行分为上升段,
[3m51s] 中段、
[3m52s] 末段
[3m53s] 三者在时空速度上的巨大差异,直接决定了拦截难度的天壤之别。
[4m0s] 上升段仅持续3~5分钟,时间窗口稍纵即逝,
[4m5s] 且需在敌国领土拦截,
[4m7s] 受部署限制难以实施拦截,成功率极低。
[4m12s] 末段导弹俯冲至几十公里至几公里高空,
[4m16s] 速度达马赫数10~20
[4m19s] 仅留数秒至十几秒的反应时间,
[4m22s] 同时面临机动变轨,
[4m24s] 多弹头诱饵及低空探测盲区,
[4m27s] 拦截风险高且成功率有限,
[4m30s] 唯有中短,
[4m31s] 导弹在100公里以上大气层外无动力滑行,
[4m35s] 飞行时间长达10~20分钟,
[4m38s] 轨迹由万有引力决定极度稳定,并未释放弹头与诱饵,反导系统有足够的时间预警测算轨迹并拦截,
[4m49s] 采用动能撞击可彻底消除地面威胁,
[4m52s] 是现代战略防空最理想效果最佳的黄金拦截期,
[4m58s] 也是各国反导技术研发的核心方向。
[5m8s] 虽然说呢中段反导拦截在整个反导作战的流程当中,那么它是比较容易实现的,或者说呢可以针对这个对方来袭的导弹
[5m20s] 进行精准的摧毁,但是最大的问题呢就是你的预警系统啊需要看得远,反导拦截弹呢需要飞得快,打得高打得远,所以对于技术方面的要求是比较高的,这就要求
[5m31s] 大型的相控阵预警雷达能够在极远的距离上,
[5m35s] 针对外大气层飞行的这种小型高速移动的目标能够进行精准的锁定,而且呢要对它进行持续的跟踪,还要有相关的这样的一个计算系统,针对它的飞行轨迹进行计算,这样的话呢
[5m49s] 才能够有效的引导反导拦截弹在外大气层对它进行精准的这样的一个截杀,而且呢反导拦截弹的性能要求非常高,不仅要飞得高飞得远飞得快,
[6m1s] 而且呢甚至还要配备有动能的战斗部,要针对处于飞行状态的弹道导弹进行精准的撞击,那么确保它
[6m10s] 是啊被彻底摧毁。
[6m15s] 3月5日,
[6m16s] 伊朗伊斯兰革命卫队发布声明称,
[6m19s] 在真实承诺4行动的第十七轮中,
[6m23s] 伊朗动用高超声速导弹和攻击无人机,成功突破美国萨德防御系统,
[6m29s] 打击了以色列国防部大楼与本古里安国际机场,
[6m34s] 并摧毁了7套雷达系统及核心配套设备。
[6m39s] 同期美国内部评估报告透露,作为中东关键中段反导装备的以色列箭式系统,
[6m46s] 在拦截伊朗中程弹道导弹时显现重大短板,
[6m50s] 实际拦截率远未达到原有设计标准。
[6m56s] 那么在以色列的国土范围内,
[6m59s] 呃有箭二箭三反导系统,也有美国
[7m2s] 部署的萨德反导系统,但是我们看仍然有数量相当多的伊朗弹道导弹成功突防,
[7m9s] 针对以色列区域内的目标进行了毁伤。
[7m12s] 为什么说箭二箭三以及萨德系统它联起手来,也没有办法针对伊朗的弹道导弹进行这种百%的防范和拦截。我认为呢首先是伊朗弹道导弹的技术有所提升,
[7m25s] 部分伊朗的弹道导弹采用了这种高超音速的战斗部,在飞行的末段会有突然的加速,或者说有急剧的飞行轨迹的变化,这样的话呢萨德系统
[7m36s] 和箭二箭三反导系统很难对它进行精准的拦截。此外呢伊朗的一些弹道导弹,比如说像霍拉姆沙赫尔四,
[7m44s] 那么它是一种重型的这样的一个弹头的设计理念,那么1.5吨到两吨的战斗部,啊它可以采用这种集束弹头啊的这样的一个模式,
[7m54s] 飞到了目标区上空之后,打出了一个满天星的效果,数10枚子弹头
[8m0s] 飞向目标区域,
[8m1s] 所有的防空反导系统都没有办法对它进行拦截。
[8m5s] 那么还有另外一种情况,美国的反导作战系统和以色列的箭二箭三反导系统,它没有办法进行联网作战,是各打各的,这样的话呢在相关的区域内没有办法进行目标信息的共享,
[8m17s] 那么也无法形成合力。
[8m20s] 2026年3月15日,
[8m23s] 伊朗伊斯兰革命卫队在真实承诺四军事行动中,
[8m27s] 首次实战部署了泥石导弹。
[8m31s] 与此同时,美军从韩国紧急调遣了萨德反导系统,
[8m35s] 以加强该地区的防御,
[8m37s] 应对伊朗导弹带来的持续威胁。
[8m43s] 若将远程反导视作千里点穴的区域防空手段,
[8m48s] 那么中程防空啊便是中空守护网,
[8m51s] 承担着衔接远近防御的关键使命。
[8m56s] 它覆盖数10公里乃至100多公里的中高空区域,
[9m0s] 作为防空体系中的
[9m1s] 中坚力量,
[9m3s] 承担起区域防空重任,护卫城市军事要地等高价值目标,成为整个防空网络中承上启下的核心组成部分。
[9m22s] 中程防空作为现代防空作战系统的关键环节,其防御半径一般在50~200公里之间,
[9m30s] 拦截高度重点覆盖10~20公里的中高空域。
[9m36s] 这片空域正是现代战争中空中威胁密集活动的核心地带。
[9m42s] 敌方战机若要突破外层防御,
[9m45s] 打击纵深关键目标必须穿越此区域。
[9m49s] 巡航导弹虽常利用低空突防,
[9m52s] 但多数情况下仍需在中高空调整姿态,
[9m56s] 寻找防御薄弱点,
[9m58s] 而中近程导弹在俯冲末段同样会经过这一高度区间,
[10m3s] 其高速特性要求中程防空系统及早介入,精确拦截。
[10m9s] 可以说
[10m10s] 这片中高空域是空中威胁,渗向核心目标的必经通道,
[10m15s] 也是防空体系中承前启后的关键拦截区。
[10m26s] 在现代化的防空作战行动当中,啊
[10m29s] 中程防空的难度是非常大的。
[10m31s] 因为
[10m32s] 很多对方的啊这种空中的来袭目标,其实它都是在中程防空的任务范围内,
[10m39s] 目标的属性是非常多的。
[10m41s] 此外呢在复杂的电磁环境的这样的一个状态下,中程的防空体系也可能会出现失灵的情况,啊因为对方可能会出动电子战飞机,
[10m52s] 针对你的防空作战体系进行压制。此外呢有的时候呢面对各种各样的目标数量众多的目标,那么中程防空系统它
[11m0s] 的这样的一个火力的密集程度可能会
[11m4s] 出现不足的情况。比如说一部发射机上,
[11m6s] 它只有数量有限的中程防空导弹打完了之后,那么对方的目标
[11m11s] 连续的持续的袭来,
[11m13s] 那中程防空体系可能也会面临弹药不足,火力不足的情况。
[11m21s] 在近期,
[11m22s] 美以联合对伊朗的军事行动中,中程导弹防御系统成为防空体系中的核心作战力量,
[11m29s] 也是双方攻防较量最为激烈的领域。
[11m33s] 为巩固中诚防御屏障,
[11m35s] 美国运用爱国者防空反导系统,
[11m39s] 结合天基预警卫星,陆基远程雷达及集成化指挥控制中心,
[11m45s] 打造了能够覆盖30公里至150公里空域的拦截体系,
[11m50s] 着重防护其军事基地与国防工业区域。
[11m54s] 然而面对伊朗高超音速武器,配合无人机集群的非对称组合式打击,
[12m1s] 此前被美军公开宣称拦截成功率可达80%~97%的爱国者系统,
[12m8s] 实际拦截率已跌至5%以下,
[12m12s] 系统还频频出现拦截假目标,漏过真实弹头的失误。
[12m20s] 爱国者曾在海湾战争期间
[12m23s] 一战成名,
[12m24s] 啊那当时呢美军爱国者系统它所拦截的导弹呢都是伊拉克
[12m30s] 萨达姆政权军队啊使用的比较老旧的飞毛腿弹道导弹,它的飞行轨迹是一个典型的抛物线,不会有这种
[12m39s] 呃机动变轨的这样的一些动作,所以呢只要是探测到了它的飞行轨迹,
[12m44s] 那么多发射一些爱国者拦截弹是可以对它进行有效拦截的,但是现代化的弹道导弹突防能力都很强,
[12m52s] 飞行轨迹是变化的,甚至可以说是有一些打水漂的这样的一个飞行轨迹,速度又很快,那么甚至可能会携带更多的诱饵弹和干扰弹,所以呢爱国者系统可能根本就看不清,
[13m3s] 可能也就追不上。
[13m6s] 爱国者导弹在部分实战场景中拦截效果未达预期,
[13m11s] 这一表现使得美以联合构建的中程防空反导系统暴露出短板,
[13m17s] 也让多国对该系统产生信任危机。
[13m23s] 在现代防空系统中,低空与超低空是远程及中程防空存在的短板,
[13m30s] 受到地球曲率地形掩蔽等影响,
[13m33s] 远程雷达难以追踪低空目标,
[13m36s] 而中程防空导弹呢在低空条件下,制导系统易受地面杂波的干扰,致使拦截效能下降。
[13m44s] 因此啊
[13m44s] 近程防空系统由此诞生,
[13m48s] 专门用于近距离的防护,
[13m50s] 弥补了防空网络在低空领域的不足。
[14m1s] 对普通公众而言,近程防空无疑是最常见最显眼且视觉冲击力最强的防空形式,那些在网络短片新闻报道和战场记录中常见的导弹腾空而起,
[14m15s] 尾焰撕裂黑夜,目标在空中爆炸,
[14m18s] 火光刹那映亮天空的拦截画面,
[14m22s] 大多源自近程防空系统,
[14m25s] 这些系统射程较短,但部署便捷,响应迅速。
[14m30s] 通常在目标接近阵地城市或重要设施的最后一刻才发动攻击,
[14m36s] 因而更易被现场捕捉,媒体记录并广泛传播。
[14m40s] 正是由于这种高频曝光,近程防空在许多人心中几乎成了防空的代名词。
[14m48s] 然而实际上近程防空在整体防空网络中充当的是最终贴身防线的功能,
[14m55s] 专门填补远程和中程防空无法覆盖的低空及超低空漏洞。
[15m4s] 近程防空作战体系在实际应用的过程当中,有的时候呢
[15m8s] 可能会成为战场上的奇兵,啊因为对方的直升机啊甚至包括这个战斗机在低空飞行的时候,啊它可能会放松警惕,另外一个来不及反应。
[15m17s] 所以这个时候近程防空火力针对目标进行射击的时候,战斗机很难对它进行规避,有的时候呢甚至连发射红外诱饵弹的这个时间都没有,
[15m27s] 但是呢近程防空也有很大的难度,因为很多近程防空作战体系,啊
[15m32s] 它的搜索系统是比较简单的,视野不够宽广,也无法针对远距离的目标进行识别,所以呢有的近程防空作战体系它本质上来讲是近视眼,
[15m43s] 而且呢还涉及到一些地形起伏的问题。
[15m45s] 如果对方利用这种地形起伏,啊那么去进行超低空的突防,那么在相关区域内部署的一些传感器,比如说低空搜索雷达或者是光学红外传感器,可能没有办法及时有效的识别目标。
[15m58s] 那么这些低空突防的这样的一些空中作战平台,也可能会针对
[16m2s] 啊这个近程的防空作战体系,包括相关的防空阵地进行打击和压制。
[16m11s] 在近期美以联合对伊朗的军事行动中,
[16m14s] 以色列拉斐尔公司研发的铁穹近程防空系统,经历了自2011年部署以来最激烈且最具转折性的实战检验。
[16m25s] 这款以往在冲突中以85%~90%拦截率享誉世界的明星武器,
[16m32s] 在伊朗真实承诺4行动的饱和攻击下,
[16m36s] 其卓越声誉荡然无存。
[16m39s] 以色列国防部3月17日向国会递交的评估报告中指出,
[16m45s] 面对伊朗连续发动的60多波攻势,
[16m48s] 铁穹系统的实际拦截率从历史标榜的90%以上骤降至约50%,
[16m55s] 而在集束弹头密集倾泻与无人机蜂群低空突袭的关键战斗阶段,
[17m1s] 其拦截成功率更是下滑至30%以下,
[17m5s] 刷新了服役以来的最低性能纪录。
[17m12s] 那么我们注意到在此次军事行动的过程当中,啊铁穹系统表现平平,我认为呢
[17m17s] 呃可能是有两方面的考虑,一方面呢就是它配套的
[17m20s] 它这种传感器啊或者是预警体系,可能是遭到了对方的火力打击,有部分铁穹系统
[17m26s] 啊那么针对空中来袭目标,它的反应速度不够快,或者说呢呃它的这样的一个情报和目标的引导体系啊不够完善。
[17m36s] 另外一个呢就是铁穹系统,它所使用的是塔米尔拦截弹,
[17m40s] 这种拦截弹,啊
[17m41s] 但是由于它装备和使用的数量比较多,所以呢它必须要进行成本的控制,啊它的飞行速度
[17m48s] 其实并没有那么快,啊那么有的时候针对高速目标进行打击的时候,可能会出现速度跟不上的情况。
[17m55s] 此外,呢现在伊朗主要是使用弹道导弹,针对以色列进行打击,
[18m0s] 面对高空高速飞行的弹道导弹,
[18m3s] 或者是配备有
[18m4s] 高超音速战斗部的弹道导弹。
[18m6s] 嗯这个铁穹系统可能它的拦截范围拦截的高度也都不足。
[18m10s] 当然了伊朗也使用了大量的这个无人机,但是这一次针对以色列进行后续的反击,伊朗更加注重使用啊这种中程的弹道导弹,或者是突防能力很强的弹道导弹。
[18m22s] 那么面对这样的一种情况,铁穹系统啊它就会出现
[18m26s] 啊反应速度不够快,啊包括呢啊这个拦截的范围拦截的高度都存在不足的情况。
[18m35s] 近程防空系统作为整个防空架构中关键的最后屏障,
[18m40s] 肩负着坚守底线,护卫要害的核心职责,
[18m45s] 但它并非完美无缺,仍存在明显的短板。
[18m50s] 其防御范围通常较为有限,
[18m53s] 探测与拦截能力都集中在较小区域,
[18m56s] 大多只能执行小尺度重点目标的局部防护任务,
[19m1s] 无法如同中远程防空系统那样,实现广大空域的大范围远距离监控与狙击。
[19m8s] 因此近程防空系统不能独自支撑一国一城或整个战区的全面防空安全,
[19m16s] 它更像是一件贴身的防弹衣,而非是能覆盖全域的保护罩,
[19m24s] 远程中程与近程防空系统相互配合,
[19m29s] 在现代战争中构筑了层次分明,分工明确、
[19m33s] 紧密衔接的全空域立体防空网络。
[19m37s] 三者各尽其责,彼此支援,
[19m40s] 逐层拦截,
[19m41s] 形成了一套由外至内,由高空到低空由远及近的周密防御体系。
[19m49s] 不过啊今年多场高强度军事冲突的实际表现,这套看似牢不可破的防空体系正持续受到新兴战场模式的严峻挑战,
[19m59s] 其传统作战思路和防御能力正在不断遭受冲击
[20m4s] 与重塑。
[20m14s] 在美以联合对伊朗发动的大规模军事行动中,
[20m18s] 可以清晰看到,
[20m20s] 美军以色列军队以及伊朗的防空系统都遭遇了前所未有的考验,
[20m26s] 在以往严密的多层防护网络,在实战中屡次被突破撕裂,甚至彻底崩溃。
[20m34s] 美军采用了以爱国者和萨德反导系统为核心的中远程导弹防御体系,
[20m41s] 以色列则调动了由铁穹、大卫投石索和箭式系统组成的全域防空反导系统。
[20m49s] 然而针对高超声速武器穿透无人机群饱和攻击,
[20m53s] 复合诱饵欺骗与强电磁干扰等策略,
[20m57s] 拦截失败的情况依然频繁发生。
[21m3s] 在此次行动的过程当中,啊美以的防空反导系统频频被这个洞穿,也就是说呢伊朗使用了啊一系列的突防手段,最终啊打穿了美国和以色列的一些防空反导的这个保护伞。
[21m17s] 那么针对相关的区域啊构成了更为猛烈的这个火力打击,我认为呢
[21m22s] 呃就是
[21m23s] 呃存在一些问题,比如说它系统之间没有进行有效的联合,存在一些这个兼容的问题,
[21m30s] 在各自为战的状态下肯定会出现漏洞。
[21m33s] 另外一个,呢
[21m33s] 而伊朗也是采取了一些灵活多变的这样的一些战术,多种火力多种模式联合运用,也让美国和以色列的防空反导系统防不胜防。
[21m47s] 在现代军事冲突中,
[21m49s] 高超声速武器的穿透与无人机蜂群的饱和攻击虽是关键威胁,
[21m55s] 但更具深远颠覆性的是战场成本效益失衡对防空体系带来的系统性难题。
[22m3s] 这已超越了单一装备的性能竞争,演变为从经济原理持久作战能力到战略自信的全方位压制,
[22m12s] 引发了拦截不起、消耗不起、补充不上的三重困局。
[22m20s] 以伊朗主力攻击武器见证者136自杀式无人机为例,
[22m25s] 这款被戏称作飞行摩托车的装备,
[22m28s] 彻底打破了传统军工的精密制造范式。
[22m32s] 机身使用民用级玻璃纤维和泡沫塑料拼装
[22m36s] 动力系统,
[22m37s] 直接借用普通摩托车的二冲程发动机,
[22m41s] 导航依靠民用GPS模块与简易惯性制导,
[22m46s] 甚至部分核心零件来自拆解的汽车和家电,从而极大压低了制造成本。
[22m53s] 凭借民用技术军用化、复杂系统简易化的理念,
[22m57s] 其批量生产单价稳定在2万至5万美元之间,
[23m2s] 某些简化版本甚至降至1.5万美元,
[23m6s] 仅相当于高端防空导弹成本的极小一部分。
[23m10s] 美以用于对抗的防空武器却如同天价奢侈品。
[23m16s] 以色列铁穹系统的拦截弹作为近程防空核心弹药,
[23m20s] 每枚成本达5万到10万美元,
[23m23s] 这意味着即便成功拦截防御方的经济损失,也远超进攻方。
[23m30s] 更甚者,美国爱国者三拦截弹作为中程防空主力,
[23m35s] 单枚成本飙升至400万至500万美元,
[23m39s] 堪比用超级跑车撞击普通代步车。
[23m43s] 无论胜负如何,防御方已在战略层面沦为输家。
[23m50s] 那么在效费比方面,啊如果说对方在进攻的过程当中啊采用了很多廉价的方式,或者说很多省钱的这样的一些技术手段,
[24m1s] 像见证者136
[24m2s] 其实呢它就是飞行的小摩托,那么它的成本控制得很低,但是对方可能要用先进的防空反导系统对它进行拦截。
[24m10s] 而伊朗的一些地区盟友,包括这个
[24m13s] 黎巴嫩真主党会使用大量的卡桑火箭弹,那么也就是说更低成本的作坊工厂当中生产出来的
[24m20s] 这种火箭弹针对以色列进行打击,那以色列方面使用的是什么?呢是这种塔米尔拦截弹,也就是铁穹系统的拦截弹。
[24m27s] 那么在这样的一种
[24m28s] 在相互的消耗战的过程当中,那以色列
[24m32s] 呃或者是美国它所面临的
[24m34s] 这种昂贵弹药的快速消耗和短缺的情况也会变得越来越多,也会变得越来越明显。
[24m42s] 由于战场上攻击和防御的效费比例严重失衡,
[24m47s] 防空弹药库存告急,
[24m49s] 形势十分紧迫。
[24m51s] 3月18日,
[24m52s] 以色列国防部火速向美国求援,
[24m55s] 美军立刻出动c17运输机,
[24m58s] 紧急空运了近千枚拦截导弹,
[25m1s] 为以色列的库存注入强心针。
[25m4s] 与此同时,
[25m5s] 美军从韩国、亚太到欧洲多个据点,迅速调集防空系统,
[25m11s] 全力缓解战场上防空装备和弹药捉襟见肘的困境。
[25m18s] 现代防空系统打造了一个从远程中段反导,
[25m22s] 近程区域防空到近程末段拦截的
[25m26s] 完整链条,
[25m28s] 构建了远近交错高低兼顾的立体防御天网。
[25m33s] 然而
[25m34s] 高超声速武器如闪电般突破,
[25m36s] 无人机群的压制以及饱和式攻击的浪潮不断袭来,
[25m40s] 让传统防空体系面临着严峻的考验,
[25m46s] 攻防成本的天平正在严重倾斜,
[25m49s] 威胁形势日新月异,
[25m51s] 如何在提升拦截效果的同时,勒紧成本的缰绳,
[25m56s] 变得
[25m57s] 至关重要,
[25m59s] 让防空网看得远,
[26m1s] 拦得准,
[26m2s] 扛得住,
[26m3s] 耗得起,
[26m4s] 成为了左右未来防空安全和战争胜负的
[26m8s] 关键棋局。
[26m11s] 好了,观众朋友们,感谢您持续关注国防军事频道,军事科技,我们下周同一时间
[26m17s] 再见。
[26m19s] 77载深蓝砺剑,
[26m21s] 77载向海图强。
[26m25s] 在人民海军的序列中
[26m27s] 有这样一些特殊的存在,
[26m29s] 他们不担负作战任务,而是青年官兵驶向大洋的第一课堂。
[26m36s] 看戚继光舰启程远航,
[26m40s] 听郑和简续写传奇。
[26m43s] 值此人民海军成立77周年之际,
[26m46s] 军事科技
[26m47s] 与您和海军青年官兵一起
[26m50s] 启程远航,乘风破浪。
@@ -0,0 +1 @@
<table><td colspan="3">As of December 31,<i>(In millions)</i>201820172016Total return4.9%5.0%5.1%</table>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,33 @@
seg_id,header,start_line,end_line,line_count,min_confidence,punct_ok,note
0,"【导视】",1,17,17,1.0000,True,""
1,"【主持人1】",18,40,23,1.0000,True,""
2,"【解说1】",41,60,20,1.0000,True,""
3,"远程防空:弹道导弹与反导系统的巅峰对决",,,0,0.0000,,"隔断"
4,"【解说2】",61,77,17,1.0000,True,""
5,"【动画1】",78,111,34,1.0000,True,""
6,"【专家1】",112,150,39,1.0000,True,""
7,"【解说2】",151,168,18,0.9500,True,""
8,"【专家2】",169,216,48,1.0000,False,"标点恢复失败已回退,需人工"
9,"【解说3】",217,225,9,1.0000,True,""
10,"【主持人2】",226,240,15,1.0000,True,""
11,"中程防空:复合威胁下的区域空天防御攻坚战",,,0,0.0000,,"隔断"
12,"【解说4】",241,270,30,1.0000,True,""
13,"【专家3】",271,300,30,1.0000,True,""
14,"【解说5】",301,327,27,1.0000,True,""
15,"【专家4】",328,353,26,1.0000,True,""
16,"【解说6】",354,360,7,1.0000,True,""
17,"【主持人3】",361,376,16,1.0000,True,""
18,"近程防空:低空突防与最后防线的生死较量",,,0,0.0000,,"隔断"
19,"【解说7】",377,403,27,1.0000,True,""
20,"【专家5】",404,442,39,1.0000,True,""
21,"【解说8】",443,470,28,1.0000,True,""
22,"【专家6】",471,516,46,1.0000,True,""
23,"【解说9】",517,539,23,1.0000,True,""
24,"【主持人 4】",540,558,19,1.0000,True,""
25,"体系防空:全域防御与精准拦截的新困境",,,0,0.0000,,"隔断"
26,"【解说10】",559,583,25,0.9900,True,""
27,"【专家7】",584,608,25,0.9900,False,"标点恢复失败已回退,需人工"
28,"【解说10】",609,668,60,1.0000,True,""
29,"【专家8】",669,696,28,1.0000,True,""
30,"【解说11】",697,713,17,1.0000,True,""
31,"【主持人 5】",714,743,30,1.0000,True,""
1 seg_id header start_line end_line line_count min_confidence punct_ok note
2 0 【导视】 1 17 17 1.0000 True
3 1 【主持人1】 18 40 23 1.0000 True
4 2 【解说1】 41 60 20 1.0000 True
5 3 远程防空:弹道导弹与反导系统的巅峰对决 0 0.0000 隔断
6 4 【解说2】 61 77 17 1.0000 True
7 5 【动画1】 78 111 34 1.0000 True
8 6 【专家1】 112 150 39 1.0000 True
9 7 【解说2】 151 168 18 0.9500 True
10 8 【专家2】 169 216 48 1.0000 False 标点恢复失败已回退,需人工
11 9 【解说3】 217 225 9 1.0000 True
12 10 【主持人2】 226 240 15 1.0000 True
13 11 中程防空:复合威胁下的区域空天防御攻坚战 0 0.0000 隔断
14 12 【解说4】 241 270 30 1.0000 True
15 13 【专家3】 271 300 30 1.0000 True
16 14 【解说5】 301 327 27 1.0000 True
17 15 【专家4】 328 353 26 1.0000 True
18 16 【解说6】 354 360 7 1.0000 True
19 17 【主持人3】 361 376 16 1.0000 True
20 18 近程防空:低空突防与最后防线的生死较量 0 0.0000 隔断
21 19 【解说7】 377 403 27 1.0000 True
22 20 【专家5】 404 442 39 1.0000 True
23 21 【解说8】 443 470 28 1.0000 True
24 22 【专家6】 471 516 46 1.0000 True
25 23 【解说9】 517 539 23 1.0000 True
26 24 【主持人 4】 540 558 19 1.0000 True
27 25 体系防空:全域防御与精准拦截的新困境 0 0.0000 隔断
28 26 【解说10】 559 583 25 0.9900 True
29 27 【专家7】 584 608 25 0.9900 False 标点恢复失败已回退,需人工
30 28 【解说10】 609 668 60 1.0000 True
31 29 【专家8】 669 696 28 1.0000 True
32 30 【解说11】 697 713 17 1.0000 True
33 31 【主持人 5】 714 743 30 1.0000 True
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,6 @@
line_no,timestamp,b_original,asr_context,final_text,change_type,confidence,reason
69,3m21s,作为现代战略空军的最高层级,弹道导弹与反导系统之间的对决,无疑是最具战略威慑力且最检验技术水平的顶峰交锋。 其中中段反导作为现代战略防空的最高层级, 更是左右战场局势的核心要素。 这项技术常被形容为 太空中以子弹击落子弹,,作为现代战略防空的最高层级,minor_edit,0.99,B稿为'作为现代战略空军的最高层级',ASR为'作为现代战略防空的最高层级'。根据上下文(反导系统、中段反导),'防空'比'空军'更准确,应为B稿OCR识别错误,以ASR修正。
90,4m12s,未段导弹,受部署限制难以实施拦截,成功率极低。 末段导弹俯冲至几十公里至几公里高空, 速度达马赫数10~20, 仅留数秒至十几秒的反应时间,,末段导弹,editor_typo,1.0,B稿'未段导弹'为明显错字,ASR为'末段导弹',且上下文均为'末段',予以修正。
426,15m40s,有的远程防空作战体系,它的搜索系统是比较简单的,视野不够宽广,也无法针对远距离的目标进行识别,所以呢有的近程防空作战体系它本质上来讲是近视眼, 而且呢还涉及到一些地形起伏的问题。 如果对方利用这种地形起伏,啊那么去进行超低空的突防,那么在相关区域内部署的一些传感器,比如说低空搜索雷达或者是光学红外传感器,可能没有办法及时有效的识别目标。,有的近程防空作战体系,minor_edit,0.95,B稿为'远程'ASR及上下文均为'近程',属于OCR错字,根据ASR修正。
652,23m18s,作为远程防空核心弹药,美以用于对抗的防空武器却如同天价奢侈品。 以色列铁穹系统的拦截弹作为近程防空核心弹药, 每枚成本达5万到10万美元, 这意味着即便成功拦截防御方的经济损失,也远超进攻方。 更甚者,美国爱国者三拦截弹作为中程防空主力,,作为近程防空核心弹药,minor_edit,0.99,B稿OCR误将“近程”识别为“远程”,根据ASR上下文及铁穹系统实际定位修正。
719,25m30s,高低坚固的立体防御天网,完整链条, 构建了远近交错高低兼顾的立体防御天网。 然而 高超声速武器如闪电般突破, 无人机群的压制以及饱和式攻击的浪潮不断袭来, 让传统防空体系面临着严峻的考验,,高低兼顾的立体防御天网,editor_typo,0.99,B稿“高低坚固”应为“高低兼顾”,ASR为“高低兼顾”,语义通顺,修正OCR错字。
1 line_no timestamp b_original asr_context final_text change_type confidence reason
2 69 3m21s 作为现代战略空军的最高层级 弹道导弹与反导系统之间的对决,无疑是最具战略威慑力且最检验技术水平的顶峰交锋。 其中中段反导作为现代战略防空的最高层级, 更是左右战场局势的核心要素。 这项技术常被形容为 太空中以子弹击落子弹, 作为现代战略防空的最高层级 minor_edit 0.99 B稿为'作为现代战略空军的最高层级',ASR为'作为现代战略防空的最高层级'。根据上下文(反导系统、中段反导),'防空'比'空军'更准确,应为B稿OCR识别错误,以ASR修正。
3 90 4m12s 未段导弹 受部署限制难以实施拦截,成功率极低。 末段导弹俯冲至几十公里至几公里高空, 速度达马赫数10~20, 仅留数秒至十几秒的反应时间, 末段导弹 editor_typo 1.0 B稿'未段导弹'为明显错字,ASR为'末段导弹',且上下文均为'末段',予以修正。
4 426 15m40s 有的远程防空作战体系 它的搜索系统是比较简单的,视野不够宽广,也无法针对远距离的目标进行识别,所以呢有的近程防空作战体系它本质上来讲是近视眼, 而且呢还涉及到一些地形起伏的问题。 如果对方利用这种地形起伏,啊那么去进行超低空的突防,那么在相关区域内部署的一些传感器,比如说低空搜索雷达或者是光学红外传感器,可能没有办法及时有效的识别目标。 有的近程防空作战体系 minor_edit 0.95 B稿为'远程',ASR及上下文均为'近程',属于OCR错字,根据ASR修正。
5 652 23m18s 作为远程防空核心弹药 美以用于对抗的防空武器却如同天价奢侈品。 以色列铁穹系统的拦截弹作为近程防空核心弹药, 每枚成本达5万到10万美元, 这意味着即便成功拦截防御方的经济损失,也远超进攻方。 更甚者,美国爱国者三拦截弹作为中程防空主力, 作为近程防空核心弹药 minor_edit 0.99 B稿OCR误将“近程”识别为“远程”,根据ASR上下文及铁穹系统实际定位修正。
6 719 25m30s 高低坚固的立体防御天网 完整链条, 构建了远近交错高低兼顾的立体防御天网。 然而 高超声速武器如闪电般突破, 无人机群的压制以及饱和式攻击的浪潮不断袭来, 让传统防空体系面临着严峻的考验, 高低兼顾的立体防御天网 editor_typo 0.99 B稿“高低坚固”应为“高低兼顾”,ASR为“高低兼顾”,语义通顺,修正OCR错字。
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,268 @@
# -*- coding: utf-8 -*-
"""
阶段 A:抽帧 + OCR(产出原始缓存 ocr_raw.jsonl)
================================================
策略:OCR 优先,每帧都 OCR绝不使用亮度判空/dHash/IoU 过滤
idx 断点续跑:读取已有 ocr_raw.jsonl 中最大 idx, max_idx+1 继续
"""
import base64
import json
import subprocess
import sys
from pathlib import Path
import requests
# 根治 Windows 控制台 GBK 崩溃:强制 stdout 用 UTF-8
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
# ========================================================================
# 全局参数(放文件顶部做常量,方便后续调)
# ========================================================================
FPS = 1
CROP_BOTTOM_RATIO = 0.2
SIM_THRESHOLD = 0.85
OLLAMA_URL = "http://localhost:11434/api/generate"
MODEL = "deepseek-ocr"
OCR_PROMPT = "Free OCR."
# 工作目录(脚本所在目录)
WORK_DIR = Path(__file__).resolve().parent
SOURCE_VIDEO_DIR = WORK_DIR / "source"
FRAMES_DIR = WORK_DIR / "frames_v2"
OCR_RAW_PATH = WORK_DIR / "ocr_raw.jsonl"
# ========================================================================
# A1: 定位源视频
# ========================================================================
def find_source_video():
"""在工作目录 source/ 下定位 .mp4 或 .mkv 源视频"""
for ext in (".mp4", ".mkv"):
candidates = list(SOURCE_VIDEO_DIR.glob(f"*{ext}"))
if candidates:
return candidates[0]
raise FileNotFoundError(
f"{SOURCE_VIDEO_DIR} 中未找到 .mp4 或 .mkv 文件"
)
def get_video_info(video_path):
"""用 ffprobe 获取时长(秒)和分辨率(宽x高)"""
cmd = [
"ffprobe", "-v", "error",
"-select_streams", "v:0",
"-show_entries", "stream=duration,width,height",
"-of", "json",
str(video_path),
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(f"ffprobe 失败:\n{result.stderr}")
info = json.loads(result.stdout)
stream = info["streams"][0]
duration = float(stream.get("duration", 0))
width = stream["width"]
height = stream["height"]
return duration, width, height
# ========================================================================
# A2: ffmpeg 抽帧
# ========================================================================
def extract_frames(video_path):
"""
ffmpeg 1fps 抽帧,裁切下方 20% 字幕区域
输出到 frames_v2/frame_%04d.png( 0001 开始)
返回按文件名排序的帧路径列表
"""
FRAMES_DIR.mkdir(parents=True, exist_ok=True)
# 清理旧帧避免残留
for old in FRAMES_DIR.glob("frame_*.png"):
old.unlink()
crop_expr = (
f"fps={FPS},"
f"crop=iw:ih*{CROP_BOTTOM_RATIO}:0:ih*{1 - CROP_BOTTOM_RATIO}"
)
frame_pattern = str(FRAMES_DIR / "frame_%04d.png")
cmd = [
"ffmpeg",
"-i", str(video_path),
"-vf", crop_expr,
"-q:v", "2",
frame_pattern,
"-y",
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(f"ffmpeg 抽帧失败:\n{result.stderr}")
frames = sorted(FRAMES_DIR.glob("frame_*.png"))
return frames
# ========================================================================
# OCR 单帧
# ========================================================================
def ocr_single_frame(image_path):
"""
调用本地 Ollama OCR 识别单帧
API: POST /api/generate,单轮,图片走 base64,stream=false
返回 strip 后的文本;异常向上抛出让调用方处理
"""
with open(image_path, "rb") as fh:
img_bytes = fh.read()
img_b64 = base64.b64encode(img_bytes).decode("utf-8")
body = {
"model": MODEL,
"prompt": OCR_PROMPT,
"images": [img_b64],
"stream": False,
}
resp = requests.post(OLLAMA_URL, json=body, timeout=120)
resp.raise_for_status()
data = resp.json()
text = data.get("response", "").strip()
return text
# ========================================================================
# A3: 单帧验证
# ========================================================================
def test_single_frame():
"""
A3: 验证单帧 OCR
frame_0076.png 调一次 OCR,打印原始返回
若对不上预期则中止,不进入批量
"""
test_frame = FRAMES_DIR / "frame_0076.png"
if not test_frame.exists():
print("[A3] [WARN] test frame not found:", test_frame)
print("[A3] skip single-frame check")
return
print("[A3] test frame:", test_frame.name)
try:
raw_text = ocr_single_frame(test_frame)
except Exception as exc:
print("[A3] [FAIL] OCR call failed:", exc)
print("[A3] check Ollama / deepseek-ocr, abort.")
sys.exit(1)
print("[A3] raw return:", repr(raw_text))
if "" in raw_text or "" in raw_text:
print("[A3] [OK] verification passed.")
else:
print("[A3] [WARN] expected '各位观众你们好', got:", repr(raw_text))
print("[A3] abort.")
sys.exit(1)
# ========================================================================
# A4: 批量 OCR(按 idx 断点续跑 + 单帧容错)
# ========================================================================
def batch_ocr(frames):
"""
idx 断点续跑:
- 读取 ocr_raw.jsonl 中已有记录的最大 idx
- 只处理 idx > max_existing 的帧
- 每帧 OCR 外包 try/except,报错写 error 字段并 continue,绝不崩整个进程
- 进度只打数字: f"{idx}/{total}", flush=True
"""
total = len(frames)
# --- 读已有记录,取最大 idx ---
max_existing_idx = 0
if OCR_RAW_PATH.exists():
with open(OCR_RAW_PATH, "r", encoding="utf-8") as fh:
for line in fh:
line = line.strip()
if not line:
continue
try:
rec = json.loads(line)
max_existing_idx = max(max_existing_idx, rec.get("idx", 0))
except json.JSONDecodeError:
pass
if max_existing_idx >= total:
print(f"[A4] all {total} frames already done (max idx={max_existing_idx}), skip.")
return
start_idx = max_existing_idx + 1
print(f"[A4] max existing idx = {max_existing_idx}, resume from idx {start_idx}")
# --- 构建 idx -> frame_path 映射 ---
idx_to_path = {}
for fp in frames:
idx = int(fp.stem.split("_")[1])
idx_to_path[idx] = fp
# --- 追加写入 ---
with open(OCR_RAW_PATH, "a", encoding="utf-8") as fh:
for idx in range(start_idx, total + 1):
t_sec = idx - 1
frame_path = idx_to_path.get(idx)
if frame_path is None:
# 帧文件缺失(不应发生,但容错)
record = {"idx": idx, "t_sec": t_sec, "text": "", "error": "frame file missing"}
fh.write(json.dumps(record, ensure_ascii=False) + "\n")
fh.flush()
print(f"{idx}/{total}", flush=True)
continue
try:
text = ocr_single_frame(frame_path)
record = {"idx": idx, "t_sec": t_sec, "text": text}
except Exception as exc:
record = {"idx": idx, "t_sec": t_sec, "text": "", "error": str(exc)}
fh.write(json.dumps(record, ensure_ascii=False) + "\n")
fh.flush()
if idx % 50 == 0:
print(f"{idx}/{total}", flush=True)
print(f"{total}/{total}", flush=True)
print(f"[A4] OCR done, {total} records in {OCR_RAW_PATH}")
# ========================================================================
# 主流程
# ========================================================================
def main():
# A1
video_path = find_source_video()
duration, width, height = get_video_info(video_path)
print(f"[A1] video: {video_path}")
print(f"[A1] duration: {duration:.1f}s resolution: {width}x{height}")
# A2: skip re-extract if frames already exist
FRAMES_DIR.mkdir(parents=True, exist_ok=True)
existing = sorted(FRAMES_DIR.glob("frame_*.png"))
if existing:
print(f"[A2] {len(existing)} frames already exist, skip re-extract")
frames = existing
else:
print(f"[A2] ffmpeg extract (fps={FPS}, crop_bottom_ratio={CROP_BOTTOM_RATIO})...")
frames = extract_frames(video_path)
print(f"[A2] extracted {len(frames)} frames -> {FRAMES_DIR}")
# A3
test_single_frame()
# A4
batch_ocr(frames)
print("\nstage A done.")
if __name__ == "__main__":
main()
@@ -0,0 +1,300 @@
# -*- coding: utf-8 -*-
"""
阶段 B:文本去重 + 出稿(只读缓存,可反复重跑调阈值)
=======================================================
策略:基于 difflib 文本相似度折叠连续重复段,绝不使用像素级过滤
"""
import csv
import json
import re
from difflib import SequenceMatcher
from pathlib import Path
# ═══════════════════════════════════════════════════════════════
# 全局参数(与阶段 A 共享,放文件顶部做常量)
# ═══════════════════════════════════════════════════════════════
FPS = 1
CROP_BOTTOM_RATIO = 0.2
SIM_THRESHOLD = 0.85
# 工作目录
WORK_DIR = Path(__file__).resolve().parent
OCR_RAW_PATH = WORK_DIR / "ocr_raw.jsonl"
B_MANUSCRIPT_PATH = WORK_DIR / "B稿_v2.txt"
DEBUG_CSV_PATH = WORK_DIR / "dedup_debug.csv"
BLANK_FILTERED_PATH = WORK_DIR / "blank_filtered.txt"
def similarity(a, b):
"""计算两个文本的 difflib 相似度(0.0 ~ 1.0)"""
if not a or not b:
return 0.0
return SequenceMatcher(None, a, b).ratio()
def is_blank_ocr(text):
"""基于文本的空场判定:空串 / HTML幻觉 / 不含汉字"""
t = text.strip()
if not t: # 空串
return True
if '<' in t and '>' in t: # HTML/markup 幻觉(如 <table>)
return True
if not re.search(r'[\u4e00-\u9fff]', t): # 不含任何汉字 → 非本节目字幕
return True
return False
# ═══════════════════════════════════════════════════════════════
# B1: 读缓存
# ═══════════════════════════════════════════════════════════════
def load_ocr_raw():
"""读 ocr_raw.jsonl,按 idx(时间)升序返回记录列表"""
if not OCR_RAW_PATH.exists():
raise FileNotFoundError(
f"ocr_raw.jsonl 不存在: {OCR_RAW_PATH}\n"
"请先跑阶段 A(stage_a_extract_ocr.py)。"
)
records = []
with open(OCR_RAW_PATH, "r", encoding="utf-8") as fh:
for line in fh:
line = line.strip()
if not line:
continue
rec = json.loads(line)
records.append(rec)
# 按 idx 升序
records.sort(key=lambda r: r["idx"])
return records
# ═══════════════════════════════════════════════════════════════
# B2: 折叠连续重复段(裁判在此,基于文本不基于像素)
# ═══════════════════════════════════════════════════════════════
def collapse_segments(records):
"""
维护"当前字幕"(文本 + 起始 t_sec),遍历每帧:
- 文本为空 空场:结束当前字幕(若有),并清空,不开新段
- 非空且与当前字幕文本的 difflib 相似度 SIM_THRESHOLD 同一条,并入当前段(记下这条文本备投票)
- 非空且相似度 < SIM_THRESHOLD 结束并输出当前段,用这帧开新段
只比时间相邻帧,只折叠连续段;绝不全局去重
每段最终文本 = 段内出现次数最多的 OCR 文本(多数投票,抵消个别帧抖动)
每段时间戳 = 段内最早 t_sec
返回: (segments, debug_rows)
segments: [{"start_t": float, "text": str}, ...]
debug_rows: [{idx, t_sec, ocr_text, decision, merged_into_start_t}, ...]
"""
segments = []
debug_rows = []
if not records:
return segments, debug_rows
current_text = None # 当前段内用于比对的参考文本
current_texts = [] # 当前段内所有 OCR 文本(用于投票)
current_start_t = None # 当前段起始 t_sec
current_segment_indices = [] # 当前段包含的帧号列表
def _finalize_segment():
"""输出当前段(若存在)"""
nonlocal current_text, current_texts, current_start_t, current_segment_indices
if current_text is not None and current_texts:
# 多数投票:选出现次数最多的文本
text_counts = {}
for t in current_texts:
text_counts[t] = text_counts.get(t, 0) + 1
best_text = max(text_counts, key=lambda k: text_counts[k])
segments.append({
"start_t": current_start_t,
"text": best_text,
})
current_text = None
current_texts = []
current_start_t = None
current_segment_indices = []
for rec in records:
ocr_text = rec["text"]
if is_blank_ocr(ocr_text):
# 空场:结束当前字幕(若有),并清空,不开新段
if current_text is not None:
_finalize_segment()
debug_rows.append({
"idx": rec["idx"],
"t_sec": rec["t_sec"],
"ocr_text": ocr_text,
"decision": "blank",
"merged_into_start_t": "",
})
continue
if current_text is None:
# 开新段
current_text = ocr_text
current_texts = [ocr_text]
current_start_t = rec["t_sec"]
current_segment_indices = [rec["idx"]]
debug_rows.append({
"idx": rec["idx"],
"t_sec": rec["t_sec"],
"ocr_text": ocr_text,
"decision": "new",
"merged_into_start_t": rec["t_sec"],
})
else:
sim = similarity(current_text, ocr_text)
if sim >= SIM_THRESHOLD:
# 同一条,并入当前段
current_texts.append(ocr_text)
current_segment_indices.append(rec["idx"])
debug_rows.append({
"idx": rec["idx"],
"t_sec": rec["t_sec"],
"ocr_text": ocr_text,
"decision": "merged",
"merged_into_start_t": current_start_t,
})
else:
# 结束当前段,用这帧开新段
_finalize_segment()
current_text = ocr_text
current_texts = [ocr_text]
current_start_t = rec["t_sec"]
current_segment_indices = [rec["idx"]]
debug_rows.append({
"idx": rec["idx"],
"t_sec": rec["t_sec"],
"ocr_text": ocr_text,
"decision": "new",
"merged_into_start_t": rec["t_sec"],
})
# 遍历结束,落最后一个段
_finalize_segment()
return segments, debug_rows
# ═══════════════════════════════════════════════════════════════
# B3: 输出 B稿_v2.txt
# ═══════════════════════════════════════════════════════════════
def write_b_manuscript(segments):
"""
输出 B稿_v2.txt,每行格式: [XmYs] 文本
: [1m18s] 我是主持人蓝皓
"""
lines = []
for seg in segments:
t = seg["start_t"]
m = int(t) // 60
s = int(t) % 60
cleaned = re.sub(r'^[#*\->`\s]+', '', seg['text']).strip()
lines.append(f"[{m}m{s}s] {cleaned}")
with open(B_MANUSCRIPT_PATH, "w", encoding="utf-8") as fh:
fh.write("\n".join(lines) + "\n")
print(f"[B3] B稿_v2.txt 写入: {B_MANUSCRIPT_PATH} ({len(lines)} 行)")
return lines
# ═══════════════════════════════════════════════════════════════
# B4: 输出 dedup_debug.csv
# ═══════════════════════════════════════════════════════════════
def write_debug_csv(debug_rows):
"""
输出 dedup_debug.csv,逐帧记:
idx, t_sec, ocr_text, decision(new/merged/blank), merged_into_start_t
"""
with open(DEBUG_CSV_PATH, "w", encoding="utf-8", newline="") as fh:
writer = csv.writer(fh)
writer.writerow([
"idx", "t_sec", "ocr_text",
"decision", "merged_into_start_t",
])
for row in debug_rows:
writer.writerow([
row["idx"],
row["t_sec"],
row["ocr_text"],
row["decision"],
row["merged_into_start_t"],
])
print(f"[B4] dedup_debug.csv 写入: {DEBUG_CSV_PATH} ({len(debug_rows)} 行)")
# ═══════════════════════════════════════════════════════════════
# 主流程
# ═══════════════════════════════════════════════════════════════
def main():
print(f"[B] SIM_THRESHOLD = {SIM_THRESHOLD}")
# B1
records = load_ocr_raw()
print(f"[B1] 读取 ocr_raw.jsonl,共 {len(records)} 条记录")
# B2
segments, debug_rows = collapse_segments(records)
print(f"[B2] 折叠完成: {len(segments)} 个不连续字幕段")
print(f"[B2] 调试行数: {len(debug_rows)}")
# B3
lines = write_b_manuscript(segments)
# B4
write_debug_csv(debug_rows)
# ═══════════════════════════════════════════════════════════
# 跑完报告
# ═══════════════════════════════════════════════════════════
total_frames = len(records)
ocr_success = sum(1 for r in records if r["text"].strip())
blank_count = sum(1 for r in records if is_blank_ocr(r["text"]))
# 收集所有"被判空场但原文本非空"的去重文本
blank_but_nonempty = set()
for r in records:
t = r["text"]
if is_blank_ocr(t) and t.strip():
blank_but_nonempty.add(t)
print("\n" + "=" * 60)
print("跑完报告")
print("=" * 60)
print(f"抽帧总数: {total_frames}")
print(f"OCR 成功数(非空): {ocr_success}")
print(f"空场数: {blank_count}")
print(f"B稿_v2.txt 最终行数: {len(lines)}")
print("=" * 60)
print(f"B 稿路径: {B_MANUSCRIPT_PATH}")
print(f"调试表路径: {DEBUG_CSV_PATH}")
# 写出"被判空场但原文本非空"去重清单到文件
blank_list = sorted(blank_but_nonempty)
with open(BLANK_FILTERED_PATH, "w", encoding="utf-8") as fh:
for t in blank_list:
fh.write(t + "\n")
print(f"\n空白过滤清单写入: {BLANK_FILTERED_PATH} ({len(blank_list)} 条去重文本)")
# 打印"被判空场但原文本非空"的去重清单
if blank_but_nonempty:
print("\n" + "=" * 60)
print("【被判空场但原文本非空】去重文本清单(请肉眼确认无真中文字幕)")
print("=" * 60)
for i, t in enumerate(blank_list, 1):
print(f" [{i}] {repr(t)}")
print(f"{len(blank_but_nonempty)} 条去重文本")
else:
print("\n'被判空场但原文本非空'的文本。")
print("\n阶段 B 完成。")
if __name__ == "__main__":
main()
@@ -0,0 +1 @@
C-17运输机|GPS模块|中段反导|中程防空|二冲程发动机|以色列国防部|伊斯兰革命卫队|低空搜索雷达|光学红外传感器|动能撞击|卡桑|塔米尔|复合诱饵欺骗|多弹头诱饵|大卫投石索|天基预警卫星|子弹头|巡航导弹|干扰弹|弹道导弹|强电磁干扰|惯性制导|投石索|拉斐尔公司|攻击无人机|无人机蜂群|末端拦截|本·古里安国际机场|机动变轨|极速弹头|标准-6|泥石|爱国者|爱国者-3|相控阵预警雷达|真主党|真实承诺4行动|短程防空|简易惯性制导|箭-2反导系统|箭-3反导系统|箭式|红外传感器|红外诱饵弹|萨德|蓝皓|见证者-136|诱饵弹|近程防空|远程反导|远程防空|铁穹|防空导弹|防空武器|陆基远程雷达|集成化指挥控制中心|集束弹头|霍拉姆沙赫尔|霍拉姆沙赫尔-4|非对称策略|飞毛腿|饱和攻击|驻韩国庆尚北道星州基地|高超音速
@@ -0,0 +1,743 @@
[0m8s] 导弹呼啸而过
[0m9s] 战机低空穿梭
[0m11s] 无人机如蜂群般袭来
[0m14s] 这些空中威胁
[0m15s] 正不断挑战防御体系的极限
[0m18s] 它们是如何被精确拦截的
[0m21s] 究竟是什么力量
[0m22s] 能在遥远距离精准打击
[0m24s] 在近在咫尺时
[0m26s] 完成致命一击呢
[0m28s] 本期《军事科技》
[0m30s] 将带您逐步解析
[0m31s] 现代防空网络的复杂机制
[0m34s] 亲眼见证
[0m35s] 科技与智慧打造的空中屏障
[0m38s] 揭秘现代防空体系
[0m40s] 御敌于空的终极密码
[1m15s] 各位观众你们好
[1m16s] 欢迎收看《军事科技》
[1m17s] 我是主持人蓝皓
[1m20s] 从第二次世界大战时期的
[1m21s] 战机轰炸
[1m22s] 到冷战时期的导弹威慑
[1m24s] 再到如今的无人机
[1m26s] 高超声速武器的崛起
[1m28s] 空中威胁的形态
[1m29s] 不断地迭代
[1m31s] 防御体系也在随之进化
[1m34s] 而如今的现代化防空系统
[1m36s] 早已不再是
[1m37s] 单一武器的简单对抗了
[1m39s] 而是构建了
[1m40s] 以远 中 近衔接
[1m42s] 高中 低覆盖为一体的
[1m44s] 立体化防御系统
[1m46s] 仿佛一块层层镶嵌的空中盾牌
[1m49s] 从千里之外的高空
[1m51s] 到咫尺之间的低空
[1m53s] 形成了无死角的防护
[1m56s] 在与新型空中威胁持续对抗当中
[1m59s] 上演着一场场激烈的
[2m1s] 攻与防的较量
[2m19s] 2026年2月28日
[2m21s] 美以两国协同向伊朗发起军事打击
[2m24s] 中东地区顿时战火纷飞
[2m27s] 硝烟弥漫
[2m29s] 随着冲突不断加剧
[2m30s] 各方频繁动用弹道导弹
[2m32s] 巡航导弹
[2m34s] 高超声速武器
[2m35s] 以及自杀式无人机等先进装备
[2m38s] 展开密集的高强度空袭
[2m41s] 波斯湾上空
[2m42s] 随即呈现出一场
[2m44s] 攻防交织的终极较量
[2m46s] 涉及突破与拦截
[2m48s] 毁灭与保卫
[2m50s] 这场关乎生死存亡
[2m51s] 与战局走向的空中攻防战
[2m54s] 彻底爆发
[3m4s] 在涉及远 中 近程
[3m7s] 和高 中 低空的
[3m8s] 全方位空战对抗中
[3m11s] 弹道导弹
[3m12s] 与反导系统之间的对决
[3m14s] 无疑是最具战略威慑力
[3m16s] 且最检验技术水平的顶峰交锋
[3m20s] 中段反导
[3m21s] 作为现代战略防空的最高层级
[3m24s] 更是左右战场局势的核心要素
[3m28s] 这项技术
[3m29s] 常被形容为
[3m30s] 太空中以子弹击落子弹
[3m34s] 它标志着
[3m35s] 当今世界军事科技的极致成就
[3m37s] 也是战略防空领域
[3m39s] 真正的制高点
[3m48s] 一枚中远程弹道导弹的飞行
[3m50s] 分为上升段
[3m52s] 中段
[3m53s] 末段
[3m54s] 三者在时空 速度上的巨大差异
[3m57s] 直接决定了
[3m58s] 拦截难度的天壤之别
[4m0s] 上升段仅持续3至5分钟
[4m3s] 时间窗口稍纵即逝
[4m6s] 且需在敌国领土拦截
[4m8s] 受部署限制难以实施
[4m10s] 拦截成功率极低
[4m12s] 末段导弹
[4m13s] 俯冲至几十公里至几公里高空
[4m16s] 速度达马赫数10至20
[4m19s] 仅留数秒至十几秒的反应时间
[4m22s] 同时面临机动变轨
[4m24s] 多弹头诱饵及低空探测盲区
[4m27s] 拦截风险高且成功率有限
[4m31s] 唯有中段
[4m32s] 导弹在100公里以上大气层外
[4m34s] 无动力滑行
[4m36s] 飞行时间长达10至20分钟
[4m39s] 轨迹由万有引力决定
[4m41s] 极度稳定
[4m42s] 并未释放弹头与诱饵
[4m44s] 反导系统有足够的时间预警
[4m47s] 测算轨迹并拦截
[4m49s] 采用动能撞击
[4m51s] 可彻底消除地面威胁
[4m53s] 是现代战略防空最理想
[4m55s] 效果最佳的黄金拦截期
[4m59s] 也是各国反导技术研发的
[5m1s] 核心方向
[5m9s] 虽然说
[5m10s] 中段反导拦截
[5m11s] 在整个反导作战的流程当中
[5m14s] 它是比较容易实现的
[5m16s] 或者说
[5m17s] 可以针对对方来袭的导弹
[5m20s] 进行精准的摧毁
[5m21s] 但是最大的问题
[5m22s] 就是你的预警系统
[5m24s] 需要看得远
[5m25s] 反导拦截弹
[5m26s] 需要飞得快
[5m27s] 打得高 打得远
[5m28s] 所以对于技术方面的要求
[5m29s] 是比较高的
[5m30s] 这就要求
[5m31s] 大型的相控阵预警雷达
[5m33s] 能够在极远的距离上
[5m35s] 针对外大气层飞行的
[5m38s] 这种小型高速移动的目标
[5m40s] 能够进行精准的锁定
[5m42s] 而且要对它进行持续的跟踪
[5m45s] 还要有相关的
[5m46s] 这样的一个计算系统
[5m47s] 针对它的飞行轨迹进行计算
[5m49s] 这样的话
[5m50s] 才能够有效地引导反导拦截弹
[5m53s] 在外大气层
[5m54s] 对它进行精准的这样的一个截杀
[5m57s] 而且反导拦截弹的性能
[5m58s] 要求非常高
[6m0s] 不仅要飞得高
[6m1s] 飞得远 飞得快
[6m2s] 而且
[6m3s] 甚至还要配备有动能的战斗部
[6m5s] 要针对处于飞行状态的
[6m7s] 弹道导弹
[6m8s] 进行精准的撞击
[6m10s] 确保它是被彻底摧毁
[6m15s] 3月5日
[6m17s] 伊朗伊斯兰革命卫队发布声明称
[6m20s] 在“真实承诺-4”行动的第17轮中
[6m23s] 伊朗动用高超声速导弹
[6m25s] 和攻击无人机
[6m27s] 成功突破美国“萨德”防御系统
[6m30s] 打击了以色列国防部大楼
[6m32s] 与本-古里安国际机场
[6m34s] 并摧毁了7套雷达系统
[6m36s] 及核心配套设备
[6m39s] 同期
[6m40s] 美国内部评估报告透露
[6m42s] 作为中东关键中段反导装备的
[6m45s] 以色列“箭式”系统
[6m46s] 在拦截伊朗中程弹道导弹时
[6m49s] 显现重大短板
[6m51s] 实际拦截率
[6m52s] 远未达到原有设计标准
[6m57s] 那么在以色列的国土范围内
[6m59s] 有“箭-2” “箭-3” 反导系统
[7m1s] 也有美国部署的“萨德”反导系统
[7m4s] 但是我们看
[7m5s] 仍然有数量相当多的伊朗弹道导弹
[7m8s] 成功突防
[7m9s] 针对以色列区域内的目标
[7m11s] 进行了毁伤
[7m13s] 为什么说“箭-2”“箭-3”
[7m14s] 以及“萨德”系统
[7m15s] 它联起手来
[7m16s] 也没有办法针对伊朗的弹道导弹
[7m19s] 进行这种百分之百的防范和拦截
[7m21s] 我认为
[7m22s] 首先是伊朗弹道导弹的技术
[7m25s] 有所提升
[7m26s] 部分伊朗的弹道导弹
[7m27s] 采用了这种高超音速的战斗部
[7m30s] 在飞行的末段
[7m31s] 会有突然的加速
[7m32s] 或者说有急剧的
[7m34s] 飞行轨迹的变化
[7m35s] 这样的话
[7m36s] “萨德”系统
[7m37s] 和“箭-2”“箭-3”反导系统
[7m38s] 很难对它进行精准的拦截
[7m40s] 此外
[7m41s] 伊朗的一些弹道导弹
[7m43s] 比如说像“霍拉姆沙赫尔-4”
[7m45s] 那么它是一种重型的
[7m46s] 这样的一个弹头的设计理念
[7m49s] 1.5吨到2吨的战斗部
[7m51s] 它可以采用这种集束弹头
[7m53s] 这样的一个模式
[7m55s] 飞到了目标区上空之后
[7m56s] 打出了一个满天星的效果
[7m58s] 数十枚子弹头飞向目标区域
[8m1s] 所有的防空反导系统
[8m3s] 都没有办法对它进行拦截
[8m5s] 那么还有另外一种情况
[8m7s] 美国的反导作战系统
[8m8s] 和以色列的“箭-2” “箭-3” 反导系统
[8m10s] 它没有办法进行联网作战
[8m12s] 是各打各的
[8m13s] 这样的话
[8m14s] 在相关的区域内
[8m15s] 没有办法进行目标信息的共享
[8m17s] 那么也无法形成合力
[8m21s] 2026年3月15日
[8m23s] 伊朗伊斯兰革命卫队
[8m25s] 在“真实承诺-4”军事行动中
[8m28s] 首次实战部署了泥石导弹
[8m31s] 与此同时
[8m32s] 美国从韩国
[8m34s] 紧急调遣了萨德反导系统
[8m36s] 以加强该地区的防御
[8m38s] 应对伊朗导弹带来的持续威胁
[8m44s] 若将远程反导
[8m45s] 视作千里点穴的区域防空手段
[8m48s] 中程防空
[8m50s] 便是中空守护网
[8m52s] 承担着衔接
[8m53s] 远近防御的关键使命
[8m56s] 它覆盖数十公里
[8m57s] 乃至一百多公里的中高空区域
[9m0s] 作为防空体系中的
[9m2s] 中坚力量
[9m4s] 承担起区域防空重任
[9m6s] 护卫城市
[9m7s] 军事要地等高价值目标
[9m9s] 成为整个防空网络中
[9m11s] 承上启下的核心组成部分
[9m22s] 中程防空
[9m24s] 作为现代防空作战系统的
[9m25s] 关键环节
[9m27s] 其防御半径
[9m28s] 一般在50到200公里之间
[9m31s] 拦截高度
[9m32s] 重点覆盖10至20公里的中高空域
[9m36s] 这片空域
[9m37s] 正是现代战争中
[9m39s] 空中威胁密集活动的核心地带
[9m42s] 敌方战机
[9m43s] 若要突破外层防御
[9m45s] 打击纵深关键目标
[9m47s] 必须穿越此区域
[9m49s] 巡航导弹
[9m50s] 虽常利用低空突防
[9m52s] 但多数情况下
[9m54s] 仍需在中高空调整姿态
[9m56s] 寻找防御薄弱点
[9m58s] 而中近程导弹在俯冲末段
[10m1s] 同样会经过这一高度区间
[10m4s] 其高速特性
[10m5s] 要求中程防空系统及早介入
[10m8s] 精确拦截
[10m9s] 可以说
[10m10s] 这片中高空域
[10m11s] 是空中威胁
[10m13s] 渗向核心目标的必经通道
[10m16s] 也是防空体系中
[10m17s] 承前启后的关键拦截区
[10m26s] 在现代化的防空作战行动当中
[10m29s] 中程防空的难度是非常大的
[10m31s] 因为很多对方的
[10m34s] 这种空中的来袭目标
[10m36s] 其实它都是在
[10m37s] 中程防空的任务范围内
[10m39s] 目标的属性是非常多的
[10m41s] 此外
[10m42s] 在复杂的电磁环境的
[10m44s] 这样的一个状态下
[10m45s] 中程的防空体系
[10m47s] 也可能会出现失灵的情况
[10m49s] 因为
[10m50s] 对方可能会出动电子战飞机
[10m52s] 针对你的防空作战体系
[10m53s] 进行压制
[10m54s] 此外
[10m55s] 有的时候面对各种各样的目标
[10m57s] 数量众多的目标
[10m59s] 中程防空系统
[11m0s] 它的这样的一个
[11m2s] 火力的密集程度
[11m3s] 可能会出现不足的情况
[11m5s] 比如说一部发射器上
[11m7s] 它只有数量有限的中程防空导弹
[11m9s] 打完了之后
[11m10s] 对方的目标连续地持续地袭来
[11m14s] 那中程防空体系
[11m15s] 可能也会面临弹药不足
[11m17s] 火力不足的情况
[11m21s] 在近期
[11m22s] 美以联合对伊朗的军事行动中
[11m25s] 中程导弹防御系统
[11m27s] 成为防空体系中的
[11m28s] 核心作战力量
[11m30s] 也是双方攻防较量
[11m32s] 最为激烈的领域
[11m34s] 为巩固中程防御屏障
[11m36s] 美国运用
[11m37s] “爱国者”防空反导系统
[11m39s] 结合天基预警卫星
[11m41s] 陆基远程雷达
[11m43s] 及集成化指挥控制中心
[11m45s] 打造了能够覆盖30公里
[11m48s] 至150公里空域的拦截体系
[11m50s] 着重防护其军事基地
[11m52s] 与国防工业区域
[11m55s] 面对伊朗高超音速武器
[11m58s] 配合无人机集群的
[11m59s] 非对称组合式打击
[12m2s] 此前被美军公开宣称
[12m3s] 拦截成功率
[12m5s] 可达80%到97%的“爱国者”系统
[12m9s] 实际拦截率
[12m10s] 已跌至5%以下
[12m13s] 系统还频频出现拦截假目标
[12m16s] 漏过真实弹头的失误
[12m21s] “爱国者”曾在海湾战争期间
[12m24s] 一战成名
[12m25s] 那当时
[12m26s] 美军“爱国者”系统
[12m27s] 它所拦截的导弹
[12m29s] 都是伊拉克萨达姆政权军队
[12m32s] 使用的比较老旧的
[12m34s] “飞毛腿”弹道导弹
[12m36s] 它的飞行轨迹是一个典型的抛物线
[12m38s] 不会有这种
[12m39s] 机动变轨的这样的一些动作
[12m41s] 所以
[12m42s] 只要是探测到了它的飞行轨迹
[12m45s] 多发射一些“爱国者”拦截弹
[12m48s] 是可以对它进行有效拦截的
[12m49s] 但是现代化的弹道导弹
[12m51s] 突防能力都很强
[12m53s] 飞行轨迹是变化的
[12m54s] 甚至可以说是有一些打水漂的
[12m56s] 这样的一个飞行轨迹
[12m57s] 速度又很快
[12m58s] 那么甚至可能会携带
[13m0s] 更多的诱饵弹和干扰弹
[13m1s] 所以“爱国者”系统
[13m2s] 可能根本就看不清
[13m4s] 可能也就追不上
[13m6s] “爱国者”导弹在部分实战场景中
[13m9s] 拦截效果未达预期
[13m12s] 这一表现
[13m13s] 使得美以联合构建的
[13m14s] 中程防空反导系统暴露出短板
[13m18s] 也让多国对该系统
[13m19s] 产生信任危机
[13m24s] 在现代防空系统中
[13m25s] 低空与超低空
[13m27s] 是远程及中程防空
[13m29s] 存在的短板
[13m30s] 受到地球曲率
[13m32s] 地形掩蔽等影响
[13m33s] 远程雷达难以追踪低空目标
[13m37s] 而中程防空导弹
[13m38s] 在低空条件下
[13m39s] 制导系统易受地面杂波的干扰
[13m41s] 致使拦截效能下降
[13m44s] 因此
[13m45s] 近程防空系统由此诞生
[13m48s] 专门用于近距离的防护
[13m50s] 弥补了防空网络
[13m52s] 在低空领域的不足
[14m1s] 对普通公众而言
[14m3s] 近程防空无疑是最常见
[14m6s] 且视觉冲击力最强的防空形式
[14m9s] 那些在网络短片
[14m11s] 新闻报道和战场记录中
[14m13s] 常见的导弹腾空而起
[14m15s] 尾焰撕裂黑夜
[14m17s] 目标在空中爆炸
[14m19s] 火光刹那映亮天空的拦截画面
[14m22s] 大多源自近程防空系统
[14m25s] 这些系统射程较短
[14m27s] 但部署便捷响应迅速
[14m30s] 通常在目标接近阵地
[14m32s] 城市或重要设施的最后一刻
[14m35s] 才发动攻击
[14m36s] 因而更易被现场捕捉
[14m38s] 媒体记录
[14m39s] 并广泛传播
[14m41s] 正是由于这种高频曝光
[14m43s] 近程防空在许多人心中
[14m45s] 几乎成了防空的代名词
[14m48s] 然而实际上
[14m49s] 近程防空在整体防空网络中
[14m52s] 充当的是最终贴身防线的功能
[14m56s] 远程和中程防空
[14m58s] 无法覆盖的低空
[15m0s] 及超低空漏洞
[15m4s] 近程防空作战体系
[15m6s] 在实际应用的过程当中
[15m7s] 有的时候
[15m9s] 可能会成为战场上的奇兵
[15m10s] 因为对方的直升机
[15m13s] 甚至包括战斗机
[15m14s] 在低空飞行的时候
[15m15s] 它可能会放松警惕
[15m16s] 另外一个来不及反应
[15m18s] 所以这个时候
[15m19s] 近程防空火力
[15m20s] 针对目标进行射击的时候
[15m21s] 战斗机很难对它进行规避
[15m23s] 有的时候甚至连
[15m25s] 发射红外诱饵弹的时间都没有
[15m28s] 近程防空也有很大的难度
[15m30s] 因为很多近程防空作战体系
[15m33s] 它的搜索系统是比较简单的
[15m35s] 视野不够宽广
[15m36s] 也无法针对远距离的目标
[15m38s] 进行识别
[15m39s] 所以
[15m40s] 有的近程防空作战体系
[15m41s] 它本质上来讲是“近视眼”
[15m43s] 而且还涉及到一些
[15m44s] 地形起伏的问题
[15m46s] 如果对方利用这种地形起伏
[15m49s] 那么去进行超低空的突防
[15m51s] 在相关区域内部署的一些传感器
[15m53s] 比如说低空搜索雷达
[15m54s] 或者是光学红外传感器
[15m56s] 可能没有办法
[15m57s] 及时有效地识别目标
[15m58s] 那么这些低空突防的
[16m0s] 这样的一些空中作战平台
[16m2s] 也可能会针对
[16m3s] 近程的防空作战体系
[16m4s] 包括相关的防空阵地
[16m6s] 进行打击和压制
[16m11s] 在近期
[16m12s] 美以联合对伊朗的军事行动中
[16m15s] 以色列拉斐尔公司研发的
[16m17s] “铁穹”近程防空系统
[16m19s] 经历了自2011年部署以来
[16m21s] 最激烈
[16m22s] 且最具转折性的实战检验
[16m25s] 这款以往在冲突中
[16m27s] 以85%至90%拦截率
[16m30s] 享誉世界的明星武器
[16m32s] 在伊朗
[16m33s] “真实承诺-4”行动的饱和攻击下
[16m36s] 其卓越声誉荡然无存
[16m39s] 以色列国防部
[16m41s] 3月17日
[16m42s] 向国会递交的评估报告中指出
[16m45s] 面对伊朗连续发动的
[16m47s] 六十多波攻势
[16m48s] “铁穹”系统的实际拦截率
[16m50s] 从历史标榜的90%以上
[16m53s] 骤降至约50%
[16m55s] 而在集束弹头密集倾泻
[16m58s] 与无人机蜂群
[16m59s] 低空突袭的关键战斗阶段
[17m1s] 其拦截成功率
[17m3s] 更是下滑至30%以下
[17m5s] 刷新了服役以来的
[17m7s] 最低性能纪录
[17m12s] 我们注意到
[17m13s] 在此次军事行动的过程当中
[17m15s] “铁穹”系统表现平平
[17m16s] 我认为
[17m17s] 可能是有两方面的考虑
[17m19s] 就是它配套的这种传感器
[17m22s] 或者是预警体系
[17m23s] 可能是遭到了对方的火力打击
[17m25s] 有部分“铁穹”系统
[17m26s] 那么针对空中来袭目标
[17m28s] 它的反应速度不够快
[17m30s] 它的这样的一个情报
[17m33s] 和目标引导体系
[17m35s] 不够完善
[17m36s] 另外一个
[17m37s] 就是“铁穹”系统
[17m38s] 它所使用的是“塔米尔”拦截弹
[17m40s] 这种拦截弹
[17m41s] 由于它装备
[17m43s] 和使用的数量比较多
[17m44s] 所以
[17m45s] 它必须要进行成本的控制
[17m46s] 它的飞行速度
[17m49s] 其实并没有那么快
[17m50s] 有的时候
[17m51s] 针对高速目标进行打击的时候
[17m53s] 可能会出现速度跟不上的情况
[17m56s] 现在伊朗主要是使用弹道导弹
[17m59s] 针对以色列进行打击
[18m0s] 面对高空高速飞行的弹道导弹
[18m3s] 或者是
[18m4s] 配备有
[18m5s] 高超音速战斗部的弹道导弹
[18m7s] “铁穹”系统可能它的拦截范围
[18m9s] 拦截的高度也都不足
[18m11s] 伊朗也使用了大量的无人机
[18m14s] 但是这一次
[18m15s] 针对以色列进行后续的反击
[18m17s] 伊朗更加注重使用
[18m19s] 这种中程的弹道导弹
[18m21s] 或者是突防能力很强的弹道导弹
[18m23s] 面对这样的一种情况
[18m25s] 它就会出现反应速度不够快
[18m28s] 包括拦截的范围
[18m30s] 拦截的高度
[18m31s] 都存在不足的情况
[18m35s] 近程防空系统
[18m37s] 作为整个防空架构中
[18m38s] 关键的最后屏障
[18m40s] 肩负着坚守底线
[18m42s] 护卫要害的核心职责
[18m45s] 但它并非完美无缺
[18m47s] 仍存在明显的短板
[18m50s] 其防御范围
[18m51s] 通常较为有限
[18m53s] 探测与拦截能力
[18m54s] 都集中在较小区域
[18m57s] 大多只能执行小尺度
[18m58s] 重点目标的局部防护任务
[19m1s] 无法如同中远程防空系统那样
[19m4s] 实现广大空域的大范围
[19m6s] 远距离监控与狙击
[19m9s] 因此
[19m10s] 近程防空系统
[19m11s] 不能独自支撑一国 一城
[19m13s] 或整个战区的全面防空安全
[19m16s] 它更像是一件
[19m17s] 贴身的防弹衣
[19m19s] 而非是能覆盖全域的保护罩
[19m24s] 远程 中程与近程防空系统
[19m28s] 相互配合
[19m29s] 在现代战争中
[19m31s] 构筑了层次分明 分工明确
[19m33s] 紧密衔接的全空域
[19m35s] 立体防空网络
[19m38s] 三者各尽其责
[19m39s] 彼此支援 逐层拦截
[19m42s] 形成了一套由外至内
[19m44s] 由高空到低空
[19m45s] 由远及近的周密防御体系
[19m49s] 不过
[19m50s] 近年多场高强度
[19m51s] 军事冲突的实际表现
[19m53s] 这套看似牢不可破的防空体系
[19m56s] 正持续受到
[19m57s] 新兴战场模式的严峻挑战
[20m0s] 其传统作战思路和防御能力
[20m2s] 正在不断遭受冲击与重塑
[20m15s] 在美以联合对伊朗发动的
[20m17s] 大规模军事行动中
[20m19s] 可以清晰看到
[20m20s] 美军
[20m21s] 以色列军队
[20m22s] 以及伊朗的防空系统
[20m24s] 都遭遇了前所未有的考验
[20m26s] 在以往严密的多层防护网络
[20m29s] 在实战中屡次被突破
[20m31s] 撕裂
[20m32s] 甚至彻底崩溃
[20m34s] 美军采用了
[20m36s] 以“爱国者”和“萨德”反导系统
[20m38s] 中远程导弹防御体系
[20m41s] 以色列则调动了由“铁穹”
[20m43s] “大卫投石索”
[20m45s] 和“箭式”系统组成的
[20m46s] 全域防空反导系统
[20m49s] 然而
[20m50s] 针对高超声速武器穿透
[20m52s] 无人机群饱和攻击
[20m54s] 复合诱饵欺骗
[20m55s] 与强电磁干扰等策略
[20m57s] 拦截失败的情况
[20m59s] 依然频繁发生
[21m4s] 在此次行动的过程当中
[21m6s] 美以的防空反导系统
[21m7s] 频频被洞穿
[21m9s] 也就是说
[21m10s] 伊朗使用了
[21m11s] 一系列的突防手段
[21m13s] 最终打穿了
[21m14s] 美国和以色列的一些
[21m15s] 防空反导的保护伞
[21m18s] 那么针对相关的区域
[21m19s] 构成了更为猛烈的火力打击
[21m22s] 我认为
[21m23s] 就是存在一些问题
[21m25s] 比如说
[21m26s] 它系统之间
[21m27s] 没有进行有效的联合
[21m28s] 存在一些兼容的问题
[21m30s] 各自为战的状态下
[21m32s] 肯定会出现漏洞
[21m33s] 另外一个
[21m34s] 伊朗也是采取了一些灵活多变的
[21m37s] 这样的一些战术
[21m38s] 多种火力 多种模式联合运用
[21m41s] 也让美国和以色列的
[21m43s] 防空反导系统
[21m48s] 在现代军事冲突中
[21m50s] 高超声速武器的穿透
[21m51s] 与无人机蜂群的饱和攻击
[21m54s] 虽是关键威胁
[21m55s] 但更具深远颠覆性的
[21m57s] 是战场成本效益失衡
[21m59s] 对防空体系带来的
[22m1s] 系统性难题
[22m3s] 这已超越了
[22m4s] 单一装备的性能竞争
[22m6s] 演变为从经济原理
[22m8s] 持久作战能力
[22m10s] 到战略自信的全方位压制
[22m13s] 引发了拦截不起
[22m15s] 消耗不起
[22m16s] 补充不上的三重困局
[22m20s] 以伊朗主力攻击武器
[22m22s] “见证者-136” 自杀式无人机为例
[22m25s] 这款被戏称作
[22m27s] 飞行摩托车的装备
[22m29s] 彻底打破了传统军工的
[22m31s] 精密制造范式
[22m33s] 机身使用民用级玻璃纤维
[22m35s] 和泡沫塑料拼装
[22m37s] 动力系统
[22m38s] 直接借用普通摩托车的
[22m40s] 二冲程发动机
[22m41s] 导航依靠
[22m42s] 民用GPS模块与简易惯性制导
[22m46s] 甚至部分核心零件
[22m48s] 来自拆解的汽车和家电
[22m50s] 从而极大压低了制造成本
[22m53s] 凭借民用技术军用化
[22m55s] 复杂系统简易化的理念
[22m57s] 其批量生产单价
[22m59s] 稳定在2万至5万美元之间
[23m2s] 某些简化版本
[23m4s] 甚至降至1.5万美元
[23m6s] 仅相当于高端防空导弹成本的
[23m9s] 极小一部分
[23m11s] 美以用于对抗的防空武器
[23m13s] 却如同天价奢侈品
[23m16s] 以色列“铁穹”系统的拦截弹
[23m18s] 作为近程防空核心弹药
[23m20s] 每枚成本
[23m21s] 达5万到10万美元
[23m24s] 这意味着
[23m25s] 即便成功拦截
[23m26s] 防御方的经济损失
[23m28s] 也远超进攻方
[23m30s] 更甚者
[23m31s] 美国“爱国者-3”拦截弹
[23m33s] 作为中程防空主力
[23m35s] 单枚成本飙升至
[23m37s] 400万至500万美元
[23m39s] 堪比用超级跑车
[23m41s] 撞击普通代步车
[23m43s] 无论胜负如何
[23m45s] 防御方已在战略层面
[23m47s] 沦为输家
[23m51s] 那么在效费比方面
[23m53s] 如果说对方在进攻的过程当中
[23m56s] 采用了很多廉价的方式
[23m58s] 或者说很多省钱的
[23m59s] 这样的一些技术手段
[24m1s] 像“见证者-136”
[24m3s] 其实它就是飞行的小摩托
[24m5s] 那么它的成本控制得很低
[24m7s] 可能要用先进的防空反导系统
[24m9s] 对它进行拦截
[24m11s] 而伊朗的一些地区盟友
[24m12s] 包括黎巴嫩真主党
[24m15s] 会使用大量的“卡桑”火箭弹
[24m16s] 也就是说
[24m17s] 更低成本的作坊工厂当中
[24m19s] 生产出来的这种火箭弹
[24m21s] 针对以色列进行打击
[24m22s] 那么色列方面使用的是什么
[24m24s] 是这种“塔米尔”拦截弹
[24m25s] 也就是“铁穹”系统的拦截弹
[24m28s] 在这样的 一种
[24m29s] 相互的消耗战的过程当中
[24m31s] 那么位列或者是美国
[24m33s] 他所面临的这种
[24m35s] 昂贵弹药的快速消耗
[24m37s] 和短缺的情况
[24m39s] 也会变得越来越多
[24m40s] 也会变得越来越明显
[24m43s] 由于战场上
[24m44s] 攻击和防御的效费比例
[24m46s] 严重失衡
[24m47s] 防空弹药库存告急
[24m49s] 形势十分紧迫
[24m51s] 3月18日
[24m52s] 以色列国防部火速向美国求援
[24m55s] 美军立刻出动C-17运输机
[24m58s] 紧急空运了近千枚拦截导弹
[25m1s] 为以色列的库存注入强心针
[25m4s] 与此同时
[25m5s] 美军从韩国
[25m7s] 亚太到欧洲多个据点
[25m9s] 迅速调集防空系统
[25m11s] 全力缓解战场上
[25m13s] 防空装备和弹药
[25m14s] 捉襟见肘的困境
[25m19s] 现代防空系统
[25m20s] 打造了一个从远程中段反导
[25m23s] 近程区域防空
[25m24s] 到近程末段拦截的完整链条
[25m28s] 构建了远近交错
[25m30s] 高低兼顾的立体防御天网
[25m33s] 然而
[25m34s] 高超声速武器如闪电般突破
[25m37s] 无人机群的压制
[25m38s] 以及饱和式攻击的浪潮不断袭来
[25m41s] 让传统防空体系
[25m42s] 面临着严峻的考验
[25m46s] 攻防成本的天平
[25m48s] 正在严重倾斜
[25m49s] 威胁形势日新月异
[25m52s] 如何在提升拦截效果的同时
[25m54s] 勒紧成本的缰绳
[25m57s] 变得至关重要
[25m59s] 让防空网看得远
[26m1s] 拦得准
[26m2s] 扛得住
[26m3s] 耗得起
[26m4s] 成为了左右未来防空安全
[26m6s] 和战争胜负的
[26m8s] 关键棋局
[26m11s] 好了观众朋友们
[26m12s] 感谢您持续关注
[26m13s] 国防军事频道《军事科技》
[26m15s] 我们下周同一时间
[26m17s] 再见
@@ -0,0 +1,30 @@
《潜艇的仿生之路》
导视:在浩瀚海洋深处,数亿年的进化,塑造了无数神奇的“军事科技”。如今的人类,用钢铁和电路,重新发明了这一切。当潜艇披上"鲨鱼皮"消声瓦,当声呐模仿海豚看穿黑暗,当乌贼的墨汁化作潜艇的电子替身,自然早已写好了深海中生存与猎杀的终极教科书。本期《军事科技》带你见证茫茫深海中潜艇的仿生之路。
Xr演播室 主持人1:观众朋友们大家好!欢迎收看本期的《军事科技》,我是主持人蓝皓。在人类出现之前,深海就是地球上最残酷的竞技场。(画侧出现水体,水体按颜色深度分层)在这里,生存的法则只有两条:隐藏自己,发现敌人。而如今,深海依旧是地球上最后的黑暗边疆。在这里,潜伏着人类工程学的杰作,也是终极的刺杀兵器——潜艇。(水体中出现潜艇,鱼类游走)提到潜艇,想必大家都并不陌生。那么这样一头深海巨兽从诞生至今都借鉴了大自然中哪些动物的“绝招”呢?
从鹦鹉螺到“潜艇压载水舱系统”
二维动画解说1: 1776年9月7日,历史上第一次潜艇攻击开始了,这次攻击是由埃兹拉·李来执行。他驾驶着“海龟号”成功潜到英国海军战舰“鹰号”的尾部,接下去的工作就是用钻头在敌舰上穿孔以便固定炸药包。但是,他打钻的地方正好是一块金属板,半个小时之后他仍然没有钻透敌舰,只好上浮返回。虽然“海龟号”没有成功取得战果,但他拉开了潜艇实战的序幕,从此人类的战场也随之从陆地、水面发展到了水下,“海龟号”也得到了与现代潜艇相同的设计原理而获得世界上“第一艘军用潜艇”的称号,在世界潜艇发展史占据了一席之地。
三维动画解说:“海龟号”潜艇,顾名思义它的外形很像海龟。舱内空气可供驾驶员呼吸半个小时,在潜艇的上部还装有2根通气管,以便上浮时打开,下沉时关闭,从而补充新鲜的空气。为了控制潜水艇的上浮以及下沉,船内设有压载水舱,用水泵控制水柜内的水。为应付紧急情况的发生,艇内装有一块90千克重的铁块,危机时刻只要抛掉铁块,潜艇就可以迅速上浮。”
专家: 潜艇通过向压载水舱注水,排水来改变自身重量,模仿的正是鹦鹉螺的浮控原理。
解说:让我们把目光投向鹦鹉螺的身上。在亿万年的进化中,鹦鹉螺掌握了一种堪称完美的浮力控制艺术。它的螺旋形外壳被一系列隔膜分隔成数十个独立的气室,如同一个精密的多舱室潜水器。通过一个被称为串管的活组织,鹦鹉螺能精确调节每个气室内液体与气体的比例——分泌液体进入气室时下潜,排出液体让位给气体时上浮。这种精妙的生理机制,让它能在数百米的深海与浅层水域之间自如升降,几乎不消耗任何额外能量,堪称自然界最古老、最经典的被动浮力控制系统。
三维动画解说:人类潜艇的压载水舱系统,正是对这一自然智慧的完美复刻。当潜艇需要下潜时,阀门打开,海水涌入压载舱,取代舱内空气,艇体重量增加;当需要上浮时,高压空气将海水强行排出,艇体重量减轻。从19世纪美国“霍兰”号潜艇的手动阀门,到现代核潜艇的计算机控制高压气系统,其核心原理始终未变——正如鹦鹉螺用气体取代液体,潜艇用海水取代空气。这种能力,鹦鹉螺用了数亿年进化完成,而人类只用了不到一百年。
鲨鱼皮肤与潜艇的外形设计
Xr主持人:在广袤的海洋中,生存是一场关乎“看见”与“不被看见”的永恒竞赛。为了在这场竞赛中胜出,无数海洋生物进化出了一套精妙的色彩伪装策略,这便是隐蔽色。像虎鲸、鲨鱼、这样一生与海洋打交道的生物,其背部往往呈现深蓝、深灰色,而其腹部通常是白色或银白色。(画侧出现虎鲸)我们经常能够看到一只从深海上浮的虎鲸,其白色的胸膛在来自下方的视线中几乎“消失”在明亮的天光里;而一只从上方发起攻击的鲨鱼,其深色的背部直到最后一刻才会被发现。这套简单而高效的系统,本质上是一种 “光学隐身衣” 。它抹平了生物体在三维空间中的立体感,极大地缩短了被天敌或猎物发现的距离。海军工程师很快意识到,这正是潜艇在海洋中生存所需要的第一层能力。
专家:在二战时期,一些潜艇的指挥塔和上层建筑就被涂上了深灰色,以期在水面状态时降低被飞机发现的概率。今天,虽然潜艇绝大部分时间在水下活动,但隐蔽色原则依然被用于其涂装设计。现代潜艇通常采用一种黑色涂装。这不仅仅是为了美观,更是一种功能性设计。
解说:然而,在深海中,仅仅“看不见”还远远不够。随着潜艇潜得更深、航行时间更长,真正致命的威胁,开始从光线转移到声音。声音,是深海中传播最远、最难隐藏的信号。一丝多余的噪音,就可能暴露位置,招来猎杀。于是,潜艇的隐身逻辑,也从颜色,升级到了形态。潜艇的外形,并非一开始就是今天的样子。很长一段时间里,它更像是一艘“能够下潜的水面舰艇”。二战时期的潜艇需要频繁浮出水面航行,水下只是暂时藏身之所,因此艇体高耸、结构复杂,水下阻力大、噪音高。真正的转折,发生在核动力出现之后。当潜艇可以长期潜航,人类第一次开始,只为“水下生存”来设计它的形状。工程师将目光投向自然界:鲸类、海豚等高速游泳动物,几乎都拥有同一种外形——前圆后尖的水滴型。1950年代,美国“大青花鱼”号实验潜艇验证了这一答案。水滴型艇体显著降低阻力和流噪,让潜艇更快、更安静,也更难被发现。从此,现代潜艇的外形几乎统一。当潜艇的外形已经足够“像一条鱼”,真正的较量,才刚刚开始。接下来要隐藏的,不再是轮廓,而是声音本身。
专家:从早期潜艇的船型艇体,到现代攻击核潜艇几乎统一的水滴形艇体,如美国“洛杉矶”级,其核心目的就是模仿这种生物流线。每一个突出物、每一个转角都被精心优化,只为让海水“感觉不到”潜艇的存在,从而实现更安静、更高效的航行。潜艇需要在声学上“隐形”,经常被使用手是“消音瓦”。
解说:海豚的皮肤,能够分泌一种特殊黏液,使水流紧贴体表滑行,从而减少湍流与阻力;而更为人熟知的,是鲨鱼的皮肤结构。鲨鱼体表覆盖着无数微小的 V 形盾鳞,这些看似粗糙的纹理,反而能打散涡流,抑制水流紊乱,大幅降低摩擦阻力。潜艇工程师从中得到启发。在现代潜艇的消声瓦表面,常会设计出类似的微沟槽或细微纹理。这种被称为“沟槽面”或“仿鲨鱼皮”的结构,并不仅仅是为了让潜艇航行得更省力,更重要的是——它能有效降低水流掠过艇体时产生的流噪声,让潜艇在深海中,变得更加安静,也更加致命。鲸类和海豚用数千万年的进化,找到了最安静的形态,而人类,只是照着这个形态,把潜艇重新造了一遍。
海豚声呐与潜艇综合声呐系统
Xr解说:海豚在浑浊的海水中能准确找到小鱼,还能分辨出哪条鱼更肥美,这种能力来自它们独特的"水下视觉"——生物声呐。当海豚发出"咔嗒咔嗒"的声音,这些声波就像无形的探照灯向前发射。声音遇到物体产生回声,海豚通过下颌接收这些回声,就能在脑海中描绘出周围环境的立体图像。它们能听出藏在泥沙里的两条鱼哪条更肥,能判断前方是岩石还是海草,这种能力让任何人工声呐都望尘莫及。人类从海豚身上获得灵感,发明了声呐技术。
解说:二战爆发后,德国U型潜艇再次成为海战中的核心力量。在广阔的大西洋上,它们潜伏在航道附近,利用夜色和水下掩护,对盟军运输船队发动突然袭击。单艘潜艇,就足以瘫痪一整条补给线。对英国而言,这不仅是军事问题,更是生存危机。粮食、燃料、军需,几乎全部依赖海上运输,而水下的敌人,却始终看不见。面对潜艇带来的巨大压力,英国海军被迫改变思路——不再依赖目视搜索,而是尝试在深海中“倾听”敌人的存在。以水听器为基础,配合主动声波探测,声呐系统逐步成型,并率先装备在驱逐舰和护航舰上。在护航战例中,声呐第一次实现了对潜艇的稳定定位,使深水炸弹不再是盲目投掷,而成为有目标的打击。随着反潜技术的成熟,曾经游刃有余的U型潜艇,开始频繁暴露行踪,损失不断增加。大西洋的海战,从潜艇的“单方面猎杀”,逐渐演变为一场围绕隐蔽与探测的技术对抗。而这一技术,正是来源于大自然中的生物声呐。
专家:声呐是一种利用声波在水中的传播和反射探测水下物体,如潜艇、水雷、冰山、暗礁等,并可用于导航和测距、定位的装备,现在已广泛装备水面舰艇、潜艇、反潜机,以及无人作战平台。虽然现在雷达、红外、激光等非声探测设备有了长足的进步,但由于海水特性和潜艇潜深等原因,声呐仍然是反潜战中对潜探测最重要的,不可替代的装备。而潜艇在水下航行时,由于电磁波和光在水中衰减很大,传播距离非常有限,对于潜艇来说水声信号是探测目标最有效的方法。
三维动画解说:这是一艘弗吉尼亚级攻击核潜艇。潜艇的艇艏,布置着大型声呐阵列。在早期批次上,它呈现为球形结构;在更新批次中,则演变为更加集成的阵面形式。无论外形如何变化,它始终承担着同一项任务——对前方海域进行持续探测。通过分析回波的时间与特征,潜艇能够判断目标的距离、方位,并对水下接触进行初步识别。沿着艇体左右两侧,分布着舷侧声呐阵列。它们如同海豚身体两侧的侧线系统,负责监听来自各个方向的细微声学变化。即使潜艇保持静默状态,不主动发声,这些阵列依然能够捕捉远处螺旋桨的低频噪声,以及水流扰动形成的异常信号。在潜艇尾部,一条细长的拖曳声呐阵列被放出。这是弗吉尼亚级最重要的远程感知手段之一。它远离艇体自身的噪声干扰,将潜艇的听觉延伸到更远的海域,用于发现极其微弱、距离极远的水下目标。艇艏声呐、舷侧阵列与拖曳声呐协同工作,使这艘潜艇即使在完全隐蔽的状态下,也能构建起一幅连续、立体的水下态势图。
解说:如今,科学家仍在向海豚学习:新的声呐系统在模仿海豚快速构建图像的能力,智能算法在研究海豚处理声音的思维方式。当我们看到海豚在海洋中自由游弋时,其实是在见证一个活生生的声学奇迹——它的每一次鸣叫,都在为人类展示着更完美的水下探测技术。
乌贼墨汁与声学诱饵
Xr主持人:当生存受到直接威胁时,顶尖的猎手与最先进的武器,都诉诸同一种策略:制造混乱,隐藏本体。乌贼遇到危险时,会迅速喷出浓密的墨汁,它并非简单的黑色液体,它在海水中会迅速扩散,形成一个与自身形态相似的、浓密的“墨汁伪影”。这个深色轮廓能立即吸引并锁定捕食者的视觉注意力,为真身的逃离创造关键的时间窗口。潜艇工程师完美地借鉴了这套复杂的求生系统,并将其转化为防御装备:声学诱饵。
解说:潜艇感知到来袭的声自导鱼雷或敌方主动声呐锁定时,会从发射管快速射出声学诱饵,它是潜艇的一种高“电子替身”。声学诱饵通常是一个自航式的水下航行器,被发射后,它会精确模仿母艇的发动机噪音、螺旋桨节拍等独特的“声学指纹”。还会主动航行,制造出比气幕弹更像一艘真实潜艇的移动声学目标。来袭的智能鱼雷能够区分简单的气泡幕和复杂的潜艇噪音。声学诱饵通过发出更具吸引力的“听觉陷阱”,引诱鱼雷偏离航道,转而追踪并攻击这个“替身”。
专家:1942 年,德军 U 型潜艇投放了一种叫 “Bold” 的声纳诱饵。它没有动力,只是释放大量气泡,在水中制造一个“虚假的潜艇回波”。简单,却足以让早期声呐迷失方向。很快,诱饵开始“动起来”。“Sieglinde” 登场。它能以几节航速自行移动,在水下拖出一条看似真实的航迹。这是诱饵第一次尝试模拟潜艇的“行为”,而不仅仅是形态。战争结束,但对抗没有停止。冷战时期,美国海军引入了 MOSS Mk70 移动潜艇模拟器。它被发射出艇后,会在水下独立航行,主动与被动同时工作,制造一个完整的“声学假目标”。在声呐画面中,它和真正的潜艇几乎没有区别。进入 70 年代,诱饵开始标准化、批量化。ADC Mk1,随后是 Mk2、Mk3 系列。这些一次性声学诱饵可以在预定深度悬停,持续发出模拟噪声,把来袭鱼雷从真正的潜艇身边“拉走”。它们不显眼,却成为冷战后期潜艇防御的常规配置。真正的转折,出现在智能化时代。新一代诱饵不再只是“发声”。它们开始计算、判断、机动。例如 Deceptor 自航式声学诱饵。它会主动离艇机动,改变航向和速度,在水下扮演一个“正在逃逸的潜艇”。面对多枚鱼雷,它可以分散、引导、拖延,为真正的潜艇争取生存窗口。而在水面与水下战场上,AN/SLQ-25 “Nixie” 系列拖曳诱饵,成为最广为人知的反鱼雷软杀伤系统之一。
解说:这套系统在实战与对抗演练中屡建奇功。其核心战术都与数百万年前的乌贼如出一辙。乌贼用一团墨汁换取生存的时间,人类则用电子替身,把这一秒无限放大。
Xr主持人:当深海的幽灵收起獠牙,当猎杀者的声呐归于沉寂,这片蔚蓝疆域的博弈却从未停止。从鹦鹉螺的浮力控制到核潜艇的无限续航,从乌贼的墨汁到潜艇的电子诱饵,我们见证了一场跨越数亿年的军事科技进化。自然用漫长的时光雕琢出生存的智慧,人类用智慧在钢铁中复刻这些法则。这不仅是矛与盾的较量,更是对生命本身最深刻的学习。下一场改变游戏规则的技术革命,或许正随着某只深海生物的游弋,在黑暗中悄然酝酿。好了,本期《军事科技》到此结束,感谢您的观看。
@@ -0,0 +1,733 @@
[0m7s] 在浩瀚海洋深处
[0m9s] 数亿年的进化
[0m10s] 塑造了无数神奇的
[0m12s] 攻防利器
[0m14s] 如今的人类
[0m15s] 用钢铁和电路
[0m16s] 重新发明了这一切
[0m19s] 当潜艇
[0m20s] 披上鲨鱼皮 消声瓦
[0m22s] 当声响模仿海豚
[0m24s] 看穿黑暗
[0m26s] 当乌贼的墨汁
[0m27s] 化作潜艇的电子替身
[0m30s] 自然早已写好了深海中
[0m32s] 生存与猎杀的
[0m34s] 终极教科书
[0m38s] 本期《军事科技》
[0m39s] 带您见证茫茫海中
[0m42s] 潜艇的仿生之路
[1m21s] 各位观众你们好
[1m22s] 欢迎收看《军事科技》
[1m23s] 我是蓝皓
[1m26s] 在人类出现之前
[1m27s] 深海就是地球上
[1m29s] 最残酷的竞技场
[1m30s] 在这里生存的法则
[1m32s] 只有两条
[1m34s] 隐藏自己
[1m35s] 发现敌人
[1m37s] 深海依旧是地球上
[1m39s] 最后的黑暗疆域
[1m43s] 潜伏着人类工程学的杰作
[1m46s] 也是终极的刺杀兵器
[1m48s] 潜艇
[1m51s] 提到潜艇
[1m52s] 想必大家并不陌生
[1m54s] 这样一头深海巨兽
[1m56s] 从诞生至今
[1m57s] 都借鉴了大自然中的
[1m59s] 哪些动物的绝招呢
[2m2s] 1776年9月7日
[2m4s] 历史上第一次潜艇攻击开始了
[2m9s] 这次攻击
[2m10s] 是由埃兹拉·李来执行
[2m12s] 他驾驶着“海龟”号
[2m14s] 成功潜到英国战舰
[2m15s] “鹰”号的尾部
[2m17s] 接下去的工作
[2m18s] 就是用钻头在敌舰上穿孔
[2m21s] 以便固定炸药包
[2m24s] 他打钻的地方
[2m26s] 正好是一块金属板
[2m28s] 半个小时之后
[2m29s] 他仍然没有钻透敌舰
[2m31s] 只好上浮返回
[2m34s] 虽然“海龟”号
[2m35s] 没有成功取得战果
[2m37s] 但它拉开了
[2m38s] 潜艇实战的序幕
[2m41s] 从此
[2m42s] 人类的战场
[2m43s] 也随之从陆地
[2m44s] 水面
[2m45s] 发展到了水下
[2m47s] “海龟”号
[2m48s] 也凭借与现代潜艇相通的设计原理
[2m51s] 获得了世界上
[2m52s] 第一艘军用潜艇的称号
[2m55s] 在世界潜艇发展史
[2m57s] 占据了一席之地
[3m0s] “海龟”号潜艇
[3m1s] 顾名思义
[3m2s] 它的外形很像海龟
[3m4s] 舱内空气可供驾驶员
[3m7s] 呼吸半个小时
[3m9s] 在潜艇的上部
[3m10s] 还装有两根通气管
[3m12s] 以便上浮时打开
[3m13s] 下沉时关闭
[3m15s] 从而补充新鲜的空气
[3m18s] “海龟”号潜艇之所以能够
[3m20s] 成功地上浮下沉
[3m22s] 离不开对一种美丽动物的
[3m24s] 深刻学习
[3m27s] 它就是鹦鹉螺
[3m31s] 鹦鹉螺
[3m32s] 它是一种海洋中的动物
[3m35s] 非常地漂亮
[3m36s] 外壳非常漂亮
[3m37s] 但是你要到鹦鹉螺的内部
[3m40s] 转一圈的话
[3m41s] 它里边有很多的腔
[3m43s] 就像我们在陆地上
[3m46s] 看到的一个一个的小房子
[3m48s] 在它的腔里边
[3m49s] 人类正是学习了
[3m51s] 鹦鹉螺的这种功能
[3m53s] 在制造潜艇的过程中
[3m56s] 就采用了水柜
[3m58s] 水柜就是在这个
[4m0s] 潜艇的艇壳的里边
[4m3s] 筑成一个一个一个方块的
[4m5s] 就是类似于储水的柜子
[4m8s] 那么需要沉底的时候
[4m10s] 就向水柜里边注水
[4m12s] 水多了
[4m13s] 沉了
[4m14s] 潜艇就下沉到海底
[4m19s] 在亿万年的进化中
[4m20s] 鹦鹉螺
[4m21s] 掌握了一种堪称完美的
[4m23s] 浮力控制艺术
[4m25s] 它的螺旋形外壳
[4m27s] 被一系列隔膜
[4m28s] 分隔成数十个独立的气室
[4m30s] 如同一个精密的
[4m32s] 多舱式潜水器
[4m34s] 鹦鹉螺
[4m35s] 能精确调节每个气室内液体
[4m37s] 与气体的比例
[4m39s] 分泌液体
[4m40s] 进入气室时下沉
[4m42s] 排出液体
[4m43s] 让位给气体时上浮
[4m46s] 这种精妙的生理机制
[4m47s] 让它能在数百米的深海
[4m50s] 与浅层水域之间
[4m51s] 自如升降
[4m53s] 几乎不消耗任何额外能量
[4m56s] 人类潜艇的压载水舱系统
[4m58s] 正是对这一自然智慧的完美复刻
[5m1s] 当潜艇需要下潜时
[5m3s] 阀门打开
[5m4s] 海水涌入压载舱
[5m6s] 取代舱内空气
[5m8s] 艇体重量增加
[5m10s] 当需要上浮时
[5m12s] 高压气体
[5m13s] 将海水强行排出
[5m15s] 艇体重量减轻
[5m18s] 1897年
[5m19s] 一艘划时代的潜艇
[5m20s] 悄然下水
[5m23s] 这就是约翰·霍兰
[5m24s] 以自己名字命名的
[5m26s] 霍兰潜艇
[5m28s] 这艘潜艇
[5m29s] 汇集了众多开创式的设计
[5m32s] 以往的潜艇
[5m33s] 要么动力不足
[5m34s] 要么水下姿态不稳
[5m36s] 很难在实战中
[5m37s] 发挥较大的作用
[5m39s] 霍兰首次将压载舱
[5m41s] 拆分为主舱与辅助舱
[5m44s] 主压载舱
[5m45s] 负责核心沉浮
[5m47s] 而辅助舱
[5m48s] 专攻姿态校准
[5m49s] 能通过精准补水
[5m51s] 维持平衡
[5m52s] 精准调节潜深
[5m54s] 解决了早期潜艇
[5m55s] 纵倾失控的难题
[5m58s] 这套系统
[5m59s] 是现代潜艇操控系统的源头
[6m2s] 这一设计
[6m3s] 让潜艇
[6m4s] 真正具备了实战价值
[6m6s] 为深海作战开辟了道路
[6m9s] 从霍兰潜艇压载舱的手动阀门
[6m12s] 到现代潜艇压载舱的
[6m14s] 计算机控制高压气系统
[6m16s] 其核心原理始终未变
[6m18s] 正如鹦鹉螺
[6m20s] 用气体取代液体
[6m22s] 潜艇用海水取代空气
[6m25s] 这种能力
[6m26s] 鹦鹉螺用了数亿年进化完成
[6m28s] 而人类
[6m29s] 只用了不到一百年
[6m34s] 在广袤的海洋中
[6m35s] 生存
[6m36s] 是一场关乎看见
[6m37s] 与不被看见的永恒竞赛
[6m41s] 为了在这场竞赛中胜出
[6m43s] 无数的海洋生物
[6m44s] 进化出了一套精妙绝伦的
[6m46s] 色彩伪装策略
[6m48s] 这便是隐蔽色
[6m51s] 像虎鲸
[6m52s] 鲨鱼
[6m53s] 这样一生与海洋打交道的生物
[6m56s] 其背部往往呈现深蓝
[6m58s] 深灰色
[6m59s] 而其腹部通常是白色
[7m2s] 或者是银白色
[7m4s] 我们经常能够看到
[7m6s] 一只从深海上浮的虎鲸
[7m8s] 其白色的胸膛
[7m9s] 在来自下方的视线中
[7m11s] 几乎消失在明亮的天光里
[7m14s] 而一只从上方
[7m15s] 发起攻击的鲨鱼
[7m16s] 其深色的背部
[7m17s] 直到最后一刻
[7m19s] 才会被发现
[7m21s] 这套系统简单而高效
[7m23s] 本质上是一种光学隐身衣
[7m26s] 它抹平了生物体
[7m27s] 在三维空间中的立体感
[7m29s] 极大缩短了
[7m30s] 不被天敌或者是猎物
[7m32s] 发现的距离
[7m34s] 海军工程师很快意识到
[7m36s] 这正是潜艇在海洋中生存
[7m38s] 所需要的第一层能力
[7m43s] 一战时期
[7m44s] 潜艇多活跃于浅海区域
[7m47s] 主要面临目视侦察的威胁
[7m51s] 各国率先借鉴海洋生物的
[7m53s] 双色伪装逻辑
[7m55s] 尝试用浅灰
[7m56s] 绿蓝等颜色涂装艇身
[7m59s] 试图让潜艇
[8m0s] 融入近海的蓝灰色海水
[8m4s] 模仿的正是虎鲸腹部
[8m6s] 适配天光的浅色隐身思路
[8m10s] 但早期设计
[8m11s] 未能兼顾深浅海环境差异
[8m14s] 效果变变
[8m20s] 到了二战
[8m21s] 德军和英军
[8m22s] 则试验新的组合涂装
[8m25s] 这些色彩既延续了
[8m26s] 上部深色
[8m28s] 下部浅色的仿生核心
[8m30s] 又试图贴合
[8m31s] 浅海藻类与海床环境
[8m34s] 可一旦潜艇下潜深度增加
[8m37s] 这套设计便迅速失效
[8m41s] 阳光穿透海水层层衰减
[8m44s] 深海本就是一片幽暗
[8m47s] 浅色系艇身
[8m48s] 反而成为显眼目标
[8m50s] 更致命的是
[8m52s] 早期涂料易被海水冲刷氧化
[8m55s] 很快褪成乳白色
[8m57s] 让仿生伪装彻底失效
[9m8s] 真正的转折来自人类
[9m10s] 对深海环境的深刻洞察
[9m13s] 科学家发现
[9m15s] 当深度超过200米
[9m17s] 阳光完全消失
[9m19s] 黑色
[9m20s] 成为海洋的天然底色
[9m23s] 这与鲨鱼深色背部
[9m25s] 在深海中的隐身逻辑
[9m27s] 不谋而合
[9m29s] 于是纯黑色涂层
[9m31s] 应运而生
[9m32s] 它不仅能让潜艇
[9m34s] 在幽暗海中
[9m35s] 完美融入环境
[9m37s] 消除视觉轮廓
[9m39s] 更凭借改良配方
[9m40s] 解决了褪色难题
[9m42s] 成为潜艇光学隐身的
[9m44s] 终极方案
[9m51s] 因为我们讲
[9m52s] 涂层主要是为了颜色
[9m54s] 也就是视觉上
[9m55s] 不被对手发现
[9m57s] 那么很多的动物
[9m59s] 都有一层保护层
[10m1s] 我们在建造潜艇的时候
[10m2s] 也是从自然界
[10m5s] 或仿生学里边学到了
[10m7s] 如何变化颜色
[10m9s] 因为颜色如果涂在潜艇上
[10m11s] 是很难随着环境变化的
[10m14s] 所以我们就要考虑
[10m15s] 潜艇它生存在
[10m16s] 一个什么样的环境里边
[10m18s] 所以随着潜艇以后
[10m20s] 也就是越来越先进
[10m23s] 它下潜的深度越来越深
[10m25s] 我们发现黑颜色
[10m27s] 在下潜以后
[10m29s] 不容易被发现
[10m30s] 可以把自己隐蔽得很好
[10m32s] 所以后来的潜艇
[10m34s] 也就是
[10m35s] 在二次大战以后的潜艇
[10m37s] 逐步逐步地就往黑色上靠近
[10m40s] 而现在
[10m41s] 世界各国的潜艇
[10m42s] 基本上全都涂成了黑颜色
[10m46s] 然而在深海
[10m48s] 仅仅看不见
[10m49s] 还远远不够
[10m51s] 随着潜艇潜得更深
[10m53s] 航行时间更长
[10m54s] 真正致命的威胁
[10m56s] 开始从光线转移到声音
[11m2s] 我听到它了
[11m11s] 找到它了 目标很近 正右舷083
[11m19s] 声音
[11m20s] 是深海中传播最远
[11m22s] 最难隐蔽的信号
[11m24s] 一丝多余的噪声
[11m26s] 就可能暴露位置
[11m27s] 招来猎杀
[11m33s] 于是
[11m34s] 潜艇的隐身逻辑
[11m35s] 也从颜色升级到了形态
[11m38s] 潜艇的外形
[11m40s] 并非一开始
[11m41s] 就是今天的样子
[11m43s] 很长一段时间里
[11m44s] 它更像是一艘
[11m45s] 能够下潜的水面舰艇
[11m48s] 二战时期的潜艇
[11m49s] 需要频繁浮出水面航行
[11m52s] 水下只是暂时藏身之所
[11m55s] 因此艇体高耸
[11m57s] 结构复杂
[11m58s] 水下阻力大
[11m59s] 噪声高
[12m1s] 真正的转折
[12m2s] 发生在核动力出现之后
[12m5s] 当潜艇可以长期潜航
[12m7s] 人类第一次开始
[12m9s] 只为水下生存
[12m10s] 来设计它的形状
[12m12s] 工程师将目光
[12m14s] 投向自然界
[12m15s] 鲸类
[12m16s] 海豚等高速游泳动物
[12m19s] 几乎都拥有同一种外形
[12m21s] 前圆后尖的水滴形
[12m24s] 1950年
[12m25s] 美国“大青花鱼”号实验潜艇
[12m28s] 验证了这一答案
[12m30s] 水滴型艇体
[12m31s] 显著降低阻力和流噪
[12m34s] 让潜艇更快 更安静
[12m36s] 也难被发现
[12m38s] 从此
[12m39s] 现代潜艇的外形
[12m41s] 几乎统一
[12m42s] 当潜艇的外形
[12m44s] 已经足够像一条鱼
[12m46s] 真正的较量才刚刚开始
[12m49s] 接下来要隐藏的
[12m51s] 不再是轮廓
[12m52s] 而是声音本身
[12m57s] 生物界
[12m58s] 鲨鱼的速度非常快
[13m0s] 因为在水中
[13m1s] 它可以
[13m3s] 以非常高的速度前进
[13m5s] 实际上
[13m6s] 它除了形体的这种流线型以外
[13m9s] 还有就是它的皮肤
[13m11s] 为什么我们很多的游泳运动员
[13m13s] 所采用的游泳衣或游泳裤
[13m16s] 采用的是叫所谓鲨鱼皮呢
[13m19s] 就是借鉴了鲨鱼的
[13m21s] 它皮肤上的这种V字形
[13m23s] 也就是它在前进的时候
[13m25s] 水流是顺着
[13m27s] 箭头的方向向后流的
[13m29s] 减少了阻力
[13m31s] 减少了就是水
[13m32s] 与鲨鱼皮肤的摩擦的声音
[13m35s] 然后使它快速地前行
[13m37s] 既减少了阻力
[13m38s] 又降低了声音
[13m40s] 所以我们在建造潜艇的时候
[13m42s] 也借鉴了这样的情况
[13m45s] 或者是说借鉴了这种功能
[13m48s] 我们在材料上
[13m49s] 采取一些特殊的材料
[13m51s] 因为在深海大洋里边
[13m54s] 这种材料
[13m55s] 就是我们讲的这种消声瓦
[13m58s] 因为消声瓦既要保证
[14m0s] 它在水中
[14m3s] 能够把噪音裹在潜艇里边
[14m7s] 不被对方探测到
[14m8s] 同时它也要像鲨鱼的皮肤一样
[14m11s] 有一些减少与水的
[14m14s] 这种摩擦和阻力
[14m16s] 来使这个声音降低
[14m19s] 也就是潜艇在高速航行的时候
[14m21s] 要降低它与水的摩擦的声音
[14m25s] 这样的话可以进一步地
[14m27s] 使我们的潜艇
[14m29s] 就是像鲨鱼一样
[14m30s] 悄然无声地在水中行进
[14m34s] 海豚的皮肤
[14m36s] 能够分泌一种特殊黏液
[14m38s] 使水流紧贴体表滑行
[14m40s] 从而减少湍流与阻力
[14m43s] 而更为人熟知的
[14m45s] 是鲨鱼的皮肤结构
[14m47s] 鲨鱼体表
[14m48s] 覆盖着无数微小的V型盾鳞
[14m52s] 这些看似粗糙的纹理
[14m54s] 反而能打散涡流
[14m55s] 抑制水流紊乱
[14m57s] 大幅降低摩擦阻力
[15m0s] 潜艇工程师
[15m1s] 从中得到启发
[15m3s] 在现代潜艇的消声瓦表面
[15m6s] 常会设计出
[15m7s] 类似的微沟槽
[15m8s] 或者细微纹理
[15m10s] 这种被称为沟槽面
[15m12s] 或仿鲨鱼皮的结构
[15m14s] 并不仅仅是为了
[15m15s] 让潜艇航行得更省力
[15m18s] 更重要的是
[15m19s] 它能够有效降低水流
[15m21s] 掠过躯体时产生的流噪声
[15m24s] 让潜艇在深海中
[15m26s] 变得更安静
[15m27s] 也更加致命
[15m29s] 海豚在浑浊的海水中
[15m31s] 能够准确地找到小鱼
[15m33s] 还能分辨出哪条更加肥美
[15m36s] 这种能力来自它们独特的
[15m38s] 水下视觉生物声呐
[15m41s] 当海豚发出
[15m43s] 咔嗒咔嗒的声音时
[15m44s] 这些声波
[15m45s] 就像无形的探照灯向前发射
[15m49s] 声音遇到物体产生回声
[15m51s] 海豚通过下颚
[15m53s] 接收这些回声
[15m54s] 就能在脑海中
[15m56s] 描绘出周围环境的立体图像
[16m0s] 它们能够听出
[16m1s] 藏在泥沙里的两条鱼
[16m3s] 哪条更加肥美
[16m5s] 前方是岩石还是海草
[16m8s] 这种能力
[16m9s] 让任何人工声呐都望尘莫及
[16m13s] 人类从海豚身上
[16m14s] 获得了灵感
[16m16s] 发明了声呐技术
[16m19s] 二战爆发后
[16m20s] 德国U型潜艇
[16m22s] 再次成为海战中的核心力量
[16m25s] 在广阔的大西洋上
[16m27s] 它们潜伏在航道附近
[16m29s] 利用夜色和水下掩护
[16m31s] 对盟军运输船队
[16m33s] 发动突然袭击
[16m35s] 单艘潜艇就足以瘫痪
[16m37s] 一整条补给线
[16m39s] 对英国而言
[16m40s] 这不仅是军事问题
[16m42s] 更是生存危机
[16m45s] 装备
[16m46s] 军需
[16m47s] 几乎全部依赖海上运输
[16m50s] 而水下的敌人却始终看不见
[16m53s] 面对潜艇带来的巨大压力
[16m56s] 英国海军被迫改变思路
[16m58s] 不再依赖目视搜索
[17m0s] 而是尝试在深海中
[17m2s] 倾听敌人的存在
[17m4s] 以水听器为基础
[17m6s] 配合主动声波探测
[17m7s] 声呐系统
[17m9s] 逐步成型
[17m11s] 并率先装备在
[17m12s] 驱逐舰和护卫舰上
[17m15s] 在护航战例中
[17m17s] 第一次实现了对潜艇的
[17m19s] 稳定定位
[17m21s] 使深水炸弹不再是盲目投掷
[17m24s] 而成为有目标的打击
[17m27s] 随着反潜技术的成熟
[17m29s] 曾经游刃有余的U型潜艇
[17m31s] 开始频繁暴露行踪
[17m33s] 损失不断增加
[17m36s] 大西洋的海战
[17m37s] 从潜艇的单方面猎杀
[17m39s] 逐渐演变成一场
[17m41s] 围绕隐蔽与探测的
[17m43s] 技术对抗
[17m45s] 而这一技术
[17m46s] 正是源自大自然中的
[17m48s] 生物声呐
[17m51s] 那么说到声呐
[17m53s] 就涉及到一个有趣的知识
[17m56s] 潜艇在水下航行时
[17m58s] 由于水的密度
[17m59s] 是空气密度的大约800倍
[18m2s] 导致雷达电磁波
[18m3s] 通讯电磁波
[18m4s] 无法穿透海水
[18m7s] 严谨地来说
[18m8s] 就是被海水快速地吸收
[18m10s] 传不远
[18m11s] 因此
[18m12s] 声呐就成为了潜艇的主要探测工具
[18m16s] 声呐的本质
[18m17s] 是一个经过微调的耳朵
[18m19s] 它的工作原理
[18m20s] 类似于鲸鱼或海豚的回声定位
[18m24s] 根据工作类型
[18m25s] 声呐可分为
[18m26s] 主动声呐和被动声呐
[18m29s] 主动声呐模式
[18m30s] 需要潜艇主动发射声呐信号
[18m33s] 声呐信号接触到目标后会反射回来
[18m37s] 再被潜艇捕捉
[18m39s] 但在战争中
[18m40s] 这不是一个明智的决策
[18m42s] 因为这种探索
[18m43s] 就像黑夜中打开手电筒
[18m46s] 照亮目标的同时
[18m47s] 也暴露了自己
[18m49s] 而被动声响模式
[18m51s] 是静默航行
[18m53s] 利用被动声呐基阵
[18m54s] 接收对方的主动声呐探测信号
[18m57s] 来确定对手位置
[18m59s] 被动声响模式下
[19m1s] 潜艇不会暴露目标
[19m3s] 隐蔽性好
[19m5s] 声呐
[19m6s] 作为动物界里边
[19m7s] 海豚
[19m9s] 是有它特异的功能
[19m11s] 因为海豚
[19m12s] 它是从嘴里
[19m13s] 发出不同的声响
[19m15s] 然后声响传出去以后
[19m18s] 如果撞在礁石上
[19m20s] 就是海底的礁石上
[19m22s] 会反射回来一个回波
[19m24s] 它用嘴巴下面的接收器
[19m28s] 去把这个声音接收到
[19m30s] 然后判定这是石头还是鱼类
[19m33s] 如果它的发出去的声音
[19m37s] 撞上的是鱼类
[19m39s] 它的回声
[19m41s] 是和撞在岩石上的声音不同的
[19m44s] 而且它收到的回波
[19m47s] 也就是声音传播
[19m50s] 回波在水里传的
[19m51s] 因为很远
[19m53s] 它接收下来就可以分辨出来
[19m55s] 这是它捕食的猎物了
[19m59s] 那么既然
[20m0s] 海豚有这么好的这种功能
[20m4s] 也就类似于声呐的功能
[20m6s] 人类就向它们学习
[20m8s] 就仿制了这种声响
[20m10s] 这就是主动声响
[20m12s] 也就是当我的潜艇
[20m14s] 航行的时候
[20m15s] 我不断地通过
[20m17s] 我的声呐向外释放声波
[20m21s] 当这些声波
[20m23s] 放到海洋里边去之后
[20m25s] 它会一去不复返
[20m26s] 但是如果这些声波
[20m29s] 撞击到了
[20m30s] 对方的潜艇或者是舰艇
[20m33s] 或者是海底的这些礁石
[20m36s] 它反射回来的声波是不一样的
[20m38s] 这样就凭借我们声呐兵
[20m41s] 来收到的回波
[20m43s] 判断前面是金属
[20m46s] 还是石头还是洋流
[20m49s] 这是一艘
[20m50s] 弗吉尼亚级攻击核潜艇
[20m53s] 潜艇的艇艙
[20m54s] 布置着大型声呐阵列
[20m56s] 在早期批次上
[20m58s] 它呈现为球形结构
[21m0s] 它始终对前方海域
[21m2s] 进行持续探测
[21m4s] 通过分析回波的时间与特征
[21m7s] 潜艇
[21m8s] 能够判断目标的距离
[21m10s] 方位
[21m11s] 并对水下接触进行初步识别
[21m14s] 沿着艇体左右两侧
[21m16s] 分布着舷侧声呐阵列
[21m19s] 海豚身体两侧的侧线系统
[21m22s] 负责监听来自各个方向的
[21m24s] 细微声学变化
[21m26s] 即使潜艇保持静默状态
[21m28s] 不主动发声
[21m30s] 这些阵列依然能够捕捉
[21m32s] 远处螺旋桨的低频噪声
[21m34s] 以及水流扰动形成的异常信号
[21m38s] 在潜艇尾部
[21m39s] 一条细长的
[21m40s] 拖曳声呐阵列被放出
[21m43s] 这是弗吉尼亚级
[21m44s] 最重要的远程感知手段之一
[21m47s] 它远离艇体自身的噪声干扰
[21m50s] 将潜艇的声觉
[21m52s] 延伸到更远的海域
[21m54s] 用于发现极其微弱
[21m55s] 距离极远的水下目标
[21m58s] 艇舯声呐
[21m59s] 舷侧阵列
[22m0s] 与拖曳声呐协同工作
[22m3s] 使这艘潜艇
[22m4s] 即使在完全隐蔽的状态下
[22m6s] 也能构建起一幅
[22m8s] 连续立体的水下态势图
[22m13s] 当生存受到直接威胁时
[22m16s] 顶尖的猎手
[22m17s] 与最先进的武器
[22m18s] 都诉诸同一种策略
[22m21s] 制造混乱
[22m22s] 隐藏本体
[22m24s] 当乌贼遇到危险时
[22m26s] 会迅速喷出浓密的墨汁
[22m29s] 它并非简单的黑色液体
[22m31s] 它在海水中会迅速扩散
[22m33s] 形成一个与自身形态相似的
[22m36s] 浓密的墨汁伪影
[22m39s] 这个深色轮廓能够立即吸引
[22m41s] 并且锁定捕食者的
[22m43s] 视觉注意力
[22m44s] 为真身的逃离
[22m46s] 创造关键的时间窗口
[22m49s] 潜艇工程师
[22m50s] 完美地借鉴了这套
[22m52s] 复杂的求生系统
[22m54s] 并将其转化为防御装备
[22m57s] 声学诱饵
[22m59s] 潜艇感知到来袭的
[23m1s] 声自导鱼雷
[23m2s] 或者敌方主动声响锁定时
[23m5s] 会从发射管
[23m6s] 快速射出声学诱饵
[23m8s] 它是潜艇的一种高电子替身
[23m12s] 声学诱饵
[23m13s] 通常是一个自航式的
[23m15s] 水下航行器
[23m16s] 被发射后
[23m17s] 它会精确模仿母艇的
[23m19s] 发动机噪声
[23m21s] 螺旋桨节拍等
[23m22s] 独特的声学指纹
[23m24s] 还会主动航行
[23m26s] 制造出比气幕弹
[23m28s] 更像一艘真实潜艇的
[23m29s] 移动声学目标
[23m31s] 来袭的智能鱼雷
[23m33s] 能够区分简单的气泡幕
[23m35s] 和复杂的潜艇噪声
[23m37s] 声学诱饵
[23m38s] 通过发出更具吸引力的
[23m40s] 听觉陷阱
[23m41s] 引诱鱼雷偏离航道
[23m43s] 转而追踪并攻击这个替身
[23m47s] 而这些诱饵的发现
[23m50s] 实际上就是根据海洋动物
[23m52s] 它一些保护自己的这些方式
[23m55s] 来学习的
[23m56s] 因为它们要么金蝉脱壳
[23m58s] 要么以假乱真
[24m2s] 所以我们看
[24m3s] 随着人工智能的发现
[24m5s] 随着我们无人装备的发现
[24m9s] 我们就制造了
[24m11s] 不同的这种诱饵
[24m13s] 主动的 被动的
[24m15s] 总之当发现潜艇
[24m17s] 被攻击或被人发觉的时候
[24m19s] 就扔出一个假目标
[24m21s] 向不同的方向
[24m22s] 甚至是相反的方向来行进
[24m25s] 让它的鱼雷
[24m26s] 让它的追踪的设备
[24m28s] 去跟踪假目标
[24m30s] 而真正的潜艇
[24m31s] 悄悄地就逃生了
[24m34s] 控制室 这里是声响室
[24m35s] 水中有鱼雷 方位241
[24m38s] 高速前进 右满舵
[24m42s] 找到它了 目标很近 正右舷083
[24m49s] 发射5英寸的反鱼雷诱饵
[24m51s] 发射反鱼雷诱饵
[25m9s] 这套系统
[25m10s] 在实战与对抗演练中
[25m12s] 屡建奇功
[25m14s] 其核心战术
[25m15s] 都与数百万年前的乌贼
[25m17s] 如出一辙
[25m19s] 乌贼用一团墨汁
[25m21s] 换取生存的时间
[25m23s] 人类则用电子替身
[25m25s] 把这一秒无限放大
[25m28s] 当深海的幽灵收起獠牙
[25m31s] 当猎杀者的声响归于沉寂
[25m34s] 这片蔚蓝疆域的博弈
[25m36s] 却从未停止
[25m38s] 从鹦鹉螺的浮力控制
[25m40s] 到核潜艇的无限续航
[25m42s] 从乌贼的墨汁
[25m43s] 到潜艇的电子诱饵
[25m46s] 我们见证了一场
[25m47s] 跨越数亿年的
[25m48s] 军事科技进化
[25m51s] 自然用漫长的时光
[25m52s] 雕琢出生存的智慧
[25m54s] 人类用智慧在钢铁中
[25m57s] 复刻这些法则
[25m59s] 这不仅是矛与盾的较量
[26m1s] 更是对生命本身
[26m3s] 最深刻的学习
[26m5s] 下一场改变游戏规则的技术革命
[26m8s] 或许正在随着
[26m9s] 某只深海生物的游弋
[26m11s] 在黑暗中悄然酝酿
[26m15s] 感谢您持续关注
[26m16s] 国防军事频道《军事科技》
[26m19s] 我们下周同一时间
[26m20s] 再见
@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
"""去重 ocr_raw.jsonl: 每个 idx 只保留第一条"""
import json, os
from pathlib import Path
WORK_DIR = Path(__file__).resolve().parent
src = WORK_DIR / "ocr_raw.jsonl"
dst = WORK_DIR / "ocr_raw_clean.jsonl"
sys_out_ok = False
try:
import sys
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
sys_out_ok = True
except:
pass
lines = [l for l in src.read_text(encoding="utf-8").strip().split("\n") if l.strip()]
seen = set()
deduped = []
for l in lines:
rec = json.loads(l)
idx = rec.get("idx")
if idx not in seen:
seen.add(idx)
deduped.append(l)
deduped.sort(key=lambda x: json.loads(x).get("idx", 0))
# write to clean file
dst.write_text("\n".join(deduped) + "\n", encoding="utf-8")
print(f"deduped: {len(lines)} -> {len(deduped)} lines")
# verify clean file
v = dst.read_text(encoding="utf-8").strip().split("\n")
print(f"verify clean: {len(v)} lines")
# replace original
os.replace(str(dst), str(src))
# verify original
v2 = src.read_text(encoding="utf-8").strip().split("\n")
print(f"verify original: {len(v2)} lines, {src.stat().st_size//1024}KB")
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
"""将 A稿 docx 转为 txt 供 doco terms 使用"""
from docx import Document
from pathlib import Path
ep_dir = Path(__file__).resolve().parent
docx_files = list(ep_dir.glob("*.docx"))
if not docx_files:
raise FileNotFoundError(f"{ep_dir} 下找不到 .docx 文件")
docx_file = docx_files[0]
print(f"Reading: {docx_file.name}")
doc = Document(str(docx_file))
text = "\n".join(p.text for p in doc.paragraphs)
out_file = ep_dir / "A稿.txt"
out_file.write_text(text, encoding="utf-8")
print(f"Written: {out_file} ({len(text)} chars, {len(text.splitlines())} lines)")
@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
"""调用 doco compose 命令"""
import subprocess
from pathlib import Path
ep_dir = Path(__file__).resolve().parent
repo_root = ep_dir.parent.parent # doco/programs/ep002... -> tps-dashboard
episode_id = ep_dir.name
cmd = [
"doco", "compose",
"--episode-id", episode_id,
]
print(f"Running: doco compose --episode-id {episode_id}")
print(f"CWD: {repo_root}")
result = subprocess.run(
cmd,
cwd=str(repo_root),
shell=True,
encoding="utf-8",
errors="replace",
)
print(f"\nExit code: {result.returncode}")
@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
"""调用 doco fuse 命令"""
import subprocess
import sys
from pathlib import Path
ep_dir = Path(__file__).resolve().parent
repo_root = ep_dir.parent.parent # doco/programs/ep002... -> tps-dashboard
episode_id = ep_dir.name
cmd = [
"doco", "fuse",
"--episode-id", episode_id,
]
print(f"Running: doco fuse --episode-id {episode_id}")
print(f"CWD: {repo_root}")
result = subprocess.run(
cmd,
cwd=str(repo_root),
shell=True,
encoding="utf-8",
errors="replace",
)
print(f"\nExit code: {result.returncode}")

Some files were not shown because too many files have changed in this diff Show More