Compare commits
123 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 886ddd8e88 | |||
| 07f71286b6 | |||
| a138fd856e | |||
| 985a5b6ada | |||
| dd17264f60 | |||
| 93db565c10 | |||
| ffb507a87c | |||
| 897b662e54 | |||
| d91c20a126 | |||
| 81b331d6d7 | |||
| d8057b07e9 | |||
| 30931d4446 | |||
| bdc59bf787 | |||
| 1c3963d17c | |||
| a87f453326 | |||
| 010128b123 | |||
| e213feb814 | |||
| 7c51498d95 | |||
| 994c57edbb | |||
| 8df9c798e7 | |||
| 10b0ab757b | |||
| 60f22e6660 | |||
| 0a4ab881bc | |||
| 1372600d71 | |||
| 487877fa08 | |||
| 7470dbb0ed | |||
| 0d52f9b707 | |||
| bf93213990 | |||
| 513ccb34d4 | |||
| d14bcc2778 | |||
| 8cb0cbfa25 | |||
| f91efef935 | |||
| 47e17179c7 | |||
| a826212302 | |||
| b138b98bc4 | |||
| 89d59f6f03 | |||
| 0508bd8f23 | |||
| aa59ef78e0 | |||
| 0e6322ad97 | |||
| 70e2647fae | |||
| 202e02c254 | |||
| 9a86c936ee | |||
| 9acb9d380d | |||
| 47c9d60f02 | |||
| cd4dcd06f8 | |||
| 783e212bb1 | |||
| 0c7d2d7400 | |||
| 70784fc70b | |||
| 347a06e48b | |||
| fff2cfe395 | |||
| a00bcd6c73 | |||
| b3157216bb | |||
| d849bc1539 | |||
| 74a0508755 | |||
| 7999fd14c8 | |||
| 5ef1b8ec8d | |||
| 855f103ce8 | |||
| d81969b4b5 | |||
| 04a35faba3 | |||
| 1325807257 | |||
| d40d46a434 | |||
| 2881b866a6 | |||
| 72ae73764a | |||
| 51a34e9ec6 | |||
| 303995a93f | |||
| d774370a4b | |||
| a6e8cc25ec | |||
| 8299b6bbc9 | |||
| 02bf4c4fe4 | |||
| d55fde7fb5 | |||
| a2f1710a7f | |||
| 59f556d986 | |||
| fcaa0969a8 | |||
| f55030b971 | |||
| 24b9bb2d08 | |||
| 247268a60e | |||
| f0f9dc493e | |||
| e0ccfb20e7 | |||
| 4eca31f8eb | |||
| 3942f01def | |||
| b7560a6cf6 | |||
| 7cc0bf21ff | |||
| 93fd327b13 | |||
| 932985feca | |||
| ff0471a6be | |||
| f4661863a7 | |||
| 14c9841b35 | |||
| c9e527ff5b | |||
| 2480d4a738 | |||
| 9439c72d30 | |||
| 3102fbec6c | |||
| b3667aa7b9 | |||
| 773441bb87 | |||
| 3dc1a1cf3c | |||
| fab15505bd | |||
| 347ec1b13a | |||
| d29b1bb394 | |||
| 2bee0ce8c8 | |||
| 0b141239cc | |||
| 483e34b4f6 | |||
| 5217d09044 | |||
| 3fa00ea393 | |||
| d8d04790fd | |||
| a43a3e48a3 | |||
| 2da78975e6 | |||
| a53017dd58 | |||
| 048f8bc158 | |||
| 31fb490b6d | |||
| f2f38f2b45 | |||
| 58e1183d05 | |||
| 7fa06c6a37 | |||
| 99c1139fbf | |||
| 6c7d88d063 | |||
| ab1a152dda | |||
| c266a5de5c | |||
| 061e07f842 | |||
| 1179246f2d | |||
| c0a23c2610 | |||
| d20548b846 | |||
| a9d1b0e416 | |||
| 110e26f752 | |||
| 906936a2d1 | |||
| 75691c8496 |
+42
-30
@@ -2,8 +2,8 @@
|
||||
|
||||
> 项目:TPS 中台(央视《军事科技》栏目内部工作台)
|
||||
> 仓库:`tps-dashboard`(Gitea,团队可见)
|
||||
> 修订版本:v3 · 2026-05-06
|
||||
> 适用对象:Cline + Sonnet(Plan)+ DeepSeek pro(Act),所有协作者必读
|
||||
> 修订版本:v5 · 2026-05-15
|
||||
> 适用对象:Cline + MiniMax M2.7(Plan + Act),所有协作者必读
|
||||
|
||||
---
|
||||
|
||||
@@ -25,33 +25,25 @@
|
||||
|
||||
| 角色 | 模型 / 工具 | 职责 |
|
||||
|---|---|---|
|
||||
| 制片人 | 刘统(人) | 拍板、审核、领域知识权威、视觉把关 |
|
||||
| 制片人 | 刘通(人) | 拍板、审核、领域权威、视觉把关 |
|
||||
| 高级技术顾问 | Claude Opus 4.7(claude.ai 网页) | 架构指导、方案审核、风险预警、Cline 卡壳兜底 |
|
||||
| **架构师(Phase 0-1)** | **Sonnet 4.6**(Cline Plan 模式) | 项目内方案设计、Act 改动审查 |
|
||||
| **架构师(Phase 2 起)** | **MiniMax M2.7**(Cline Plan 模式)| **Sonnet 余额耗尽后接班 Plan,API Key 已 ready** |
|
||||
| 执行者 | DeepSeek v4 pro(Cline Act 模式) | 实际写代码,**全程不变** |
|
||||
| 架构师 | MiniMax M2.7(Cline Plan 模式) | 项目内方案设计、Act 改动审查 |
|
||||
| 执行者 | MiniMax M2.7(Cline Act 模式) | 实际写代码 |
|
||||
| 备选执行者 | DeepSeek v4 flash | 仅日常小改(改字段、加字段、改文案) |
|
||||
|
||||
**模型阶段性安排(2026-05-06 拍板)**:
|
||||
**当前模型配置(2026-05-14 拍板)**:
|
||||
|
||||
- **Phase 0-1(骨架期)**:**Plan = Sonnet 4.6**,Act = DeepSeek pro
|
||||
- **Phase 2 起**:**Plan = MiniMax M2.7**(标准版,非 highspeed),Act 仍 DeepSeek pro
|
||||
- **切换的核心动机**:Sonnet 单次调用昂贵且 Plan 模式调用频次最高,换 MiniMax M2.7 后单次成本降至 1/50
|
||||
- DeepSeek 全程在岗,不退役
|
||||
- **Plan 模式**:MiniMax M2.7(标准版,非 highspeed)
|
||||
- **Act 模式**:MiniMax M2.7
|
||||
- 双机开发:单位 4090D(E:\tps-dashboard) + 家里电脑,通过腾讯云 Gitea 同步
|
||||
- 远程仓库:`http://101.42.29.217:3000/simonkoson/tps-dashboard.git`
|
||||
- **开工先 pull、收工必 push、push 前先 pull**——三条铁律,详见 `docs/git_workflow.md` 第 7 章
|
||||
- API 配置:`base_url = https://api.minimaxi.com/v1`,Model ID = `MiniMax-M2.7`
|
||||
|
||||
**MiniMax M2.7 接入信息(配置在 Cline 的 Plan 模式)**:
|
||||
**模型降级口诀**:
|
||||
|
||||
- 官方 API:`base_url = https://api.minimaxi.com/v1`(注意是 minimaxi)
|
||||
- Model ID:`MiniMax-M2.7`(标准版)
|
||||
- **不用** highspeed 版(贵一倍,Plan 模式不需要那点速度)
|
||||
- API Key 已由制片人申请就绪
|
||||
|
||||
**模型切换的判断口诀**(Phase 2 起):
|
||||
|
||||
- Plan 出方案、审 Act 改动 → **MiniMax M2.7**
|
||||
- Act 写新代码 / 建结构 / 改架构 → **DeepSeek pro**
|
||||
- Act 改小 bug / 加字段 / 改文案 → **DeepSeek flash**(可选)
|
||||
- Plan 连续两次方案质量明显下降 → **临时切回 Sonnet 顶住**(允许)
|
||||
- Plan 连续两次方案质量明显下降 → **临时切回 claude.ai 找 Opus 4.7 重新规划,不在 Cline 里换模型**
|
||||
- Act 连续两次实现质量明显下降 → **优先切回 claude.ai 找 Opus 4.7 审 Plan,看是不是任务拆得太大;如果是小改任务,可临时切到备选执行者 DeepSeek v4 flash;不在 Cline 里硬撑**
|
||||
|
||||
---
|
||||
|
||||
@@ -59,8 +51,8 @@
|
||||
|
||||
### 3.1 Plan → Act → 完工三阶段
|
||||
|
||||
1. **Plan 阶段**:Sonnet 出方案,**复杂方案先发 Opus 4.7 审一遍**,审过后再切 Act
|
||||
2. **Act 阶段**:DeepSeek pro 严格按 Plan 实施,**禁止自作主张扩展范围**
|
||||
1. **Plan 阶段**:MiniMax M2.7 出方案,**复杂方案先发 Opus 4.7 审一遍**,审过后再切 Act
|
||||
2. **Act 阶段**:MiniMax M2.7 严格按 Plan 实施,**禁止自作主张扩展范围**
|
||||
3. **完工阶段**:Act 必须给出"完工报告",列出所有改动文件、设计决策、潜在风险
|
||||
|
||||
### 3.2 哪些事 Act 必须停下让 Plan 审
|
||||
@@ -120,7 +112,7 @@ Phase 内的子任务、修复独立 bug、前端骨架、后端骨架——都
|
||||
|
||||
### 5.2 年度目标只增不改(时间不变性)
|
||||
|
||||
`yearly_targets` 表每年新增一行,**历史年份的目标行绝不可被覆盖或删除**。
|
||||
`yearly_targets` 表每年新增一行,**历史年份的目标行绝不可被覆盖或删除**:
|
||||
|
||||
- 前端展示某期数据时,**根据该期播出年份的目标判颜色**,不是"当前年份的目标"
|
||||
- 2024 年播的节目永远按 2024 年的 0.6448 / 0.8989 判,2026 年回看也不会变色
|
||||
@@ -134,7 +126,7 @@ SQL 端:`yearly_targets.year` 加 UNIQUE 约束,代码端 INSERT 时用 `ON CONF
|
||||
|
||||
- **在职编导**:两字段都填(冗余但稳)
|
||||
- **离职编导**:`editor_id` 留空(NULL),只填 `editor_name_snapshot`(如"张颖")
|
||||
- **历史导入**:按姓名匹配在职用户,匹配不上只填快照
|
||||
- **历史导入**:按姓名匹配在职用户,匹配不上只填快照,不报错不丢数据
|
||||
- **查询某编导历史作品**:**先按 editor_id,失败回退按 editor_name_snapshot 文本匹配**
|
||||
|
||||
DeepSeek 容易写"editor_id 外键非空 + 删人级联"——**禁止**。一旦张颖离职,她做过的几十期节目就全成"无主"了,这是栏目最不能接受的。
|
||||
@@ -157,6 +149,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 鉴权、前端权限判断、菜单显示**——一律使用拼音值,**不得替换为英文**。
|
||||
|
||||
---
|
||||
@@ -350,7 +347,7 @@ allow_origins=["http://localhost:5173"] # 必须显式列,不能用 ["*"]
|
||||
|
||||
**单位机器 Phase 1 后端可选路径**:
|
||||
|
||||
- **路径 A**:从 0 重写(Sonnet + DeepSeek 重新生成)
|
||||
- **路径 A**:从 0 重写(MiniMax M2.7 重新生成所有后端文件)
|
||||
- **路径 B**(推荐):用户把家用机的 backend 目录打包 zip 拷到单位机,解压进新仓库,Cline 跑一遍跑通即可
|
||||
|
||||
由制片人在 Phase 1 启动时拍板选 A 或 B。
|
||||
@@ -376,11 +373,26 @@ 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 全程在岗** |
|
||||
| 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 移除 |
|
||||
| **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 经验)。*
|
||||
*下次修订建议时机:Phase 2 末(评估 MiniMax 切换、整理 Phase 2 经验)。*
|
||||
+91
-1
@@ -29,7 +29,97 @@ Thumbs.db
|
||||
# 日志运行产物
|
||||
*.log
|
||||
cookie.txt
|
||||
cookies*.txt
|
||||
|
||||
# 临时文件
|
||||
*.tmp
|
||||
*.bak
|
||||
*.bak
|
||||
homePC/
|
||||
|
||||
# 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/programs/*/.c4_cache_spk/
|
||||
|
||||
# Doco 中间产物(流水线可重生成,只保留 deliverables/ 终版)
|
||||
doco/programs/*/*.json
|
||||
doco/programs/*/*.jsonl
|
||||
doco/programs/*/*.csv
|
||||
doco/programs/*/*.txt
|
||||
doco/programs/*/*.py
|
||||
doco/programs/*/*.docx
|
||||
|
||||
# Doco 根目录散落的初稿 docx
|
||||
doco/programs/*.docx
|
||||
|
||||
# Doco 临时核验/草稿 & Word 锁文件 & 手批稿(不入库)
|
||||
~$*.docx
|
||||
~$*.xlsx
|
||||
doco/programs/*/_cropcheck/
|
||||
doco/programs/*/_seg_check.txt
|
||||
doco/programs/ep002_004/
|
||||
doco/_*.py
|
||||
doco/verify_out.txt
|
||||
doco/programs/*/*_批改*.docx
|
||||
|
||||
# AI-labeling: Excel 锁文件 & 参考资料
|
||||
ai-labeling/example/~$*
|
||||
ai-labeling/example/program_docs/
|
||||
|
||||
# 临时提示词文件
|
||||
CLAUDE生成prompt.md
|
||||
|
||||
# 大文件全局禁入(视频/设计源文件/数据库dump)
|
||||
*.mp4
|
||||
*.mov
|
||||
*.avi
|
||||
*.mkv
|
||||
*.psd
|
||||
*.dump
|
||||
footage/
|
||||
|
||||
# git 备份目录(filter-repo 清理前的备份)
|
||||
.git-backup-before-cleanup/
|
||||
|
||||
# 迁移文档(含敏感信息,不入库)
|
||||
migrate-local-gitea-to-cloud.md
|
||||
@@ -0,0 +1,151 @@
|
||||
- # 项目协作主控文件 (CLAUDE.md)
|
||||
|
||||
<!--
|
||||
Claude Code 每次开 session 自动读取。两个角色:
|
||||
1) 项目说明书(稳定:是什么、用什么、怎么跑、什么不能碰)
|
||||
2) 跨工具活快照交接区(动态:现在到哪、下一步、刚定的决定)
|
||||
维护原则:增量更新,改对应小节,不整篇重写、不删历史。
|
||||
本文件与项目宪法(.clinerules / project_plan.md / dev_plan.md / docs/git_workflow.md)并列,
|
||||
不复制宪法内容,只指向 + 抽取仍生效的关键决策。新对话接手时连同宪法一并阅读。
|
||||
-->
|
||||
|
||||
---
|
||||
|
||||
## 🔖 状态栏(每次结束 session 前必须更新这三行)
|
||||
|
||||
- **最后更新**:Claude(顾问)| 2026-07-03
|
||||
- **当前状态一句话**:远程仓库已迁至腾讯云 Gitea,双机开发就绪。Phase 3 知识库已完成。**看板升级子项目原型阶段收工**(2026-07-03),交付清单已写入寄存条,主项目可启动收视分析看板 React 前端实现。
|
||||
- **下一个动手的人从这里开始**:见下方「⏩ 交接备注」
|
||||
|
||||
---
|
||||
|
||||
## 🤖 给 AI 的工作约定(READ FIRST)
|
||||
|
||||
- 开工前先读完:本文件「状态栏 / 交接备注 / 关键决策」三节 + 四份宪法 + 两份外迁寄存条(`寄存条_看板升级已外迁.md`、`寄存条_doco子项目已外迁.md`)。
|
||||
- **角色定位**:Claude Code 默认是**顾问**——出方案、审 Plan、卡壳兜底、写交接文档。**写代码(Plan + Act)交给 Cline,不要架空 Cline**。顾问位是为**省 token**:日常 CRUD、常规实现不该 Claude Code 亲自写。**仅两种情况下场动手**:① Cline 连续几次改都过不去的坎;② 制片人明确点名让 Claude Code 操刀。
|
||||
- 给 Cline 的指令一律用**代码块**封装,方便复制;不要让制片人手动粘贴整份文件内容。
|
||||
- 完成一段工作 → **增量更新**:在「已完成」追加一条、「待办」勾掉/新增、必要时补「关键决策」,最后改「状态栏」三行。只追加/勾选,不重写、不删历史。
|
||||
- 涉及**发送、发布、删除、改权限、付款、跑 schema 迁移**等不可逆操作前,先在对话里跟制片人确认。
|
||||
- Git 红线:`git reset --hard` / `push --force` / `rebase` / 删分支删 tag —— **必须停下问制片人**,永不自动执行;删整个 `.git` 永远禁止。
|
||||
- **沟通**:全程简体中文;制片人不懂代码,解释说人话、最小化技术语言;给明确推荐而非反复抛选项;复杂决策配可视化产物(HTML 原型 / 气泡图)辅助。回复简明,不堆中间过程。
|
||||
|
||||
---
|
||||
|
||||
## 🧩 子项目关系 / 话题边界(外迁禁区)
|
||||
|
||||
主干保持稳定,复杂功能拆成独立子项目(独立 chat / 独立 PRD)。**以下话题已外迁,主项目里聊到要先反问「这是子项目的事还是主干的事」,是子项目的就提醒制片人切到对应 chat,不在主干默默施工。**
|
||||
|
||||
- **看板分析升级**(`寄存条_看板升级已外迁.md`):关键词——看板升级 / 双引擎象限图 / 题材热度 / AI 打标 / Prompt 1·2·3 / ground-truth / episodes 加字段 / 23 期回填 / 能力地图 / 置信度三档 / 本期诊断小结。
|
||||
- **Doco 文稿整理**(`寄存条_doco子项目已外迁.md`):关键词——doco / 文稿整理 / 三方融合 / 视频双路拆分 / A·B 稿 / ASR / 讯飞 / DeepSeek Vision / 终版文稿 / 差异报告 / 段落对齐。**⚡ 2026-06-26 已交付:22 期融合A稿成品在 `doco/deliverables/`(按播出顺序命名),缺第01期(无A稿)。下一步:批量导入知识库(走 Phase 3 上传/embedding 链路)。**
|
||||
|
||||
**主干仍管**:Phase 0–3 主干代码、全部 backend schema/API/迁移、全部前端 React 实施、主干 bug/性能/新需求、Cline 的全部 Plan+Act。
|
||||
|
||||
**接收子项目交付物的纪律**:只接稳定版(草稿不实施)→ 跑迁移前先 `pg_dump` 备份 → 实施完回访子项目登记状态。Schema 变更走「Cline Plan + Opus 审 + 制片人批准 Act」。
|
||||
|
||||
**跨子项目通用规则**见 `跨子项目协作规则.md`(现有规则 1:PRD 版本管理——主项目只留最新版 PRD+回复,翻新先删再传)。`docs/api_credentials_inventory.md` 登记各子模块外部 API 元信息(不存真 key)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 项目概览(稳定)
|
||||
|
||||
- **项目名**:TPS 工作台(Topic Planning System)— 央视《军事科技》栏目 8 人团队(制片人/责编/6 编导)内部工作台。
|
||||
- **目标**:把散在 Excel/Word/微信/个人电脑里的工作流收拢成一个系统——仪表盘(收视+排期)、知识库(往期文稿/军报/检索)、对话式选题策划+报题单生成、编导个人首页+热点雷达。
|
||||
- **设计哲学红线(贯穿所有功能/Prompt/UI)**:**辅助而不替代,AI 给方向不给答案,保护编导创作主权。** 任何让 AI 替编导拍板、或把编导逼向「按 AI 建议改」的设计,不做或重做。AI 输出永远是草稿,制片人审核后才发布,**绝不自动发布**;未审核内容对编导显示「待审核」,绝不展示 AI 草稿值。
|
||||
- **范围边界(1.0 不做,别自由发挥)**:TypeScript、Alembic、async SQLAlchemy、AI 模型配置 UI、文稿写作辅助系统、Karpathy LLM Wiki 形态(仅预埋接口)、移动端、多栏目复用。
|
||||
|
||||
---
|
||||
|
||||
## 2. 技术栈与运行方式(稳定,红线,禁止擅改)
|
||||
|
||||
- **前端**:React 18 + Vite + Ant Design 5(中文 locale)+ Zustand + React Router v6,**JavaScript(不用 TS)**。
|
||||
- **后端**:FastAPI + **SQLModel(同步,不用 async)**;**手写 SQL `backend/sql/00X_*.sql`(不用 Alembic)**。
|
||||
- **库**:PostgreSQL 16 + **pgvector**;向量**统一 1536 维(不可改)**。
|
||||
- **认证**:**Session + Cookie(不用 JWT)**;CORS 必须 `allow_credentials=True` + 显式列 `allow_origins`(不能 `["*"]`)。
|
||||
- **其他锁定**:Word 用 docxtpl;甘特图用 frappe-gantt;定时任务 APScheduler。任何「换成 XX 更好」先在 Plan 讨论,绝不直接改。
|
||||
- **关键目录**:`backend/app/{api,core,services,models,db}`、`backend/sql/`、`frontend/src/{pages,components,services,stores}`、`logs/`、`docs/`。
|
||||
- **远程仓库**:`http://101.42.29.217:3000/simonkoson/tps-dashboard.git`(腾讯云 Gitea,用户 simonkoson)。**双机开发**:单位 4090D + 家里电脑,开工先 `git pull`,收工必 `git push`,详见 `docs/git_workflow.md` 第 7 章。
|
||||
- **本地启动**(开发模式,两个 PowerShell 窗口都别关;仓库根 `E:\tps-dashboard`):
|
||||
- 后端:`cd backend` → 激活虚拟环境 → `uvicorn app.main:app --reload --port 8000`(文档 `/docs`)
|
||||
- 前端:`cd frontend` → `npm run dev`(端口 5173 或 5174)
|
||||
- **环境变量/密钥**:放 `backend/.env`,**永不进 git、永不写进本文件**。需要:数据库连接、SECRET_KEY、MiniMax API Key + GroupId。
|
||||
- **依赖锁版本**(勿乱升):`bcrypt==4.0.1`(新版与 passlib 不兼容)、`itsdangerous==2.2.0`、`python-multipart==0.0.9`;`starlette` 不单独装。
|
||||
|
||||
---
|
||||
|
||||
## 3. 当前进度(动态,核心交接区 — 以最新快照为准)
|
||||
|
||||
- **已完成至**:Phase 3 / Task 3(知识库树形视图·按来源两层分组,路线 B),含 Task 3.1、3.2,验收通过,最终 commit `3409d48`。
|
||||
- **正在做**:无。看板升级子项目原型阶段已收工(2026-07-03),交付物就绪,可启动收视分析看板 React 前端实现。
|
||||
- **卡点/待解**:下一刀方向未定(见「待办」)——现在多了一个选项:收视分析看板前端实现(子项目已交付设计规范+原型)。
|
||||
- **Schema 状态**:知识库两表 schema 已就绪,**Phase 3 不再改表**。episodes 表已通过 003 迁移加 7 列 AI 标签字段(看板升级子项目 2026-06-26 执行)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 已完成(只追加,最新在上)
|
||||
|
||||
- [2026-07-03] 看板升级子项目原型阶段收工:L2 打标流水线就绪(GT v0.6.0 / 25 期 / 三个 Prompt 可用)+ L3 HTML 原型验证通过(双引擎象限图+5 模块+视觉定稿)+ 003 迁移已执行(episodes +7 AI 标签列)。交付清单见寄存条。
|
||||
- [2026-05-27] Phase 3 Task 3/3.1/3.2:知识库树形视图(按来源大类→二级维度分组、左右布局、节点联动、展开收起;修出处筛选/宽度跳动/排版)。commit `3409d48`。
|
||||
- [2026-05-27] Phase 3 Task 2:知识库管理后台最小版(上传/列表/删除)。
|
||||
- [2026-05-27] Phase 3 Task 1:embedding 链路(入库即调 MiniMax embo-01 算 1536 维向量存 pgvector)。
|
||||
- [—] Phase 2:仪表盘 + 批量导入 + 写权限收紧(yearly_targets 写权限)。
|
||||
- [—] Phase 1:后端骨架(认证三件套)+ 前端骨架(登录/主布局/路由守卫)。
|
||||
- [—] Phase 0:环境 + Git。
|
||||
|
||||
---
|
||||
|
||||
## 5. 待办(下一刀候选,开局前定先做哪件)
|
||||
|
||||
- [ ] **下一刀四选一**:① 语义搜索界面(不依赖任何材料,随时能开,是 Phase 4a 硬门槛);② PDF 原文关联 + 大文件存储架构(需 Opus 审方案,优先级较高);③ 导入真实收视 Excel + 清测试数据(Phase 4a 另一前置);④ **收视分析看板 React 前端实现**(看板升级子项目已交付设计规范+HTML 原型,详见 `note/寄存条看板升级已外迁.md` 交付清单)。
|
||||
- [ ] PDF 大文件存储:**大文件不入库,单独文件仓库,DB 只存地址指针**(md 正文+向量留库参与检索;pdf 仅按需调阅)。地基一次定对,避免上云返工。
|
||||
- [ ] 200+ Obsidian md 批量录入(**建议在 PDF 存储方案定后做**;先试 10 篇验证解析/落位再全量,每篇都真调 MiniMax 耗额度)。
|
||||
- [ ] 「按编导看稿」独立筛选视图(路线 A 重构时一并处理,从来源树里迁出来)。
|
||||
- [ ] 操作留痕 schema(episodes 等加 created_by/updated_by/updated_at;涉 schema 须 Opus 审)。
|
||||
- [ ] 界面像素级打磨(连同全栏目视觉规范统一弄,参考「色调字体倒角」图)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 关键决策(跨 session 最易丢,先看这节再动手)
|
||||
|
||||
### 栏目业务红线(AI 凭通用习惯 100% 写反,死守)
|
||||
|
||||
- **收视颜色反直觉**:只对 `audience_share` 上色(`audience_rating` 只显示不判定)。🔴红=份额**>**当年摸高目标=**优秀**;🔵蓝=基础≤份额≤摸高=达标;🟢绿=份额**<**基础=**待提升**。(与通用红警绿安相反)
|
||||
- **yearly_targets 只增不改**:按节目**播出年份**的目标判色(非当前年);`year` 加 UNIQUE,INSERT 用 `ON CONFLICT DO NOTHING`,**永不 ON CONFLICT UPDATE**。
|
||||
- **编导关联用软引用+快照双字段**:`editor_id` + `editor_name_snapshot`。离职编导 `editor_id` 留 NULL、保留快照;查历史作品先按 id、回退按 name 文本匹配。**禁止外键非空 + 级联删**(否则离职者历史节目全成无主)。
|
||||
- **报题单内容概要双段结构**:默认 引子(200–300 字)+ 列举(3–5 条,每条以 `**加粗关键词**` 起头);纯叙述式仅编导明确指定时用。Prompt 与 docxtpl 模板同此。
|
||||
- **角色枚举是拼音**:`zhipianren` / `zebian` / `biandao`,不增枚举、不换英文。yearly_targets:POST/PATCH 限 zhipianren+zebian(biandao 403),DELETE 仅 zhipianren,GET 三角色均可。
|
||||
|
||||
### 架构决策
|
||||
|
||||
- 知识库树**交互层 vs 数据分组层必须解耦**:分组逻辑在 `useKnowledgeGrouping.js` + 后端 `get_grouped_items()`;树组件只管展开/高亮/联动,不感知按什么字段分组。
|
||||
- 二级分组维度做成**可配置映射表** `SECONDARY_GROUP_FIELD`(manuscript→author,military_report→source_detail,其余 None),前后端同名对齐。将来转主题树(路线 A)只改这一处映射,原料已在库(tags/related_entities JSONB),大概率无需补字段重传。
|
||||
- 向量 1536 维、Session 认证、不用 Alembic/TS/async —— 见技术栈节,宪法锁定。
|
||||
- embedding 用 **MiniMax embo-01**:请求用 `texts` 数组 + `type`(db/query),返回在 `vectors` 字段,需 API Key + GroupId;花 MiniMax 账号额度(与 Claude token 两笔账,一篇几厘钱)。
|
||||
- **PDF 等大文件不入数据库**,单独文件仓库 + DB 存指针(详见待办,待立项)。
|
||||
|
||||
---
|
||||
|
||||
## 7. ⏩ 交接备注(换人/换账号 0 摩擦续上)
|
||||
|
||||
- **下一刀未定**,开工第一件事:与制片人确认四选一(语义搜索 / PDF 存储 / 真实收视导入 / **收视分析看板 React 前端**)。看板前端的设计规范+原型已由子项目交付(见 `note/寄存条看板升级已外迁.md` 交付清单 + `ai-labeling/CLAUDE.md` §1/§6/§7)。PDF 存储须先和 Opus 讨论方案才立项。
|
||||
- **schema 红线**:Phase 3 不改知识库两表;`topics` / `topic_embeddings` 属 Phase 4a,**勿碰**;五类 `source_type` 枚举固定不增减,细分杂志/军报=改表=须 Opus 审。
|
||||
- **`logs/` 是受保护资产**:Act 模式严禁 Cline 改;phase 日志由顾问协助生成。Task 3 三轮内容待并入 `phase3_log`(Task 3 段)。
|
||||
- **避雷(Phase 3 实战教训)**:
|
||||
1. **不信 Cline 自检**——它多次虚标 ✅,一律由制片人真实点页面逐条验。
|
||||
2. 改树节点 **key 格式属高风险**,生成端与过滤端必须同步,改后必实测点击联动。
|
||||
3. **已验证好用的布局别为对齐整体重构**(flex 改 Grid 连坏两轮);样式微调优先。
|
||||
4. Cline 报告自相矛盾时,**把源码发给 Opus 一眼定位**胜过让 Cline 反复试错。
|
||||
5. 像素级打磨边际收益递减,**见好就收**,记 backlog 改天连视觉规范统一弄。
|
||||
|
||||
---
|
||||
|
||||
## 8. 受保护资产 / 改前必经制片人确认
|
||||
|
||||
**严禁 Act 自动修改/删除**:`.clinerules`、`project_plan.md`、`dev_plan.md`、`README.md`、`docs/**`、`logs/**`、`samples/*.xlsx`、`.git/`、`.env`。如需改,切 Plan 经制片人确认。
|
||||
|
||||
---
|
||||
|
||||
## 9. 待确认 / 开放问题(需制片人拍板,AI 别自行假设)
|
||||
|
||||
- [ ] 下一刀选谁(三选一)。
|
||||
- [ ] PDF 文件存哪、笔记如何关联中台内文件位置(Backlog #2 的前提)。
|
||||
- [ ] 视觉风格参考图、6 名编导初始画像(各 200 字)、甘特图样本 —— 后续 Phase 需要。
|
||||
@@ -10,7 +10,7 @@
|
||||
一个面向《军事科技》栏目 8 人团队的内部工作台,核心模块包括:
|
||||
|
||||
- **TPS**(选题策划系统):查重 + 知识库参考 + 报题单一键生成
|
||||
- **仪表盘**:近 4 期收视成绩 + 未来 6 期排期一目了然
|
||||
- **仪表盘**:近 9 期收视成绩 + 未来 6 期排期一目了然
|
||||
- **知识库**:往期文稿、军报材料、专家库、外拍线索一站式检索
|
||||
- **个人首页**:每位编导有自己的工作台 + 今日热点雷达
|
||||
- **拍摄资源库**:外拍联系人 / 单位 / 线索来源结构化沉淀
|
||||
@@ -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 4–5 | 未开始 |
|
||||
|
||||
---
|
||||
|
||||
@@ -58,15 +59,35 @@
|
||||
|
||||
| 角色 | 谁 / 什么 | 做什么 |
|
||||
|---|---|---|
|
||||
| 制片人 | 刘统(人) | 拍板、产品决策、领域知识 |
|
||||
| 制片人 | 刘通(人) | 拍板、产品决策、领域知识 |
|
||||
| 高级技术顾问 | Claude Opus 4.7(claude.ai 网页) | 架构指导、关键决策审核 |
|
||||
| 架构师 | Sonnet 4.6 → MiniMax M2.7(Phase 2 起) | Cline Plan 模式,出方案 |
|
||||
| 执行者 | DeepSeek v4 pro | Cline Act 模式,写代码 |
|
||||
| 架构师 | MiniMax M2.7 | Cline Plan 模式,出方案 |
|
||||
| 执行者 | MiniMax M2.7 | Cline Act 模式,写代码 |
|
||||
| 备选执行者 | DeepSeek v4 flash | 仅日常小改(改字段、加字段、改文案) |
|
||||
|
||||
详见 `.clinerules` 第二章。
|
||||
|
||||
---
|
||||
|
||||
## 角色权限表
|
||||
|
||||
> 数据来源:`.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)
|
||||
@@ -153,10 +174,10 @@ uvicorn app.main:app --reload
|
||||
|
||||
| 状况 | 找谁 |
|
||||
|---|---|
|
||||
| 项目方向 / 产品决策 | 刘统(制片人) |
|
||||
| 项目方向 / 产品决策 | 刘通(制片人) |
|
||||
| 关键架构 / 风险 / 卡壳兜底 | Claude Opus 4.7(claude.ai 网页) |
|
||||
| 日常方案 / 改动审查 | Sonnet 4.6(VS Code Cline Plan) |
|
||||
| 写代码 | DeepSeek v4 pro(VS Code Cline Act) |
|
||||
| 日常方案 / 改动审查 | MiniMax M2.7(VS Code Cline Plan) |
|
||||
| 写代码 | MiniMax M2.7(VS Code Cline Act) |
|
||||
| Git 历史搞乱了 / 不小心 commit 了 .env | **立刻找 Claude Opus 4.7,不要自己瞎搞** |
|
||||
|
||||
---
|
||||
@@ -166,6 +187,8 @@ 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"历史表述,不再保留迁移叙事** |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# 复制此文件为 .env,填入真实 API key
|
||||
MIMO_API_KEY=your_key_here
|
||||
DEEPSEEK_API_KEY=
|
||||
@@ -0,0 +1,6 @@
|
||||
.env
|
||||
experiments/*.json
|
||||
output/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.venv/
|
||||
@@ -0,0 +1,417 @@
|
||||
# 看板升级子项目协作主控文件 (CLAUDE.md)
|
||||
|
||||
<!--
|
||||
Claude Code 每次开 session 自动读取。
|
||||
本文件是看板升级子项目(ai-labeling/)的全局指导文件。
|
||||
通用协作原则、主项目技术栈、角色定位、Git 红线等
|
||||
已在上一级 tps-dashboard/CLAUDE.md 中覆盖,不重复。
|
||||
维护原则:增量更新,不整篇重写。
|
||||
-->
|
||||
|
||||
---
|
||||
|
||||
## 🔖 状态栏(每次结束 session 前必须更新这三行)
|
||||
|
||||
- **最后更新**:Claude Opus(顾问)| 2026-07-03
|
||||
- **当前状态一句话**:**子项目原型阶段收工**。交付清单已写入寄存条(`note/寄存条看板升级已外迁.md`),主项目可启动 React 前端实现。L2 打标就绪、L3 原型验证通过、视觉/布局/设计 token 全部定稿。
|
||||
- **下一个动手的人从这里开始**:本子项目暂停,后续工作回到主项目。如需重开子项目(如 L4 诊断 Prompt 撰写、打标迭代),见下方「⏩ 交接备注」
|
||||
|
||||
---
|
||||
|
||||
## 🤖 子项目专属工作约定(READ FIRST)
|
||||
|
||||
- **先读上一级 `tps-dashboard/CLAUDE.md`**,那里有通用协作原则和主项目全局信息。本文件只补看板升级自己的事。
|
||||
- **沙盒自治**:`ai-labeling/` 内改动允许 Cline 直接 Act(改坏重跑就是)。但 `backend/` 任何改动必须切 Plan + Opus 审——这是宪法红线。
|
||||
- **Prompt 内容不让 Cline 创作**——Cline 是执行者不是作者。Prompt 由 Opus/Claude Code 写,Cline 只负责落文件。
|
||||
- **改 ground-truth 必须留 changelog**(谁、何时、改了什么、为什么改),不能悄悄改。
|
||||
- **长文件(Prompt / ground-truth)让制片人手工替换最稳**——Cline 的 `write_to_file` 对长内容会截断,别让它反复 workaround。短文件(<200 行)用 `write_to_file` 整体写,老文件用 `replace_in_file` 局部改。
|
||||
|
||||
---
|
||||
|
||||
## 1. 子项目全貌(核心,所有开发决策的锚点)
|
||||
|
||||
### 1.1 定位与终极目标
|
||||
|
||||
看板升级是 TPS-Dashboard 项目中**最重要、最首页**的工程。
|
||||
|
||||
**终极目标**:在 TPS 工作台上建立**持续运行的收视诊断分析能力**——栏目每做完一期节目,系统自动消化选题单、文稿、收视成绩,按策划周期输出诊断报告,让制片人和全体编导都能看到"我们做得怎么样、问题出在哪、下一步怎么调"。
|
||||
|
||||
**质量标杆**:`example/` 目录下的 PPT(14 页诊断报告)和 Excel(8 个分析 Sheet)是制片人与 Opus 4.8 手工做出的 21 期阶段性分析成果。看板升级要达到**同等分析深度**,并把"一次性手工分析"变成"累积持续自动分析"。
|
||||
|
||||
### 1.2 核心理论框架——"双引擎 + 开篇钩子"
|
||||
|
||||
> 收视 ≈ 题材热度(横轴)× 叙事结构(纵轴)+ 开篇钩子(独立维度,叠加显示)
|
||||
|
||||
- **引擎 1 题材热度(地基)**:热点/新装备/大舰/节日天然有人看。即使叙事并列,热点也能撑住收视(如防空网 0.875、X-76 0.873)。
|
||||
- **引擎 2 叙事结构(放大器)**:有没有贯穿全片的主线悬念。强主线能把冷题材拉到高收视(如仿生潜艇 0.92、组装武器 0.85)。
|
||||
- **开篇钩子(独立维度)**:前 1-2 分钟决定观众换不换台。功能与叙事结构不同(开篇=留住,结构=留住之后的体验),AI 打标也分开判。
|
||||
|
||||
**两条腿至少占一条**:冷题材 + 并列结构 = 必然低分(如枪械图鉴 0.533)。
|
||||
|
||||
**"话题性"已拆解消化**(v2 快照确认):题材稳态热度→横轴承接,时机加成→scene_tags 承接,切口话题感→program_format"横切类比"部分承接。话题性不独立立维。
|
||||
|
||||
这个模型是所有 AI 打标、分析计算、诊断结论的理论基础。开发任何功能都要回到这个公式上来。
|
||||
|
||||
### 1.3 招牌视觉——双引擎象限图(v2 快照定稿)
|
||||
|
||||
| 元素 | 编码 |
|
||||
|------|------|
|
||||
| 横轴 | 题材热度(= 该题材类别窗口内均值 − 栏目均值,零点=栏目自身平均线) |
|
||||
| 纵轴 | 叙事结构 2 档 |
|
||||
| 气泡大小 | 收视份额 |
|
||||
| 气泡颜色 | 收视判定三色(红 `#c0584f` 优秀 / 蓝 `#5b8db8` 达标 / 绿 `#7aa874` 待提升) |
|
||||
| 气泡边框 | 开篇钩子(粗实线=强 / 细实线=中 / 虚线=弱) |
|
||||
| 悬停 tooltip | 节目名 + 期次 + 编导 + 题材 + 份额 + 钩子档 |
|
||||
| 重叠处理 | 横轴 jitter + 垂直 jitter;同点聚集本身就是诊断信号 |
|
||||
|
||||
这张图是整个看板升级的**核心视觉交付物**。制片人已认可视觉方向。
|
||||
|
||||
### 1.4 6 字段完整枚举定义(v2 快照锁定,v3 修订叙事结构)
|
||||
|
||||
**字段一:`program_format` 主题材(单选)** — 节目内容切口,不是播出形态
|
||||
|
||||
| 枚举值 | 含义 |
|
||||
|--------|------|
|
||||
| `装备深解` | 围绕一款/一型具体装备深度剖析 |
|
||||
| `横切类比` | 用某切口(趣味/功能/技术)跨装备类比 |
|
||||
| `历史纵深` | 沿时间线讲装备/战术演进 |
|
||||
| `前沿科技` | 新技术在军事装备上的应用 |
|
||||
| `事件战例` | 围绕某次行动、战役、冲突 |
|
||||
| `人物牵引` | 用人物作叙事入口和牵引线 |
|
||||
|
||||
**字段二:`equipment_domain` 装备域(多选,至少 1 项)**
|
||||
|
||||
`陆战装备` / `海军舰船` / `航空航天` / `导弹与反导` / `信息化` / `后勤保障` / `轻武器` / `跨域` / `其他`
|
||||
|
||||
注:原"枪械"升级为"轻武器";原"空军防空"改为"导弹与反导";"无人作战"从装备域剥离到 tech_tags。
|
||||
|
||||
**字段三:`scene_tags` 场景标签(多选,可空)** — 回答"为什么这周播"
|
||||
|
||||
`节点策划`(纪念日/周年/节庆/装备节点)/ `热点跟进`(突发新闻)/ `装备首发`(新装备亮相)/ `展会`
|
||||
|
||||
注:原"节日策划"从 program_format 移到 scene_tags 并改名"节点策划"。
|
||||
|
||||
**字段四:`tech_tags` 技术线标签(多选,可空)** — 新质新域追踪
|
||||
|
||||
`无人作战` / `智能化`(AI/算法/智能决策)/ `有人无人协同`
|
||||
|
||||
**字段五:`narrative_structure` 叙事结构(单选,2 档)**
|
||||
|
||||
`主线演进` / `并列结构`(v3 从 3 档收为 2 档)
|
||||
|
||||
**字段六:`opening_hook` 开篇钩子强度(单选,3 档)**
|
||||
|
||||
`强`(新闻/反差/画面震撼/悬念)/ `中`(背景铺陈/装备亮相/历史溯源)/ `弱`(直接讲技术/概念/平铺直叙)
|
||||
|
||||
### 1.5 系统四层架构
|
||||
|
||||
| 层 | 名称 | 做什么 | 当前状态 |
|
||||
|----|------|--------|----------|
|
||||
| L1 | **数据沉淀** | 每期节目的收视数据、文稿全文、编导、选题单持续入库 | 主项目已有 episodes 表基础结构 |
|
||||
| L2 | **AI 打标** | 读文稿自动判别:题材分类(4字段)、叙事结构(2档)、开篇钩子(3档) | Prompt 1 v0.3(80%) + Prompt 2 v0.2(100%) + Prompt 3 v0.2(90%),三个均达可用水平 |
|
||||
| L3 | **分析引擎** | 基于双引擎模型,计算走势、编导对比、题材对比、高低样本对照等多维分析 | HTML 原型 v0.1 已出(5 模块),页面布局已定 |
|
||||
| L4 | **诊断报告** | 调用 LLM 生成文字诊断结论 + 改进建议(对标 PPT 第 8-10 页水平) | 位置已定(走势图下方),模拟文字已预览 |
|
||||
|
||||
### 1.6 三层分步交付策略(v1 快照定稿)
|
||||
|
||||
| 层 | 解锁条件 | 交付物 |
|
||||
|----|---------|--------|
|
||||
| 第一层"纯算的" | 只需题材字段(program_format + equipment_domain) | 核心指标卡、走势图(含基础/摸高目标线+滚动3期均值)、阶段对比、题材对比、编导维度 |
|
||||
| 第二层"打标后才能算的" | 叙事结构 + 开篇钩子标完 | **双引擎象限图**(招牌视觉)+ 高低样本下钻 |
|
||||
| 第三层"算不出只能写的" | L3 数据就绪 + LLM API 配通 | AI 季度诊断小结(制片人审改后发布)+ 一句话诊断 + 病因 + 提振方案 |
|
||||
|
||||
**编导视角:能力地图,不是排名榜**(v1 §2.7)。团队视角看"谁擅长哪种结构/哪类题材",把强项做成全组可复用模板。制片人视角可看完整排名+下钻。底层同一套数据,锁可后开。
|
||||
|
||||
### 1.7 分析周期策略
|
||||
|
||||
- **默认轨:季度报告**(Q1/Q2/Q3/Q4)。对标台里考核节奏,每季约 12-13 期,数据量合适。
|
||||
- **灵活轨:自定义周期**。所有栏目人员均可自由选择时间范围查看诊断(如"暑期档 7-8 月"、"最近 10 期")。
|
||||
- 系统按季度自动生成,用户也能手动选范围——两不误。
|
||||
|
||||
### 1.8 受众与权限
|
||||
|
||||
- **不只给制片人看**——全栏目人员(制片人、责编、6 名编导)都能查看分析结果、自由选择周期、调用 LLM 查看诊断。
|
||||
- AI 打标结果仍须制片人审核后才正式落库(设计哲学红线:AI 给方向不给答案)。
|
||||
- 但分析报告(L4)是基于已审核数据生成的,可直接展示。
|
||||
|
||||
### 1.9 样板文件参照表(`example/` 目录)
|
||||
|
||||
| 文件 | 内容 | 对标系统层 |
|
||||
|------|------|-----------|
|
||||
| PPT 第 2-5 页 | 收视走势、阶段对比、月度均值 | L3 分析引擎(走势分析模块) |
|
||||
| PPT 第 6 页 | 题材结构饼图 + 各题材均值 | L3 分析引擎(题材分析模块) |
|
||||
| PPT 第 7 页 | 高分 vs 低分样本对照 | L3 分析引擎(样本对照模块) |
|
||||
| PPT 第 8-10 页 | 5 大病因、双引擎发现、提振方案 | L4 诊断报告(LLM 生成) |
|
||||
| PPT 第 11 页 | 标题"3秒可懂"原则 | L4 诊断报告(建议模块) |
|
||||
| PPT 第 12-13 页 | 选题日历、复盘会议程 | 超出系统范围,供人工参考 |
|
||||
| Excel 8 个 Sheet | 核心指标卡、数据明细、走势图、编导对比、题材对比、阶段对比、高低样本、诊断结论 | L3 分析引擎的完整数据底稿 |
|
||||
|
||||
### 1.10 范围边界
|
||||
|
||||
- ✅ 做:Prompt 撰写与迭代、模型选型基准测试、ground-truth 维护、打标脚本、分析引擎设计与实现、诊断报告生成、前端看板页面
|
||||
- ❌ 不做:选题日历自动编排、复盘会自动组织、标题自动改写(这些是人的工作)
|
||||
- ❌ 不用旧 21 期数据冷启动——文稿和数据已重新整理清洗,拿新数据重新做
|
||||
|
||||
---
|
||||
|
||||
## 2. 技术栈与运行方式(稳定)
|
||||
|
||||
### 2.1 AI 打标沙盒(当前工作区)
|
||||
|
||||
- **语言**:Python 3.x
|
||||
- **依赖锁定**(`scripts/requirements.txt`):
|
||||
- `openai==1.55.0` — **不能升级**,新版自动转 developer 角色,部分兼容接口不支持
|
||||
- `python-dotenv>=1.0.0`
|
||||
- `python-docx>=1.1.0`
|
||||
- **生产模型**:mimo-v2.5-pro(通过 OpenAI SDK 兼容接口调用)
|
||||
- **备选模型**:DeepSeek V4 Pro(命中率 80%,MiMo 中断时备用)
|
||||
- **工作目录**:`E:\tps-dashboard\ai-labeling\`
|
||||
|
||||
### 2.2 诊断报告生成(L4 层,待实现)
|
||||
|
||||
- **多 LLM 共识机制**:配置多个 LLM API(如 MiMo + GLM 5.2 + 其他),分析相同数据、遵守相同规定,取共识结果。目的:弥补单一非顶级模型洞察力不足的问题,用"多家独立一致"提高诊断可信度。
|
||||
- **模型候选**:mimo-v2.5-pro、GLM 5.2(智谱)、其他国产合规模型。具体阵容待选型测试。
|
||||
- **与 AI 打标的区别**:打标是结构化判别(JSON 输出),诊断是开放式分析(自然语言输出)。两者用不同的 Prompt、可能用不同的模型。
|
||||
|
||||
### 2.3 关键目录
|
||||
|
||||
```
|
||||
ai-labeling/
|
||||
├── example/
|
||||
│ ├── 军事科技栏目收视复盘与提振方案_v2.pptx ← 质量标杆(14 页诊断报告)
|
||||
│ └── 军事科技栏目收视诊断分析_v2.xlsx ← 质量标杆(8 个分析 Sheet)
|
||||
├── benchmark-set/
|
||||
│ ├── transcripts/ ← 20 期文稿 .md(基准卷子)
|
||||
│ └── ground-truth.json ← 基准答案 v0.6.0(核心资产,入 git)
|
||||
├── prompts/
|
||||
│ ├── prompt1_classification.md ← 题材分类判别 v0.3(入 git)
|
||||
│ ├── prompt2_narrative.md ← 叙事结构判别 v0.2(已锁定,入 git)
|
||||
│ └── prompt3_opening_hook.md ← 开篇钩子判别 v0.3(入 git)
|
||||
├── experiments/ ← 跑批结果 .json(不入 git)
|
||||
├── scripts/
|
||||
│ ├── import_transcripts.py ← docx → md 解析清洗
|
||||
│ ├── run_labeling.py ← 调模型 + 写结果
|
||||
│ └── summarize.py ← 汇总命中率
|
||||
├── .env ← 真 Key(不入 git)
|
||||
├── .env.example ← Key 占位模板(入 git)
|
||||
├── .gitignore ← 拦 .env / experiments/ / __pycache__/
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### 2.4 常用命令(PowerShell)
|
||||
|
||||
```powershell
|
||||
# 单期打标(narrative / classification / opening_hook)
|
||||
cd E:\tps-dashboard\ai-labeling
|
||||
python scripts/run_labeling.py --ep 1 --model mimo-v2.5-pro --field classification
|
||||
|
||||
# 批量跑 20 期基准
|
||||
python scripts/run_labeling.py --all --model mimo-v2.5-pro --field classification
|
||||
|
||||
# 汇总命中率
|
||||
python scripts/summarize.py --model mimo-v2.5-pro --field classification
|
||||
python scripts/summarize.py --model mimo-v2.5-pro --field narrative
|
||||
python scripts/summarize.py --model mimo-v2.5-pro --field opening_hook
|
||||
```
|
||||
|
||||
### 2.5 模型配置(`scripts/run_labeling.py` 内 MODEL_CONFIG)
|
||||
|
||||
| key | base_url | model_name | env key |
|
||||
|------------------|---------------------------------------|------------------|------------------|
|
||||
| `mimo-v2.5-pro` | `https://api.xiaomimimo.com/v1` | mimo-v2.5-pro | MIMO_API_KEY |
|
||||
| `deepseek-v4-pro`| `https://api.deepseek.com` | deepseek-v4-pro | DEEPSEEK_API_KEY |
|
||||
|
||||
### 2.6 环境变量
|
||||
|
||||
放 `.env`,**不进 git、不写进本文件**。需要:`MIMO_API_KEY`、`DEEPSEEK_API_KEY`。
|
||||
|
||||
---
|
||||
|
||||
## 3. 当前进度(动态,核心交接区)
|
||||
|
||||
- **已完成至**:L3 原型改版完成(双引擎象限图+视觉全面打磨+25期数据全覆盖),GT v0.6.0,Prompt 3 v0.3
|
||||
- **正在做**:无。原型阶段收工,下一步进 React 前端实现
|
||||
- **卡点/待解**:无硬卡点
|
||||
|
||||
---
|
||||
|
||||
## 4. 已完成(只追加,最新在上)
|
||||
|
||||
- [2026-07-01 | Opus] L3 原型改版完成:①双引擎象限图实现(25 期 scatter,横轴=题材热度偏离,纵轴=叙事结构 2 档分类+jitter,气泡大小=份额/颜色=三色判定/边框=钩子强度,四象限标注+读图说明);②视觉打磨(去卡片竖线/去走势折线层/横切类比改紫#9b7eb8/题材数据扩至 25 期/饼柱间距加大);③象限图位置提升至 AI 诊断报告下方(招牌位);④GT v0.6.0(20→25 期,ep021-025 制片人手标);⑤Prompt 3 v0.3(增第 7 条强钩子路径"零门槛切口")。制片人对原型阶段满意,准备进 React。
|
||||
- [2026-06-30 | Opus] L3 原型视觉二审:制片人同时看两版原型(`output/l3_report.html` 浅色版 vs `output/military_tech_2026_l3_premium.html` Gemini 深色版),定调浅色方案不变。从 Gemini 版借鉴 5 项:①卡片左侧 4px 彩色竖线(深绿 `#6b8e6b`)②题材柱图渐变填充+胶囊圆角 ③走势图叠加折线趋势层(柱图保留)④双引擎象限图参考 scatter 框架 ⑤AI 诊断保留竖排干条 markdown 式(不学 Gemini 横排双卡,1100px 宽度下会挤)。模块布局重排:左列上下=季度对比+编导对比,右列通高=题材对比(饼图上+柱图下),解决饼图标签截断问题。
|
||||
- [2026-06-26 | Opus] L3 分析引擎原型 v0.1:Python 脚本读 Excel(25期)+ ground-truth(20期AI标签),计算 5 个模块(核心指标卡/走势图/季度对比/题材对比/编导对比),生成 HTML+ECharts 可视化报告。制片人审定:磨砂玻璃+大倒角+1100px 限宽+暖色渐变底;L4 诊断报告位置定在走势图下方(摘要-详情模式);题材饼图+柱图组合;摸高完成率动态变色。脚本 `scripts/l3_analysis_prototype.py`,输出 `output/l3_report.html`。
|
||||
- [2026-06-26 | Opus] 003_add_ai_labels.sql 迁移已在 milsci_dev 执行:episodes 表 +7 列(program_format / equipment_domain / scene_tags / tech_tags / narrative_structure / opening_hook / ai_label_confidence)+3 索引。COMMENT 因 Windows GBK 编码未写入,不影响功能。Schema(schemas/episode.py)留待 L3 API 时同步更新。
|
||||
- [2026-06-26 | Opus] GT v0.5.0:补完10期narrative_structure标注(MiMo跑批85%命中+制片人审定,ep019从AI判主线演进改为并列结构)。20期6字段全覆盖。Schema加字段方案:003_add_ai_labels.sql(episodes表+7列)+ episode.py Model同步。summarize.py修复无标注期次崩溃。Git大扫除:doco/programs中间产物移出git、.gitignore补全、deliverables/寄存条入库。commit `cdb213f`。
|
||||
- [2026-06-25 | Opus] Prompt 1 迭代 v0.1→v0.3(全对率40%→80%):v0.2加多选字段显著篇幅门槛+从属零件反例+前沿科技vs横切类比拆清+装备深解同类体系测试;v0.3修正跨域规则(传播分类框非军事术语)+训练舰窄类别判定+装备首发校准。ground-truth v0.4.1(ep008补标跨域)。
|
||||
- [2026-06-25 | Opus] Prompt 3 v0.1→v0.2(开篇钩子,命中率50%→90%):v0.1撰写+脚本opening_hook支持;v0.2修复三问题——阅读范围扩至3段(导视+主持人+首段解说)、强判定从紧迫感硬门槛改为6条独立路径、弱判定区分信息性提问vs悬念式提问。ground-truth v0.4.2(20期opening_hook全标注,制片人逐期审定含钩子分析)。summarize.py改为从源GT文件读取。
|
||||
- [2026-06-25 | 制片人] 开篇钩子经验规则:0-1钩≈弱,2钩≈中,3钩及以上≈强(钩子质量也影响,3个轻钩可能仍为中)。
|
||||
- [2026-06-24晚 | Opus] Prompt 1 v0.1 撰写完成(4分类字段:题材/装备域/场景/技术)+ ground-truth v0.4.0(10→20期扩展,旧ep编号重映射,4分类字段全填,制片人逐期审定)+ 脚本升级(ep编号2位→3位、ALL_EPISODES扩至1-20、summarize classification比对逻辑实现)+ MiMo首轮跑批20期(题材75%/装备域70%/场景95%/技术70%/全对40%)。主要错误模式:多选字段过度标注、装备深解/前沿科技与横切类比混淆。
|
||||
- [2026-06-24 | Opus+Cline] ground-truth v0.3.0 结构扩展(4 分类字段骨架预留)+ run_labeling.py / summarize.py 扩展 --field 参数支持 classification。脚本可直接跑 Prompt 1,等 prompt 文件就位即用。
|
||||
- [2026-06-24 | Opus] CLAUDE.md 二次更新:精读 v1-v4 四份续接快照,补入双引擎象限图视觉规格、6 字段完整枚举定义、三层分步交付策略、编导能力地图设计、v1-v4 关键决策。
|
||||
- [2026-06-24 | Opus] 全貌对齐:精读 PPT+Excel 样板、明确四层架构(数据沉淀→AI打标→分析引擎→诊断报告)、确认多 LLM 共识机制方向、确认季度+自定义双轨周期、确认全员可见。CLAUDE.md 从打标沙盒文档升级为全局指导文件。
|
||||
- [2026-06-24 | Opus] 模型配置统一更正:M3 配置移除,生产模型改为 mimo-v2.5-pro,更新 run_labeling.py / summarize.py / CLAUDE.md / README.md / .env.example。
|
||||
- [2026-06-11 | Opus+Cline] AI 打标流水线跑通:Prompt 2 v0.2 锁定、ground-truth v0.2.1 锁定、M3 100% 命中率、模型选型完成。Git commit `38f3728` + `edfb298`。
|
||||
- [2026-06-11 | Opus+Cline] 叙事结构 3 档收为 2 档(主线演进/并列结构)、10 期基准答案集 v0.1 锁定。
|
||||
- [2026-06-11 | Cline] 工作区目录结构搭建、三个脚本(import_transcripts / run_labeling / summarize)编写完成、10 期文稿导入。
|
||||
<!-- 新条目往上加 -->
|
||||
|
||||
---
|
||||
|
||||
## 5. 开发路线图(按层级展开,层内按优先级排序)
|
||||
|
||||
### L2 — AI 打标(当前主战场)
|
||||
|
||||
- [ ] **Prompt 1 继续迭代(可选)** — v0.3 全对率 80%,剩 3 期硬骨头(ep005/011/016:前沿科技节目中装备域从属判定边界)。边际收益递减,可暂停。
|
||||
- [ ] **Prompt 3 继续迭代(可选)** — v0.2 命中率 90%,剩 2 期(ep004 枪王对决强→中:AI复活人物的反差未识别;ep007 巨舰反差中→强:旗语开头过度放大)。可考虑融入"数钩子"辅助判据(0-1钩弱/2钩中/3+钩强)。
|
||||
- [x] **新增 10 期跑 Prompt 2 补 narrative_structure** — GT v0.5.0 已全覆盖(MiMo 85% + 制片人审定)。
|
||||
- [ ] **示例集月度更新机制** — 上线后启用,每月集中替换,不碎片化(吃 prompt 缓存红利)。
|
||||
- [x] Prompt 1 v0.3 迭代完成(80% 全对)
|
||||
- [x] Prompt 2 v0.2 撰写 + 锁定(100%)
|
||||
- [x] Prompt 3 v0.3 撰写完成(90%,增"零门槛切口"路径)
|
||||
- [x] ground-truth v0.6.0(25期6字段全标注)
|
||||
- [x] 模型选型(mimo-v2.5-pro 生产)
|
||||
|
||||
### L1 — 数据沉淀(与主项目交界,需 Opus 审)
|
||||
|
||||
- [x] **episodes schema 加字段** — 003 迁移已执行(2026-06-26),7 列 + 3 索引到位。
|
||||
- [ ] **文稿入库流程** — 每期文稿全文入知识库(主项目 Phase 3 已有知识库基础设施)。
|
||||
- [ ] **收视数据导入** — 真实收视 Excel 批量导入 + 清测试数据。
|
||||
|
||||
### L3 — 分析引擎(原型已出,进入前端实现)
|
||||
|
||||
- [x] **HTML 原型 v0.1** — 5 模块计算验证通过(指标卡/走势/季度/题材/编导),25 期数据全部读入。
|
||||
- [x] **视觉方向定稿** — 磨砂玻璃+大倒角+1100px 限宽+暖色渐变底+三色判定+题材饼图柱图组合。二审追加:卡片左侧竖线、柱图渐变圆角、走势叠加折线。
|
||||
- [x] **页面布局定稿** — 指标卡 → 走势图(含dataZoom滑块) → AI诊断报告(L4,竖排干条) → **双引擎象限图(招牌位)** → 左列(季度对比+编导对比) / 右列通高(题材对比:饼图+柱图)。
|
||||
- [x] **双引擎象限图实现** — 25 期完整数据,ECharts scatter + 双轴 name 端点标注 + 四象限注释 + 读图说明。题材热度=类别均值−栏目均值偏离。横切类比 #9b7eb8(紫)。
|
||||
- [ ] **范围选择器(方案 A 已定)** — 按自然年分页,年内用 ECharts dataZoom 滑块选范围,**页面上所有模块全部联动刷新**(包括核心指标卡的平均份额/最高/最低/达标期数、L4 诊断报告、季度对比、题材对比、编导对比)。历史年份(如 2025)仅展示走势+编导(无 AI 标签的模块标灰)。
|
||||
- [ ] **单期详情页入口** — 走势图柱子可点击跳转到该期节目分析页(独立子项目)。
|
||||
- [ ] **计算逻辑实现(React)** — 将原型 Python 计算逻辑搬进 backend API。
|
||||
- [ ] **API 设计** — 支持按年度+自定义期次范围查询,返回结构化分析数据。
|
||||
|
||||
### L4 — 诊断报告(位置已定,待实现)
|
||||
|
||||
- [x] **位置定稿** — 走势图下方,摘要-详情模式(看板页固定高度干条 + 点击展开详情页全文)。
|
||||
- [ ] **自适应色调 Prompt 机制** — 根据选定范围的达标率/均值动态切换分析角度:优秀期→复盘总结模式;问题期→病因诊断模式;混合→综合对比模式。
|
||||
- [ ] **多 LLM 共识选型测试** — 用样板数据测试 MiMo / GLM 5.2 / 其他模型的诊断分析能力,确定阵容。
|
||||
- [ ] **诊断 Prompt 撰写** — 输入结构化分析数据,输出自然语言诊断结论 + 改进建议。需融入双引擎理论框架。
|
||||
- [ ] **外部因素关联分析** — 结合互联网搜索(同时段竞争节目、社会热点)丰富诊断深度。
|
||||
- [ ] **共识聚合逻辑** — 多家结果如何合并/投票/呈现。
|
||||
- [ ] **UI 置信度三档** — AI 草稿/制片人确认/标题推断一眼可辨。
|
||||
|
||||
### 跨层
|
||||
|
||||
- [x] **横轴量化方法落地** — 题材类别窗口内均值 − 栏目均值(0.7524),已在象限图实现。装备深解+0.144/历史纵深+0.048/前沿科技+0.039/横切类比−0.053/人物牵引−0.174。
|
||||
- [ ] **prompt 缓存月度更新纪律** — AI 打标上线后第 1 个月起执行。
|
||||
|
||||
---
|
||||
|
||||
## 6. 关键决策(跨 session 最易丢)
|
||||
|
||||
### 项目全貌(v1-v4 快照 + 2026-06-24 对齐)
|
||||
|
||||
- [2026-06-24] **看板升级 ≠ AI 打标**。AI 打标只是 L2 层,完整系统是四层:数据沉淀 → AI 打标 → 分析引擎 → 诊断报告。目标是把 PPT+Excel 样板里的阶段性手工分析变成累积持续自动分析。
|
||||
- [2026-06-24] **双引擎模型是理论基础**:收视 = 题材热度 × 叙事结构。所有打标维度、分析计算、诊断结论都围绕这个公式展开。
|
||||
- [2026-06-24] **全员可见**:看板不只给制片人,所有栏目人员都能查看分析、自选周期、调用 LLM 看诊断。
|
||||
- [2026-06-24] **分析周期双轨**:默认季度 + 自定义范围。
|
||||
- [2026-06-24] **不用旧 21 期冷启动**:数据已重新整理清洗,拿新数据重新做。
|
||||
- [2026-06-24] **诊断报告用多 LLM 共识机制**:因合规限制无法用顶级模型(Opus),改为多个国产模型(MiMo / GLM 5.2 / 待定)分析相同数据取共识,弥补单模型洞察力不足。
|
||||
- [2026-06-09] **配色走浅色**:沿用 App 米色底 `#fbf9f1` + 深绿 `#6b8e6b`,不做深色大屏(留 2.0 独立数据大屏蓝本)。
|
||||
- [2026-06-09] **DataEase 不进 1.0**:栈冲突 + 资源不够。图表用 ECharts / @ant-design/charts 在 React 栈直接复刻。
|
||||
- [2026-06-09] **编导板块不做排名**(团队视角走"能力地图"),制片人视角可看完整排名+下钻。
|
||||
- [2026-06-09] **"活的边界"**:实时脉搏(走势/滚动均值)永远滚动;周期复盘(象限/病因/提振)按周期生成。每张图都要有显式时间选择器。
|
||||
- [2026-06-10] **话题性不独立立维**:稳态热度→横轴,时机→scene_tags,切口→横切类比形态。
|
||||
- [2026-06-10] **6 字段枚举锁定**(见 §1.4):program_format 6 值 / equipment_domain 9 值多选 / scene_tags 4 值 / tech_tags 3 值 / narrative_structure 2 档 / opening_hook 3 档。
|
||||
|
||||
### 题材分类判别
|
||||
|
||||
- [2026-06-25] **`跨域`是传播分类框,不是军事术语**:一期节目天上、水里、地上都讲了没法归单一域就标跨域。涉及≥3个具体装备域时额外加选,不需要节目主题是"跨域协同作战"。
|
||||
- [2026-06-25] **同一功能体系/窄类别内的多装备 = 装备深解**:防空体系远/中/近程、三艘训练舰、航母编队舰/机/护航——这些不是横切类比。换装测试要区分"换同类型号"和"换完全不同类别"。
|
||||
- [2026-06-25] **前沿科技 vs 横切类比的核心判据**:串联各案例的角度本身是不是一项新兴技术?是→前沿科技,否→横切类比。
|
||||
|
||||
### 开篇钩子判别
|
||||
|
||||
- [2026-06-25] **"数钩子"经验规则**:0-1钩≈弱,2钩≈中,3钩及以上≈强。钩子质量也影响(3个轻钩可能仍为中)。
|
||||
- [2026-06-25] **紧迫感不是判强的唯一门槛**:画面震撼、情感冲击、认知冲突(反差)、叙事吸引(战场故事)、节日/文化共鸣都是独立的强钩子路径。
|
||||
- [2026-06-25] **阅读范围是前三段**(导视+主持人+首段解说),不是两段。有些节目的钩子在主持人之后才引爆。
|
||||
- [2026-06-25] **信息性提问 vs 悬念式提问**:能用一句话/列表回答的是信息性提问(倾向弱),需要讲故事或揭示原理才能回答的是悬念式提问(至少中)。
|
||||
|
||||
### 叙事结构判别
|
||||
|
||||
- [2026-06-11] **3 档收为 2 档**:`主线演进` / `并列结构`。原"强主线悬念 vs 主线演进"实操中区分不开,合并。
|
||||
- [2026-06-11] **判别心法**:先列全片大章节(3-5 个)→ 做"打乱测试"(调换顺序观众是否懵)。**段内承接 ≠ 全片骨架**——看到"递进""演进"字样先别急,回到大章节之间判断。
|
||||
- [2026-06-11] **关键边界**:命名共性、装备域共性、空间方位共性均不构成主线。
|
||||
|
||||
### 模型选型
|
||||
|
||||
- [2026-06-11] 初始选型 MiniMax M3(Prompt 2 v0.2 命中率 100%)。DeepSeek V4 Pro 80%(备用)、MiMo v2.5 Pro 70%。
|
||||
- [2026-06-24] **生产模型改为 mimo-v2.5-pro**(Cline 统一配 MiMo),M3 配置移除。DeepSeek V4 Pro 保留为备用。
|
||||
- [2026-06-11] **好 Prompt 不为单家定制**——v0.2 同时提升了三家命中率,说明判别规则讲清楚就是最好的优化。
|
||||
|
||||
### ground-truth 版本史
|
||||
|
||||
- v0.1:10 期初始标注
|
||||
- v0.2:改 2 期(ep12 逆袭战局改为并列→后经复议改回主线演进、ep14 X76 确认并列)
|
||||
- v0.2.1:改 1 期(ep12 经制片人复读稿 + 三家模型独立一致,最终定为并列结构)。**边界期用"多家独立一致 + 人工复审"双重证据机制**。
|
||||
- v0.3.0:为 Prompt 1 预留 4 分类字段骨架(null/[]),ep 编号仍为旧制。
|
||||
- v0.4.0:期号重映射(旧ep3-15→新ep001-020按播出日期排序),10→20期扩展,4分类字段全部填入。新增10期暂无narrative_structure/opening_hook。
|
||||
- v0.4.1:ep008马年装备图鉴补标跨域(制片人确认遗漏)。
|
||||
- v0.4.2:新增10期opening_hook标注(制片人逐期审定含钩子分析)。20期6字段覆盖:4分类+opening_hook全填,narrative_structure仍有10期null待补。
|
||||
- v0.5.0:补完10期narrative_structure(MiMo跑批+制片人审定)。20期6字段全覆盖。
|
||||
- **v0.6.0(当前锁定)**:新增5期(ep021-025)补齐Excel全部25期。ep021-023制片人手工标注;ep024/025制片人手工标注(无文稿待后续校准)。Prompt 3 v0.3增第7条强钩子路径"零门槛切口"。25期6字段全覆盖。
|
||||
|
||||
### Prompt 设计原则
|
||||
|
||||
- [2026-06-11] **三个 Prompt 独立**(分类 / 叙事结构 / 开篇钩子),不合并成一个大 Prompt。
|
||||
- [2026-06-11] **示例集"慢慢长大"**:10-30 期全塞 prompt → 30-80 期 pgvector 检索相似 → 80+ 期固定 5 + 动态 5-8。栈里 pgvector 已就位,第二阶段不新增依赖。
|
||||
|
||||
### L3/L4 页面设计决策
|
||||
|
||||
- [2026-06-26] **范围选择器走方案 A(按自然年分页)**:每年目标清零+基础/摸高数值可能变+2025 年以前无文稿无 AI 标签,跨年混算问题太多。年内用 ECharts dataZoom 滑块选范围,所有模块联动刷新。
|
||||
- [2026-06-26] **L4 诊断报告位置:走势图下方**(不是最底部):制片人定调——诊断分析是最有含金量的内容,不能埋在最后。摘要-详情模式:看板页放 3-5 条干条,点击展开详情页看全文。
|
||||
- [2026-06-26] **L4 诊断内容自适应选定范围色调**:全红→复盘总结模式;全绿→病因诊断模式(语气重);混合→综合对比模式。
|
||||
- [2026-06-26] **页面幅面 max-width 1100px 居中**:参考 Crextio 风格,不撑满全屏。
|
||||
- [2026-06-26] **视觉方向**:磨砂玻璃卡片(backdrop-filter: blur)+ 大倒角(16px)+ 暖色渐变底 + 充足留白。参考风格图 `example/页面风格.webp`。
|
||||
- [2026-06-30] **视觉二审(浅色 vs Gemini 深色)**:深色大屏风格否决(压迫感强、英文标题不适合栏目场景),浅色方案定稿。从 Gemini 版吸收:卡片左侧 4px 竖线(`#6b8e6b`)、题材柱图渐变+胶囊圆角、走势图叠加折线趋势层、象限图 scatter 框架。AI 诊断保留竖排干条式(不横排)。
|
||||
- [2026-06-30] **模块布局重排**:季度对比+题材对比原为左右平分两栏,改为——左列上下:季度对比(上)+ 编导对比(下);右列通高:题材对比(饼图上 + 柱图下)。解决 1100px 宽度下饼图标签截断问题。
|
||||
- [2026-06-26] **摸高完成率动态效果**:≥95% 红色微光闪烁;85-94% 蓝色;80-84% 绿色脉冲;<80% 黑色+红色警报光晕。
|
||||
- [2026-06-26] **收视分析看板 ≠ TPS 仪表盘首页**:首页是每天扫一眼的门面(题图/近9期/热点/排播),分析看板是左侧工具栏里的深度分析入口。首页从看板抽关键数字展示。
|
||||
- [2026-06-26] **2026 年收视目标**:基础 0.6448,摸高 0.8989。
|
||||
- [2026-07-01] **双引擎象限图位置:AI 诊断报告正下方**(不在页面最底部)。逻辑链:指标卡→走势→诊断→象限图(印证诊断)→细分对比(下钻)。象限图是招牌视觉,承上启下。
|
||||
- [2026-07-01] **象限图轴标注用 ECharts 双轴 name 机制**:主轴 nameLocation='end'(故事化↑/热门题材→),隐藏副轴 nameLocation='start'(板块化↓/←冷门题材),不手动猜像素。
|
||||
- [2026-07-01] **题材色板**:装备深解 #5b8db8 / 历史纵深 #e8a838 / 前沿科技 #7aa874 / 横切类比 #9b7eb8(紫,原红色与人物牵引太近) / 人物牵引 #d4816b。
|
||||
- [2026-07-01] **象限图纵轴是分类轴非连续刻度**:行内散布仅为避免气泡重叠(jitter),不代表"更故事化"程度。读图说明中已澄清。
|
||||
|
||||
### 技术约束
|
||||
|
||||
- [2026-06-11] MiMo / M3 等 reasoning model 输出带 `<think>...</think>` 前缀。脚本用 `extract_json_from_response` 正则剥掉后再 `json.loads`。**换其他 reasoning model(R1、o1 系列)同样适用。**
|
||||
- [2026-06-11] `openai==1.55.0` 锁版本——新版自动转 developer 角色,部分兼容接口不支持。
|
||||
- [2026-06-11] API 调用不用 `response_format={"type": "json_object"}`——部分模型不完整支持,改为在 Prompt 末尾加输出强约束。
|
||||
- [2026-06-11] `experiments/*.json` 不入 git(结果不固化);`prompts/*.md` + `benchmark-set/` 入 git(核心资产)。
|
||||
|
||||
---
|
||||
|
||||
## 7. ⏩ 交接备注(换人/换工具 0 摩擦续上)
|
||||
|
||||
- **L3 原型改版完成**(2026-07-01):制片人对看板原型阶段满意。原型文件 `output/l3_report.html`(不入 git,output/ 在 .gitignore)。Gemini 深色版 `output/military_tech_2026_l3_premium.html` 仅参考。
|
||||
- **页面布局最终版**:指标卡 → 走势图(含 dataZoom) → AI 诊断报告(L4,竖排干条) → **双引擎象限图(招牌位)** → 左列(季度对比上 + 编导对比下) / 右列通高(题材对比:饼图上 + 柱图下)。范围选择器走方案 A(按自然年分页 + 年内 dataZoom 滑块)。
|
||||
- **双引擎象限图实现细节**:ECharts scatter,25 期数据硬编码在 qData 数组([x, y, 份额, Excel期次, 标题, 钩子, 题材, 编导])。热度偏离值:装备深解+0.144/历史纵深+0.048/前沿科技+0.039/横切类比−0.053/人物牵引−0.174。jitter: 纵向±0.15、横向±0.04。轴标注用双轴 name 机制(主轴 end + 隐藏副轴 start)。
|
||||
- **关键数据**:2026 年基础目标 0.6448,摸高目标 0.8989。25 期完整收视数据在 `example/2026收视update.xlsx`。5 位编导:张彤枫/付天雨/孙逸昊/穆佩弦/左鑫。
|
||||
- **GT v0.6.0**(2026-07-01):25 期 6 字段全覆盖。ep021-023 制片人手标;ep024/025 无文稿待后续校准。
|
||||
- **Prompt 3 v0.3**(2026-07-01):增第 7 条强钩子路径"零门槛切口"(日常概念消除专业门槛)。
|
||||
- **L2 打标已收摊**(2026-06-26):三个 Prompt 均可用(分类80%/结构85%/钩子90%)。
|
||||
- **003 迁移已执行**(2026-06-26):episodes 表 +7 列 +3 索引到位。`schemas/episode.py` 尚未加新字段,留待 L3 API 时同步。
|
||||
- **下一步**:进 React 前端实现(Cline 主力,Claude 审关键逻辑)。优先级:① 走势图+指标卡(纯数据,不依赖 AI 标签)→ ② 题材/季度/编导对比 → ③ 双引擎象限图 → ④ L4 诊断报告(LLM 调用)。
|
||||
- **单期详情页**:走势图柱子可点击跳转(已登记需求入口,独立子项目,暂不实施)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 待确认 / 开放问题(需制片人拍板)
|
||||
|
||||
- [x] ~~Prompt 1 的 4 分类字段枚举最终口径~~ → 已在 v2 快照锁定(见 §1.4)
|
||||
- [ ] 多 LLM 共识机制的模型阵容(MiMo + GLM 5.2 + ?)待选型测试后定
|
||||
- [x] ~~L3 分析引擎是否需要超出 Excel 8 Sheet 的额外分析维度~~ → 暂不增加,先复刻 Excel 能力
|
||||
- [x] ~~范围选择器方案 A vs B~~ → 方案 A(按自然年分页),2025 年以前无文稿无 AI 标签不适合跨年混算
|
||||
- [x] ~~L4 诊断报告放在页面什么位置~~ → 走势图下方(制片人定调:诊断是最有含金量的内容)
|
||||
- [ ] L4 诊断需要哪些外部搜索 API(同时段竞争节目热度等)
|
||||
- [x] ~~下一步优先级:先推 Prompt 1 还是先推 schema 加字段方案?~~ → Prompt 1 先行,已完成 v0.3(80%)
|
||||
- [x] ~~Prompt 1 示例集是否需要扩充?~~ → v0.3 已扩至 7 个示例(含防空反导、硅基大脑等易错案例),全对率 80%
|
||||
@@ -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
|
||||
# 填入真实 MIMO_API_KEY
|
||||
```
|
||||
|
||||
### 3. 导入文稿
|
||||
```bash
|
||||
python scripts/import_transcripts.py
|
||||
```
|
||||
> 将源目录(刘瑞桦收集)的 10 期 docx 文稿解析并清洗,落入 `benchmark-set/transcripts/`。
|
||||
|
||||
### 4. 跑单期打标
|
||||
```bash
|
||||
python scripts/run_labeling.py --ep 4 --model mimo-v2.5-pro
|
||||
```
|
||||
|
||||
## 当前版本
|
||||
- v0.1: Prompt 2(叙事结构)含 3 示例 + 10 期 ground-truth
|
||||
- 生产模型: mimo-v2.5-pro / 备用: DeepSeek V4 Pro
|
||||
|
||||
## 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,291 @@
|
||||
{
|
||||
"version": "v0.6.0",
|
||||
"locked_at": "2026-07-01",
|
||||
"locked_by": "制片人刘通 + Opus 顾问(逐期审定)",
|
||||
"fields_covered": ["narrative_structure", "opening_hook", "program_format", "equipment_domain", "scene_tags", "tech_tags"],
|
||||
"fields_reserved": [],
|
||||
"changelog": "v0.1→v0.2: ep10/ep15 从并列改主线演进。v0.2→v0.2.1: ep12 经三家模型独立一致+制片人复审改为并列结构。v0.2.1→v0.3.0: 预留4分类字段骨架(null/[])。v0.3.0→v0.4.0: 期号重映射(旧ep3-15→新ep001-020按播出日期排序),从10期扩展至20期,4分类字段(program_format/equipment_domain/scene_tags/tech_tags)全部填入,制片人逐期审定。新增10期暂无narrative_structure/opening_hook标注(待Prompt 2/3跑批后补)。v0.4.0→v0.4.1: ep008马年装备图鉴补标跨域(制片人确认遗漏)。v0.4.1→v0.4.2: 新增10期opening_hook标注(制片人逐期审定,含钩子分析):ep004强/ep007中/ep009中/ep014中/ep015中/ep016中/ep017中/ep018强/ep019强/ep020中。v0.4.2→v0.5.0: 新增10期narrative_structure标注(MiMo跑批+制片人审定):ep004并列/ep007并列/ep009并列/ep014并列/ep015主线演进/ep016并列/ep017主线演进/ep018并列/ep019并列(AI判主线演进,制片人改)/ep020并列。20期6字段全覆盖。v0.5.0→v0.6.0: 新增5期(ep021-025)补齐Excel全部25期。ep021形状规律/ep022海战颠覆者/ep023不走寻常路的飞行由制片人手工标注;ep024福建舰/ep025无人加油机制片人手工标注(无文稿未经AI打标,待后续校准)。Prompt 3升级v0.3增加第7条强钩子路径'零门槛切口'。25期6字段全覆盖。",
|
||||
"ep_number_migration": "v0.3.0旧编号→v0.4.0新编号: ep15→001, ep4→002, ep13→003, 新增→004, ep3→005, ep5→006, 新增→007, ep7→008, 新增→009, ep10→010, ep11→011, ep12→012, ep14→013, 新增→014, 新增→015, 新增→016, 新增→017, 新增→018, 新增→019, 新增→020",
|
||||
"episodes": [
|
||||
{
|
||||
"ep": 1,
|
||||
"title": "防空反导大对决",
|
||||
"share": 0.875,
|
||||
"narrative_structure": "主线演进",
|
||||
"opening_hook": "强",
|
||||
"program_format": "装备深解",
|
||||
"equipment_domain": ["导弹与反导"],
|
||||
"scene_tags": ["热点跟进"],
|
||||
"tech_tags": []
|
||||
},
|
||||
{
|
||||
"ep": 2,
|
||||
"title": "潜艇仿生",
|
||||
"share": 0.92,
|
||||
"narrative_structure": "主线演进",
|
||||
"opening_hook": "强",
|
||||
"program_format": "装备深解",
|
||||
"equipment_domain": ["海军舰船"],
|
||||
"scene_tags": [],
|
||||
"tech_tags": []
|
||||
},
|
||||
{
|
||||
"ep": 3,
|
||||
"title": "枪械小尺寸大讲究",
|
||||
"share": 0.533,
|
||||
"narrative_structure": "并列结构",
|
||||
"opening_hook": "中",
|
||||
"program_format": "横切类比",
|
||||
"equipment_domain": ["轻武器"],
|
||||
"scene_tags": [],
|
||||
"tech_tags": []
|
||||
},
|
||||
{
|
||||
"ep": 4,
|
||||
"title": "枪王对决",
|
||||
"share": null,
|
||||
"narrative_structure": "并列结构",
|
||||
"opening_hook": "强",
|
||||
"program_format": "人物牵引",
|
||||
"equipment_domain": ["轻武器"],
|
||||
"scene_tags": [],
|
||||
"tech_tags": []
|
||||
},
|
||||
{
|
||||
"ep": 5,
|
||||
"title": "硅基大脑",
|
||||
"share": 0.85,
|
||||
"narrative_structure": "并列结构",
|
||||
"opening_hook": "中",
|
||||
"program_format": "前沿科技",
|
||||
"equipment_domain": ["航空航天", "陆战装备"],
|
||||
"scene_tags": [],
|
||||
"tech_tags": ["智能化", "有人无人协同"]
|
||||
},
|
||||
{
|
||||
"ep": 6,
|
||||
"title": "舰证不凡",
|
||||
"share": 0.72,
|
||||
"narrative_structure": "并列结构",
|
||||
"opening_hook": "中",
|
||||
"program_format": "横切类比",
|
||||
"equipment_domain": ["海军舰船"],
|
||||
"scene_tags": [],
|
||||
"tech_tags": []
|
||||
},
|
||||
{
|
||||
"ep": 7,
|
||||
"title": "大国巨舰反差",
|
||||
"share": null,
|
||||
"narrative_structure": "并列结构",
|
||||
"opening_hook": "中",
|
||||
"program_format": "横切类比",
|
||||
"equipment_domain": ["海军舰船"],
|
||||
"scene_tags": [],
|
||||
"tech_tags": []
|
||||
},
|
||||
{
|
||||
"ep": 8,
|
||||
"title": "马年军事图鉴",
|
||||
"share": 0.95,
|
||||
"narrative_structure": "并列结构",
|
||||
"opening_hook": "强",
|
||||
"program_format": "横切类比",
|
||||
"equipment_domain": ["海军舰船", "航空航天", "陆战装备", "跨域"],
|
||||
"scene_tags": ["节点策划"],
|
||||
"tech_tags": []
|
||||
},
|
||||
{
|
||||
"ep": 9,
|
||||
"title": "重金造神器",
|
||||
"share": null,
|
||||
"narrative_structure": "并列结构",
|
||||
"opening_hook": "中",
|
||||
"program_format": "横切类比",
|
||||
"equipment_domain": ["海军舰船", "陆战装备", "航空航天", "跨域"],
|
||||
"scene_tags": [],
|
||||
"tech_tags": []
|
||||
},
|
||||
{
|
||||
"ep": 10,
|
||||
"title": "射速决定论",
|
||||
"share": 0.703,
|
||||
"narrative_structure": "主线演进",
|
||||
"opening_hook": "弱",
|
||||
"program_format": "横切类比",
|
||||
"equipment_domain": ["轻武器"],
|
||||
"scene_tags": [],
|
||||
"tech_tags": []
|
||||
},
|
||||
{
|
||||
"ep": 11,
|
||||
"title": "空战演进",
|
||||
"share": 0.859,
|
||||
"narrative_structure": "主线演进",
|
||||
"opening_hook": "中",
|
||||
"program_format": "历史纵深",
|
||||
"equipment_domain": ["航空航天"],
|
||||
"scene_tags": [],
|
||||
"tech_tags": ["无人作战"]
|
||||
},
|
||||
{
|
||||
"ep": 12,
|
||||
"title": "逆袭战局",
|
||||
"share": 0.85,
|
||||
"narrative_structure": "并列结构",
|
||||
"opening_hook": "强",
|
||||
"program_format": "横切类比",
|
||||
"equipment_domain": ["航空航天", "陆战装备"],
|
||||
"scene_tags": [],
|
||||
"tech_tags": []
|
||||
},
|
||||
{
|
||||
"ep": 13,
|
||||
"title": "X-76飞行器",
|
||||
"share": 0.873,
|
||||
"narrative_structure": "并列结构",
|
||||
"opening_hook": "中",
|
||||
"program_format": "装备深解",
|
||||
"equipment_domain": ["航空航天"],
|
||||
"scene_tags": ["装备首发"],
|
||||
"tech_tags": []
|
||||
},
|
||||
{
|
||||
"ep": 14,
|
||||
"title": "启程远航",
|
||||
"share": null,
|
||||
"narrative_structure": "并列结构",
|
||||
"opening_hook": "中",
|
||||
"program_format": "装备深解",
|
||||
"equipment_domain": ["海军舰船"],
|
||||
"scene_tags": ["节点策划"],
|
||||
"tech_tags": []
|
||||
},
|
||||
{
|
||||
"ep": 15,
|
||||
"title": "舰炮暴力美学",
|
||||
"share": null,
|
||||
"narrative_structure": "主线演进",
|
||||
"opening_hook": "中",
|
||||
"program_format": "历史纵深",
|
||||
"equipment_domain": ["海军舰船"],
|
||||
"scene_tags": [],
|
||||
"tech_tags": []
|
||||
},
|
||||
{
|
||||
"ep": 16,
|
||||
"title": "无人舰艇",
|
||||
"share": null,
|
||||
"narrative_structure": "并列结构",
|
||||
"opening_hook": "中",
|
||||
"program_format": "前沿科技",
|
||||
"equipment_domain": ["海军舰船"],
|
||||
"scene_tags": [],
|
||||
"tech_tags": ["无人作战"]
|
||||
},
|
||||
{
|
||||
"ep": 17,
|
||||
"title": "逆袭的鸭翼",
|
||||
"share": null,
|
||||
"narrative_structure": "主线演进",
|
||||
"opening_hook": "中",
|
||||
"program_format": "横切类比",
|
||||
"equipment_domain": ["航空航天"],
|
||||
"scene_tags": ["装备首发"],
|
||||
"tech_tags": []
|
||||
},
|
||||
{
|
||||
"ep": 18,
|
||||
"title": "机枪双雄",
|
||||
"share": null,
|
||||
"narrative_structure": "并列结构",
|
||||
"opening_hook": "强",
|
||||
"program_format": "人物牵引",
|
||||
"equipment_domain": ["轻武器"],
|
||||
"scene_tags": [],
|
||||
"tech_tags": []
|
||||
},
|
||||
{
|
||||
"ep": 19,
|
||||
"title": "玩具总动员",
|
||||
"share": null,
|
||||
"narrative_structure": "并列结构",
|
||||
"opening_hook": "强",
|
||||
"program_format": "横切类比",
|
||||
"equipment_domain": ["陆战装备", "航空航天", "海军舰船", "跨域"],
|
||||
"scene_tags": [],
|
||||
"tech_tags": []
|
||||
},
|
||||
{
|
||||
"ep": 20,
|
||||
"title": "武器颜值洼地",
|
||||
"share": null,
|
||||
"narrative_structure": "并列结构",
|
||||
"opening_hook": "中",
|
||||
"program_format": "横切类比",
|
||||
"equipment_domain": ["航空航天", "海军舰船"],
|
||||
"scene_tags": [],
|
||||
"tech_tags": []
|
||||
},
|
||||
{
|
||||
"ep": 21,
|
||||
"title": "形状规律",
|
||||
"share": 0.89,
|
||||
"narrative_structure": "并列结构",
|
||||
"opening_hook": "中",
|
||||
"program_format": "横切类比",
|
||||
"equipment_domain": ["跨域"],
|
||||
"scene_tags": [],
|
||||
"tech_tags": [],
|
||||
"note": "Excel第1期。钩子路径:零门槛+反差。制片人手工标注(2026-07-01)"
|
||||
},
|
||||
{
|
||||
"ep": 22,
|
||||
"title": "海战颠覆者",
|
||||
"share": 0.92,
|
||||
"narrative_structure": "主线演进",
|
||||
"opening_hook": "中",
|
||||
"program_format": "历史纵深",
|
||||
"equipment_domain": ["海军舰船"],
|
||||
"scene_tags": [],
|
||||
"tech_tags": [],
|
||||
"note": "Excel第2期。钩子路径:战场故事+反差。制片人手工标注(2026-07-01)"
|
||||
},
|
||||
{
|
||||
"ep": 23,
|
||||
"title": "不走寻常路的飞行",
|
||||
"share": 0.86,
|
||||
"narrative_structure": "并列结构",
|
||||
"opening_hook": "强",
|
||||
"program_format": "横切类比",
|
||||
"equipment_domain": ["航空航天"],
|
||||
"scene_tags": [],
|
||||
"tech_tags": [],
|
||||
"note": "Excel第9期。钩子路径:反差+悬念+具体故事。制片人手工标注(2026-07-01)"
|
||||
},
|
||||
{
|
||||
"ep": 24,
|
||||
"title": "福建舰竣工密码",
|
||||
"share": 1.149,
|
||||
"narrative_structure": "主线演进",
|
||||
"opening_hook": "中",
|
||||
"program_format": "装备深解",
|
||||
"equipment_domain": ["海军舰船"],
|
||||
"scene_tags": ["热点跟进"],
|
||||
"tech_tags": [],
|
||||
"note": "Excel第24期。钩子路径:节日+新闻。制片人手工标注(2026-07-01),无文稿未经AI打标,待后续校准"
|
||||
},
|
||||
{
|
||||
"ep": 25,
|
||||
"title": "无人加油机",
|
||||
"share": 0.94,
|
||||
"narrative_structure": "主线演进",
|
||||
"opening_hook": "中",
|
||||
"program_format": "前沿科技",
|
||||
"equipment_domain": ["航空航天"],
|
||||
"scene_tags": ["装备首发"],
|
||||
"tech_tags": ["无人作战"],
|
||||
"note": "Excel第25期。钩子路径:新闻+反差。制片人手工标注(2026-07-01),无文稿未经AI打标,待后续校准"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
---
|
||||
标题: 现代防空反导大对决
|
||||
播出日期: 2026-06-12
|
||||
类型: 节目文稿
|
||||
期号: ep001
|
||||
---
|
||||
|
||||
# 现代防空反导大对决
|
||||
|
||||
|
||||
## 【导视】
|
||||
|
||||
导弹呼啸而过,战机低空穿梭,无人机如蜂群般袭来,这些空中威胁,正不断挑战防御体系的极限,它们是如何被精确拦截的?究竟是什么力量,能在遥远距离精准打击,在近在咫尺时,完成致命一击呢?本期《军事科技》,将带您逐步解析,现代防空网络的复杂机制,亲眼见证,科技与智慧打造的空中屏障,揭秘现代防空体系,御敌于空的终极密码。
|
||||
|
||||
|
||||
## 【主持人1】
|
||||
|
||||
各位观众,你们好,欢迎收看《军事科技》,我是主持人蓝皓,从第二次世界大战时期的,战机轰炸,到冷战时期的导弹威慑,再到如今的无人机,高超声速武器的崛起,空中威胁的形态,不断地迭代,防御体系也在随之进化,而如今的现代化防空系统,早已不再是,单一武器的简单对抗了,而是构建了,以远、中、近衔接,高、中、低覆盖为一体的,立体化防御系统,仿佛一块层层镶嵌的空中盾牌,从千里之外的高空,到咫尺之间的低空,形成了无死角的防护,在与新型空中威胁持续对抗当中。
|
||||
|
||||
|
||||
## 【解说1】
|
||||
|
||||
上演着一场场激烈的,攻与防的较量。2026年2月28日,美以两国协同向伊朗发起军事打击,中东地区顿时战火纷飞,硝烟弥漫。随着冲突不断加剧,各方频繁动用弹道导弹、巡航导弹、高超声速武器,以及自杀式无人机等先进装备,展开密集的高强度空袭。波斯湾上空,随即呈现出一场,攻防交织的终极较量,涉及突破与拦截、毁灭与保卫,这场关乎生死存亡,与战局走向的空中攻防战,彻底爆发。
|
||||
|
||||
远程防空:弹道导弹与反导系统的巅峰对决
|
||||
|
||||
|
||||
## 【解说2】
|
||||
|
||||
在涉及远、中、近程,和高、中、低空的,全方位空战对抗中,弹道导弹,与反导系统之间的对决,无疑是最具战略威慑力,且最检验技术水平的顶峰交锋,中段反导,作为现代战略防空的最高层级,更是左右战场局势的核心要素,这项技术,常被形容为,太空中以子弹击落子弹,它标志着,当今世界军事科技的极致成就,也是战略防空领域,真正的制高点。
|
||||
|
||||
|
||||
## 【动画1】
|
||||
|
||||
一枚中远程弹道导弹的飞行,分为上升段、中段、末段,三者在时空、速度上的巨大差异,直接决定了拦截难度的天壤之别,上升段仅持续3至5分钟,时间窗口稍纵即逝,且需在敌国领土拦截,受部署限制难以实施,拦截成功率极低,末段导弹俯冲至几十公里至几公里高空,速度达马赫数10至20,仅留数秒至十几秒的反应时间,同时面临机动变轨、多弹头诱饵及低空探测盲区,拦截风险高且成功率有限,唯有中段,导弹在100公里以上大气层外无动力滑行,飞行时间长达10至20分钟,轨迹由万有引力决定极度稳定,并未释放弹头与诱饵,反导系统有足够的时间预警、测算轨迹并拦截,采用动能撞击可彻底消除地面威胁,是现代战略防空最理想、效果最佳的“黄金拦截期”,也是各国反导技术研发的核心方向。
|
||||
|
||||
|
||||
## 【专家1】
|
||||
|
||||
虽然说,中段反导拦截,在整个反导作战的流程当中,它是比较容易实现的,或者说,可以针对对方来袭的导弹,进行精准的摧毁,但是最大的问题,就是你的预警系统,需要看得远,反导拦截弹,需要飞得快,打得高、打得远,所以对于技术方面的要求,是比较高的,这就要求,大型的相控阵预警雷达,能够在极远的距离上,针对外大气层飞行的,这种小型、高速移动的目标,能够进行精准的锁定,而且要对它进行持续的跟踪,还要有相关的,这样的一个计算系统,针对它的飞行轨迹进行计算,这样的话,才能够有效地引导反导拦截弹,在外大气层,对它进行精准的这样的一个截杀,而且反导拦截弹的性能,要求非常高,不仅要飞得高,飞得远、飞得快,而且,甚至还要配备有动能的战斗部,要针对处于飞行状态的,弹道导弹,进行精准的撞击,确保它是被彻底摧毁。
|
||||
|
||||
|
||||
## 【解说2】
|
||||
|
||||
3月5日,伊朗伊斯兰革命卫队发布声明称,在“真实承诺-4”行动的第17轮中,伊朗动用高超声速导弹和攻击无人机,成功突破美国“萨德”防御系统,打击了以色列国防部大楼与本-古里安国际机场,并摧毁了7套雷达系统及核心配套设备。同期,美国内部评估报告透露,作为中东关键中段反导装备的以色列“箭式”系统,在拦截伊朗中程弹道导弹时显现重大短板:实际拦截率远未达到原有设计标准。
|
||||
|
||||
|
||||
## 【专家2】
|
||||
|
||||
那么在以色列的国土范围内,有“箭-2” “箭-3” 反导系统,也有美国部署的“萨德”反导系统,但是我们看,仍然有数量相当多的伊朗弹道导弹,成功突防,针对以色列区域内的目标,进行了毁伤,为什么说“箭-2”“箭-3”,以及“萨德”系统,它联起手来,也没有办法针对伊朗的弹道导弹,进行这种百分之百的防范和拦截,我认为,首先是伊朗弹道导弹的技术,有所提升,部分伊朗的弹道导弹,采用了这种高超音速的战斗部,在飞行的末段,会有突然的加速,或者说有急剧的,飞行轨迹的变化,这样的话,“萨德”系统,和“箭-2”“箭-3”反导系统,很难对它进行精准的拦截,此外,伊朗的一些弹道导弹,比如说像“霍拉姆沙赫尔-4”,那么它是一种重型的,这样的一个弹头的设计理念,1.5吨到2吨的战斗部,它可以采用这种集束弹头,这样的一个模式,飞到了目标区上空之后,打出了一个满天星的效果,数十枚子弹头飞向目标区域,所有的防空反导系统,都没有办法对它进行拦截,那么还有另外一种情况,美国的反导作战系统,和以色列的“箭-2” “箭-3” 反导系统,它没有办法进行联网作战,是各打各的,这样的话,在相关的区域内,没有办法进行目标信息的共享,那么也无法形成合力。
|
||||
|
||||
|
||||
## 【解说3】
|
||||
|
||||
2026年3月15日,伊朗伊斯兰革命卫队,在“真实承诺-4”军事行动中,首次实战部署了泥石导弹,与此同时,美国从韩国,紧急调遣了萨德反导系统,以加强该地区的防御,应对伊朗导弹带来的持续威胁。
|
||||
|
||||
|
||||
## 【主持人2】
|
||||
|
||||
若将远程反导,视作千里点穴的区域防空手段,中程防空,便是中空守护网,承担着衔接,远近防御的关键使命,它覆盖数十公里,乃至一百多公里的中高空区域,作为防空体系中的,中坚力量,承担起区域防空重任,护卫城市,军事要地等高价值目标,成为整个防空网络中,承上启下的核心组成部分。
|
||||
|
||||
中程防空:复合威胁下的区域空天防御攻坚战
|
||||
|
||||
|
||||
## 【解说4】
|
||||
|
||||
中程防空,作为现代防空作战系统的关键环节,其防御半径一般在50到200公里之间,拦截高度重点覆盖10至20公里的中高空域。这片空域正是现代战争中空中威胁密集活动的核心地带,敌方战机若要突破外层防御、打击纵深关键目标,必须穿越此区域;巡航导弹虽常利用低空突防,但多数情况下仍需在中高空调整姿态、寻找防御薄弱点;而中近程导弹在俯冲末段同样会经过这一高度区间,其高速特性要求中程防空系统及早介入、精确拦截。可以说,这片中高空域是空中威胁渗向核心目标的必经通道,也是防空体系中承前启后的关键拦截区。
|
||||
|
||||
|
||||
## 【专家3】
|
||||
|
||||
在现代化的防空作战行动当中,中程防空的难度是非常大的,因为很多对方的这种空中的来袭目标,其实它都是在中程防空的任务范围内,目标的属性是非常多的。此外,在复杂的电磁环境的这样的一个状态下,中程的防空体系也可能会出现失灵的情况,因为对方可能会出动电子战飞机针对你的防空作战体系进行压制。此外,有的时候,面对各种各样的目标,数量众多的目标,中程防空系统它的这样的一个火力的密集程度可能会出现不足的情况,比如说一部发射器上它只有数量有限的中程防空导弹,打完了之后,对方的目标连续地持续地袭来,那中程防空体系可能也会面临弹药不足、火力不足的情况。
|
||||
|
||||
|
||||
## 【解说5】
|
||||
|
||||
在近期,美以联合对伊朗的军事行动中,中程导弹防御系统,成为防空体系中的核心作战力量,也是双方攻防较量最为激烈的领域。为巩固中程防御屏障,美国运用“爱国者”防空反导系统,结合天基预警卫星、陆基远程雷达及集成化指挥控制中心,打造了能够覆盖30公里至150公里空域的拦截体系,着重防护其军事基地与国防工业区域。面对伊朗高超音速武器配合无人机集群的非对称组合式打击,此前被美军公开宣称拦截成功率可达80%到97%的“爱国者”系统,实际拦截率已跌至5%以下,系统还频频出现拦截假目标、漏过真实弹头的失误。
|
||||
|
||||
|
||||
## 【专家4】
|
||||
|
||||
“爱国者”曾在海湾战争期间,一战成名,那当时,美军“爱国者”系统,它所拦截的导弹,都是伊拉克萨达姆政权军队,使用的比较老旧的,“飞毛腿”弹道导弹,它的飞行轨迹是一个典型的抛物线,不会有这种,机动变轨的这样的一些动作,所以,只要是探测到了它的飞行轨迹,多发射一些“爱国者”拦截弹,是可以对它进行有效拦截的,但是现代化的弹道导弹,突防能力都很强,飞行轨迹是变化的,甚至可以说是有一些打水漂的,这样的一个飞行轨迹,速度又很快,那么甚至可能会携带,更多的诱饵弹和干扰弹,所以“爱国者”系统,可能根本就看不清,可能也就追不上。
|
||||
|
||||
|
||||
## 【解说6】
|
||||
|
||||
“爱国者”导弹在部分实战场景中,拦截效果未达预期,这一表现,使得美以联合构建的,中程防空反导系统暴露出短板,也让多国对该系统,产生信任危机。
|
||||
|
||||
|
||||
## 【主持人3】
|
||||
|
||||
在现代防空系统中,低空与超低空,是远程及中程防空,存在的短板,受到地球曲率、地形掩蔽等影响,远程雷达难以追踪低空目标,而中程防空导弹,在低空条件下,制导系统易受地面杂波的干扰,致使拦截效能下降,因此,近程防空系统由此诞生,专门用于近距离的防护,弥补了防空网络,在低空领域的不足。
|
||||
|
||||
近程防空:低空突防与最后防线的生死较量
|
||||
|
||||
|
||||
## 【解说7】
|
||||
|
||||
对普通公众而言,近程防空无疑是最常见,且视觉冲击力最强的防空形式。那些在网络短片,新闻报道和战场记录中,常见的导弹腾空而起,尾焰撕裂黑夜,目标在空中爆炸,火光刹那映亮天空的拦截画面,大多源自近程防空系统。这些系统射程较短,但部署便捷,响应迅速,通常在目标接近阵地,城市或重要设施的最后一刻,才发动攻击,因而更易被现场捕捉,媒体记录,并广泛传播。正是由于这种高频曝光,近程防空在许多人心中,几乎成了防空的代名词。然而实际上,近程防空在整体防空网络中,充当的是最终贴身防线的功能,远程和中程防空,无法覆盖的低空,及超低空漏洞。
|
||||
|
||||
|
||||
## 【专家5】
|
||||
|
||||
近程防空作战体系,在实际应用的过程当中,有的时候,可能会成为战场上的奇兵,因为对方的直升机,甚至包括战斗机,在低空飞行的时候,它可能会放松警惕,另外一个来不及反应,所以这个时候,近程防空火力,针对目标进行射击的时候,战斗机很难对它进行规避,有的时候甚至连,发射红外诱饵弹的时间都没有,近程防空也有很大的难度,因为很多近程防空作战体系,它的搜索系统是比较简单的,视野不够宽广,也无法针对远距离的目标,进行识别,所以,有的近程防空作战体系,它本质上来讲是“近视眼”,而且还涉及到一些,地形起伏的问题,如果对方利用这种地形起伏,那么去进行超低空的突防,在相关区域内部署的一些传感器,比如说低空搜索雷达,或者是光学红外传感器,可能没有办法,及时有效地识别目标,那么这些低空突防的,这样的一些空中作战平台,也可能会针对,近程的防空作战体系,包括相关的防空阵地,进行打击和压制。
|
||||
|
||||
|
||||
## 【解说8】
|
||||
|
||||
在近期,美以联合对伊朗的军事行动中,以色列拉斐尔公司研发的“铁穹”近程防空系统,经历了自2011年部署以来最激烈且最具转折性的实战检验。这款以往在冲突中以85%至90%拦截率享誉世界的明星武器,在伊朗“真实承诺-4”行动的饱和攻击下,其卓越声誉荡然无存。以色列国防部3月17日向国会递交的评估报告中指出,面对伊朗连续发动的六十多波攻势,“铁穹”系统的实际拦截率从历史标榜的90%以上骤降至约50%,而在集束弹头密集倾泻与无人机蜂群低空突袭的关键战斗阶段,其拦截成功率更是下滑至30%以下,刷新了服役以来的最低性能纪录。
|
||||
|
||||
|
||||
## 【专家6】
|
||||
|
||||
我们注意到,在此次军事行动的过程当中,“铁穹”系统表现平平,我认为,可能是有两方面的考虑,就是它配套的这种传感器,或者是预警体系,可能是遭到了对方的火力打击,有部分“铁穹”系统,那么针对空中来袭目标,它的反应速度不够快,它的这样的一个情报,和目标引导体系,不够完善,另外一个,就是“铁穹”系统,它所使用的是“塔米尔”拦截弹,这种拦截弹,由于它装备,和使用的数量比较多,所以,它必须要进行成本的控制,它的飞行速度,其实并没有那么快,有的时候,针对高速目标进行打击的时候,可能会出现速度跟不上的情况,现在伊朗主要是使用弹道导弹,针对以色列进行打击,面对高空高速飞行的弹道导弹,或者是,配备有,高超音速战斗部的弹道导弹,“铁穹”系统可能它的拦截范围,拦截的高度也都不足,伊朗也使用了大量的无人机,但是这一次,针对以色列进行后续的反击,伊朗更加注重使用,这种中程的弹道导弹,或者是突防能力很强的弹道导弹,面对这样的一种情况,它就会出现反应速度不够快,包括拦截的范围,拦截的高度,都存在不足的情况。
|
||||
|
||||
|
||||
## 【解说9】
|
||||
|
||||
近程防空系统,作为整个防空架构中,关键的最后屏障,肩负着坚守底线、护卫要害的核心职责,但它并非完美无缺,仍存在明显的短板。其防御范围,通常较为有限,探测与拦截能力,都集中在较小区域,大多只能执行小尺度、重点目标的局部防护任务,无法如同中远程防空系统那样,实现广大空域的大范围、远距离监控与狙击。因此,近程防空系统,不能独自支撑一国、一城,或整个战区的全面防空安全。它更像是一件,贴身的防弹衣,而非是能覆盖全域的保护罩。
|
||||
|
||||
|
||||
## 【主持人 4】
|
||||
|
||||
远程、中程与近程防空系统,相互配合,在现代战争中,构筑了层次分明、分工明确、紧密衔接的全空域、立体防空网络,三者各尽其责,彼此支援、逐层拦截,形成了一套由外至内、由高空到低空、由远及近的周密防御体系,不过,近年多场高强度、军事冲突的实际表现,这套看似牢不可破的防空体系,正持续受到、新兴战场模式的严峻挑战,其传统作战思路和防御能力,正在不断遭受冲击与重塑。
|
||||
|
||||
体系防空:全域防御与精准拦截的新困境
|
||||
|
||||
|
||||
## 【解说10】
|
||||
|
||||
在美以联合对伊朗发动的,大规模军事行动中,可以清晰看到,美军,以色列军队,以及伊朗的防空系统,都遭遇了前所未有的考验,在以往严密的多层防护网络,在实战中屡次被突破,撕裂,甚至彻底崩溃,美军采用了,以“爱国者”和“萨德”反导系统,中远程导弹防御体系,以色列则调动了由“铁穹”,“大卫投石索”,和“箭式”系统组成的,全域防空反导系统,然而,针对高超声速武器穿透,无人机群饱和攻击,复合诱饵欺骗,与强电磁干扰等策略,拦截失败的情况,依然频繁发生。
|
||||
|
||||
|
||||
## 【专家7】
|
||||
|
||||
在此次行动的过程当中,美以的防空反导系统,频频被洞穿,也就是说,伊朗使用了,一系列的突防手段,最终打穿了,美国和以色列的一些,防空反导的保护伞,那么针对相关的区域,构成了更为猛烈的火力打击,我认为,就是存在一些问题,比如说,它系统之间,没有进行有效的联合,存在一些兼容的问题,各自为战的状态下,肯定会出现漏洞,另外一个,伊朗也是采取了一些灵活多变的,这样的一些战术,多种火力 多种模式联合运用,也让美国和以色列的,防空反导系统。
|
||||
|
||||
|
||||
## 【解说10】
|
||||
|
||||
在现代军事冲突中,高超声速武器的穿透,与无人机蜂群的饱和攻击,虽是关键威胁,但更具深远颠覆性的,是战场成本效益失衡,对防空体系带来的,系统性难题。这已超越了,单一装备的性能竞争,演变为从经济原理,持久作战能力,到战略自信的全方位压制,引发了拦截不起,消耗不起,补充不上的三重困局。以伊朗主力攻击武器,“见证者-136” 自杀式无人机为例,这款被戏称作,飞行摩托车的装备,彻底打破了传统军工的,精密制造范式:机身使用民用级玻璃纤维,和泡沫塑料拼装,动力系统,直接借用普通摩托车的,二冲程发动机,导航依靠,民用GPS模块与简易惯性制导,甚至部分核心零件,来自拆解的汽车和家电,从而极大压低了制造成本。凭借民用技术军用化,复杂系统简易化的理念,其批量生产单价,稳定在2万至5万美元之间,某些简化版本,甚至降至1.5万美元,仅相当于高端防空导弹成本的,极小一部分。美以用于对抗的防空武器,却如同天价奢侈品。以色列“铁穹”系统的拦截弹,作为近程防空核心弹药,每枚成本,达5万到10万美元,这意味着,即便成功拦截,防御方的经济损失,也远超进攻方。更甚者,美国“爱国者-3”拦截弹,作为中程防空主力,单枚成本飙升至,400万至500万美元,堪比用超级跑车,撞击普通代步车——无论胜负如何,防御方已在战略层面,沦为输家。
|
||||
|
||||
|
||||
## 【专家8】
|
||||
|
||||
那么在效费比方面,如果说对方在进攻的过程当中,采用了很多廉价的方式,或者说很多省钱的这样的一些技术手段,像“见证者-136”,其实它就是飞行的小摩托,那么它的成本控制得很低,可能要用先进的防空反导系统对它进行拦截,而伊朗的一些地区盟友,包括黎巴嫩真主党,会使用大量的“卡桑”火箭弹,也就是说,更低成本的作坊工厂当中生产出来的这种火箭弹,针对以色列进行打击,那么色列方面使用的是什么,是这种“塔米尔”拦截弹,也就是“铁穹”系统的拦截弹,在这样的 一种相互的消耗战的过程当中,那么位列或者是美国,他所面临的这种昂贵弹药的快速消耗和短缺的情况,也会变得越来越多,也会变得越来越明显。
|
||||
|
||||
|
||||
## 【解说11】
|
||||
|
||||
由于战场上,攻击和防御的效费比例,严重失衡,防空弹药库存告急,形势十分紧迫,3月18日,以色列国防部火速向美国求援,美军立刻出动C-17运输机,紧急空运了近千枚拦截导弹,为以色列的库存注入强心针,与此同时,美军从韩国,亚太到欧洲多个据点,迅速调集防空系统,全力缓解战场上,防空装备和弹药,捉襟见肘的困境。
|
||||
|
||||
|
||||
## 【主持人 5】
|
||||
|
||||
现代防空系统,打造了一个从远程中段反导、近程区域防空,到近程末段拦截的完整链条,构建了远近交错、高低兼顾的立体防御天网,然而,高超声速武器如闪电般突破、无人机群的压制,以及饱和式攻击的浪潮不断袭来,让传统防空体系面临着严峻的考验,攻防成本的天平正在严重倾斜,威胁形势日新月异,如何在提升拦截效果的同时,勒紧成本的缰绳,变得至关重要,让防空网看得远、拦得准、扛得住、耗得起,成为了左右未来防空安全和战争胜负的关键棋局,好了观众朋友们,感谢您持续关注国防军事频道《军事科技》,我们下周同一时间,再见。
|
||||
@@ -0,0 +1,127 @@
|
||||
---
|
||||
标题: 潜艇的仿生之路
|
||||
编导: 穆佩弦
|
||||
播出日期: 2026-01-27
|
||||
类型: 节目文稿
|
||||
期号: ep002
|
||||
---
|
||||
|
||||
# 《潜艇的仿生之路》
|
||||
|
||||
|
||||
## 【导视1】
|
||||
|
||||
在浩瀚海洋深处,数亿年的进化,塑造了无数神奇的攻防利器。如今的人类,用钢铁和电路,重新发明了这一切。当潜艇披上鲨鱼皮消声瓦,当声呐模仿海豚看穿黑暗,当乌贼的墨汁化作潜艇的电子替身,自然早已写好了深海中生存与猎杀的终极教科书。本期《军事科技》,带您见证茫茫深海中潜艇的仿生之路。
|
||||
|
||||
|
||||
## 【演播室主持人1】
|
||||
|
||||
各位观众你们好,欢迎收看《军事科技》,我是蓝皓。在人类出现之前,深海就是地球上最残酷的竞技场。在这里,生存的法则只有两条:隐藏自己,发现敌人。深海依旧是地球上最后的黑暗疆域,潜伏着人类工程学的杰作,也是终极的刺杀兵器——潜艇。提到潜艇,想必大家并不陌生。这样一头深海巨兽,从诞生至今,都借鉴了大自然中的哪些动物的绝招呢?
|
||||
|
||||
从鹦鹉螺到“潜艇压载水舱系统”
|
||||
|
||||
|
||||
## 【二维动画解说1】
|
||||
|
||||
1776年9月7日,历史上第一次潜艇攻击开始了。这次攻击,是由埃兹拉·李来执行。他驾驶着“海龟”号,成功潜到英国战舰“鹰”号的尾部。接下去的工作,就是用钻头在敌舰上穿孔,以便固定炸药包。他打钻的地方,正好是一块金属板。半个小时之后,他仍然没有钻透敌舰,只好上浮返回。虽然“海龟”号,没有成功取得战果,但它拉开了潜艇实战的序幕。从此,人类的战场,也随之从陆地、水面,发展到了水下。“海龟”号,也凭借与现代潜艇相通的设计原理,获得了世界上第一艘军用潜艇的称号,在世界潜艇发展史,占据了一席之地。
|
||||
|
||||
|
||||
## 【三维动画解说1】
|
||||
|
||||
“海龟”号潜艇,顾名思义,它的外形很像海龟。舱内空气可供驾驶员呼吸半个小时。在潜艇的上部,还装有两根通气管,以便上浮时打开,下沉时关闭,从而补充新鲜的空气。“海龟”号潜艇之所以能够成功地上浮下沉,离不开对一种美丽动物的深刻学习。它就是鹦鹉螺。鹦鹉螺,它是一种海洋中的动物,非常地漂亮,外壳非常漂亮。但是你要到鹦鹉螺的内部转一圈的话,它里边有很多的腔,就像我们在陆地上看到的一个一个的小房子。在它的腔里边,人类正是学习了鹦鹉螺的这种功能。在制造潜艇的过程中,就采用了水柜。水柜就是在这个潜艇的艇壳的里边,筑成一个一个一个方块的,就是类似于储水的柜子。那么需要沉底的时候,就向水柜里边注水,水多了,沉了,潜艇就下沉到海底。在亿万年的进化中,鹦鹉螺掌握了一种堪称完美的浮力控制艺术。它的螺旋形外壳,被一系列隔膜分隔成数十个独立的气室,如同一个精密的多舱式潜水器。鹦鹉螺能精确调节每个气室内液体与气体的比例。分泌液体进入气室时下沉,排出液体让位给气体时上浮。这种精妙的生理机制,让它能在数百米的深海与浅层水域之间自如升降,几乎不消耗任何额外能量。人类潜艇的压载水舱系统,正是对这一自然智慧的完美复刻。当潜艇需要下潜时,阀门打开,海水涌入压载舱,取代舱内空气,艇体重量增加。当需要上浮时,高压气体将海水强行排出,艇体重量减轻。1897年,一艘划时代的潜艇悄然下水,这就是约翰·霍兰以自己名字命名的霍兰潜艇。这艘潜艇汇集了众多开创式的设计。以往的潜艇,要么动力不足,要么水下姿态不稳,很难在实战中发挥较大的作用。霍兰首次将压载舱拆分为主舱与辅助舱。主压载舱负责核心沉浮,而辅助舱专攻姿态校准,能通过精准补水。
|
||||
|
||||
|
||||
## 【专家1】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【解说1】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【三维动画解说2】
|
||||
|
||||
维持平衡,精准调节潜深,解决了早期潜艇纵倾失控的难题。这套系统,是现代潜艇操控系统的源头。这一设计,让潜艇真正具备了实战价值,为深海作战开辟了道路。从霍兰潜艇压载舱的手动阀门,到现代潜艇压载舱的计算机控制高压气系统,其核心原理始终未变。正如鹦鹉螺用气体取代液体,潜艇用海水取代空气。这种能力,鹦鹉螺用了数亿年进化完成,而人类只用了不到一百年。
|
||||
|
||||
鲨鱼皮肤与潜艇的外形设计
|
||||
|
||||
|
||||
## 【主持人1】
|
||||
|
||||
在广袤的海洋中,生存,是一场关乎看见,与不被看见的永恒竞赛。为了在这场竞赛中胜出,无数的海洋生物,进化出了一套精妙绝伦的,色彩伪装策略,这便是隐蔽色。像虎鲸,鲨鱼,这样一生与海洋打交道的生物,其背部往往呈现深蓝,深灰色,而其腹部通常是白色,或者是银白色。我们经常能够看到,一只从深海上浮的虎鲸,其白色的胸膛,在来自下方的视线中,几乎消失在明亮的天光里;而一只从上方,发起攻击的鲨鱼,其深色的背部,直到最后一刻,才会被发现。这套系统简单而高效,本质上是一种光学隐身衣。它抹平了生物体,在三维空间中的立体感,极大缩短了,不被天敌或者是猎物,发现的距离。海军工程师很快意识到,这正是潜艇在海洋中生存,所需要的第一层能力。
|
||||
|
||||
|
||||
## 【专家2】
|
||||
|
||||
一战时期,潜艇多活跃于浅海区域,主要面临目视侦察的威胁。各国率先借鉴海洋生物的双色伪装逻辑,尝试用浅灰、绿蓝等颜色涂装艇身,试图让潜艇融入近海的蓝灰色海水。模仿的正是虎鲸腹部适配天光的浅色隐身思路。但早期设计未能兼顾深浅海环境差异,效果寥寥。到了二战,德军和英军则试验新的组合涂装。这些色彩既延续了上部深色、下部浅色的仿生核心,又试图贴合浅海藻类与海床环境。可一旦潜艇下潜深度增加,这套设计便迅速失效。阳光穿透海水层层衰减,深海本就是一片幽暗,浅色系艇身反而成为显眼目标。更致命的是,早期涂料易被海水冲刷氧化,很快褪成乳白色,让仿生伪装彻底失效。真正的转折来自人类对深海环境的深刻洞察。科学家发现,当深度超过200米,阳光完全消失,黑色成为海洋的天然底色。这与鲨鱼深色背部在深海中的隐身逻辑不谋而合。于是纯黑色涂层应运而生。它不仅能让潜艇在幽暗深海中完美融入环境,消除视觉轮廓,更凭借改良配方解决了褪色难题,成为潜艇光学隐身的终极方案。因为我们讲,涂层主要是为了颜色,也就是视觉上不被对手发现。那么很多的动物都有一层保护层。我们在建造潜艇的时候,也是从自然界或仿生学里边学到了如何变化颜色。因为颜色如果涂在潜艇上,是很难随着环境变化的。所以我们就要考虑潜艇它生存在一个什么样的环境里边。所以随着潜艇以后,也就是越来越先进,它下潜的深度越来越深,我们发现黑颜色在下潜以后不容易被发现,可以把自己隐蔽得很好。所以后来的潜艇,也就是在二次大战以后的潜艇,逐步逐步地就往黑色上靠近。而现在,世界各国的潜艇基本上全都涂成了黑颜色。然而在深海,仅仅看不见还远远不够。随着潜艇潜得更深,航行时间更长,真正致命的威胁开始从光线转移到声音。我听到它了,找到它了,目标很近,正右舷083。声音是深海中传播最远、最难隐蔽的信号。一丝多余的噪声就可能暴露位置,招来猎杀。于是,潜艇的隐身逻辑也从颜色升级到了形态。潜艇的外形并非一开始就是今天的样子。很长一段时间里,它更像是一艘能够下潜的水面舰艇。二战时期的潜艇需要频繁浮出水面航行,水下只是暂时藏身之所。因此艇体高耸,结构复杂,水下阻力大,噪声高。真正的转折发生在核动力出现之后。当潜艇可以长期潜航,人类第一次开始只为水下生存来设计它的形状。工程师将目光投向自然界。鲸类、海豚等高速游泳动物几乎都拥有同一种外形:前圆后尖的水滴形。1950年,美国“大青花鱼”号实验潜艇验证了这一答案。水滴型艇体显著降低阻力和流噪,让潜艇更快、更安静,也难被发现。从此,现代潜艇的外形几乎统一。当潜艇的外形已经足够像一条鱼,真正的较量才刚刚开始。接下来要隐藏的不再是轮廓,而是声音本身。生物界,鲨鱼的速度非常快,因为在水中。
|
||||
|
||||
|
||||
## 【解说2】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【专家3】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【解说3】
|
||||
|
||||
它可以以非常高的速度前进。实际上,它除了形体的这种流线型以外,还有就是它的皮肤。为什么我们很多的游泳运动员,所采用的游泳衣或游泳裤,采用的是叫所谓鲨鱼皮呢?就是借鉴了鲨鱼的,它皮肤上的这种V字形。也就是它在前进的时候,水流是顺着箭头的方向向后流的,减少了阻力,减少了就是水与鲨鱼皮肤的摩擦的声音,然后使它快速地前行。既减少了阻力,又降低了声音。所以我们在建造潜艇的时候,也借鉴了这样的情况,或者是说借鉴了这种功能。我们在材料上,采取一些特殊的材料。因为在深海大洋里边,这种材料,就是我们讲的这种消声瓦。因为消声瓦既要保证它在水中,能够把噪音裹在潜艇里边,不被对方探测到,同时它也要像鲨鱼的皮肤一样,有一些减少与水的这种摩擦和阻力,来使这个声音降低。也就是潜艇在高速航行的时候,要降低它与水的摩擦的声音。这样的话可以进一步地,使我们的潜艇,就是像鲨鱼一样,悄然无声地在水中行进。海豚的皮肤,能够分泌一种特殊黏液,使水流紧贴体表滑行,从而减少湍流与阻力。而更为人熟知的,是鲨鱼的皮肤结构。鲨鱼体表,覆盖着无数微小的V型盾鳞。这些看似粗糙的纹理,反而能打散涡流,抑制水流紊乱,大幅降低摩擦阻力。潜艇工程师,从中得到启发。在现代潜艇的消声瓦表面,常会设计出类似的微沟槽,或者细微纹理。这种被称为沟槽面,或仿鲨鱼皮的结构,并不仅仅是为了让潜艇航行得更省力,更重要的是,它能够有效降低水流掠过艇体时产生的流噪声,让潜艇在深海中,变得更安静,也更加致命。海豚在浑浊的海水中,能够准确地找到小鱼,还能分辨出哪条更加肥美。这种能力来自它们独特的水下视觉生物声呐。当海豚发出咔嗒咔嗒的声音时,这些声波,就像无形的探照灯向前发射。声音遇到物体产生回声,海豚通过下颚。
|
||||
|
||||
海豚声呐与潜艇综合声呐系统
|
||||
|
||||
|
||||
## 【解说4】
|
||||
|
||||
接收这些回声,就能在脑海中,描绘出周围环境的立体图像。它们能够听出,藏在泥沙里的两条鱼,哪条更加肥美,前方是岩石还是海草。这种能力,让任何人工声呐都望尘莫及。人类从海豚身上,获得了灵感,发明了声呐技术。
|
||||
|
||||
|
||||
## 【解说5】
|
||||
|
||||
二战爆发后,德国U型潜艇,再次成为海战中的核心力量。在广阔的大西洋上,它们潜伏在航道附近,利用夜色和水下掩护,对盟军运输船队,发动突然袭击。单艘潜艇就足以瘫痪,一整条补给线。对英国而言,这不仅是军事问题,更是生存危机。装备,军需,几乎全部依赖海上运输,而水下的敌人却始终看不见。面对潜艇带来的巨大压力,英国海军被迫改变思路,不再依赖目视搜索,而是尝试在深海中,倾听敌人的存在。以水听器为基础,配合主动声波探测,声呐系统,逐步成型,并率先装备在,驱逐舰和护卫舰上。在护航战例中,第一次实现了对潜艇的,稳定定位,使深水炸弹不再是盲目投掷,而成为有目标的打击。随着反潜技术的成熟,曾经游刃有余的U型潜艇,开始频繁暴露行踪,损失不断增加。大西洋的海战,从潜艇的单方面猎杀,逐渐演变成一场,围绕隐蔽与探测的,技术对抗。而这一技术,正是源自大自然中的,生物声呐。那么说到声呐,就涉及到一个有趣的知识。潜艇在水下航行时,由于水的密度,是空气密度的大约800倍,导致雷达电磁波,通讯电磁波,无法穿透海水。严谨地来说,就是被海水快速地吸收,传不远。因此,声呐就成为了潜艇的主要探测工具。声呐的本质,是一个经过微调的耳朵。它的工作原理,类似于鲸鱼或海豚的回声定位。根据工作类型,声呐可分为,主动声呐和被动声呐。主动声呐模式,需要潜艇主动发射声呐信号,声呐信号接触到目标后会反射回来。
|
||||
|
||||
|
||||
## 【专家4】
|
||||
|
||||
再被潜艇捕捉,但在战争中,这不是一个明智的决策。因为这种探索,就像黑夜中打开手电筒,照亮目标的同时,也暴露了自己。而被动声纳模式,是静默航行,利用被动声呐基阵,接收对方的主动声呐探测信号,来确定对手位置。被动声纳模式下,潜艇不会暴露目标,隐蔽性好。
|
||||
|
||||
|
||||
## 【三维动画解说3】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【解说6】
|
||||
|
||||
声呐,作为动物界里边,海豚,是有它特异的功能。因为海豚,它是从嘴里,发出不同的声响,然后声响传出去以后,如果撞在礁石上,就是海底的礁石上,会反射回来一个回波。它用嘴巴下面的接收器,去把这个声音接收到,然后判定这是石头还是鱼类。如果它的发出去的声音,撞上的是鱼类,它的回声,是和撞在岩石上的声音不同的。而且它收到的回波,也就是声音传播,回波在水里传的,因为很远,它接收下来就可以分辨出来,这是它捕食的猎物了。那么既然,海豚有这么好的这种功能,也就类似于声呐的功能,人类就向它们学习,就仿制了这种声响,这就是主动声响。也就是当我的潜艇,航行的时候,我不断地通过,我的声呐向外释放声波。当这些声波,放到海洋里边去之后,它会一去不复返。但是如果这些声波,撞击到了,对方的潜艇或者是舰艇,或者是海底的这些礁石,它反射回来的声波是不一样的。这样就凭借我们声呐兵,来收到的回波,判断前面是金属,还是石头还是洋流。这是一艘,弗吉尼亚级攻击核潜艇。潜艇的艇首,布置着大型声呐阵列。在早期批次上,它呈现为球形结构。它始终对前方海域,进行持续探测。通过分析回波的时间与特征,潜艇,能够判断目标的距离,方位,并对水下接触进行初步识别。沿着艇体左右两侧,分布着舷侧声呐阵列。海豚身体两侧的侧线系统,负责监听来自各个方向的,细微声学变化。即使潜艇保持静默状态,不主动发声,这些阵列依然能够捕捉,远处螺旋桨的低频噪声,以及水流扰动形成的异常信号。在潜艇尾部,一条细长的,拖曳声呐阵列被放出。这是弗吉尼亚级,最重要的远程感知手段之一。它远离艇体自身的噪声干扰,将潜艇的声觉,延伸到更远的海域,用于发现极其微弱,距离极远的水下目标。艇舯声呐,舷侧阵列,与拖曳声呐协同工作,使这艘潜艇,即使在完全隐蔽的状态下,也能构建起一幅,连续立体的水下态势图。当生存受到直接威胁时,顶尖的猎手,与最先进的武器,都诉诸同一种策略,制造混乱,隐藏本体。当乌贼遇到危险时,会迅速喷出浓密的墨汁。它并非简单的黑色液体,它在海水中会迅速扩散,形成一个与自身形态相似的,浓密的墨汁伪影。这个深色轮廓能够立即吸引,并且锁定捕食者的,视觉注意力,为真身的逃离,创造关键的时间窗口。潜艇工程师,完美地借鉴了这套。
|
||||
|
||||
乌贼墨汁与声学诱饵
|
||||
|
||||
|
||||
## 【主持人2】
|
||||
|
||||
复杂的求生系统,并将其转化为防御装备:声学诱饵。
|
||||
|
||||
|
||||
## 【解说7】
|
||||
|
||||
潜艇感知到来袭的声自导鱼雷,或者敌方主动声呐锁定时,会从发射管快速射出声学诱饵。它是潜艇的一种高电子替身。声学诱饵通常是一个自航式的水下航行器。被发射后,它会精确模仿母艇的发动机噪声、螺旋桨节拍等独特的声学指纹。还会主动航行,制造出比气幕弹更像一艘真实潜艇的移动声学目标。来袭的智能鱼雷能够区分简单的气泡幕和复杂的潜艇噪声。声学诱饵通过发出更具吸引力的听觉陷阱,引诱鱼雷偏离航道,转而追踪并攻击这个替身。
|
||||
|
||||
|
||||
## 【专家5】
|
||||
|
||||
而这些诱饵的发现,实际上就是根据海洋动物,它一些保护自己的这些方式,来学习的,因为它们要么金蝉脱壳,要么以假乱真。所以我们看,随着人工智能的发现,随着我们无人装备的发现,我们就制造了,不同的这种诱饵,主动的,被动的。总之当发现潜艇,被攻击或被人发觉的时候,就扔出一个假目标,向不同的方向,甚至是相反的方向来行进,让它的鱼雷,让它的追踪的设备,去跟踪假目标,而真正的潜艇,悄悄地就逃生了。控制室,这里是声响室,水中有鱼雷,方位241,高速前进,右满舵,找到它了,目标很近,正右舷083,发射5英寸的反鱼雷诱饵,发射反鱼雷诱饵。这套系统,在实战与对抗演练中,屡建奇功,其核心战术,都与数百万年前的乌贼,如出一辙。乌贼用一团墨汁,换取生存的时间,人类则用电子替身,把这一秒无限放大。当深海的幽灵收起獠牙,当猎杀者的声呐归于沉寂,这片蔚蓝疆域的博弈,却从未停止。从鹦鹉螺的浮力控制,到核潜艇的无限续航,从乌贼的墨汁,到潜艇的电子诱饵,我们见证了一场,跨越数亿年的,军事科技进化。自然用漫长的时光,雕琢出生存的智慧。
|
||||
|
||||
|
||||
## 【解说8】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【主持人3】
|
||||
|
||||
人类用智慧在钢铁中复刻这些法则。这不仅是矛与盾的较量,更是对生命本身最深刻的学习。下一场改变游戏规则的技术革命,或许正在随着某只深海生物的游弋,在黑暗中悄然酝酿。感谢您持续关注国防军事频道《军事科技》,我们下周同一时间再见。
|
||||
@@ -0,0 +1,109 @@
|
||||
---
|
||||
标题: 枪械设计中的小尺寸大讲究
|
||||
编导: 孙逸昊
|
||||
播出日期: 2026-03-31
|
||||
类型: 节目文稿
|
||||
期号: ep003
|
||||
---
|
||||
|
||||
# 枪械设计中的长短智慧
|
||||
|
||||
|
||||
## 【导视1】
|
||||
|
||||
藏于方寸的敌后利器,锋芒毕露的火力先锋,穿梭街巷的近战精灵,主宰阵地的压制王者,同为枪械家族成员,为何长度取舍各不相同?每一寸伸缩,都是对实战需求的精准回应;每一款设计,都凝结着军工领域的深邃考量。《军事科技》带您解码枪械设计背后的长短智慧。
|
||||
|
||||
|
||||
## 【主持人1】
|
||||
|
||||
各位观众你们好,欢迎收看《军事科技》,我是主持人蓝皓。提到枪械,威力、精度,往往是我们关注的焦点,但是,有一个看似基础,却至关重要的设计,始终左右着枪械的实战性能,它就是长度。今天要聊的枪械,每一款的长度都大不相同,有的枪械要极致缩短,甘愿舍弃部分舒适与功能,有的却要刻意加长,坦然放弃部分便携性。首先我们来看手枪。首先出场的是,短到可以塞进火柴盒里的,美国FP-45解放者手枪。我手里的这款模型,是为了让大家看得更加清楚,所以做了放大。据资料显示,它的实际长度只有141毫米,比我手掌的宽度还要短。大家仔细来看它的结构,握把的下半部分是中空的,打开滑动挡板,能够装10发.45 ACP备用弹。瞄准具也非常的简单,只有在枪管的前端,有一个小小的片状准星,在尾端挡板上,有一个切削的简易缺口,不仔细看几乎发现不了。当然,不能说它没有瞄准具,做到了极致的简单。更特别的是,它没有半自动的功能,每打一发,都得手动把尾部的待击块,然后再向左旋转,才能装下一发弹。那么如此短小的设计,却让这款不起眼的小手枪,成为了敌后斗争的特殊利器。
|
||||
|
||||
|
||||
## 【解说1】
|
||||
|
||||
这是一把用来换取更好枪支的武器,一个构思独特、简单,却又精妙绝伦的设计。FP-45,绰号解放者,是一款外形笨拙的手枪,却从未真正投入过实战。解放者算是最酷的未投入使用枪支之一。该枪诞生于二战时期,是针对德国和日本开展的一项秘密心理战行动的产物。其设计理念是制造一种小巧廉价的枪械,让盟军可以成千上万地空投给欧洲和亚洲的抵抗战士。设想让当地人捡到这种枪后,能悄悄接近德国或日本军官,扣动扳机将其击毙,然后夺走对方的枪支。每把FP-45解放者会连同10发.45 ACP弹装在纸盒里,盒内的绘画说明书就算不识字的人也能看懂操作步骤。这些“火柴盒杀手”被大量投放到敌后,抵抗组织成员用它近距离突袭德国哨岗后就随手丢弃,即便被缴获,也能对对方产生极大的心理威慑效果——当成千上万的FP-45被德军发现,他们便会心生疑惑:天知道当地人手里到底还有多少把?它是在二战期间比较特殊的这样的一款手枪,主要是秘密携带,秘密使用。它完全是采用这种单发的这样的一个设计。这个退壳也没有特别的这样的一个设备,必须要单独配一个小木棍,把使用之后的这个弹壳再把它给捅出来,所以操作起来是比较复杂的。但是,解放者这款手枪最大的优势就是结构简单,不需要经过专门的培训,任何人都可以对它进行使用。甚至这个枪管非常的短,连这个膛线都没有,在近距离可以针对人员构成比较大的杀伤。
|
||||
|
||||
|
||||
## 【主持人2】
|
||||
|
||||
如今FP-45,成了收藏家眼中的珍品,它短小的尺寸,不是偷工减料,而是战略需求优先的必然结果,少一毫米,都可能影响空投效率,多一毫米,都可能失去隐蔽性。接下来我们看一看手枪中的,大家伙,它就是沙漠之鹰.50AE,它全长约273毫米,几乎是FP-45的两倍,仅枪管长152毫米,比FP-45的整枪还长11毫米,握在手里能明显感觉到它的分量,它完全放弃了手枪的便携性,所有的设计都为了适配,.50AE大口径弹药的,极致火力。
|
||||
|
||||
|
||||
## 【解说2】
|
||||
|
||||
沙漠之鹰,是当今世界上最知名的,大口径手枪之一,从射击游戏到电影银幕,沙漠之鹰表现的强大威力,让其成为许多人的最爱。20世纪80年代,欧州狩猎运动兴起,面对熊、野猪等大型猛兽,传统9毫米,或.45口径的威力,根本不足以致命。在反恐作战中,警方和特种部队,也需要一种,能击穿轻型防弹衣,车辆挡风玻璃的便携武器。随着.50AE弹药的出现,让这些需求,有了实现的可能,这种弹药的弹头重量,超过15克,动能,是普通9毫米手枪弹的3倍以上,但是,也对枪械设计,提出了严峻挑战。
|
||||
|
||||
|
||||
## 【三维动画1】
|
||||
|
||||
大口径弹药燃烧时,会产生巨大的高温高压,必须有足够长的枪管,才能让火药充分燃烧。沙漠之鹰152毫米的枪管,能让弹头初速,达到每秒402米,比短枪管版本提升15%以上,有效射程,也从50米延伸到100米。随之而来的是巨大后坐力,需要273毫米的枪身,和两千克的重量来平衡。如果枪身过短,射击时枪口会剧烈上跳,甚至可能伤到射手的手腕。生产商还提供,254毫米枪管的加长版本,全长会达到374毫米,但因便携性太差,只有少数狩猎爱好者才会选择。
|
||||
|
||||
|
||||
## 【解说3】
|
||||
|
||||
如今,沙漠之鹰虽未大规模列装军队,却因独特的外形,和强大的威力,成为影视剧中的常客,用极致的长度,诠释了,火力至上的设计理念,也成为枪械文化中,力量象征的代名词,为什么说要把它的枪管,设计得比较长,因为它弹药的尺寸比较大,另外一个本身,在射击的过程当中,为了保证精度,为了保证可靠性,所以它整体的这个(重量),是比较重的,重达两公斤,正常人对它进行持握射击,确实是很费劲儿的,沙漠之鹰它是采用导气结构,这一点和步枪是非常相似,能够保证,它发射大威力的弹药,但是在实战的过程当中,要进行短时间内的快速射击,这个就很难打得准,因为它的后坐力,是比较大的,所以从它的设计理念来看,它更像是靶场手枪,或者是狩猎手枪,在军警部队当中进行使用,首先是不便于携带,另外一个枪体太重了,那么在操作使用的过程中,会带来一系列的麻烦。
|
||||
|
||||
|
||||
## 【主持人3】
|
||||
|
||||
一款手枪能藏进火柴盒,一款堪称手枪巨无霸,FP-45和沙漠之鹰,用132毫米的长度差,精准命中了两种极端需求。它们的设计看似矛盾,却都在各自的场景下,实现了长度与功能的最优匹配。接下来,我们把目光转向冲锋枪家族,看一看它们的长度差,又藏着怎样的战场逻辑?先看我手里的这把PP-2000冲锋枪,这是我见过最“露骨”的一款冲锋枪了。同样,为了让大家看清楚,这款模型也做了一点放大。可以看到,它的枪机前半部分,完全是裸露在外的。根据设计方的官方参数,它的全长仅340毫米,比常见的500毫升矿泉水瓶,就长那么一点,刚好能够放进战术背心的口袋里,单手就能握住操作,兼顾近战火力和一定的有效射程,不会因为追求短尺寸而牺牲核心威力。
|
||||
|
||||
|
||||
## 【解说4】
|
||||
|
||||
PP-2000的设计,源于俄罗斯在城市反恐作战中遇到的实战痛点。20世纪90年代,车臣战争中的巷战让俄军深刻体会到:传统冲锋枪过长的枪身在狭窄的楼道、房间中根本无法灵活转向,士兵在破门后,常常因枪身剐蹭门框而延误举枪时机,甚至被敌方近距离突袭。为了解决这一问题,图拉仪器设计局从1993年开始研发紧凑式冲锋枪,核心目标就是在保证火力的前提下,把长度控制在350毫米以内。为此,设计师采用了多项针对性设计。PP-2000冲锋枪的设计非常小巧精妙,整枪上下两个部分可以快速拆开,整体部件非常少,便于维护保养。PP-2000冲锋枪的枪机前部分暴露在机匣外,并兼作上膛拉机柄,在枪机连杆的前方设有一个铰接手柄,当需要拉动的时候可以扳起向左或向右旋转,然后再拉动连杆进行上膛操作。这种枪机外露的方式,不仅能够减少整枪的长度,还可以让枪机和枪管的一部分直接暴露在外面,有利于整枪的散热。PP-2000冲锋枪配有两种容量的弹匣,一种是20发,另一种是44发,供弹弹匣插在握把内,而在枪尾还有一个弹匣插口,把不用的弹匣插上去,便可以当作枪托来使用。量产型版本则增加了折叠式枪托,这个可选配件也是像弹匣一样插到枪尾上使用。全枪的重量非常低,而且非常的短,特别适合特种部队或者是强力部门的执法人员去使用,可以在近距离的过程当中针对对方进行密集的火力压制。由于它的枪身比较短,其实也适合乘车作战的部队,比如说坦克装甲车的这个车组乘员进行佩戴和使用,作为一种自卫武器。当然了战斗机的飞行员和直升机的机组乘员也可以把它作为一种大威力的自卫武器,因为它的持续的火力压制能力要比一般的手枪要强很多,而且和这种短的冲锋枪相比,它的枪身确实更短,实现了最大程度地减重。
|
||||
|
||||
|
||||
## 【主持人4】
|
||||
|
||||
用340毫米的紧凑长度,加上暴露枪机设计,把巷战灵活做到了极致,短身带来的机动优势,在狭小空间里无可替代。接下来我们看一把长尺寸的冲锋枪,它就是MP40冲锋枪。根据二战时期的德军技术手册记载,它折叠后全枪长630毫米,比前面讲到的PP-2000长近100毫米,当它展开之后,长833毫米。我抵肩握住它,能明显感觉到枪身特别舒展,枪托与肩部的贴合度很好,250毫米的枪管让枪口在射击时跳动很小,在二战的平原和阵地战中特别实用,士兵在冲锋的时候能够稳定抵肩射击,精准压制200米内的敌方目标。
|
||||
|
||||
|
||||
## 【解说5】
|
||||
|
||||
MP40是二战期间产量较大、影响力较广的一款冲锋枪,其因结构相对简单、稳定性强,成为当时多个作战单位的装备。二战前,德军步兵班组的火力核心是毛瑟98k栓动步枪和MG34重机枪,冲锋枪仅作为辅助武器。第二次世界大战爆发后,纳粹德军强调快速推进、近距离火力压制的理念,这就需要一种能伴随步兵冲锋、兼顾射程与精度的武器。而传统冲锋枪,如MP18,尺寸虽短,但枪管仅198毫米,有效射程仅100米,无法满足压制需求,而MP40很好地解决了这一问题。MP40冲锋枪的设计围绕中近距离持续压制展开:250毫米的枪管采用镀铬工艺,能让枪口初速提升至每秒380米,有效射程达到200米,比MP18提升一倍;833毫米的枪身长度,让士兵在抵肩射击时能更好地控制后坐力,配合32发弹匣,能形成持续的火力输出。更值得一提的是,它的生产工艺特别适合战时量产:枪身采用冲压和焊接技术,零件数量比MP18减少40%,一个熟练工人每天能组装10支,到1944年累计生产了120万支。833毫米的长度,适配了二战时期开阔战场的作战需求,也成为了二战中纳粹德军步兵火力支柱的标志性武器之一。MP40是在MP38的基础上改进升级而来的。可以说在二战期间,在德国军队的武器装备当中,它是一款设计比较优秀的冲锋枪。优先装备的是德军的装甲部队和它的空降部队,也就是伞兵。装甲部队和伞兵在使用的过程中,就是要求在近距离针对对方的作战人员短时间内倾泻大量的弹药。所以我们看这个MP40,它的弹匣的长度是比较长,所以能够在近距离,比如说100到150米的这样的一个距离上,针对对方进行这种持续的这样的一个射击。那么采用这种比较长的枪身和枪管,就是要保证射击的精度。所以,在近距离射击的过程当中,MP40它的精准程度也是比较高的。
|
||||
|
||||
|
||||
## 【主持人5】
|
||||
|
||||
从340毫米的紧凑灵活,到833毫米的稳定压制,冲锋枪的长度选择,本质上是对作战空间和战术节奏的精准回应。短有短的便捷,长有长的稳定,这正是枪械设计中长短智慧的核心之一。接下来,我们看一看步枪家族。先来看我手中的这把AUG突击步枪。大家有没有发现,它的外形非常的独特?没有传统步枪的枪托,弹匣直接插在了扳机后方。根据奥地利斯泰尔公司的官方数据,它的全长只有665毫米,比折叠后的MP40冲锋枪长不到40毫米。大家可别被它的短身所迷惑了,它的枪管长度足足有508毫米,和传统M16A4步枪的枪管几乎是一样长的。设计师用无托结构,硬生生在不缩短枪管的前提下,把枪身缩短了将近340毫米,真正实现了短身不短能。
|
||||
|
||||
|
||||
## 【解说6】
|
||||
|
||||
AUG的出现,掀起了步枪设计的一场结构革命。随着城市化进程加快,传统长枪管步枪在城市作战、装甲车内部操作时的弊端日益凸显:在楼宇作战中,过长的枪身导致转向不便,甚至影响战术动作。各国都在寻找一种能兼顾射击精度与机动灵活的步枪,但传统有托结构的设计逻辑是枪管长则枪身长,似乎无法突破这一局限。奥地利斯泰尔公司的设计师大胆创新,提出了无托结构的理念。AUG突击步枪将枪托与机匣融为一体,让射手的肩部直接抵在机匣尾部,这样一来,枪管的长度不再受枪托位置的限制——508毫米的枪管能保证5.56×45毫米弹药的初速达到每秒970米,有效射程600米,与传统步枪持平;而665毫米的全长,让士兵在装甲车内能轻松操作,在楼宇中能快速转向。更创新的是,AUG采用了模块化设计,枪管可以快速更换,能在几分钟内切换成步枪、卡宾枪、轻机枪甚至狙击枪,适配不同作战场景。首先我们看它是大量采用了这种塑料和合成材料,所以全枪可以有效地实现减重。士兵在携带和使用的过程当中,可以最大程度地节省体力。由于采用了这种无托化的设计理念,它全枪的重量和长度是可以进行进一步优化。但是,由于采用无托的设计理念,也可以保证它的枪管比较长,这样的话无论是有效的射击距离和它的这样的一个火力的持续性,其实是达到了一种均衡的状态。
|
||||
|
||||
|
||||
## 【主持人6】
|
||||
|
||||
AUG用无托枪身的设计,真正做到,短小精悍。接下来我们看一看,长家伙。下面要讲到的这款步枪,就是大名鼎鼎的。根据德国黑克勒-科赫公司的官方参数,HK416在配备20英寸枪管之后,全枪长可达1030毫米,比AUG足足长了365毫米。长长的枪身,给可伸缩枪托、全长度的皮卡汀尼导轨和活塞导气系统,留出了适配的空间。大家看它的顶部导轨,能从头到尾加装瞄准镜、激光指示器,护木两侧还能扩展战术握把、手电,完全是为现代特种作战多任务、多环境,量身打造的长枪身设计。
|
||||
|
||||
|
||||
## 【解说7】
|
||||
|
||||
如果你曾接触过枪战类游戏,那么你一定知道,这把突击步枪。在各种枪战游戏中,这把枪的性能排名,都是稳居前列。近几年爆火的吃鸡游戏玩家,更是将其称之为“吃鸡神器”。在游戏中,只要能装备上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击毙本·拉登。当时,队员在狭小房间内完成近距离突袭,后续撤离时,又在开阔地带用其压制可能的追兵,证明其能同时适配近距离突袭和中距离压制双重需求。如今,被三十多个国家的特种部队列装,法国、挪威等国更将其定为全军制式步枪,成为现代步枪的代表之一。和AUG相比,HK416最大的优势就是它的适应性比较好,可以衍生出很多的这种型号。HK416它的皮卡汀尼导轨的这样的一个安装也比较方便,在枪管外面的护套上,它可以安装四条皮卡汀尼导轨。也就是说,不仅可以加装战术手电,或者是这种激光的指示系统,还可以加装外挂的榴弹发射器,所以HK416既可以作为普通士兵所使用的突击步枪,也可以作为精准射手步枪,进行这种精准的火力支援。
|
||||
|
||||
|
||||
## 【主持人7】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【解说8】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【主持人8】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【解说9】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【主持人9】
|
||||
|
||||
今天我们展示的每一款枪械,它们没有绝对的优劣,只有是否贴合使命的区别,这正是枪械设计中,最珍贵的长短智慧。长度从来不是孤立的数字,而是设计师平衡使命需求、核心功能、使用环境的结果,每一寸增减,都凝结着对战场的深刻理解,和对功能的极致追求。好了,观众朋友们,感谢您持续关注,国防军事频道《军事科技》,我们下周同一时间,再见。
|
||||
@@ -0,0 +1,299 @@
|
||||
---
|
||||
标题: 枪王对决_卡拉什尼科夫与尤金斯通纳的设计传奇
|
||||
编导: 左鑫
|
||||
播出日期: 2026-05-26
|
||||
类型: 节目文稿
|
||||
期号: ep004
|
||||
---
|
||||
|
||||
# 枪王对决:卡拉什尼科夫与尤金·斯通纳的设计传奇
|
||||
|
||||
|
||||
## 【导视1】
|
||||
|
||||
他们的一生是创造的一生,他们的灵感,或许来自不经意间的触动,他们的成就,却改变了战争形态,甚至改变了历史。《军事科技》,就让我们借助AI时空复原技术,走进两位枪械大师,不为人知的人生故事,触摸藏在钢铁枪械背后的,柔软与坚守。
|
||||
|
||||
|
||||
## 【主持人1】
|
||||
|
||||
各位观众你们好,欢迎收看《军事科技》,我是主持人蓝皓。回望轻武器的发展史,有两个名字,注定被世人永远铭记。一位是从二战硝烟中走来,心怀悲悯的苏联老兵,米哈伊尔·季莫费耶维奇·卡拉什尼科夫。另一位,是深耕航空机械领域,以极致突破传统桎梏的工程师,尤金·斯通纳。他们中一位设计了皮实耐用,风靡全球的AK枪族,一位开创了模块化,小口径先河的枪械体系。那么,是什么样的原因,让两位天才设计师,走上了枪械研发之路呢?在战火洪流,与时代浪潮的裹挟之下,他们的设计理念,又有何区别呢?本期节目,我们依托AI数字技术,穿越时空,回溯历史。循着两位大师的自传手迹,与生前专访,还原真实的枪械宗师人生,解锁两种风格迥异,却同样著名的,工业设计。
|
||||
|
||||
|
||||
## 【解说1】
|
||||
|
||||
2013年12月23日,俄罗斯枪械设计大师,AK-47步枪之父,卡拉什尼科夫,在刚度过94岁生日后病逝。鉴于他对俄罗斯,乃至世界武器设计领域的卓越贡献,俄罗斯为其举行了国葬,总统普京亲自出席。前总理梅德韦杰夫,沉痛表示,卡拉什尼科夫的离世,是整个国家难以弥补的巨大损失。卡拉什尼科夫设计的,AK系列步枪,是全球最广为人知,且使用最普遍的武器。其核心优势在于操作简便,可靠耐用,加之价格低廉,在不少国家,甚至低于一只活鸡的售价,仍被众多国家和地区,广泛使用。这是2009年11月10日留存下的一段,珍贵历史影像。既是卡拉什尼科夫90岁寿辰,也是AK-47,诞生62周年纪念日。俄罗斯政府,在克里姆林宫,为他举办了盛大的生日招待会。时任总统梅德韦杰夫,亲手将代表国家最高荣誉的,俄罗斯英雄金质奖章,授予卡拉什尼科夫,并盛赞他,创造了令全体俄罗斯人,引以为傲的民族品牌。
|
||||
|
||||
|
||||
## 【主持人2】
|
||||
|
||||
卡拉什尼科夫,于1919年11月10日,出生在苏联阿尔泰边疆区的一个普通农家。1930年前后,苏联开展农业集体化运动,并且推行消灭富农政策,他们家被定性为富农,年仅11岁的卡拉什尼科夫,随家人强制流放。1936年毕业后,卡拉什尼科夫在铁路系统找到了工作。正是在这段时间,他逐渐对机械构造,产生了浓厚的兴趣。
|
||||
|
||||
|
||||
## 【解说2】
|
||||
|
||||
少年时期的卡拉什尼科夫,最大的梦想,并非设计枪械,而是制造农用机械。在自传《我的枪中人生》里,他曾写道:“我小时候最大的愿望,就是造出一台更高效的割草机、一台更省力的播种机,让乡亲们不用再顶着烈日在田里劳作,让土地长出更多粮食,让大家都能吃饱饭。”
|
||||
|
||||
|
||||
## 【小剧场1-主持人1】
|
||||
|
||||
卡拉什尼科夫先生,您年少时,一直想要钻研农用机械,当时有没有便利条件呢?
|
||||
|
||||
|
||||
## 【小剧场1-卡1】
|
||||
|
||||
当时是没有的,我的家庭是比较贫穷,家里面的孩子也比较多。你可以看到,远处那几间简陋的房子,当时是我的家。虽然这里的环境优雅,但是相对于来说,比较偏僻闭塞。直到1936年,我才第一次见到了蒸汽机车。读到了七年级的时候,我就被迫辍学了。
|
||||
|
||||
|
||||
## 【小剧场1-主持人2】
|
||||
|
||||
辍学之后,您依旧没有放弃,对机械的研究,对吗?
|
||||
|
||||
|
||||
## 【小剧场1-卡2】
|
||||
|
||||
是的,其实我打心里,非常地喜欢机械。这个时候,我就自己开始学习,机械的相关知识。平时,我也会把别人,丢掉的一些机械零件,捡回来,然后一点一点地拆解,反复去研究里面的结构。我还亲自动手,制作了一些简单的小型农具,小型的机械,一点一点地摸索机械的结构原理,为我后来的设计之路,打下了基础。
|
||||
|
||||
|
||||
## 【解说3】
|
||||
|
||||
1938年,19岁的卡拉什尼科夫应征入伍,成为苏联红军的一名坦克兵。凭借对机械构造的熟悉,他常帮战友修理坦克、枪械等装备,很快便成了部队里的机械能手。然而,平静的生活在1941年被彻底打破。这一年,纳粹德国突然入侵苏联,卫国战争全面爆发,残酷的东线战场彻底改写了卡拉什尼科夫的人生轨迹。作为坦克兵,他随部队奔赴前线,参与了无数次激烈战斗。1942年10月的一场激战中,卡拉什尼科夫所在的坦克被德军炮弹击中,他的肩部受了重伤。更让他痛心的是,他发现许多战友并非败于敌人的炮火,而是栽在了手中劣质且故障频发的制式枪械上。
|
||||
|
||||
|
||||
## 【小剧场1-主持人3】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【小剧场1-卡3】
|
||||
|
||||
当时战场的情况,可以说是糟透了,我们根本没有还手之力,只能硬扛。德军的士兵,普遍装备了自动步枪,火力是又连贯又凶猛,我们前线的战士是,完全被压制的。当时我躺在病床上疗伤的时候,战场上的画面,就不断在我脑海当中回闪,战友们牺牲的场景,枪械卡壳时候的无助,日夜煎熬着我。我每天都在反思,那些英勇的战友,不应该因为武器的不可靠,而白白丧命。
|
||||
|
||||
|
||||
## 【小剧场1-主持人4】
|
||||
|
||||
也就是在那个时候,您下定决心放弃了农机梦想,转而研发枪械的吗?
|
||||
|
||||
|
||||
## 【小剧场1-卡4】
|
||||
|
||||
这个念头一旦产生,就永远没有办法消散。我变得越来越着迷,伤病还没有痊愈,我就找来笔记本、铅笔、橡皮,一笔一画勾勒出,我想象当中步枪的草图。
|
||||
|
||||
|
||||
## 【解说4】
|
||||
|
||||
经过刻苦钻研,卡拉什尼科夫先后设计出冲锋枪和轻机枪,并于1946年设计出一款新式突击步枪,并将其送往国家靶场参加选型竞赛。这款步枪一路过关斩将,最终赢得了竞赛胜利。仅为上士军衔的卡拉什尼科夫一举成名,那一年他27岁。1947年,这款步枪被正式定为苏军制式装备,命名为AK-47突击步枪。AK-47问世之初,苏联军方在生产过程中对其设计严格保密,就连用过的弹壳都必须回收。直到上世纪60年代,它的设计秘密才公之于众。
|
||||
|
||||
|
||||
## 【主持人3】
|
||||
|
||||
在卡拉什尼科夫3岁那年,大洋彼岸的美国,一位天才枪械设计师悄然降生。伴随着他的成长,一场足以颠覆全球轻武器发展格局的行业变革悄然酝酿。这位引领时代风潮,被誉为现代突击步枪之父的传奇巨匠,就是尤金·斯通纳。
|
||||
|
||||
|
||||
## 【解说5】
|
||||
|
||||
1922年11月,尤金·斯通纳出生于美国印第安纳州的一个普通家庭,随家人迁居加利福尼亚州。高中毕业后,因父亲无力供他继续深造,斯通纳只能辍学打工。幸运的是,他被当时的维加飞行公司,即后来的洛克希德·马丁公司录取。
|
||||
|
||||
|
||||
## 【小剧场2-主持人1】
|
||||
|
||||
您刚进入航空企业的时候,从事的是什么工作?
|
||||
|
||||
|
||||
## 【小剧场2-斯1】
|
||||
|
||||
我刚刚入职的时候,还是做一些非常基础的工作,比如说像航空机械装配,飞机结构加工,航空零部件钳工一类的工作。当时我非常知道自己,学历不是很高,所以,一旦有学习的机会,就格外地珍惜。
|
||||
|
||||
|
||||
## 【小剧场2-主持人2】
|
||||
|
||||
那想必过程是非常艰辛的。
|
||||
|
||||
|
||||
## 【小剧场2-斯2】
|
||||
|
||||
对,当时确实非常辛苦。只要有业余的时间,我就会去上夜校,去航空学院旁听专业的课程。当时的经济条件也不是很好,只能一边打工,一边自费求学。为了节省一些通勤时间,能够获得更多的学习机会,我干脆就在学校附近,搭了一间非常简易的房子去居住。
|
||||
|
||||
|
||||
## 【解说6】
|
||||
|
||||
日本海军,突然地攻击了夏威夷群岛。1941年12月7日,日本海军舰队偷袭珍珠港,美国对法西斯国家宣战。获得这一消息的斯通纳,怀揣着一腔热血报名参军,成为了一名海军陆战队队员。由于他在维加飞行公司,学习了许多有关飞行机械的技术,被分配到海军陆战队加利福尼亚埃尔托罗航空站,负责维护螺旋桨战机。斯通纳先后在冲绳岛、菲律宾群岛以及中国北部参与作战。在这个过程中,他接触并熟悉了战斗机上配备的12.7毫米口径机枪和20毫米口径机关炮,为日后投身枪械设计,埋下了伏笔。
|
||||
|
||||
|
||||
## 【小剧场2-斯3】
|
||||
|
||||
战争结束后,我就光荣退伍了,先后在多家知名的航空企业工作,深度参与到各类航空部件的设计与研发当中。
|
||||
|
||||
|
||||
## 【解说7】
|
||||
|
||||
但二战结束后,战争形态发生了彻底变革,堑壕战成为历史,丛林作战、机械化机动与快速反应作战成为主流。为适应新时代作战需求,美军方提出了全新的轻武器研发诉求,需要一款轻量化、高精度、人机适配性好、可多功能改装的新型单兵武器。在这样的背景下,深耕精密机械设计、拥有丰富航空制造经验的斯通纳,承接了这一时代课题,正式跨界投身枪械设计领域。
|
||||
|
||||
|
||||
## 【小剧场2-斯4】
|
||||
|
||||
当时我亲眼目睹了我们的士兵,在作战当中的种种困难。当时给他们列装的武器,非常地笨重烦琐,并且操作也不方便,负重非常大。这让他们的作战灵活性,是大打折扣,完全跟不上现代战争的节奏。身为一名工程师,我非常清楚自己有能力,可以优化这些难题。
|
||||
|
||||
|
||||
## 【小剧场2-主持人3】
|
||||
|
||||
所以说航空技术,成为了您设计M16的独家优势吗?
|
||||
|
||||
|
||||
## 【小剧场2-斯5】
|
||||
|
||||
是的,航空制造领域,追求的是轻量化、高精度,以及高稳定性。这和现代枪械的改良需求,是一致的。所以,我就把航空轻量化材料,气动优化原理,精密结构设计,全部融入到枪械的研发当中。我想打造一款,更轻便、更精准,并且更可靠的单兵武器。我毫不夸张地说,如果没有这么多年,在航空领域积攒的经验,就没有M16系列步枪的诞生。
|
||||
|
||||
|
||||
## 【解说8】
|
||||
|
||||
M16步枪,因枪身通体呈黑色,被人们戏称为“黑枪”。它是世界上第一款列装的小口径军用自动步枪,也是二战后美国换装的第二代制式步枪。其列装标志着军用枪械进入崭新的发展时期,也宣告着一代名枪正式登上历史舞台。
|
||||
|
||||
|
||||
## 【主持人4】
|
||||
|
||||
一位是战火幸存者的自救式设计,一位是工业体系下的专业化革新。成长经历的不同,目的不同,两位枪械大师,会造出截然不同的经典武器。1990年5月16日,应美国史密森学会邀请,在华盛顿,尤金·斯通纳第一次见到了他的老对手,卡拉什尼科夫。他们共处了一个多星期,围绕两款枪械进行了深入交流。那么接下来,我们就和两位枪械大师,从枪械结构、设计逻辑等方面,聊一聊他们的核心设计思想。首先,AK-47采用冲压焊接、简易铆接工艺,枪身线条硬朗,没有精致修饰,木质配件也打磨素朴,质感粗犷。与此同时,为了适应沙尘、严寒等恶劣环境,特意放大零件配合公差,留有较大间隙,枪机晃动明显。正是因为这些特点,它可靠性极强,环境适应性拉满。
|
||||
|
||||
|
||||
## 【小剧场3-主持人1】
|
||||
|
||||
卡拉什尼科夫先生,对于AK-47,外界有人评价它做工粗糙,设计简陋。面对这样的评价,您怎么看?
|
||||
|
||||
|
||||
## 【小剧场3-卡1】
|
||||
|
||||
我从来不认为,它是一把粗糙的枪,反倒是一种非常实用化的设计。因为当初,我设计这把枪的时候,就是为了让他们在前线,能够更好地作战。我们知道士兵们在前线作战,经常会碰到泥泞、严寒等等的恶劣条件。我给他们设计的不是一个工艺品,而是一把能真正拿出去,去作战的武器。
|
||||
|
||||
|
||||
## 【小剧场3-主持人2】
|
||||
|
||||
所以说您的设计都是围绕可靠,实战性开展的吗?
|
||||
|
||||
|
||||
## 【小剧场3-卡2】
|
||||
|
||||
对,没错。我认为一把好的枪,应该是作为战士身体的一部分,而不是一种负担。在战场上,战士是没有时间,去维修这种复杂的枪械的。我简化这个枪械的结构,就是为了减少零件的数量,也是为了在战场上,降低这个故障率。比如说使用这种,高强度结构的钢,就是为了适配,极其恶劣的战场环境。优化这个供弹系统,也是为了在战场上,杜绝这种卡壳的存在。这就是我的设计准则。实践证明,我的设计是正确的。
|
||||
|
||||
|
||||
## 【解说9】
|
||||
|
||||
枪长870毫米,枪管长415毫米,保险、快慢机柄在机匣右侧。全枪主体采用钢木结构,采用容量30发子弹的弧形弹匣供弹。实战状态下,全枪重5.2千克,有效射程300到400米。采用导气式自动原理,回转式闭锁枪机。
|
||||
|
||||
|
||||
## 【小剧场3-卡3】
|
||||
|
||||
其实我的这把枪设计结构,并不算是复杂。当击锤打击针,引燃底火和发射药,子弹就会发射出去。这时候枪管里面,会产生高压高热的这个热气。我在枪管上开了一个小孔,这个时候就可以,推动枪械里面的结构进行滑动。这股力量会推开枪膛,将里面的弹壳弹出去。之后会一颗子弹重新上膛,这样就可以进行连续射击。
|
||||
|
||||
|
||||
## 【小剧场3-主持人3】
|
||||
|
||||
那么斯通纳先生,相较于卡拉什尼科夫的AK-47的皮实,您的M16,它的核心竞争力是什么呢?
|
||||
|
||||
|
||||
## 【小剧场3-斯4】
|
||||
|
||||
在我的想法里面,单纯的皮实耐用,还不能适配到现代战场当中。所以,我在M16设计上只聚焦于三点,一个就是精准度,还有一个是轻量化,最后一个就是模块化,可以适配多元的现代战场。
|
||||
|
||||
|
||||
## 【小剧场3-主持人4】
|
||||
|
||||
您提到的模块化,在当今也是非常创新新颖的设计,在当时您为什么会有这样的想法?
|
||||
|
||||
|
||||
## 【小剧场3-斯5】
|
||||
|
||||
其实,这现代战争场景,是越来越复杂,所以作战的任务灵活多变,单一功能的枪械,始终会被淘汰掉的。所以我想打造的,是一款万能的枪械,同一个枪身,就可以迅速更换其他的配件,比如说切换成步枪,狙击枪,还有轻机枪多种作战形态。
|
||||
|
||||
|
||||
## 【解说10】
|
||||
|
||||
M16轻便且质感高级,加工工艺精密,零件公差控制严格,拼接缝隙规整严密,外表打磨光滑,线条流畅简约。整枪修长,枪长986毫米,枪管长508毫米。全枪采用航空铝合金、工程塑料轻量化材质,搭配钢制核心构件。实战满弹状态,全枪重量只有3.6千克。有效射程400至600米。采用吹气式导气自动原理。
|
||||
|
||||
|
||||
## 【小剧场3-斯6】
|
||||
|
||||
我设计的导气方式,和卡拉什尼科夫先生是完全不同的。我采用的是,吹气式导气原理,由高压气体直接进入,枪栓后方机框内的一个内室,机框带动机枪后退。这使得,单独的活塞室和活塞,就没有什么必要了,从而减少了移动部件的数量。这种原理可以让枪,更加地紧凑,重量也更轻,从而让后坐力也变小,使得射击精度大大地提高。
|
||||
|
||||
|
||||
## 【解说11】
|
||||
|
||||
M16还十分注重枪械的人机工学设计,优化了枪械的重心分布,枪托、握把的角度和形状,贴合士兵的持握习惯,减少长时间射击的疲劳感。与此同时,两款枪也采用截然不同的弹药规格,发射7.62X39毫米中间威力枪弹,弹头质量大,动能充足,枪口初速每秒710米。则使用5.56×45毫米小口径高速枪弹,弹头轻巧修长,初速可达每秒975米。口径的不同,二者杀伤机理差异也显著,AK-47穿透力强劲,子弹击中人体,多为规整贯穿伤,依靠动能造成机体冲击。而M16的小口径子弹,初速极高,进入人体后极易翻滚碎裂,形成严重空腔创伤,毁伤效果更为惨烈。
|
||||
|
||||
|
||||
## 【小剧场3-主持人5】
|
||||
|
||||
1990年,两位首次碰面,而且相互试射了对方设计的步枪,近距离体验了彼此的作品。让我们一起回顾一下那时的画面。那么试射之后,两位对对方的枪械有怎样的评价呢?
|
||||
|
||||
|
||||
## 【小剧场3-卡8】
|
||||
|
||||
斯通纳先生设计的这款枪械,精准度非常高,子弹初速度也很快,设计得十分精巧。不过这把M16,我个人认为是更偏向于,这种比较良好的作战环境,以及经过专业训练的士兵使用。
|
||||
|
||||
|
||||
## 【小剧场3-斯9】
|
||||
|
||||
我是十分认可卡拉什尼科夫先生的设计。因为AK-47这把枪,纯粹就是为战争而生的完美战斗工具。结构简单到任何人都可以直接上手使用,而且在恶劣的环境下,可靠性更是令人惊叹。
|
||||
|
||||
|
||||
## 【小剧场3-主持人6】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【小剧场3-斯通纳10】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【解说12】
|
||||
|
||||
两款经典步枪问世后,在越南战争的战场上迎来了巅峰对决。北越军队大量列装AK-47,凭借极致可靠的性能,在越南热带丛林、泥泞沼泽的恶劣环境中稳定输出,表现出众。而美军列装的M16,则依托高精度与轻量化的先天优势,实现了对目标的精准压制打击。
|
||||
|
||||
|
||||
## 【小剧场4-主持人1】
|
||||
|
||||
没有优劣之分,只有适配之别。不论是AK-47还是M16,都在轻武器发展史上,成为了经典之作。全球轻武器发展的进程,那么我想请问两位,在您的心中,一把好枪的终极标准,到底是什么呢?
|
||||
|
||||
|
||||
## 【小剧场4-卡1】
|
||||
|
||||
在我看来,一把好的枪最重要的标准,就是稳定可靠,是一定要排在第一位的。同时结构一定要简单,是可以进行大批量的生产的,适配于各种各样的战争场景。枪械毕竟不是一个精致的工艺品,在战争之中,多余的花里胡哨的设计,实际上没有任何的价值。可靠、简单、实用,是永远的标准。
|
||||
|
||||
|
||||
## 【小剧场4-主持人2】
|
||||
|
||||
那么斯通纳先生,您怎么认为?
|
||||
|
||||
|
||||
## 【小剧场4-斯2】
|
||||
|
||||
我的评判标准,更加贴合现代战术。优秀的单兵武器,必须贴合人体,贴合战术。轻量化、高精度,人机工学一定是基础,同时还要有持续迭代升级的能力,可以适配不断变革的战争模式。
|
||||
|
||||
|
||||
## 【小剧场4-主持人3】
|
||||
|
||||
二位的设计理念不同,但是都保持着自己的初衷。但不可否认的是,轻武器的滥用,造成了大量的无辜伤亡。接下来我们看一个短片。
|
||||
|
||||
|
||||
## 【解说13】
|
||||
|
||||
2025年11月10日,联合国安理会就小武器和轻武器问题举行公开辩论。其中有代表指出,目前全球枪支已超十亿支,而非法小武器在部分冲突地区造成的平民死亡比例高达三成。尤其是突击步枪问世后,凭借造价低、易上手、火力猛的优势,迅速被多国列装。同时因获取渠道便捷,也遭到武装人员、游击势力与不法分子的大肆使用。而AK-47更是其中极具代表性的枪械。这款武器既助力众多亚非拉国家实现民族独立,也成了极端分子和武装暴徒们的杀人凶器。在有组织的暴力活动中,可以说人人手中都有把AK-47和它的“子嗣”。正因如此,晚年的卡拉什尼科夫深感痛心,曾呼吁订立国际条约,管控含AK-47在内的轻武器跨国交易。
|
||||
|
||||
|
||||
## 【小剧场4-卡3】
|
||||
|
||||
我为我这把枪能在战场上,守护战士的生命,其实是感到骄傲的。我的初衷,从来不是制造一款杀戮的武器。我原本只想设计一些,农用机械用来造福百姓。是战争逼迫我,转向枪械设计的研发。我万万没有想到,我设计的这款枪,会成为极端势力和犯罪分子,杀戮平民的工具。那些倒在枪口下的老人和孩子,是我这辈子,都无法释怀的梦魇。罪孽从来不源自于步枪本身,而源自于扣动扳机的人。可我依旧终生反思,渴望能减少它所带来的伤害。AK-47是守护国土的防御盾牌,而不是侵略的利刃。
|
||||
|
||||
|
||||
## 【小剧场4-斯5】
|
||||
|
||||
技术本身并不会制造杀戮,而决定善恶的,永远是使用它的人。站在设计师的角度,我们的核心职责就是优化装备,弥补缺陷,最大限度地保护士兵的生命安全。武器本身是中性的,并没有善恶之分。战争的正义与否,武器的使用方向,从来不是设计师能够决定的,而这,是国家和政治家们的选择。我们只负责打磨技术,优化装备。
|
||||
|
||||
|
||||
## 【小剧场4-主持人6】
|
||||
|
||||
我相信二位绝非战争的狂热者,只是顺应时代的武器设计者。卡拉什尼科夫先生一生呼吁和平,斯通纳先生精通设计,是一个典型的理性思维。那么可以说,武器是顺应时代而生的,设计是回应实战所需。技术本无善恶,恪守底线,正是两位枪械大师留给世人最深刻的启示。
|
||||
|
||||
|
||||
## 【主持人5】
|
||||
|
||||
好了观众朋友们,感谢您持续关注,国防军事频道《军事科技》,我们下周同一时间,再见。
|
||||
@@ -0,0 +1,129 @@
|
||||
---
|
||||
标题: 当武器装备长出“硅基大脑”
|
||||
编导: 孙逸昊
|
||||
播出日期: 2026-01-20
|
||||
类型: 节目文稿
|
||||
期号: ep005
|
||||
---
|
||||
|
||||
# 《当武器装备长出“硅基大脑”》
|
||||
|
||||
|
||||
## 【导视1】
|
||||
|
||||
当战斗机拥有思考能力,当坦克集群学会自主协同,人工智能正在重塑武器装备的神经中枢。从有人装备改装到无人系统创新,本期《军事科技》带您拆解AI如何为传统装备注入智能基因,为您揭秘全球AI军事化浪潮下的尖端装备。
|
||||
|
||||
|
||||
## 【主持人1】
|
||||
|
||||
各位观众你们好,欢迎收看《军事科技》。2025年4月,伴随第六架改装的F-16,抵达佛罗里达州,埃格林空军基地,美军“毒液”项目,完成了机队组建。这个类似,漫威超级英雄名字的项目,其背后,实则是一项把人工智能,引入空战领域的计划。那么,“毒液”项目究竟是什么呢?这一波操作,到底是噱头,还是真的能够改变,未来空战的格局呢?
|
||||
|
||||
|
||||
## 【解说1】
|
||||
|
||||
2024年,美国启动“毒液”项目,为现役F-16 Block70机型加装AI自主决策系统。这套系统是由洛马公司研发,核心是将传统火控计算机升级为具备机器学习能力的战术中枢,目标是验证有人无人协同作战模式。为了检验人工智能在真实环境中的表现,美国军方选择了广泛服役且技术成熟的F-16战斗机。与F-16改装为QF-16无人靶机不同,“毒液”计划核心目标是评估人工智能在复杂空战环境下的自主作战效能,而非仅仅扮演被动挨打的角色。是在真实的战斗机上安装人工智能驾驶员。截至目前,已经有6架F-16被改装并组成“毒液”测试中队。那么融入了人工智能的这个战斗机,在作战的过程当中,它的这种反应速度也会变得更快,此外信息的处理能力会变得更强。也就是说,美国空军是要利用这个F-16战斗机作为人工智能这种AI飞行员的这样的一个验证平台。那么在实际测试的过程当中,F-16战斗机融入了人工智能,也就是AI系统之后,无论是在对抗性的训练的过程当中,还是在联合执行任务的过程当中,它都表现出了强大的技术优势。
|
||||
|
||||
|
||||
## 【解说2】
|
||||
|
||||
作为全球装备数量最大的四代机,机长15.03米,最大速度马赫数1.8,载弹量7.8吨,配备AN/APG-83有源相控阵雷达。其经典设计历经40年迭代,此次改装并未改变机身结构,而是通过软件定义实现战力倍增。据悉,AI系统使F-16的目标处理能力从同时跟踪四个目标提升至16个,威胁排序耗时从2.3秒压缩至0.1秒。在演习中,装机首次实现双机编队对抗六机集群,交换比达到1:3.2,展现出以少胜多的智能优势。从目前来看就是,“毒液”项目主要是要让美国的这个空中的作战体系变得更加地智能化。不过现在只是在模拟的环境当中,针对人工智能的操控系统进行验证。那么还是按照美军的训练模式,以及美军飞行员的这样的一个操作战斗机的这样的一种样本模式,去进行这种融合的这样的一个作战。所以说,它的学习目标以及样本库和数据库相对而言是比较单一的。那如果说美国空军要把“毒液”计划作为一种实战化的软件系统,和它现有的战斗机进行融合,当它面对对手的时候,可能就会出现不适应,因为都是利用美军的这种训练成果以及相关的这个数据。那么我认为它的指向性目前还是无法满足实战需求,还是处于测试,或者说处于数据搜集和这种样本学习的状态。
|
||||
|
||||
|
||||
## 【主持人2】
|
||||
|
||||
从“毒液”项目中的F-16改装方案,我们已经看到,AI如何让传统战机,突破人类决策的瓶颈,将战术响应,压缩至毫秒级别。当AI从辅助工具,进化为决策中枢,空战正在经历从飞行员主导,到算法博弈的转移。世界各国的科研人员,都在试图给武器装备,装上智慧大脑。接下来,我们就去看一看,这场硅基大脑,与钢铁身躯的融合,正在打开怎样的,战争新维度。
|
||||
|
||||
隔断1:空战革命之自主空战先锋——X-62A VISTA验证机
|
||||
|
||||
|
||||
## 【解说3】
|
||||
|
||||
美军“毒液”计划的背后,离不开一个关键的技术验证平台,X-62A战斗机,与“毒液”项目同步进行。“毒液”项目主要聚焦于人机协作,而X-62A计划则专注于AI独立作战。这架由F-16D改装的飞行实验室,凭借开放式架构和洛马公司的模型跟踪算法,成为破解AI空战控制难题的先锋。大家都知道以前人工智能,它主要是在无人机的这个平台上,去进行融合使用,主要执行的是简单的空中侦察,或者说针对地面目标进行打击的任务。但是和F-16这个平台进行融合之后,美国空军主要是想利用人工智能系统,也就是AI的这样的一个控制系统,和具备很强空战能力的这种主力的战斗机进行结合。那么之所以要进行这样的一个验证,就是要利用美国空军大批量装备的F-16,去验证人工智能飞行员,在近距离空中格斗的这个过程当中它的价值。
|
||||
|
||||
|
||||
## 【解说4】
|
||||
|
||||
2024年,美国空军部长弗兰克·肯德尔,亲自试驾了美国国防部正在研制的X-62A人工智能战斗机。在一小时的飞行过程中,肯德尔全程未干预,所有动作都由AI自主完成。X-62A人工智能战斗机,又被称为VISTA,是可变稳定性飞行模拟器测试飞机的缩写,是由F-16D战机经过大量改装而来。相比常规的F-16D战斗机,X-62A技术验证机最大的不同是,它使用了空战人工智能软件,来操纵飞机的飞控计算机,控制飞机在空中做出各种机动动作。此次,由美国空军部长亲自搭乘的X-62A人工智能战斗机,与另一架由人类飞行员驾驶的战斗机,进行了一场一对一模拟空战演练。最接近的时候,仅相距大约三百米。在飞行结束的采访中,肯德尔表示,经过飞行,他愿意相信这个仍在学习的AI,有能力决定是否在空战中发射武器。人工智能操控的F-16战斗机,它的技术层面具有优势性,那么这主要是人类飞行员的生理承受能力所导致的。我们讲,像F-16的王牌飞行员,在做出大过载机动动作的时候,他会面临6到7个G的这样的一个过载。但是如果说长时间连续地做出大过载的机动动作,战斗机可能还是可以承受的。但是飞行员的生理的这样的一个人体的承受极限,其实是已经到达了红线的这样的一个标准。如果说飞得更快一些,或者说做出了更大过载的这个机动动作,那飞行员可能就晕过去了,就无法正常地执行作战任务。人工智能的F-16不存在这个问题,它打破了人的生理的这种承受的能力,对于战斗机,也就是空中作战平台的限制。
|
||||
|
||||
|
||||
## 【解说5】
|
||||
|
||||
X-62A验证机的出现,标志着AI从空战辅助,迈向自主决策的关键一步。当美国在验证机领域,探索AI边界时,英国选择在六代机“暴风雨”身上,植入智能基因。作为全球空战系统的核心,该机从设计图纸开始,就将AI列为第五大系统,其协同作战管理器,可同时指挥8架忠诚僚机无人机,形成覆盖数百公里的分布式侦察打击网络。
|
||||
|
||||
隔断2:空战革命之集群指挥官——暴风雨战斗机
|
||||
|
||||
|
||||
## 【解说6】
|
||||
|
||||
与X-62A的后天升级不同,“暴风雨”的AI,更像是天生的决策者,重点实现无人机协同与电子战自主化。该机型被定义为具备无人驾驶能力的多任务作战平台,气动结构兼顾隐身性能与超音速巡航需求。能够进行空对空、空对地打击、侦察电子战及战场指挥控制等复合任务。该机采用革命性的模块化架构设计,可通过快速更换业务模块实现功能重构。新武器系统整合了定向能武器与高超音速导弹等前沿装备,配合虚拟信息头盔显控系统,显著提升作战效率。这个项目非常地复杂,目前来看已经是进入到了这种基础的研发阶段,但是困难重重,甚至可以说它面临巨大的风险。
|
||||
|
||||
|
||||
## 【解说7】
|
||||
|
||||
这个“暴风雨”项目,究竟能不能造出真正的,具有实战功能的,这个所谓的第六代战斗机,我个人是要打上一个大大的问号了。因为这个项目是英国主导,我们客观来说,英国在战斗机的,飞行控制系统,包括在发动机方面,它是有独特的技术优势的。大家都知道大名鼎鼎的,这个罗尔斯·罗伊斯发动机公司,它就是属于这个英国的。英国牵头,但是它自身的资金,和技术研发能力,还是存在不足的情况,所以就开始找国际的合作伙伴。目前现在已经确定的是两家,分别是意大利和日本。所以它是,欧洲和亚洲国家联合的,这种战斗机的研发项目。虽然说,英国 意大利和日本,把“暴风雨”定位为第六代战斗机,但是我个人认为,它充其量就是,五代机或者是五代半。因为此前欧洲国家也好,包括日本也好,它都没有独立自主地研制,这个第五代战斗机。所以在隐身战斗机,在这种先进空中作战平台方面,他们都是缺乏经验的。那么现在想要赶鸭子上架,直接从四代机,迈入到六代机的,这样的一个序列当中,我认为这个项目,确实是困难重重。“暴风雨”的核心技术亮点,在于深度融合。人工智能,不仅实现自主飞行决策,还能通过蜂群技术,指挥无人机编队协同作战。“暴风雨”战机的,AI协同作战管理器,可同时控制4种型号,共8架无人机。这些忠诚僚机,既可为战机充当外置传感器,也能作为自杀式诱饵,消耗敌方弹药。我认为,这个“暴风雨”项目当中的,人工智能系统,它更像是一种,辅助的这样的一个,智能化的操控体系。也就是说,在“暴风雨”的这个战斗机的,这个驾驶舱当中,会融入一个AI助手。那么人工智能,它可以利用自身的分析能力,和独特的逻辑运算能力,把最重要的,甚至关乎飞行员性命,战斗机安全的,这样的一个重要信息,进行重点展示,进行提前展示。那么在作战信息的筛选方面,在战场环境的感知能力方面,它有很快的,这样的一个运算能力,也可以进行辅助决策。
|
||||
|
||||
|
||||
## 【主持人3】
|
||||
|
||||
在刚才的介绍当中,我们见证了AI如何将天空变为算力对抗的新战场。从自主生成非常规战术的智能验证机,到指挥无人机群编织察打网络的六代机,人工智能正在把空战的胜负从飞行员的操纵杆,转向AI的代码库。就在天空的智能博弈日趋激烈的同时,陆地战场的钢铁洪流也在经历一场静默的蜕变。让我们把视角从云端拉回地面,看一看AI对地面战场又会带来哪些新的改变。
|
||||
|
||||
隔断3:地面战颠覆之集群大脑——MCGS地面作战系统
|
||||
|
||||
|
||||
## 【解说8】
|
||||
|
||||
MGCS项目于2017年,与未来作战航空系统同步启动。其目标,不仅是替换老旧的德国“豹”2,与法国“勒克莱尔”主战坦克,而是要通过一个多平台、高度集成的系统,彻底改变陆地战争。从目前来看,这个坦克的定位,应该是未来多功能的地面主战系统,已经不叫这个主战坦克了。那么这样的一个装甲作战平台,首先,它的火炮威力进一步地提升。目前有两种方案,就是德国提出来的,换装130毫米口径的滑膛炮。那么由于火炮的口径变得更大,身管变得更长,那么肯定是可以有效地延伸这个装甲作战平台主炮它的射击距离,也可以发射尺寸更大的炮弹,也可以发射个头更大、射程更远的炮射的导弹。所以德国方面给出的这种火力增强方案,就是让滑膛炮的口径进一步地提升。此外,这样的一种装甲作战平台,它的防御能力全方位地提升,不仅会有各种各样的这种附加装甲、复合装甲,包括爆炸反应装甲,包括隔栅装甲,那么也会加装更加先进的这种主动防御系统,针对战场上的比如说无人机,甚至包括慢速来袭的弹药,进行有效的防范。当然了,它的战场感知能力会全方位地提升。那么坦克的车组乘员,可以利用多块的显示屏,有效地感知它周边更广阔区域内的这样的一个作战环境,也有可能会采用无人化的炮塔。炮塔上不仅仅会有主炮,也会有这个遥控武器站,上面可以集成机枪,也可以发射巡飞弹,甚至可以携带无人机。它已经不是传统意义上的主战坦克,是一个地面的综合的装甲火力打击系统。既可以作为主战坦克使用,那么在一定程度上,也可以扮演移动的装甲炮兵,利用比如说曲射火力,包括巡飞弹,针对远距离的目标进行打击。
|
||||
|
||||
|
||||
## 【解说9】
|
||||
|
||||
被设想为一个网络化生态系统,而非单一的装甲车辆。该系统将包括一个配备大口径火炮的主坦克平台,以及一系列配套装备:配备强大反坦克导弹的重型装甲车辆、配备定向能武器的无人机器人车辆、战术无人机和其它创新武器系统。这些模块将通过数字方式相互连接,在一个共享的作战云环境下运行,以最大化作战协同和实时响应能力。那么AI系统和主战坦克进行结合,最大的优势就是它可以实现更好的联网作战,能够把其它作战平台和侦察平台的信息,集成到这个未来地面的主战系统的内部。这样的话,这个坦克的车组乘员就可以了解到更为准确更为精准的这种战场的作战环境。那么哪些目标应该优先打击,哪些目标要尽快地对它进行消灭,可能是AI要辅助地进行决策。那么AI利用这种大数据,利用其它平台的这种传感器的视野,马上就可以做出决策,可以在触碰的显示屏上进行标注。车组乘员所做出的这样的一个攻击动作,非常地简单,用手指轻触一下屏幕,就可以完成一击必杀。
|
||||
|
||||
|
||||
## 【解说10】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
隔断4:地面战颠覆之地空协同——格里芬装甲车
|
||||
|
||||
|
||||
## 【解说11】
|
||||
|
||||
除了MGCS,这种全新架构的地面AI系统,在传统装备的身上,进行AI化升级,也是各国对地面武器,进行升级的一种尝试。
|
||||
|
||||
|
||||
## 【解说12】
|
||||
|
||||
法国在其格里芬装甲车上,加装“天蝎”作战系统,就是一个典型的例子。格里芬多用途装甲车,是一款6×6轮式装甲运兵车,战斗全重25吨,根据法国“天蝎”计划研制,旨在取代法国陆军现役的VAB装甲运兵车。该车安装一台400马力雷诺卡车公司6缸涡轮增压柴油发动机,和ZF七挡自动变速箱,最大公路速度每小时90千米,续航里程800千米。格里芬多用途装甲车,能够防御14.5×114毫米穿甲弹、炮弹破片和地雷简易爆炸装置的打击,并配有模块化装甲套件,以增强防护能力。格里芬装甲车,是法国陆军的一款主战装备。我们看它的外形,和传统的装甲车不太一样,有点像一个加强版的六轮的装甲卡车。它的全向的防御能力,是比较强的。不仅法国陆军会装备,你像比利时、卢森堡这些国家的军队,可能也会采购,因为他们所使用的主战装备,都是这个法系的。格里芬装甲车最大的优势,就是防护能力非常地全面。首先我们可以看一下它的底盘,和相关的这个悬挂系统,即使遭遇到了这个反坦克地雷,包括路边炸弹,也可以保证车体结构的这样的一个可靠性,和它的这个完整性。包括它的载员舱,也是经过了特殊设计,即使装甲车被炸翻了,或者说面临大威力的路边炸弹的袭击,也要最大程度地保证它的这种载员舱的这样的一个整体性,最大程度要保障士兵的生命安全。那除此之外,我们也注意到,格里芬它的主要功能还是运输,而且也可以作为一个多功能的装甲作战平台。在车体前部靠上这个位置,会配备有多功能的遥控武器站,可以集成轻机枪、重机枪和自动榴弹发射器,能够针对下车作战的步兵,进行一定程度的火力支援。而且它还有衍生的型号,比如说为它加装大口径的迫击炮,它就成了机动部署的这种迫击炮车,可以针对作战的步兵,进行精准的曲射的火力支援。也有工程车的变型,可能还有装甲救护车、装甲抢救车的这样的一个变型。从目前法军部队使用的反馈来看,它的机动性、防护能力和联网作战能力,是可以达到要求的。格里芬多用途装甲车,配备了“天蝎”计划的作战信息系统和泰雷兹战术电台,用于实时战场协调。衍生型号,包括装甲运兵车、指挥车、炮兵侦察车、医疗后送车和战斗工程车,所有这些型号,都旨在确保配备“天蝎”系统的部队之间协同作战能力。它是一套非常完善的这样的一个一体化的指挥控制系统,那么也是在大范围内,能够进行信息和情报共享的这样的一个系统。通过终端设备的加装,像格里芬装甲车,包括勒克莱尔主战坦克,以及其他欧洲国家的主战装备,一旦融入了“蝎子”系统,或者说我们叫“毒蝎”系统,就可以形成合力,因为信息和情报,可以在最大的范围内进行这种共享。也可以把每个作战平台,连接到这种作战的指挥网络和体系当中。这样的话,整个的地面装甲作战单位,包括像凯撒自行火炮,就可以构成合力,更好地感知战场环境,更好地进行目标的分发,而且不同的作战单位之间,可以打出组合拳。
|
||||
|
||||
|
||||
## 【解说13】
|
||||
|
||||
依托“蝎子”信息与战斗系统,格里芬多用途装甲车,与美洲豹侦察车、凯撒自行火炮等装备,形成数据互联。AI算法可自动分析战场态势,动态调整火力分配策略。例如,当发现敌方装甲集群时,系统会优先调度射程最远的凯撒火炮进行打击,同时指挥狮鹫机动至侧翼实施伏击,实现探测、定位、打击链路的秒级闭环。这种协同模式,使传统线性推进战术,演变为动态猎杀矩阵。
|
||||
|
||||
|
||||
## 【主持人4】
|
||||
|
||||
在地面战变革当中,我们看到了AI如何让钢铁洪流拥有群体智慧。那么,有着地面装备克星之称的无人机,在近几年的局部冲突中,可谓是出尽了风头。当无人机蜂群战术成为现代战场的灰色威胁,传统防空体系正在面临发现难、跟踪难、拦截难的挑战。而AI技术的注入,正在将被动防御转化为智能防御。
|
||||
|
||||
隔断5:AI编织的光速拦截网——卢卡斯激光武器
|
||||
|
||||
|
||||
## 【解说14】
|
||||
|
||||
2024年10月,美国莱昂纳多DRS公司与蓝晕公司,展示了斯特赖克8×8步兵战车的最新变体。该版本被称为定向能反无人机系统斯特赖克,配备蝗虫激光定向能武器系统,及各种雷达和传感器。蝗虫激光系统,结合了高速计算机处理,先进算法,和新一代激光武器,能够在几毫秒内识别、区分,并焚毁敌方无人机目标。
|
||||
|
||||
|
||||
## 【解说15】
|
||||
|
||||
卢卡斯车载激光系统,主要是和斯特赖克,八轮的装甲车进行结合。之所以要融入AI系统,其实主要是针对目标进行分析和判断。激光武器针对低慢小目标,它具备瞬间的秒杀能力。但是它有一个技术的问题,必须要进行一个很好的解决,也就是在低空空域的目标识别和分析的问题。因为在这种八轮的装甲车上,不可能加装特别复杂的光学传感器,或者说太多的、太大个头的这个雷达。那么针对空中的目标,进行快速的分析,进行快速的这个识别,必须要和这个激光器进行一个紧密的联动。也就是说,激光器它的光学和红外的瞄准设备,直接融入了AI的这种分析软件。蝗虫激光武器系统,旨在以人工智能技术实现即时识别,并消除无人机威胁。蝗虫激光武器系统采用托盘式设计,由两部分组成,左侧是激光器,右侧是跟踪系统。该系统将精密光学和激光硬件与先进的软件、人工智能和处理技术相结合,可识别、跟踪以及利用高能激光攻击目标。这种AI的分析软件,也就是一种智能化的算法。这个智能化的算法,起到什么样的作用呢?比如说天空有一群飞鸟,天空当中又有一群无人机,如何根据它的飞行特征、飞行速度以及这个外观和相关的这种影像的这样的一个图像,要对它进行马上的判断。
|
||||
|
||||
|
||||
## 【解说16】
|
||||
|
||||
在这样的一个决策的过程当中,是不需要人力介入的。精准快速地识别目标,完成激光束的攻击。人工智能系统,能够接收传感器数据,并以比人类快得多的速度,执行分析,组织数据和识别相关项目。这意味着,可以在几秒钟内,确定最佳解决方案或对策。这大大提高了反无人机的速度,并更好地实现目标甄别。只要敌方无人机被发现,识别和验证的速度越快,蝗虫就能在越远的地方,越安全的地方摧毁它们。
|
||||
|
||||
|
||||
## 【主持人5】
|
||||
|
||||
从F-16的AI魔改,到“暴风雨”战机的原生智能,从MGCS的无人集群,到防空体系的智能化升级,AI的加入让武器装备有了质的飞跃。当硅基大脑与钢铁之躯完成融合,科幻电影当中的那些作战场景正在逐步走进现实。好了观众朋友们,感谢您持续关注国防军事频道《军事科技》,我们下周同一时间再见。
|
||||
@@ -0,0 +1,219 @@
|
||||
---
|
||||
标题: “舰” 证不凡
|
||||
编导: 左鑫
|
||||
播出日期: 2026-02-03
|
||||
类型: 节目文稿
|
||||
期号: ep006
|
||||
---
|
||||
|
||||
# 《“舰”证不凡》
|
||||
|
||||
|
||||
## 【宣传词1】
|
||||
|
||||
从万吨大驱,到两栖攻击舰,再到航空母舰,大国巨舰,勾勒人民海军深蓝征程。本期《军事科技》,走进055咸阳舰,075湖北舰,山东舰,解码国产战舰,体形大的硬核底气,揭秘拳头硬的体系战力,感悟责任大的使命担当。
|
||||
|
||||
|
||||
## 【主持人1】
|
||||
|
||||
各位观众你们好,欢迎收看《军事科技》,我是蓝皓。人民海军的装备发展,堪称一部波澜壮阔的崛起史诗。我们用数十年的时间,完成了从近海防御,到远海防卫的跨越式发展。那么,提起人民海军的装备实力,今天的节目,我们想用几个关键词,国之重器的硬核密码。第一个关键词就是体形大。此刻,我们正走在军港的码头上。大家顺着镜头望去,我身旁的这艘巨舰,就是055型驱逐舰,咸阳舰。当我们第一次接触它的时候,能够充分感受到它的力量感,与美感,是一艘美丽而威武的海上利刃。当然,镜头里呈现的震撼,远不及亲眼看到它的万分之一。当您真正地站在它的甲板之下,仰望这艘钢铁巨舰的时候,才能够真正地感受到,万吨大驱这四个字,沉甸甸的体量,与一往无前的底气。
|
||||
|
||||
|
||||
## 【解说1】
|
||||
|
||||
咸阳舰,是055型驱逐舰其中的一艘。它的诞生,突破了大型舰艇总体设计、信息集成、总装建造等一系列关键技术,满载排水量达到1.2万吨。可能有观众,对1.2万吨没有直观概念,不妨做一组对比。建国之初,我国自主研制的第一款驱逐舰,051型驱逐舰,满载排水量约3600吨,055的体量是它的三倍之多。即便是与现役被誉为“中华神盾”的052D型驱逐舰相比,055的体量也几乎是它的两倍。站在甲板下仰望,高耸的舰桥、修长的舰体,搭配流畅的隐身设计,既有钢铁巨兽的威严,又不失现代军工的精致。这种体量的提升,绝非简单的放大,而是综合作战能力的全面跃升。
|
||||
|
||||
|
||||
## 【主持人2】
|
||||
|
||||
此刻,我们已经登上了咸阳舰的甲板。我发现这个甲板,比我们之前看到的驱逐舰,或者护卫舰的甲板都要大。这样的设计有怎样的特别之处呢?
|
||||
|
||||
|
||||
## 【嘉宾1】
|
||||
|
||||
第一个,大家现在所处的,我们的起降平台在海上,主要用于直升机的起降,和这个临时的停放。大家可以往这边看,这个就是我们的双机库。一般的驱护舰艇,只有我们的单机库,所以说,对比它们,我们的搭载能力更强了。我们可以搭载多型的直升机及无人机,单机库的话就只能搭载一架,但是双机库我们就可以搭载两架。
|
||||
|
||||
|
||||
## 【主持人3】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【嘉宾2】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【解说2】
|
||||
|
||||
咸阳舰的外形设计,充满了未来感。舰体采用了大量平滑的斜面,减少了棱角和突出物。这种设计大幅降低了雷达反射截面积,让它在敌方雷达面前,如同一个隐形战士。与此同时,它采用典型的全封闭飞剪式舰首。它并非二战前后战列舰的飞剪式设计,而是融合中国创新智慧的升级成果。从正面看,舰艏折线在顶部开始内倾,到主炮位置前方又稍稍外倾,随后再次内倾。最前端与主甲板构成尖锐的多面体全封闭结构,与前甲板齐平过渡。整体外形简洁现代,兼具美观与实战效能。
|
||||
|
||||
|
||||
## 【主持人4】
|
||||
|
||||
现在我们来到了咸阳舰的前甲板。相比我之前看到的其他战舰来说,咸阳舰的甲板会更加地干净整洁一些。那些系留装置,锚点都去哪儿了呢?
|
||||
|
||||
|
||||
## 【嘉宾3】
|
||||
|
||||
这个主要是为了突出我们的隐形化设计。我们将这些所有的舱面设备,我们的锚机,系统柱都置于我们的甲板之下,提高我们舰艇的这个隐蔽效果。第一个,可以(增加)我们人员,在舰艇大幅度摇摆的过程中,在舱面作业的安全系数。第二个,减小敌方的雷达发现我的概率,提高我的舰艇的防护能力。
|
||||
|
||||
|
||||
## 【主持人5】
|
||||
|
||||
咸阳舰的主桅杆,相比于其他军舰来说,变得更加粗壮,外观也会更加简洁。这样的细节变化有怎样的深意吗?
|
||||
|
||||
|
||||
## 【嘉宾4】
|
||||
|
||||
我们055的桅杆,采用一体化主桅设计,不但看起来简洁美观,它实际上更内藏乾坤。我们将所有的雷达、电子战及通信天线,整合于我们的主桅之内。它可以同时跟踪数百批海面、空中目标。它不但可以引导导弹更精准地打击目标,也可以提升我们舰艇的战场态势感知能力,是我们决胜战场的“千里眼”与“顺风耳”。
|
||||
|
||||
|
||||
## 【解说3】
|
||||
|
||||
如果说055驱逐舰的“大”,是“标杆级”的突破,眼前这艘075型两栖攻击舰,湖北舰的“大”,就是“平台级”的跨越。就像网友调侃的那样,湖北舰之大,连镜头都快装不下了。湖北舰,舷号34,是中国自主研制的075型两栖攻击舰中,最新的一艘。2025年7月,湖北舰正式入列人民海军。它的到来,对提升中国两栖作战能力,完善远海作战体系,具有里程碑式的意义。
|
||||
|
||||
|
||||
## 【嘉宾5】
|
||||
|
||||
在我身后这艘舰艇,是我们075型两栖攻击舰,第四艘舰艇,舷号是34,舰名是湖北。大家可以看到,我站在它下面,显得非常渺小。我们湖北舰的长度,超过200米,高度有15层楼那么高。它从外观来看,与我们的航母有点相似。当然,我们的湖北舰,也是内含乾坤,它既能容纳两栖车辆,又能装载直升机。它的一个主要的使命任务,就是遂行两栖作战。
|
||||
|
||||
|
||||
## 【解说4】
|
||||
|
||||
拥有巨大的直通甲板,面积相当于20个篮球场,可同时快速起降多款舰载直升机。它的入列,加强了我国“平面登陆加立体登陆”的综合两栖作战能力。它既能通过舰载直升机实现垂直投送,又能利用坞舱释放登陆装备展开平面突击,可快速投送两栖作战力量,应对远海岛屿防御、人道主义救援等多种任务,是人民海军维护国家主权、海洋权益的两栖先锋。
|
||||
|
||||
|
||||
## 【主持人6】
|
||||
|
||||
说到体形大,人民海军的装备,还有比海南舰更大的存在。此时,我就站在这艘巨舰的身旁。通过结构,大家可能已经猜出了,它就是我们的首艘国产航母山东舰。站在平台上去仰望它的时候,它就像是一座高楼;而当它在大海上全速奔跑起来的时候,那就是一座劈波斩浪的海上高山。
|
||||
|
||||
|
||||
## 【解说5】
|
||||
|
||||
山东舰的满载排水量,约6万多吨,舰长300米左右,是我国首艘完全自主设计、自主建造、自主配套的国产航母。从055的1.2万吨,到湖北舰的4万吨,再到山东舰的6万多吨,这种体量的递增,清晰勾勒出人民海军装备发展的战略轨迹。每一次放大,都对应着战略需求的升级;每一次体量的突破,都意味着远海作战能力的跃升。
|
||||
|
||||
|
||||
## 【嘉宾6】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【解说6】
|
||||
|
||||
山东舰,于2019年12月17日入列。它一入列就备受关注。全舰高度相当于十几层楼,内部舱室密布,宛如迷宫。6万多吨的排水量,赋予了它远洋航行的充沛底气。现在我们就从我们的主船体,来到了我们舰岛。我们主要是分为这两个部分。我们船的总的这个排水量,达到了六万多吨,也就相当于三个埃菲尔铁塔的重量。在海上航行,我们的总的发电量,相当于一个中型的一个城市。所以这个体量还是特别大的。我们的全船的舱室,据我们初步统计都有三千多个。网上有一个流传这么一个说法,就是一个婴儿从刚出生,一直到12岁的时候,才能把我们全船每天住一个舱室,才能都住个遍。现在来到是我们的舰上的图书馆。我们舰上遵循了一个社区化的设计的理念,保证舰员在生活的一个区段之内,保证了基本的工作、学习、生活的这种状态。然后我们现在图书馆藏了,收藏了能有七千多本书。我们船上还有(很多)像这类生活舱室,还有我们的健身房,我们的医疗区,甚至我们有自己的超市,还有邮局。以上种种说明,我们航母确实是一个体量庞大、功能强大的海上巨系统。航空母舰,是一种供舰载机起飞和降落的大型舰艇。换句话说,航母就是一座海上机场,是目前世界上最庞大、最复杂、威力最强的武器平台之一,是现代战争中名副其实的海上重器。
|
||||
|
||||
|
||||
## 【嘉宾7】
|
||||
|
||||
从我们舰岛的前部走出来,就来到了我们的飞行甲板。我们整个飞行甲板,可以说一下视野就开阔了,没错。整个飞行甲板,有一万五千多平方米,相当于三个标准的足球场,还要再大。我们分了三个区域,现在我们脚下的这个区域,是停机区。可以看到好多黄色的标志线,就是我们飞机停放的位置。然后在这些停放的位置,我们可以进行各类的加油、加气、挂弹,各类的保障作业。在我们正前方,舰船的正前方,是我们的起飞区,有设置了两个短距的起飞位。在我们的左边、左手边,设置了是我们的着舰区。在舰艇的方向有4道拦阻索。我们飞行甲板的右边,布设的是我们岛式上层建筑。整个上层建筑,有大概十余层楼那么高。以中间的这个格栅为分界,前半部分,是我们主要掌管航行安全的驾驶室,以及我们舰岛上面,各类的预警探测、我们的雷达等设备。在我们的混合室后边,是我们的塔台,主要掌管我们飞行安全。
|
||||
|
||||
|
||||
## 【解说7】
|
||||
|
||||
从1.2万吨,到4万吨,再到6万多吨,人民海军舰艇的体形大,从来不是盲目追求规模,而是战略需求与工业实力的精准匹配。这种递进式的体量突破,为后续的拳头硬,也就是强大的打击能力,奠定了坚实基础。接下来,我们进入第二个关键词,拳头大。
|
||||
|
||||
|
||||
## 【主持人7】
|
||||
|
||||
我们都说舰艇的体形大是基础,但是火力系统,拳头硬才是核心。那么对于055驱逐舰而言,它的火力系统,有什么特别之处呢?
|
||||
|
||||
|
||||
## 【嘉宾8】
|
||||
|
||||
目前飞机和这个,空中来袭的导弹,是对我们最大的威胁。我们055型舰,构建了远、中、近三层防空抗击体系。现在映入眼帘,就是我们的一个近程防空导弹,主要担负我们对近距离来袭的导弹或飞机进行一个抗击和拦截,精度高、反应时间短。这个就是我们所说的副炮系统,也是咱们所说的万发炮。万发炮,顾名思义也就是射速高,依靠它强大的射速,来提供弹幕,对末端的导弹,进行一个有效地拦截。同时,它上面具有这个雷达及光电系统,为它来抗击导弹,提供一些更精确地指引。我们刚才看的就是我们远、中、近三层防空体系。这个里边就可以搭载我们的远程防空武器。从它的外形上我们看到,它像一块一块的巧克力,虽然看着像巧克力,可不要小觑它的威力,这个就是我们的通用垂直发射系统。我们刚才提到的远、中、近三层防空体系的远程作战能力,它可以装载各型的导弹,为我们打击空中、岸上以及这个海面的目标。然后同时还具有这个冷热兼容发射,大大提高了我舰的综合作战能力。
|
||||
|
||||
|
||||
## 【解说8】
|
||||
|
||||
既然说到了垂直发射系统,我们就来科普一下,导弹飞出发射筒的两种主要方式:热发射和冷发射。两者的核心区别在于,弹出筒的动力来源不同,发射筒的内部结构也因此存在差异。热发射,是导弹在发射筒内直接点火,依靠自身发动机的推力飞出发射筒,这种方式反应速度快,发射效率高。冷发射,则是先利用高压气体,将导弹弹出发射筒,待导弹上升到一定高度后,再启动发动机飞向目标。简单来说,热发射是一步到位,冷发射则需要两步走。
|
||||
|
||||
|
||||
## 【主持人8】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【嘉宾9】
|
||||
|
||||
这个就是我们的主炮系统,近距离地接触它,才能感受到它的威严的。它主要担负我们一些对岸火力支援任务,或者说一些对海打击任务。具有两个特点:一个是我们口径大,也是目前国内舰炮搭载口径最大的,所以说我们的火力就很强;再有一个,我们依托它的平台的自身的优势,我们平台大,所以说它就有更稳定的射击稳定性,然后更精确地命中目标。
|
||||
|
||||
|
||||
## 【解说9】
|
||||
|
||||
强大的武器配置,让055型驱逐舰不仅能独立执行防空、反舰、反潜等作战任务,还能在航母编队中承担“带刀护卫”的核心角色,是人民海军远海作战的核心战力之一。
|
||||
|
||||
|
||||
## 【主持人9】
|
||||
|
||||
如果说055的拳头是精准打击,那么两栖攻击舰的拳头就是立体投送加协同打击,是一套威力更加强大的组合拳。
|
||||
|
||||
|
||||
## 【解说10】
|
||||
|
||||
作为两栖攻击舰,它的核心打击能力体现在搭载能力上,它所搭载的直升机、登陆装备和陆战队员,就是它最强大的拳头。可搭载几十架各型直升机,能快速投送陆战队员和轻型装备,实现昼夜立体登陆。在平面登陆方面,湖北舰的坞舱可容纳多艘气垫登陆艇,这种气垫登陆艇速度快、适应性强,能够在浅滩、礁石等复杂地形登陆。
|
||||
|
||||
|
||||
## 【嘉宾10】
|
||||
|
||||
那么我现在所在这个位置,是属于湖北舰,驾驶室平台的外侧。在我们湖北舰设计理念上,跟航母有着异曲同工之妙。我们采用的也是,边岛式上层建筑,和这种全纵通式的飞行甲板。站在我这个位置,可以清晰地看到我们整个,飞行甲板的全部概况。从前至后,分别有6个起降点。在整个飞行甲板的后侧,还设置了一个7号点,也就是一个小停机坪。这个停机坪,主要是用来保障无人机的起降。在我们机库的下方,是我们的车辆库和坞舱。它主要是用来存放气垫艇,两栖装甲车,坦装以及自行榴弹炮。当我们进行,登陆投送任务的时候,我们的坞舱门会打开。我们的气垫艇,会带着我们的坦装,我们两栖装甲车,一块儿进入到水中,进行登陆作战。
|
||||
|
||||
|
||||
## 【解说11】
|
||||
|
||||
当然,作为一款大型作战平台,湖北舰也配备了自卫武器。分布在舰艇四周的万发炮,和舰空导弹系统,构成了它的近程防御圈。这些武器虽然看似不多,却能在关键时刻,拦截来袭目标,为湖北舰,撑起全向防御的保护伞。而作为人民海军的核心作战平台,山东舰的拳头则更为强硬,它体现的是,远洋制空加制海的,体系作战能力,而这个拳头的核心,就是舰载机部队。歼-15舰载战斗机,就是山东舰最锋利的尖刀。山东舰目前搭载的,是歼-15舰载战斗机,这是我国自主研发的,重型舰载战斗机,具备制空、制海、对陆攻击等,多种作战能力,它的最大起飞重量超过三十吨。
|
||||
|
||||
|
||||
## 【嘉宾11】
|
||||
|
||||
我们现在所在的这个位置,就是我们3号起飞位。在我们舰上,像这样的起飞位我们一共有三个,另外的两个是在舰艏的位置。舰载机就位了以后,我们会有三种工作人员:一种是机务检查人员,他们会检查飞机放飞前的这个准备状态,舰载机的状态是否良好,它发动机状态是否良好;还有一类就是我们止动轮挡的检查员,主要是检查我们止动轮挡和飞机主轮的啮合程度,轮挡是否顶在中间,它没有完全顶上的话是不合格的;还有一类是我们偏流板的检查人员,他主要是检查我们偏流板的冷却系统还有支撑系统,是否是处于一个正常的状态。在完成这些一系列检查之后,起飞助理会到飞机的右侧,确认好各位置的状态之后,完成放飞动作,也就是我们熟知的航母style的动作。之后,舰载机会在一个短的滑跑距离之内完成放飞。
|
||||
|
||||
|
||||
## 【解说12】
|
||||
|
||||
除了歼-15舰载战斗机,山东舰还搭载了预警直升机、反潜直升机等辅助作战机型。这些舰载机构成了一个完整的海空一体作战体系,让山东舰具备了遂行远洋作战任务的能力。
|
||||
|
||||
|
||||
## 【嘉宾12】
|
||||
|
||||
山东舰出海执行任务,通常不是只有单舰的出海,通常是组成编队去执行任务。我们的编队,还会有驱逐舰、护卫舰,以及补给舰等等。这些附属舰艇,也都会搭载各型武器,来完成我们编队体系化构建,让我们山东舰编队的拳头,越来越硬。
|
||||
|
||||
|
||||
## 【主持人10】
|
||||
|
||||
从055驱逐舰的全方位精准打击,到两栖舰的立体投送协同打击,再到山东舰的远洋体系作战,人民海军的拳头大,指的是从单点打击,到体系作战的能力跃升。而这些强大的装备,都服务于一个核心的使命,那就是守护祖国主权、安全和发展利益。就引出了我们今天的第三个关键词。
|
||||
|
||||
|
||||
## 【解说13】
|
||||
|
||||
近年来,人民海军在实战实训中,持续向远海远域拓展。实战化训练的规模、强度和体系化水平显著提升,标志着海军走向深蓝的能力不断增强。
|
||||
|
||||
|
||||
## 【嘉宾13】
|
||||
|
||||
我来到咸阳舰已经第三年了。当我第一次抬头,看见咸阳舰的时候,如此的宏伟和巨大。后面当我可以独立执勤的时候,每次双手握住舵轮,眼前看着现代化的操作界面,都能感受到这艘先进舰艇蕴含的强大力量。我心里明白,我以后的责任、荣光和青春,都将与它不可分割。
|
||||
|
||||
|
||||
## 【嘉宾14】
|
||||
|
||||
我们咸阳舰就是,自从我们入列以后,执行了很多战备和训练任务。其实说通俗一点,我们这个作战任务,其实主要就两项。第一个,就是通过我们的舰艇的军事外交,向全世界,包括我们到达的国家,传达我们的善意和诚意。另外,通过展示我们的能力,让对方能够感受到,我们是有能力保护和维护,自由交往和贸易。我们这型舰,就像我们是一个武林高手一样的,但是我们是爱好和平的,但是我们绝对不会畏惧武力。就是需要的时候,该出手时就出手,让那些想到我们周边,来耀武扬威的敌人,让他们知道,我们虽然以德服人,但是也可以以力服人。
|
||||
|
||||
|
||||
## 【嘉宾15】
|
||||
|
||||
湖北舰交舰入列的时候,我们作为舰员,站在湖北舰下面,第一个感受就是震撼,第二感受就是激动、自豪。每名军人都希望自己手中,有一把钢枪。我当时看到这艘舰艇,我当时就想,湖北舰就是我们,所有舰员的钢枪,是我们手中的利刃。通过我们的刻苦训练,湖北舰,已经是一把锋利的,更是有生命力的利刃。这把利刃交到我们手里,我们作为光荣的湖北舰舰员,丝毫没有懈怠过。有了这把利刃,我们在海上遇到强敌对手,我们有信心有能力,消灭一切来犯之敌。
|
||||
|
||||
|
||||
## 【嘉宾16】
|
||||
|
||||
航母这样一个巨系统工程,不仅代表了国家的综合实力,更代表着科技水平,和军事水平的一个集中体现。从2017年辽宁舰访问香港,全体舰员在甲板摆出了“香港你好”,到今年7月份我们舰访问香港,我们摆出了“国安家好”。这种转变体现了我们一个海军强国,海军大国,一种负责任的态度。同时,我们也有能力有信心,维护国家利益,保护人民安全。近些年来,我们演训任务也是逐渐增多,我们航母也是从近海走向远海,双航母编队在西太平洋部署。今年11月份,我们也是在码头,有幸见证到18舰交接入列,我们的海军实力进一步增强。我们始终相信,国家利益所至,航母航迹必达。
|
||||
|
||||
|
||||
## 【解说14】
|
||||
|
||||
舰艇的吨位有大小,武器的威力有强弱,但人民海军的责任与担当,始终如一。从近海到远海,从维护主权到国际救援,055驱逐舰、湖北舰、山东舰,用一次次行动,践行着人民海军为人民的誓言,诠释着大国海军的责任与担当。
|
||||
|
||||
|
||||
## 【主持人11】
|
||||
|
||||
深蓝砺剑,逐梦海天,人民海军装备的发展之路,是一条自主创新,砥砺前行的奋斗之路,从无到有,从弱到强,从近海走向深蓝,每一次巨舰的下水,每一次编队的远航,都见证着中华民族,向海图强的百年夙愿,随着更多先进装备的入列,人民海军必将以更强大的姿态,守卫国家主权,安全和发展利益,当然,我们也会总结更多的关键词,带您揭秘国之重器的硬核密码,好了观众朋友们,感谢您持续关注,国防军事频道《军事科技》,我们下周同一时间。
|
||||
@@ -0,0 +1,207 @@
|
||||
---
|
||||
标题: 大国巨舰上的奇妙反差
|
||||
编导: 左鑫
|
||||
播出日期: 2026-02-10
|
||||
类型: 节目文稿
|
||||
期号: ep007
|
||||
---
|
||||
|
||||
# 《大国巨舰上的奇妙反差》
|
||||
|
||||
|
||||
## 【导视1】
|
||||
|
||||
万吨航母的“方向盘”,竟比家用轿车的还小巧。几十吨重的舰载机,全靠呆萌小车灵活挪位。数字化战舰,为何执着于手绘海图?古老旗语与黑科技同框,又藏何玄机?本期《军事科技》,带您解锁大国巨舰的反差彩蛋,探寻这些奇妙细节背后的硬核故事。
|
||||
|
||||
|
||||
## 【主持人1】
|
||||
|
||||
各位观众你们好,欢迎收看《军事科技》,我是蓝皓。提起大国巨舰,大家脑海中往往浮现的是,万吨体量的钢铁巨兽,劈波斩浪的远洋航迹,先进雷达的敏锐探测,精确导弹的雷霆打击。正是这些宏大的画面,勾勒出人民海军装备的硬核轮廓,使我们对这些国之重器充满了敬畏与自豪。但很少有人知道,在这些钢铁巨舰的身体当中,藏着一些让人意想不到的反差细节,它们看似矛盾,但却是促使它们驶向深蓝的底层逻辑。本期节目,就让我们走进这些国产战舰,揭秘大国巨舰的反差密码,从细节之处读懂国之重器的硬核实力。
|
||||
|
||||
信息化战场上的 “古老对话密码”
|
||||
|
||||
|
||||
## 【解说1】
|
||||
|
||||
2024年5月1日,福建舰从上海江南造船厂启航,开展首次海试。当时消息一出,瞬间点燃全网热情。不少网友逐帧欣赏福建舰海试的报道与视频,却不约而同生出一个疑问:在我国航母海试这一重要时刻,舰上为何醒目地悬挂着外国国旗呢?其实,这些旗帜并非某国国旗,而是国际信号旗。顾名思义,国际信号旗是全球通用的舰船通信工具,专供舰船之间、舰岸之间快速联络。其起源可追溯至17世纪的欧洲航海时代,当时的舰船通过悬挂不同组合的信号旗,就能让其他船只快速明晰自身意图。有人追问,如今一艘现代顶尖战舰,正朝着信息化、智能化、体系化深度演进。以人民海军咸阳舰为例,作为世界现役先进驱逐舰之一,其搭载的一体化桅杆,将多种探测、通信、电子对抗设备融为一体,实现了信息的高效融合和精准传递,代表着世界舰艇电子装备的顶尖水平。可是,就是这样一艘汇集多种黑科技的万吨驱逐舰,现代化桅杆之上,却依然保留着醒目的信号旗。这一延续数百年的古老通信方式,与先进电子系统同框,一今一古,形成了极强的视觉与认知反差。
|
||||
|
||||
|
||||
## 【主持人2】
|
||||
|
||||
我们知道现在的科技,日新月异。那么对于055这种智能化,数字化的战舰来说,为什么还需要古老的旗语呢?
|
||||
|
||||
|
||||
## 【专家1】
|
||||
|
||||
首先就是因为我们是,旗语通信和旗号通信,它在海军通信中,占据了不可替代作用。首先第一点,就是它的抗干扰性,还有它的隐蔽性,因为它是不依靠,各类电子设备进行发波,所以敌方就不会收到,我们的各种电磁信号。第二点就是它的快速性,就比如说我们在舰艇,航行过程中,受到了敌方的干扰,我们无法使用我们的,其他的通信设备,那么我们旗号通信,和旗语通信,就发挥到作用,它可以进行快速地,点对点进行通信。
|
||||
|
||||
|
||||
## 【解说2】
|
||||
|
||||
陆地上,用特定信号传递信息,有着悠久历史。烽火台传递警报,战场击鼓吹号调度,传令兵奔走传讯,手段丰富多样。而海上,因物理空间隔绝,每艘船都是相对独立的个体,信息传递更为艰难。17世纪起,各国商船为规避冲突,传递警示,逐渐形成统一的旗帜通信规则,用旗帜代表数字,再通过数字传译具体信息。1799年,波帕姆爵士,公布了首份,可通过数字引用单词,句子的列表,即“波帕姆旗语系统”,较以往通信效率大幅提升。1805年10月21日,特拉法尔加海战前夕,英国海军胜利号上的,纳尔逊勋爵,便是通过波帕姆信号旗,发出了闻名后世的,著名决战旗语,意思是,英格兰希望每一位官兵,恪尽职守。这一段对话,共计花了四分钟发出,当舰队相继收到,这份决战旗语后,每位官兵欢欣鼓舞,士气大增。而对于我国而言,同样承载着厚重的历史记忆。
|
||||
|
||||
|
||||
## 【嘉宾1】
|
||||
|
||||
挂冲锋旗!是,挂冲锋旗!是,挂冲锋旗!
|
||||
|
||||
|
||||
## 【解说3】
|
||||
|
||||
甲午海战中,北洋水师致远舰,在通信受阻时,曾靠悬挂信号旗,指挥编队冲锋,用热血诠释了中国人的风骨。
|
||||
|
||||
|
||||
## 【嘉宾2】
|
||||
|
||||
我等从军卫国,今日一死,也要撞沉吉野。
|
||||
|
||||
|
||||
## 【解说4】
|
||||
|
||||
19世纪中叶,国际信号规则正式成型,体系更趋规范、全面,并沿用至今。
|
||||
|
||||
|
||||
## 【专家2】
|
||||
|
||||
我们海军信号旗,一共分为两种,一种是信号旗,—(种)是手旗。那么信号旗又分为三类,就比如说我们的字母旗、数字旗,还有特种旗。那我今天就一共带来了四面旗帜,分别它们不同的意思。首先我先介绍,为大家介绍的是数字旗。数字旗一共有10面,而我们手中的这一面旗,现在是2。然后不同的数字旗,有不同的样式。这就是我们的字母旗,它在海军通信中代表着A。在不同的字母,其中它们的样式都不一样。像这种旗,这个旗帜确实像某一世国家的国旗,但它并不是。各种字母,它的旗帜样式都是不一样的,也代表着不同的含义和意思,有些就代表着航向,有些是代表着我在进行危险作业。好,接下来介绍就是我们的特种旗。特种旗在海军旗号通信中,各种特种旗有不同的意思。我们特种旗一共有8面,它就相当于是海上,我们挂出旗号的时候,要进行某一种,同时执行的这种(行动),就会挂出这一类旗型,表示大家一起进行行动或者运动。
|
||||
|
||||
|
||||
## 【解说5】
|
||||
|
||||
一套完整的国际信号旗,由26面字母旗、10面数字旗、3面代旗和1面回答旗组成。多数国家海军信号旗的样式、数量与国际信号旗基本相同。除各国专属含义外,国际通用场景中,每面旗帜要么代表特定字母、数字,要么单独承载固定含义,部分复杂信息则需多面旗帜组合表达。了解了这一规则,依据国际信号手册,我们便能读懂福建舰海试时悬挂旗帜的深意。当时舰岛右侧悬挂的三面旗帜,分别对应字母R、U和数字1,组合为“RU1”,其释义为“我正在试航”。即便如此,舰载无线电技术已经十分成熟,旗帜信号作为基础通信手段,依然被各国商船、民船及军舰广泛采用。无论是欧美海军,还是亚洲军舰,均严格遵守这一传统。而与旗语一同保留下来的,还有另一种古老的通信工具——传声筒。传声筒大家肯定不陌生,在抗日影片《地道战》里,就有用它互通消息的桥段。
|
||||
|
||||
|
||||
## 【嘉宾3】
|
||||
|
||||
各小组注意!你们各自为战!各小组注意!你们各自为战!打一枪换一个地方!不准放空枪!不准放空枪!开火!
|
||||
|
||||
|
||||
## 【解说6】
|
||||
|
||||
可别小瞧这看似“土得掉渣”的简陋装备,它不仅没被时代淘汰,反而在历代战舰的装备清单上,稳稳占据着一席之地。传声筒又称扩音筒,外形简约,其桶身采用耐腐蚀金属材质,依托物理声学原理传导声音,无需依赖电力。海图室 作战室,作战室 海图室。检查雷达工作情况,明白。
|
||||
|
||||
|
||||
## 【嘉宾4】
|
||||
|
||||
军舰上的传声筒,是一个古老设备。在两次世界大战期间,几乎所有主力舰,都配备传声筒,作为核心通信手段。其核心作用是实现舰内不同舱室、甲板间的通信。管路贯穿多个甲板,进行点对点信息传递。比如传输距离较短,为了让对方听清,必须非常大声。但是当军舰遭受攻击,致使电子通信、电力系统损坏,或是遇到电磁脉冲干扰时,能成为最后的通信手段,避免舱室间失联。
|
||||
|
||||
|
||||
## 【解说7】
|
||||
|
||||
传声筒,曾是舰艇上的主流通信工具,下达操舵指令,舱室间联络协调,都离不开它。如今,即便无线对讲,内置通信耳机,舰内电话等先进设备广泛应用,传声筒依然被保留下来,成为电子通信失效时的应急生命线。
|
||||
|
||||
数字化战舰上的 “手绘图纸”
|
||||
|
||||
|
||||
## 【解说8】
|
||||
|
||||
在多数人的认知中,咸阳舰、山东舰这类现代化战舰,理应是全数字化、无纸化运作,指挥系统、武器系统、导航系统的数据,全靠电脑屏幕显示。但当我们走进咸阳舰内部,却看到了令人意外的场景,先进的液晶显示屏旁,整齐摆放着纸质版的舰操绘算图、铅笔和比例尺。而这并非咸阳舰独有,而是现代战舰的标准配置。
|
||||
|
||||
|
||||
## 【专家3】
|
||||
|
||||
在舰艇的机动之前,以及舰艇机动的过程中,我们需要对舰艇的运动要素,进行一个汇算。在这过程中我们就要用到,舰操绘算图。这个舰操绘算图,是各国海军,通用的一个汇算手段。它是由两个圆组成,外面的数字代表的是航向,里面的数字代表的是方位。我们假设目标舰航向是,340 18节,我们在图上标绘一下。假设目标舰艇在我舰的,方位是090 10海里,我们在图上标绘一下。我们将方位与圆心,做一个连线,将这个连线平移至,平移至我们的航向线上。假设说我舰以32节的速度,我们在32节的速度上。
|
||||
|
||||
|
||||
## 【解说9】
|
||||
|
||||
这番讲解听起来很复杂,但核心原理,却是基于四则运算。已知我舰航向、速度,及敌舰相对位置等数据,通过手绘标注与比例尺测算,就能推断出敌舰的航行方向、速度,进而判断其战术意图。舰操绘算图,几乎是我们值更官人手必备的一项技能。在我们与目标舰艇进行交锋、博弈的时候,我们彼此之间都在进行一个绘算。那么谁进行一个更加快速的绘算,谁就会占领一个先机和主动权。
|
||||
|
||||
|
||||
## 【解说10】
|
||||
|
||||
舰上航海部门每天都会安排舰员手绘海图,用铅笔仔细标注舰艇实时位置、预设航线,每一笔都工整清晰、精准无误。明明有先进的电子海图系统,为何还要坚持手绘呢?
|
||||
|
||||
|
||||
## 【专家4】
|
||||
|
||||
电子海图目前来说,固然是比较先进和发达了,但是我觉得我们,执行任务的过程中,是需要有一个底线思维的,是需要有保底手段的。电子产品很有可能,比如说像全舰失电了,或者说受到干扰了,那电子海图就没有办法使用了,这个时候就需要,我们手动进行标绘,这相当于说,一个双备份一个手段。更重要的是,手绘海图的过程,也是舰员熟悉海况的过程。不仅如此,在一些特殊的通信受限场景下,绘算的结果还能通过灯光、旗语,传递给编队的其他舰艇。可以说先进的技术,是战斗力的倍增器,但传统的技能,也是保底的压舱石。在风帆战舰时期,我们常常看到水手,握着巨大的舵轮,驾驶木质帆船驰骋在大海上。而现代战舰,体形远比风帆战舰更庞大,尤其是航空母舰,以绝对体量,堪称海上“巨无霸”,也正因这份庞然。
|
||||
|
||||
|
||||
## 【解说11】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
“海上巨无霸” 的 “迷你方向盘”
|
||||
|
||||
|
||||
## 【解说12】
|
||||
|
||||
不少观众心生好奇,驾驭这样一艘巨舰,它的“方向盘”,会不会也是庞然大物呢?答案或许会颠覆你的想象。走进山东舰驾驶室,映入眼帘的“方向盘”,竟比家用轿车的还要小一圈,样式简约,手感轻盈,单手就能轻松转动。要知道,山东舰的体量,堪比一座20层楼高的海上城市,舰体拥有三千多个舱室,满载重量,相当于数万辆家用轿车。如此庞大的身躯,竟靠一个小小的“方向盘”,掌控方向,这份反差令人直呼意外。
|
||||
|
||||
|
||||
## 【主持人3】
|
||||
|
||||
我们很难想象,如此规模庞大的航空母舰,依靠一个小小的舵轮,就可以实现轻松地转向。
|
||||
|
||||
|
||||
## 【专家5】
|
||||
|
||||
是的没错,这个就是我们这个日常操作的舵轮。它在上面有小的刻度尺,当我们值更官给我们下达这个指令,比如说右舵10的时候,我们只需要把这个舵轮打到10度。您看,这地方有这个刻度,那么我们的舰艇就会逐渐地向右转向。如果是我们要把这个航向操作得更准的话,比如说我们在放那个舰载机的时候,它这个舰载机是有放飞条件的,比如说航向航速之类的,这方面就要考验到我们这个操舵员的技术了。所以说我们这个操舵员就必须要很精确地去把这个航向给稳定住。但是如何去稳定这个航向呢?那么我们需将舵轮反压,舰艇在这个海上滑行的时候,它是有一个惯性的,不像我们日常生活中的车一样。所以说我们反压之后,是为了去抵消这个舰体的一个惯性,然后才能够稳定到我们那个航向上面。
|
||||
|
||||
|
||||
## 【解说13】
|
||||
|
||||
与此同时,山东舰采用先进的电传操控系统,舵轮本质上只是一个信号输入装置,转动舵轮的力度、角度,会被传感器精准转化为电信号,传递至舰尾舵机。可以说,小小的舵轮,哪怕是一度的微调,都能让万吨巨舰在海上完成灵活转向,适配复杂海域航行需求。
|
||||
|
||||
|
||||
## 【专家6】
|
||||
|
||||
我们操作兵在操作航母的时候,不是靠的蛮力,而是靠着我们平时积累的经验,以及对值更官口令的,一个精准地执行,这样我们在海上,才能够精确地去操纵,六万多吨的航母,能够安全地去航行。
|
||||
|
||||
|
||||
## 【解说14】
|
||||
|
||||
不过,航母还有一个极具辨识度的设计特征,它采用全通式飞行甲板,舰桥被巧妙地布置在甲板的一侧。这一设计能最大限度释放飞行甲板空间,满足舰载机起降调度的核心需求,但也让航母的操控中枢随之偏向一侧。不同于常规船舶的居中驾驶布局,航母驾驶员的操作席位设置在右侧舰桥内,视野和操控参照系都与传统舰船截然不同。这种非对称的操控环境,无疑给驾驶带来了更高的技术挑战。由于我们要放飞舰载机,所以说我们的舰岛,以及我们这个操作的,都是在靠右舷。在我们实际上进行执行的时候,我们前面有一根那个天线,有一个口令,叫作跟前舰走的一个口令。当我们值更官给我们下达这个口令的时候,我们会以前面的这个天线为参照物,前面的天线对准前面舰体的主桅,然后由主桅往后延伸,到我们这个天线,到人眼这个位置,三点一线,这样我们就可以把这个航向给走直了。更有意思的是,这个迷你舵轮还配备了多种模式。在开阔海域航行时,可以切换到自动导航模式,舰艇会按照预设航线行驶;在复杂海域或者靠港时,又能切换到手动模式,进行巨舰的“绣花式”操作。
|
||||
|
||||
甲板上的 “小个子 大力士”
|
||||
|
||||
|
||||
## 【解说15】
|
||||
|
||||
提及山东舰甲板,大家脑海中浮现的,往往是歼-15舰载机腾空而起的雄姿。这些舰载机每架重达三十多吨,堪称空中巨兽。但鲜有人知,在航母甲板上移动这些“巨兽”的,竟是一些身材低矮、体重不足一吨的“小个子”——无杆牵引车。
|
||||
|
||||
|
||||
## 【专家7】
|
||||
|
||||
这台小装备是无杆牵引车,它的整体很小,而且无杆牵引车一共有四个轮胎。这是无杆牵引车的驱动轮,它的驱动轮只有我膝盖一边高,而且无杆牵引车的底盘很低,在满足甲板通过性的同时,确保舰载机的转运安全,保护舰载机的前起落架。这是无杆牵引车的转向轮,那么无杆牵引车是如何实现转运作业的呢?请看我身后的这个装置,是将无杆牵引车和飞机连接,通过它将舰载机的前轮抱起,进一步实现转运作业。
|
||||
|
||||
|
||||
## 【解说16】
|
||||
|
||||
别看它们个头小,力气却大得惊人。航母空间寸土寸金,舰载机停放间距仅数米,要将几十吨重的战机,精准停放到指定位置,难度堪比在针尖上跳舞。而这些“小个子”,能轻松拉动歼-15舰载机,在狭窄的甲板空间灵活转弯。
|
||||
|
||||
|
||||
## 【专家8】
|
||||
|
||||
无杆牵引车的操作很简单,如果向前拉时,驾驶员可以正坐在驾驶舱内;如果向后拉,可以将座椅180度旋转后,向后进行顶推舰载机。大家看我们车,有两套刹车和油门装置,可以实现驾驶员从前拉,和向后推的过程。
|
||||
|
||||
|
||||
## 【解说17】
|
||||
|
||||
与航母上的歼-15舰载机体形不同,两栖攻击舰搭载的直升机体形更为低矮,机身腹部与甲板间隙更小,常规无杆牵引车很难抱住起落架。在湖北舰飞行甲板上,我们见到了一种外形更呆萌的专用装备——舰载直升机牵引车,因身形方正,宛如一个四四方方的移动托盘,被舰员们亲切地称为“甲板小黄车”。
|
||||
|
||||
|
||||
## 【专家9】
|
||||
|
||||
在我们的舰上,如何调运十几吨的直升机?就是我们眼前这辆,075型舰载直升机的牵引车。用几句话来讲,它的整个特点就是体积小巧、力量大、操作便捷。第一,体积小巧,因为我们的直升机,它的底盘比较低,直升机的机型不同,它每个机型的底盘高低也不同,为了适配较低底盘的直升机,它设计得就比较小巧。然后我们说这个牵引车力量大,就是因为我们前面这个举升机构,它可以轻而易举地将我们十几吨的直升机举起,然后完成我们的直升机的转运。
|
||||
|
||||
|
||||
## 【解说18】
|
||||
|
||||
因为对牵引车的整体高度有很大限制,那么传统的内燃机显然不适合作为配套,毕竟大功率的发动机体积更大。而小功率的内燃机虽然体积小,但是输出功率也小,无力牵引直升机,所以采用电动驱动更为合适。
|
||||
|
||||
|
||||
## 【专家10】
|
||||
|
||||
说它操作方便,就是用我们这个遥控盒,就像家里的玩具遥控车一样,简单的几个按钮,就可以轻松地操纵我们的牵引车,完成对飞机的一系列的操作。像这样子简单地往前、往后,同时它可以原地左右进行旋转。可以看出,我们的牵引车,它的操作是十分方便的。
|
||||
|
||||
|
||||
## 【解说19】
|
||||
|
||||
大国巨舰上的每一处反差,都不是技术的取舍,而是互补共生。先进技术让巨舰拥有了驰骋深蓝的硬实力,传统技能与基础装备则守住了安全底线,二者相辅相成,共同铸就了人民海军的钢铁长城。
|
||||
|
||||
|
||||
## 【主持人4】
|
||||
|
||||
在今天的节目当中,我们总结了大国巨舰的几个反差:数字化战舰与手绘图纸,信息化战场与旗语密码,巨无霸舰体与迷你“方向盘”,还有舰载机与“小个子大力士”。这些反差看似矛盾,实则是统一的。在这些细节的设计当中,我们看到了设计师的巧思,也感受到了舰员们日常的坚守,还有人民海军从近海走向深蓝的底气。将会有更多先进装备入列,当然也会有更多更有趣的细节反差,等着我们去发现。好了观众朋友们,感谢您持续关注国防军事频道《军事科技》,我们下周同一时间。
|
||||
@@ -0,0 +1,139 @@
|
||||
---
|
||||
标题: 马年装备图鉴
|
||||
编导: 张彤枫
|
||||
播出日期: 2026-02-17
|
||||
类型: 节目文稿
|
||||
期号: ep008
|
||||
---
|
||||
|
||||
# 导视:
|
||||
|
||||
|
||||
## 【导视】
|
||||
|
||||
金戈铁马,龙马精神。2026年,是农历马年。那么在武器装备领域中,有哪些与马相关,或以马命名的经典装备?它们是驰骋大洋的海上“野马”,是纵横疆场的陆战“骏马”,抑或是翱翔天际的空中“飞马”?值此新春佳节之际,《军事科技》带您走近那些骏马疾驰的武器世界。
|
||||
|
||||
|
||||
## 【主持人1】
|
||||
|
||||
各位观众你们好,欢迎收看《军事科技》,我是主持人蓝皓。今天是农历大年初一,万家灯火,共庆新春。在此,我谨代表《军事科技》栏目,向全国观众,致以最诚挚的新春祝福,愿您在新的一年里,如骏马奔腾,勇往直前。2026年是农历马年,马象征着力量、速度与进取。在现代军事装备中,马元素也屡见不鲜。本期节目,我们就来回顾一下,那些以马命名的武器装备。就是从海平面,踏浪而来的,国产726型气垫登陆艇,它被军迷亲切地称为野马。这款装备航速快,载重大,可搭载主战坦克,跨越波涛直抵滩头,正如古代战马驰骋沙场。那么这款踏浪而来的战马,有着怎样的非凡性能呢?
|
||||
|
||||
|
||||
## 【解说1】
|
||||
|
||||
当舰队逼近海岸,如何在敌方火力封锁下,快速完成兵力投送,是两栖突击的核心难题。此时,数艘726型“野马”气垫登陆艇,从母舰舱内缓缓驶出,如海上奔腾的骏马般,划破浪涛,搭载着主战坦克与陆战队员,以极高的速度冲向滩头,完成登陆的“最后一跃”。
|
||||
|
||||
|
||||
## 【专家1】
|
||||
|
||||
早期进行大规模的两栖作战,主要是使用机械化登陆艇。机械化登陆艇,它在靠近岸滩的时候,容易受到障碍物的阻挡。也就是说,它无法让士兵直接实现登陆作战。参与两栖作战的这些士兵,携带的武器和装备是比较重的。所以如果相关的区域内水比较深,那么可能就会出现溺亡的情况。而且在行动的过程当中,容易遭到对方的这种火力打击。有的时候由于相关的沿海和近岸区域,防守的兵力设置了一些拒马,包括其他的这样的一些障碍物,它只能是在靠近岸滩的区域进行这种兵员的投放。但是气垫登陆艇,是完全不同的一种装备。本质上来讲,它并不是传统意义上的这种船艇类的登陆平台,而是一种采用特殊气垫设计理念的这样的一种登陆平台。可以越过滩头阵地上的很多的障碍物,直接把步兵投送到岸滩上,可以让步兵实现鞋不沾水的登陆夺岛作战。而且大型化的气垫登陆艇,运载能力比较强,可以搭载主战坦克装甲车辆,直接把陆军的主战装备投送到滩头阵地。这样的话,在己方火力的掩护和支援之下,能够用最短的时间,把最多的作战人员和作战装备,投送到两栖登陆作战和夺岛作战的最前沿。
|
||||
|
||||
|
||||
## 【动画1】
|
||||
|
||||
野马气垫登陆艇的外形,看起来就像是一个巨大的扁平盒子。中空的船体中,可以容纳主战坦克、装甲车辆或大量士兵。而气垫登陆艇最大的特点,就是在于“气垫”二字。它们不像传统水面舰艇一样,采用全金属或硬质材料的外壳,而是在底部,采用柔性围裙结构的充气气垫。就好像是我们游泳时使用的救生圈一样。为了给这个气垫充气,野马气垫登陆艇的动力系统,也不同于传统舰艇的螺旋桨推进。首先由燃气轮机带动的,不是螺旋桨,而是艇体的升力风扇。升力风扇将大量空气压入艇体底部,使艇身脱离水面或地面约10厘米。而这层柔软的气垫,不仅让“野马”摆脱了水的阻力,还能在松软沙滩、沼泽甚至礁石地形上平稳行驶。就像给登陆艇垫上了一层“空气棉花”,完全不惧怕沙滩上的复杂地形。由于气垫登陆艇是悬浮于水面之上,因此,前进的动力也不能依靠水下螺旋桨,而是依靠艇尾两台巨大的推进风扇产生强大推力,推动登陆艇在海面高速航行。总而言之,野马气垫登陆艇通过升力风扇加推进风扇的组合,实现了武侠世界中的轻功绝学“水上漂”。
|
||||
|
||||
|
||||
## 【解说2】
|
||||
|
||||
726型气垫登陆艇,是我国两栖作战体系的核心投送装备,堪称“超地平线登陆”的踏浪利器。其50至60吨的载重,可灵活搭载一辆99A主战坦克,或两辆两栖步兵战车,也可搭载80名全副武装士兵,实现重型装备与人员的快速输送。依托60至80节高速性能,它能在敌方火力射程外发起突击,两小时内完成近距离海峡跨越,大幅缩短暴露时间。轻松突破沙滩、沼泽等复杂地形,彻底摆脱传统登陆艇的地形与潮汐限制。作为071、075型两栖舰的关键配套,它构建起立体投送网络,同时可承担岛礁补给、应急救援等任务,是攻防兼备的两栖作战中坚力量。
|
||||
|
||||
|
||||
## 【专家2】
|
||||
|
||||
726型野马气垫登陆艇,它的研制成功,尤其是装备相关的部队,能够让我们体系化的这种登陆作战能力,以及岛屿环境当中的作战能力大幅度地提升。为什么这么说?因为我们是采用了这种大小组合的方式,先由大型的海上兵力投送平台,无论是两栖攻击舰,还是船坞登陆舰,把更多的人员装备转运到相关的区域进行集结待命。然后在坞舱当中的野马级的气垫登陆艇,也就是726型的气垫登陆艇,可以迅速地把装备和人员转运到滩头阵地。那么通过这种以大带小的方式,能够实现兵力的快速集结,也可以实现兵力的快速转运,所以这个快字非常重要。也就是说,726型的气垫登陆艇,它和现代化的两栖作战体系它是融为一体的。看完了拥有绝世武功“水上漂”的野马气垫登陆艇,我们再将目光投向万米高空。有一匹会飞的马正在翱翔,它就是安-2运输机。可能您对这个名字有点陌生,但是在我国,它还有一个更广为人知的名字,运-5。运-5运输机的外形短小精悍,仿佛一匹灵巧的草原骏马,所以也被称为马驹。它和我们常见的大型运输机相比,显得小巧许多,造型也有许多不同之处,看起来没有什么科技感。但是,自它1957年首飞以来,始终坚守在军事和民用的航空领域,这款“空中小马驹”。
|
||||
|
||||
|
||||
## 【主持人2】
|
||||
|
||||
有着怎样的独特魅力,能让它在近七十年的岁月中,依旧驰骋于蓝天之上呢?
|
||||
|
||||
|
||||
## 【解说3】
|
||||
|
||||
相较于目前,可以运载数十吨货物的大型运输机,看起来就像是一个没长大的孩子。所以,“马驹”这个称号,生动地描绘了它娇小的身形。但其实,这些新生代的庞然大物,却要称运-5一声前辈。运-5运输机,是中国航空工业史上的标志性机型。1954年,还处在起步阶段的新中国航空工业,从苏联引进了首批9架安-2轻型运输机,随即展开仿制攻关,仅用三年便完成首飞。这款机型,便扎根祖国广袤疆域,在高原、边疆、海岛及偏远山区,持续服役。
|
||||
|
||||
|
||||
## 【专家3】
|
||||
|
||||
运-5它是仿制而来的运输机,可以说皮实耐用,结构简单,非常地靠谱。那么之所以采用仿制的技术,就是早期,我们在运输机的研发方面,缺乏这种足够多的经验,所以我们选择最简单的,最可靠的,或者说在此前应用过程当中,它的表现是非常好的,这样的一个平台进行了仿制,很快就实现了批量列装。为什么说运-5非常地重要,因为它的功能实在太多了,既可以满足日常的人员,物资的运输需求,同样也可以用于训练伞兵,这对于伞兵部队,也就是空降兵部队,来讲是非常重要的。它的勤务保障要求是非常低,而且是一台发动机,那么通过大批量地列装和使用,大家认为,运-5它是一款非常优秀的,这种单发的运输机,那么既可以执行运输任务,也可以用于伞兵的训练。
|
||||
|
||||
|
||||
## 【动画2】
|
||||
|
||||
运-5运输机,和目前主流的大型运输机,有着很多截然不同的设计理念。采用活塞式发动机的它,可以说是一台空中“拖拉机”。这种设计,相比采用涡轮发动机的现代运输机,虽然在飞行速度和载重能力上都处于劣势,但却在维护成本和燃油经济性上,有着巨大的优势。采用双机翼设计的运-5,在飞行时,上下两个机翼都能够产生升力。这种牺牲飞行速度换来的,是极佳的低速飞行性能和短距起降能力。在极限状态下,运-5仅需180米的跑道,即可完成起降。如果说起运-5身上最老派的设计,一定是它的固定式起落架。这种二战时期就已经被淘汰的设计方式,为运-5带来了极高的结构可靠性。同时,不需考虑收起起落架所占用的空间,为本就不大的机体内部,腾出了更多的载物空间。正因如此,运-5可轻松承载1.5吨货物,穿梭于简易跑道之间,成为偏远地区空中运输的可靠选择。
|
||||
|
||||
|
||||
## 【解说4】
|
||||
|
||||
目前运-5,仍是我国通用航空领域的,重要辅助装备,同时在军事训练中,持续发挥价值,凭借180米短距起飞,低速稳定飞行的特性,在多场景中不可替代,在农业领域,它作为绿色战鹰,承担飞播造林与病虫害防治,从内蒙古草原到秦岭山区,精准播撒籽种,或喷施药剂,完成大面积生态作业,军用领域,运-5是空降兵基础跳伞训练的,核心机型,低速稳定的飞行特性,为新兵提供安全的跳伞环境,累计培养数万空降兵骨干,其简易场地适配性,也可满足,边防物资快速投送,应急机动支援等军事需求,运-5服役的数量比较大,另外一个操作非常地简单,不需要,特别复杂的,这样的一个技术保障条件,那么由于,还有一部分运-5,它的发动机,还是处于可靠的状态,所以在它的寿命周期内,还是可以用的,为什么说现在仍然有,一部分运-5,还是可以执行任务的,用于日常的飞行训练任务,包括伞兵的日常训练,它还是合格的,还是能够胜任的,除此之外,它也可以实现军转民,比如说,转为这个农用飞机。
|
||||
|
||||
|
||||
## 【专家4】
|
||||
|
||||
进行飞播造林,或者说进行喷洒农药,所以说目前仍然有一部分运-5,它可以继续地发挥余热。那么如果利用可靠的平台,改成这种运输型的无人机,也是一个很好的选择。
|
||||
|
||||
|
||||
## 【主持人3】
|
||||
|
||||
运-5这款“空中小马驹”,在数十年的服役生涯中,凭借出色的低空飞行性能,极强的短距起降能力,以及极高的可靠性,成为一款老而弥坚的经典机型,在一众大型运输机中,仍然发挥着不可替代的作用。但是,并不是所有以“马”命名的飞机都像“小马驹”一样温顺实用,它们也可以充满野性与力量。如果在空中,有哪一匹“马”称得上是力量担当,那非美国的CH-53直升机莫属了,并且它也有一个霸气的名字——“海上种马”。
|
||||
|
||||
|
||||
## 【解说5】
|
||||
|
||||
20世纪60年代,美军部队面对越南战场上,大量覆盖丛林的山地战场,大量需要依赖地面跑道才能起降的运输机,无法有效投送重型装备与部队。面对前线胶着的战事,直接催生了直升机这种不依赖地形,可以垂直起降的航空装备的发展。此时,美国陆军已经拥有了以CH-47支奴干双旋翼运输直升机等为代表的重型运输直升机,而海军陆战队在这一方面还是空白。当时的海军陆战队决定,自行研制一款属于自己的重型运输直升机,而且性能必须超越陆军现有机型,能吊运火炮、车辆等超重装备。在几番竞标与技术攻关后,西科斯基公司最终中标,由此便开启了属于海军陆战队的“海上种马”时代。
|
||||
|
||||
|
||||
## 【专家5】
|
||||
|
||||
我们客观来讲,CH-47也可以满足美国海军陆战队的这种任务需求,因为它的载重能力,和针对一些轻型火炮的吊运能力是比较强的。如果美国海军陆战队直接使用CH-47,没有任何问题。但是为什么说美国海军陆战队还要发展CH-53?美军的内部它是有军种之争的。那么海军陆战队,虽然说在军迷和网友的眼中被称为“四等人”,但是美国海军陆战队它的作战环境比较特殊,尤其是执行夺岛作战和两栖作战的过程当中,它必须有一个独立的这样的装备体系。这样的话,它可以争取到更多的经费,保证它自身的任务需求,关键的时候是不求人的。那么为什么“超级种马”这款重型的直升机设计是比较独特的呢?就是因为海军陆战队它要求装备在海上长时间部署的时候,有非常强的可靠性,而且对于重型直升机它的长度,以及它所占的两栖攻击舰内部的机库空间的这样的一个面积,要做到一个最小值。所以“超级种马”和CH-47相比,更能满足海军陆战队的需求。
|
||||
|
||||
|
||||
## 【动画3】
|
||||
|
||||
CH-53 “种马” 重型运输机家族,最大的本事就是,力气大,我们要如何量化它的力气呢,“种马”家族中,最强的CH-53K,“超级种马”,配备三台发动机,——共可以输出,约1.8万匹马力,要知道,普通家用轿车的动力,一般在150至200匹马力,也就是说,一架“超级种马”大约相当于,100辆家用轿车的力气,“超级种马”的极限,因为它还预留出了,第四台发动机的位置,所以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年代之后,可以说一直到现在,美军仍然装备大量的“悍马”车,在部队进行大纲深区域内进行机动的时候,它可以发挥重要的运输和保障作用。当然在一些低烈度的这样的一个巡逻任务当中,它也可以有效地掩护步兵,也可以通过安装附加装甲的方式,强化自身针对轻武器的防御能力。所以不仅美军用,很多其他国家也是引进装备和采购。
|
||||
|
||||
|
||||
## 【动画4】
|
||||
|
||||
“悍马”车最大的特点,就是全能。首先,作为一款可以在复杂路况下进行人员运输的车辆,它除了不能飞,不能潜水,几乎做到了全地形。非承载式底盘和大行程独立悬挂,使它的离地间隙达到了40厘米,可以跨越0.6米高的障碍,或者接近一米的水域。其次就是功能全。宽大的车体可以针对不同任务进行模块化改装,配备电子系统可以作为战术指挥车辆,宽大的后排空间可以改造为战场救护车。最重要的是,可以更换武器站,可以在重机枪、榴弹发射器等单兵武器间进行更换,甚至可以加装反坦克导弹,客串一把坦克杀手。加上全车覆盖的轻型装甲,让“悍马”车成为低烈度地面作战的最佳选择,成为廉价版本的多功能装甲车。
|
||||
|
||||
|
||||
## 【解说8】
|
||||
|
||||
“悍马”车并非完美无缺。虽然相较于其他车辆,“悍马”车的防护能力已属上乘,但其本质上还是一款轻型运输车辆,在面对RPG火箭弹或路边炸弹时仍显单薄。并且为了在极端复杂战场环境中保持高机动性,导致“悍马”车的油耗居高不下,又与它经常参与的远距离奔袭和城市作战需求形成矛盾。尤其是在2003年伊拉克战争后,大量的“悍马”车因防护不足而遭袭损毁,从而促使美军加速推进联合轻型战术车辆项目的研发。“悍马”车逐步退出一线作战序列,转而承担后勤运输、基地巡逻等低风险任务。其经典形象则通过民用版H1,化作了一个时代的标志。
|
||||
|
||||
|
||||
## 【专家8】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【主持人5】
|
||||
|
||||
看完了这么多,以“马”命名的装备,不难发现,它们虽然形态各异,却都承载着人类对速度、力量与自由的永恒追求。无论是运-5的坚韧执着,还是CH-53的雄健霸气,还是“悍马”的无畏前行,这些机械身躯,都延续了马这种动物的特点。那么在节目的最后,《军事科技》栏目全体成员,武器装备中的“骏马”,祝您在新的一年,一马当先,马到成功,马年吉祥。好了观众朋友们,感谢您持续关注,国防军事频道《军事科技》,我们下周同一时间。
|
||||
@@ -0,0 +1,139 @@
|
||||
---
|
||||
标题: 重金造“神器”还是造“摆设”
|
||||
编导: 付天雨
|
||||
播出日期: 2026-02-24
|
||||
类型: 节目文稿
|
||||
期号: ep009
|
||||
---
|
||||
|
||||
# 军武“翻车”记
|
||||
|
||||
|
||||
## 【导视1】
|
||||
|
||||
在全球军工范畴内,那些造价不菲,技术炫酷的武器装备,本应是纵横战场的硬核强者。偏偏存在那么一批天价装备,它们没有在对手面前展现过几次风采,反倒先让自身陷入了困境。有些服役数年,却无法投入实战;有些经历数十年研发,最终沦为摆设。这些荒诞的失误表现,堪称军工史上的尴尬时刻。本期《军事科技》,将与您一同盘点那些“出道即折戟”的天价装备,揭开它们光鲜外表下的致命硬伤。
|
||||
|
||||
|
||||
## 【主持人1】
|
||||
|
||||
各位观众你们好,欢迎收看《军事科技》,我是主持人蓝皓。在人类军工发展的历史长河中,从不缺少叱咤风云,威震八方的传奇装备。但是与此同时,也有一批天价装备,它们带着黑科技,划时代的耀眼光环诞生,花费数以亿计的资金,凝聚着数年乃至数十年的心血,最终,却因种种荒诞的设计缺陷,决策失误,沦为军工史上,令人哭笑不得的“翻车”典型。
|
||||
|
||||
|
||||
## 【解说1】
|
||||
|
||||
我们首先来看,当今世界上,造价最为高昂的核动力航母,美国福特号,作为美国海军,下一代航母的标杆之作,福特号被寄予了,领跑全球航母技术,巩固海上霸权的厚望。舰上汇集了电磁弹射,新一代核反应堆,综合电力系统等多项前沿技术。美国海军希望,它能全面超越尼米兹级,成为未来远洋作战的核心力量。可是,就是这样一款,被捧上神坛的超级航母,却在服役之后,彻底走向了预期的反面。福特号航母的设计架构,可追溯到,1988年启动的,CVN-21未来航母计划,其核心目标在于,打造一款全方位超越尼米兹级的,新一代核动力航母,以达成战力跃升,成本可控,寿命延长这三大诉求。为实现这一战略目标,新一代核动力航母,明确将电磁弹射系统,先进阻拦装置,双波段雷达,全电推进系统,列为核心技术标配。这四大关键技术,共同构建起核心战力。其中,电磁弹射系统,是新航母的关键创新所在。与尼米兹级的蒸汽弹射器相比,它无需提前储备蒸汽,能够通过电力精确调节力度,以适配轻重不同的舰载机,与无人机。同时,它还能大幅提高舰载机的,日均出动量,使效率得到显著提升。此外,该系统结构更为简单,零部件减少一半以上,这将大幅降低维护成本,和人力需求,使航母操作人员,缩减至两千人左右。
|
||||
|
||||
|
||||
## 【专家1】
|
||||
|
||||
福特号核动力航母,也就是福特级核动力航母,确实是当时美国海军求新求变的一种大型的海上主力作战平台。除了电磁弹射器之外,它也设计了电磁弹药升降机和电磁的拦阻装置。除此之外,它的舰岛进行了进一步地优化,个头变得更小,采用了隐身化的设计理念。而且,内部结构其实也是出现了很大的变化,因为这个福特级核动力航母,它所搭载的舰载机的种类会变得更多。除了超级大黄蜂之外,还有F-35C,针对不同的这种舰载机和无人机,进行这种勤务保障。从航母的研发和制造来讲,当时福特号航母提出的这样的一些技术标准和设计标准,可以说是比较领先的。那么对于美国的造船体系,对于美国的设计单位来讲,其实也是一个难题。
|
||||
|
||||
|
||||
## 【解说2】
|
||||
|
||||
2017年5月31日,福特号举办了一场盛大的服役仪式。当时的媒体盛赞它是工程与军事领域的巅峰之作。这份光辉消逝得也很迅速。随着海上测试和部署工作的推进,这艘耗费130多亿美元打造的“海上巨无霸”,彻底沦为“问题航母”。其核心系统频繁出现故障,实战能力严重下滑,甚至遭到了美国国会的质疑,认为它“不具备完整作战能力”。首先是电磁弹射系统。美国海军设定的设计指标为平均弹射4166次才出现一次严重故障。然而,在2018年海试期间,仅弹射747次便出现了10次故障,平均故障间隔仅有181次,这还不及设计标准的1/23。更为关键的是,电磁弹射系统实际最大弹射重量仅为25吨,远远达不到设计的41吨标准。这就导致F-35C无法在满油满弹的情况下使用,只能勉强让超级大黄蜂凑数。其次,与电磁弹射系统配套的阻拦装置,情况也不乐观。按照设计,该阻拦装置能够实现16500次阻拦无故障。但实际测试时,仅拦阻48次就出现问题,故障率是预期的三百多倍。即便到了2025年,最新数据依旧不乐观,平均每拦阻460次就会出现状况,其性能远不如老款液压挡阻索可靠。除此之外,升降机、动力也频繁出现故障。截至2026年初,福特号核动力航空母舰已服役近九年。服役期间,该舰多次遭遇设备故障与系统问题,不得不提前中断部署任务,返回诺福克海军造船厂实施大修与整改。福特号累计停场维修时长已突破20个月,整改费用更是超过15亿美元。
|
||||
|
||||
|
||||
## 【专家2】
|
||||
|
||||
从设计的理念来看,福特号核动力航母,或者说同一级别航母是没有问题的。那么之所以出现了各种各样不靠谱的情况,是由于美国造船厂整体的这种制造能力出现了严重的下滑。也就是说,技术并没有按照它的设计标准进行同步提升,甚至出现了一些比较严重的人才流失。像一些专业的技术人员,或者说有经验的这种造船工人,已经是严重不足。所以在制造的过程当中,并没有达到它预想的标准,电磁弹射器频繁地出故障,包括一些供电的设备,以及其他的一些子系统,也出现了不靠谱的情况。所以,福特级核动力航母这个标准提得很高,而且也是想要最大程度地求新求变。但是美国制造业整体的这种实力的下滑,和造船体系的空心化,导致它的这样的一个工业基础和造船体系,无法支撑起福特号的这种高标准,出现了各种各样不靠谱的情况。
|
||||
|
||||
|
||||
## 【解说3】
|
||||
|
||||
如今的福特号,成了诺福克海军基地的常驻嘉宾,日常状态不是在维修,就是在等待维修零件,百亿美金下去,只换来一个港口“吉祥物”。
|
||||
|
||||
|
||||
## 【主持人2】
|
||||
|
||||
如果说“福特”号航母,是因技术难题,而频繁受挫的典型案例,那么印度的“阿琼”主战坦克,则是一部“三十年磨一剑,终究磨出一把钝刀”的军工辛酸史。20世纪80年代,壮志满怀的印度,立志打造一款能比肩,甚至超越美苏主流型号的国产王牌主战坦克,决心在陆战装备领域崭露头角。无奈理想很丰满,现实却很骨感,这款承载着印度军工殷切期望的“国产神车”,其研发过程,堪称一部高开低走的窘迫实录。从项目立项到列装部队,整整耗费了30年时间,投入了45亿美元巨资,硬生生创下了全球主战坦克研发周期最长的尴尬纪录。
|
||||
|
||||
|
||||
## 【解说4】
|
||||
|
||||
这是一场自1974年开启的豪赌。其核心目标在于打造一款第三代主战坦克。此坦克需全面超越苏制T-72,且能与美制M1艾布拉姆斯相媲美,以达成火力强劲、防护出众、完全国产这三大诉求。为实现这一宏伟愿景,这款承载着印度军工自主梦想的坦克,明确将国产120毫米线膛炮、自研复合装甲、大功率柴油发动机、数字化火控系统列为核心技术的标配。这四项关键技术,共同被给予了构筑印度陆军下一代核心战力的厚望。
|
||||
|
||||
|
||||
## 【专家3】
|
||||
|
||||
阿琼主战坦克的创新,体现在印度陆军,要用这种所谓的国产坦克,要替代它以前,大批量装备的苏系主战坦克。也就是说,要实现针对战场目标的快速识别,要使用更先进的火炮系统,针对目标进行快速的打击,而且要提升这个防护能力。所以阿琼坦克,是印度陆军试图摆脱,苏系俄系坦克,转而追求欧洲标准的一个产物。
|
||||
|
||||
|
||||
## 【解说5】
|
||||
|
||||
2004年8月,阿琼主战坦克,正式被引进印度陆军编制。彼时媒体纷纷盛赞它为“南亚陆战之王”,视作印度军工,自主研发领域的巅峰之作。然而,这份荣耀如昙花一现,随着列装进程的推进,与训练的深入,这款耗费30年研发,投入超过45亿美元打造的国产王牌,彻底沦为了问题坦克。问题出在,其核心120毫米线膛炮。印度军工设定的设计指标,为炮管寿命达到1000发,实际测试时,仅发射200发炮弹,就出现了炮管线膛磨损、炮口炸裂等故障,寿命不及设计标准的1/5。更要命的是,这款主炮配备的自研穿甲弹,实际穿深仅为350毫米,均质钢装甲,远远达不到设计的500毫米标准。这就导致阿琼坦克,面对主流主战坦克时,根本不具备正面击穿能力,只能勉强用高爆弹,执行支援任务。
|
||||
|
||||
|
||||
## 【专家4】
|
||||
|
||||
阿琼主战坦克的终极改进型号,也就是印度陆军能够拿到的最先进的型号,它的全车的重量接近69吨。那么,对于保障体系而言,它会变成一个很笨重的钢铁怪物。此外,阿琼主战坦克现在已经出现了发动机功率无法满足它笨重车体的这样的一个情况。如果是在高温环境下长时间的运行,可能也会出现整个的车体出现过热的情况,发动机的可靠性是下降的。虽然说它是采用了德国设计的这种先进的柴油机,但是和车体的适配性一直有问题。此外就是它的装甲,并不像印度陆军所吹嘘的那么先进。
|
||||
|
||||
|
||||
## 【解说6】
|
||||
|
||||
如今这款坦克,大多时候仅作为阅兵式上的“花瓶”存在。鉴于其维护难度大,且实用性欠佳,它被长期搁置在仓库里。
|
||||
|
||||
|
||||
## 【主持人3】
|
||||
|
||||
如果说印度的军工发展之路,万国牌拼凑的“翻车”闹剧,那么韩国的KF-21“猎鹰”战斗机,无疑是将打肿脸充胖子的戏码演绎到了极致。韩国喊出了自研五代机的口号,其不仅立志打造一款能与F-22、歼-20相媲美的战斗机,还拉拢印尼共同分担高达60亿美元的研发费用。这款被寄予厚望的国产神机,最终却沦为了航空史上伪五代机的典型案例。
|
||||
|
||||
|
||||
## 【解说7】
|
||||
|
||||
2022年7月19日,一架与美军F-22隐身战机颇为相似的战机,从位于韩国庆尚南道泗川市的一座空军机场起飞。战机在空中飞行33分钟后成功降落。飞行结束后,这架战机获得韩国总统称赞。韩方认为,这是韩国国防独立取得的重大成就。同时,韩媒宣称,此次飞行成功,意味着韩国成为继美国、中国和俄罗斯之后,全球第四个能制造超音速隐形战斗机的国家。
|
||||
|
||||
|
||||
## 【专家5】
|
||||
|
||||
韩国之所以要研制KF-21,这种双发中型的隐身战斗机。
|
||||
|
||||
|
||||
## 【解说8】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【专家6】
|
||||
|
||||
就是要在航空工业体系方面,要有一个比较过硬的产品。当然了,它这个技术指标,定得不是特别高。韩国对于KF-21,它的定位是一种中低端的隐身战斗机,能够满足它自身的这种使用需求。那么面对对方不太先进的防空预警体系,对方不太先进的战斗机,让它们看不见就可以了。此外对外出口的时候,它的价格非常低,要比现在其他国家对外推销和出售的隐身战斗机,这个价格都要低很多。自认为可以采用低价的方式,能够找到更多的潜在客户和这种国际合作伙伴。
|
||||
|
||||
|
||||
## 【解说9】
|
||||
|
||||
三年多的时光已然流逝,6架原型机历经42个月,圆满完成1600余架次试飞,以及1.3万余项试验科目,并且计划于今年下半年开始量产。这看似成功迈出了,从原型机,到列装部队的关键一步。当初被韩媒大肆吹嘘的,隐身五代机定位,早已被韩国官方,悄然修正为4.5代战机。这款外形酷似F-22的战机,终究未能突破核心的,隐身技术壁垒。真正的隐形战斗机,是对空气动力学,与电磁学的双重极致挑战。首先在外形设计方面,必须犹如经过精密计算的,偏转镜一般,将敌方雷达波,巧妙地散射至非探测方向,使电磁波无法形成有效反射。这种对机身线条的严格打磨,乃是隐身的第一道防线。然而相较于外形设计,更为困难的是“五脏六腑”的布局。内置弹舱,这堪称五代机的身份证。一旦导弹采用外挂方式,巨大的雷达反射截面,便会瞬间戳破隐身伪装。F-22与歼-20均拥有经典的,一主两侧弹舱布局,而俄制苏-57,则采用独立的串列主弹舱,配合茧包式格斗弹舱。反观韩国的KF-21,它在这一核心指标上,做出了妥协。它并未采用内置弹舱的设计,而是直接在翼下,设置了6个翼下挂点,以及4个机腹半埋式挂点。这种设计让它从隐身刺客,摇身一变成了,半遮半掩的弓箭手。那么KF-21,它最大的问题,就是设计思路比较保守。我们可以看到,F-22猛禽隐身战斗机的影子,还可以看到,KF-21借鉴,F-35隐身战斗机的,这样的一些特征。但是,韩国他的航空工业体系,并不是特别完善。采用的是两台,这种中等推力的发动机。而且,KF-21这个机身的尺寸,也并不是特别小。在这样的一种状态下,KF-21 它服役之后,就已经不是特别先进了。在此基础上,其实对它进行升级,完善和改进,并不能很大程度上地,提升作战能力。甚至想要改出一个内置弹舱,韩国都需要更多的,过渡时间。现在韩国对于KF-21的定义,是4.5代战斗机。但是由于韩国,在隐身战斗机的设计方面,以前是缺乏基础的,那么很多的体系都是要从零起步,进行配套研发和构建。所以他相当于花了五代机的,一个研发和制造经费,最后搞出了一个4.5代的,半吊子隐身战斗机。尽管KF-21,已经变更成为了4.5代战机,先天的设计缺陷,与核心技术的缺失,让它即便在四代半战机的阵营里,也难言出众。无内置弹舱的隐身硬伤,高度依赖海外的供应链,高成本低性能的尴尬,终究让这款战机,成了一款既达不到五代机标准,又在四代半领域,缺乏竞争力的平庸之作。如果说KF-21的失利,是源于技术底气匮乏,那么法国。
|
||||
|
||||
|
||||
## 【主持人4】
|
||||
|
||||
勒克莱尔坦克面临的困境,则是财大气粗、肆意而为的体现。20世纪80年代,凭借深厚军工底蕴的法国,立志要摆脱AMX-30坦克“皮薄馅大”的指责,决心打造一款在全球领先的新一代主战坦克。然而,最终耗费百亿资金打造的“法兰西骄傲”,却一直问题频发。
|
||||
|
||||
|
||||
## 【解说10】
|
||||
|
||||
1986年,一场围绕坦克的革新,在法国正式拉开帷幕。其目标,是打造一款超越德国豹-2,媲美美国M1的坦克,重塑欧洲陆战装备的实力标准。作为全球首款,真正意义上的全数字化主战坦克,勒克莱尔的信息大脑,堪称划时代的技术突破。它开创性地集成了,战场数据链,实时导航,敌我识别,故障诊断等多项核心功能,能够将自身侦察获取的精确数据,与友军单位,指挥中枢,实现无缝对接与实时共享。这一革命性的设计,使乘员,能够实时掌握全域战场态势,彻底突破传统坦克,孤军奋战的战术局限,成功引领主战坦克,步入体系化协同作战的,全新时代。
|
||||
|
||||
|
||||
## 【专家7】
|
||||
|
||||
设计勒克莱尔主战坦克的时候,法国是吸取了,二战期间的一些教训,认为主战坦克不能太重,一定要强调它自身的机动性,所以勒克莱尔主战坦克,在当时有两方面的,这样的一些比较先进的设计理念,第一方面,就是车体比较轻,而且车体比较短,无论是运输还是作战使用,那么对它进行保障,和维护比较简单,机动性比较强,在战场上跑得很快,而且法国的一些火控系统,当时也是比较先进的,可以实现,针对对方的坦克和装甲车辆,进行快速射击,然后快打快撤,此外勒克莱尔主战坦克,它在后续发展的过程当中,在升级的过程当中,非常注重,这种信息化和数字化的,这样的一些系统的融入,它可以更好地去接收,不同作战平台的信息,那么针对战场上的,数据的处理能力比较强。
|
||||
|
||||
|
||||
## 【解说11】
|
||||
|
||||
1993年,勒克莱尔主战坦克,正式装备法国陆军。凭借全球首款,数字化坦克的荣耀光环,它被媒体盛赞为,第三代坦克先驱。阿联酋,以单价超过1000万美元的高价,一口气订购了388辆,让这款坦克一时间风头无两,俨然成为欧洲军工的骄傲。这份高光时刻并未持续太久。随着实战部署,以及时间的推移,这款造价是德国豹-2两倍的天价坦克,开始暴露出层出不穷的问题,辉煌背后的隐患彻底爆发。首先崩溃的,是核心动力系统。它搭载的,新型柴油机组组合结构过于复杂,故障频发。实际发动机无故障间隔,仅为设计值的60%,多次在军演中突然抛锚。尽管法国,早在1995年,就启动了首批改进计划,但却未能触及结构缺陷的根源。紧接着,曾经引以为傲的数字化大脑,也出现了问题。早期火控计算机,战场数据链频繁死机,尤其在沙漠环境中,沙尘极易导致电路板短路,平均每10小时,就出现一次电子故障。更致命的是,数字化系统维护,需要专用设备与技术,基层维修单位,根本无法独立处理,故障坦克只能长期搁置。
|
||||
|
||||
|
||||
## 【专家8】
|
||||
|
||||
法国陆军装备的勒克莱尔主战坦克,以及中东地区用户所使用的勒克莱尔主战坦克,也爆出了很多的问题。比如说,空调性能无法满足炎热环境的这样的一个使用需求,一些动力系统也经常地出故障。我认为,这是勒克莱尔主战坦克,它在后续升级,或者说它在完善的过程当中,很多核心系统,并没有进行有效的匹配,它导致的问题。那么后续无论是动力系统,还是其他的一些方面出现的问题,主要是这个主战坦克的升级和改造,没有跟上它的这种使用环境的这种变化的这样的一个需求。也就是说,如果没有办法对它进行优化,对它进行完善,又出现了一些全新的使用环境,和不同的这样的一个任务需求,那么就会出现平台无法适应任务环境的情况。
|
||||
|
||||
|
||||
## 【解说12】
|
||||
|
||||
如今,勒克莱尔已彻底告别了昔日的高光时刻。多数坦克静置于库房之中,尘封已久,积满灰尘。法国更计划于2030年前,将其现役数量削减至200辆以下,加快其退出主力序列的步伐。
|
||||
|
||||
|
||||
## 【主持人5】
|
||||
|
||||
从美军的福特号航母,到韩国的KF-21战斗机,这些曾经头顶黑科技、划时代耀眼光环的天价装备,如今早已褪去往昔荣光,沦为被各类问题紧紧纠缠的麻烦制造者。维护费用如流水般持续消耗,更是如同家常便饭一般,拖慢进程。当初投入的数十亿,乃至上百亿美元真金白银,反倒成了为华而不实付出的一笔笔冤枉钱。而这,也恰好揭示了一个朴素,却无可辩驳的道理:武器装备的核心价值,永远在于实战。再炫酷的纸面数据,再高昂离谱的造价,一旦脱离了战场需求本质,背离了实战的基本准则,终究只会沦为持续吞噬资金的怪兽。好了,观众朋友们,感谢您持续关注国防军事频道《军事科技》,我们下周同一时间。
|
||||
@@ -0,0 +1,114 @@
|
||||
---
|
||||
标题: 枪械射速决定论
|
||||
编导: 张彤枫
|
||||
播出日期: 2026-03-10
|
||||
类型: 节目文稿
|
||||
期号: ep010
|
||||
---
|
||||
|
||||
# 导视:
|
||||
|
||||
|
||||
## 【导视1】
|
||||
|
||||
对于枪械而言,很多设计思路,都可以影响枪械的性能。但其中非常重要的一个指标,就是射速。在众多类型的枪械中,射速最快的枪械都是哪些?它们是如何实现更快的射速?更高的射速对它们来说,又有着怎样的意义呢?《军事科技》带您探究,枪械射速决定论。各位观众你们好,欢迎收看《军事科技》,我是主持人蓝皓。在瞬息万变的战场上,火力的压制与覆盖,往往能决定战斗的走向。枪械射速的快慢,直接关系到,单位时间内,弹药投送的密度,和对目标的打击效率。究竟哪些枪械,能够称得上,是射速领域的佼佼者呢?它们又凭借着怎样的设计奥秘,实现了令人惊叹的高射速呢?
|
||||
|
||||
|
||||
## 【主持人1】
|
||||
|
||||
首先我们来看看手枪,手枪作为近距离自卫和辅助作战的武器,并不会像步枪和机枪一样,追求极高的射速。尤其是早期的左轮手枪,其射速,很大程度上,依赖于射手的熟练度。那么,现代的手枪,是通过什么方式,去提升射速呢?
|
||||
|
||||
|
||||
## 【解说1】
|
||||
|
||||
在早期热武器时代,无论是燃发枪还是火绳枪,都因为需要将火药和弹丸,通过枪口进行装填,所以导致它们的射速,受到极大的限制。在这一时期,前膛枪的射速,一般在每分钟三发,极其熟练的射手,也仅能做到每分钟五发。所以我们会看到,在两军对垒时,双方都会排着整齐的密集阵型,进行射击,通过密集的齐射,弥补单支枪械的射速劣势。随着枪械技术的发展,后装枪的出现,使得装填速度,有了一定提升,同时定装弹药的普及,也让装填流程大幅缩减。作为这一时期的代表武器,左轮手枪,可以一次性装填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】
|
||||
|
||||
因其独特的全自动模式,成为手枪家族射速的担当。最高每分钟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发。水冷式机枪水套筒重,增加枪械整体重量,机动性差,且依赖水源,在缺水或需快速转移的战场受限大。风冷式机枪依靠枪管自身散热及更换枪管解决过热问题,取消水套筒,通过设置大量散热片增加与空气接触面积、加快散热。枪管温度过高时,射手可以迅速更换备用枪管恢复射击能力。比如MG42通用机枪,采用风冷式设计,理论射速高达每分钟1200发,兼具高射速和良好机动性。在二战期间,机枪可以说是针对对方步兵进行火力压制的一款主要的武器。那么制约机枪射速的方式,可能和冷却的这样的一个模式和设计理念存在密切关联。像马克沁机枪,它是采用水冷的模式,所以长时间连续射击的时候,枪管不会因为过热问题而出现故障,导致它无法开火。针对对方的冲锋的兵力或者是进攻的兵力进行压制,不需要频繁地更换枪管,这个优势是非常明显的。但是像MG42,它是采用这种风冷的方式,射速又非常地快。所以在运动的过程当中,或者说在进攻作战的过程当中,它可以扮演非常重要的角色。而且MG42虽然会容易出现枪管过热的问题,但是更换枪管速度比较快,操作起来也比较容易。一般这种德军它在操作和使用机枪的过程当中,都是有其他的士兵去进行配合的,帮他去搬运弹药或者说携带枪架。当然了也配备有更换的这样的一个枪管。所以风冷机枪它在使用的过程中,就是要看更换枪管的速度究竟有多快,操作使用是否方便。
|
||||
|
||||
|
||||
## 【解说8】
|
||||
|
||||
无论是风冷式散热,还是水冷式散热,都可以有效地解决连续射击带来的热量问题。通过这种方式,也将机枪的射速推进到每分钟千发以上。但这并不是机枪所追求的射速极限。如果想在每分钟千发的基础上再次提高射速,就需要有更加激进的设计。在种种需求的推动下,枪械射速的巅峰——转管机枪出现了。
|
||||
|
||||
|
||||
## 【动画解说5】
|
||||
|
||||
转管机枪提升射速的方式,简单粗暴。因为单管机枪,连续射击容易过热,设计师便将多枪管捆绑,让其轮流射击,如同机枪手,同时用数挺机枪进行轮换。不过,这种简单逻辑下的产物,是转管机枪体积普遍较大。早期手摇式转管机枪,需要马匹运输。射击时,射手要手摇把,驱动枪管旋转和供弹,射速受摇动速度限制,通常每分钟200至300发。而现代转管机枪,普遍以电动机,或液压马达为动力源,通过齿轮传动系统。
|
||||
|
||||
|
||||
## 【解说9】
|
||||
|
||||
带动多根枪管,绕中心轴高速旋转。每根枪管旋转到特定位置,完成装弹、击发、抛壳等动作。由于多枪管交替工作,每根枪管有足够时间自然冷却,可以使机枪以极高射速持续射击。以M134转管机枪为代表的射速王者,依靠6根枪管循环射击的叠加效果,将机枪的射速推进到每分钟6000发。这意味着在一秒钟内,就足足有100发子弹倾泻而出,形成一道几乎无法逾越的金属弹幕。如此恐怖的射速,使其在对付集群目标、低空慢速目标或进行区域压制时,展现出无与伦比的威力。无论是在直升机舱门、舰艇甲板,还是车载平台上,M134转管机枪都能凭借其火神炮般的咆哮,瞬间覆盖目标区域,给予敌人毁灭性的打击,成为现代战场上令人胆寒的火力象征。那么这种机枪,单兵是没有办法携带的,因为实在是太重了,而且需要有外部的这种供电的设备。往往是在车辆上,比如说在悍马车上,架设这种6管的这样的一个转膛机枪、速射机枪,可以针对对方的步兵进行密集的火力压制,短时间内让对方的步兵出现大量的伤亡。所以,这种利用电力驱动的速射机枪,或者说我们叫它多管机枪,往往是在装甲车上、在直升机上,或者说在一些特种作战的这种冲锋舟和快艇上进行使用,可以在突击作战任务的过程当中,在特种作战行动的过程当中,提供近距离的火力压制能力。
|
||||
|
||||
|
||||
## 【主持人7】
|
||||
|
||||
不同枪型在设计之初,都会将射速作为一个关键要素。手枪提升射速,极大提升了单兵的自卫火力。步枪的不同射击模式,用以适应不同的作战场景。机枪的火力压制,将射速推向了顶点。所以,射速的提升,始终是枪械发展历程中,贯穿始终的核心追求之一。射速的不断突破,不仅是枪械技术进步的直观体现,更深刻地影响着战术思想的演变,和战场格局的塑造。它使得枪械,在不同作战环境下,能够更高效地完成其使命任务,成为士兵手中可靠的战力倍增器。好了观众朋友,感谢您持续关注国防军事频道《军事科技》,我们下周同一时间。
|
||||
@@ -0,0 +1,151 @@
|
||||
---
|
||||
标题: 武器进化论:空战格局的演进之路
|
||||
编导: 付天雨
|
||||
播出日期: 2026-03-17
|
||||
类型: 节目文稿
|
||||
期号: ep011
|
||||
---
|
||||
|
||||
# 武器进化论:空战颠覆者
|
||||
|
||||
|
||||
## 【导视】
|
||||
|
||||
从螺旋桨战机的低空缠斗,到无人机的智能作战,空战武器的每一次革命性突破,都在重塑天空战场的制胜逻辑。当一件装备,彻底打破速度、隐蔽的三角平衡,当它的出现,让延续数十年的空战范式,轰然倒塌,就是天空的规则改写者。这不是装备的简单升级,而是空战法则的,彻底重构。本期《军事科技》,带您翱翔苍穹,揭开空战史上,最震撼的转折点,看这些颠覆性装备,如何定义制空权的归属。
|
||||
|
||||
|
||||
## 【主持人1】
|
||||
|
||||
各位观众你们好,欢迎收看《军事科技》,我是主持人蓝皓。从螺旋桨战机时代,飞行员凭借驾驶技巧,与临场应变展开的近距离空中缠斗、航炮对射,通过智能巡飞与协同作战,实现的零伤亡、全范围精准打击,空战武器的演进,始终围绕着如何在广袤天空中高效打击敌人、保护自己这一核心议题。每一款革命性装备的诞生,并非是简单的性能提升,而是对空战形态的根本变革。
|
||||
|
||||
螺旋桨飞机:空战时代的“开拓者”,近距离缠斗的“定调者”
|
||||
|
||||
|
||||
## 【解说1】
|
||||
|
||||
20世纪初期,飞机的问世,首次把人类的战场,拓展到了天空。起初,飞机只负责执行侦察任务,并未配备专门的作战武器。一战爆发之后,各国着手对飞机,进行武器改装,将机枪和轻型炸弹,安置在机身上。以活塞式螺旋桨为动力的,螺旋桨飞机,就此诞生。真正意义上的空战时代,也正式拉开帷幕。
|
||||
|
||||
|
||||
## 【专家1】
|
||||
|
||||
螺旋桨飞机,之所以被人们称为,真正意义上的空战的到来,其实主要在于它,摆脱了战场辅助作用。在螺旋桨飞机出现之前,人类尝试的航空载体,比如像飞艇、热气球,能承载战场的侦察、通信,传递的一些辅助的任务,本质上也就是,空中的一个观察哨,不存在真正的空战行为。而螺旋桨飞机,依靠活塞式发动机,和螺旋桨推进系统,才真正实现了,真正意义上的空中对抗。同时螺旋桨飞机发展过程中,机枪同步射击装置的发明,是个关键的转折点,它解决了机枪射击,不会击中自身螺旋桨的难题,让飞机拥有了,可实战的机载攻击武器,直接赋予了螺旋桨飞机,空中作战的能力。
|
||||
|
||||
|
||||
## 【解说2】
|
||||
|
||||
第一次世界大战的天空,成为螺旋桨飞机的练兵场。机载机枪的射击精度与射速持续提升,战机的机动性能也在不断优化。横滚、俯冲、筋斗等专属空战动作,成为飞行员的必备技能。同时,人们发现,将机枪安装在飞行员座舱前,射击方向与飞行方向一致时,能获得最佳瞄准精度和火力效果,且便于飞行员操作与排除故障。但是问题随之出现,由于螺旋桨的阻挡,每次射击都有击中自身桨叶的风险。直至枪口协调器的出现,才成功突破了战机机枪射击与螺旋桨旋转的技术瓶颈,让机枪子弹可精准穿过螺旋桨旋转面,无需担心击伤桨叶。1916年,索姆河战役期间,德国福克E.III战斗机凭借其搭载的核心装备射击协调器,该型飞机在短短数周内便击落协约国战机六十余架,创下了震惊战场的空战纪录,后世也将其称之为“福克灾难”。这场战争让各国逐渐意识到制空权的重要性,空战也从单纯的侦察护航,发展为独立的作战单元。螺旋桨飞机不仅开创了近距离缠斗的经典空战模式,更推动人类形成制空权影响战场胜负的核心军事理念,为后续战机动力升级、武器革新及战术体系构建,奠定了坚实的实践与理论基础。
|
||||
|
||||
|
||||
## 【主持人2】
|
||||
|
||||
螺旋桨飞机,将天空开辟为全新战场,以航炮的轰鸣,开启了空战的新纪元。却因速度缓慢,火力薄弱,探测范围有限,使空战始终局限于,近距离的贴身较量。随着战争需求的不断升级,人们开始追求更快的飞行速度,更高的作战高度,更远的打击距离。一种采用全新动力的战机,应运而生,彻底终结了螺旋桨时代,将空战推向,高空高速的新维度。
|
||||
|
||||
喷气式飞机:螺旋桨时代的“终结者”,高空高速空战的“开启者”
|
||||
|
||||
|
||||
## 【解说3】
|
||||
|
||||
第二次世界大战中后期,航空发动机技术迎来革命性突破。喷气式发动机的诞生,彻底打破了活塞式螺旋桨的性能瓶颈。喷气式战机由此登上历史舞台。1944年,德国Me-262喷气式战机首次投入实战,其最大飞行速度突破900公里每小时,远超当时最先进的螺旋桨战机,让各国看到了空战的全新可能。喷气式战机一登场,空战直接换了个时代!它的革命性突破,全靠两大硬核升级:第一,动力彻底革命!喷气发动机不靠螺旋桨,直接靠燃气猛喷往前冲!一下子挣脱了老式飞机的物理天花板,速度狂飙,爬升像火箭,轻松冲破音障,一头扎进超音速世界!直接飞到万米高空的平流层,把螺旋桨战机远远甩在身后。第二,火力与“眼睛”全面进化!大威力航炮取代小机枪,打得更狠、更猛;简易雷达测距仪装上战机,飞行员不再只靠肉眼盲打,看得更远,瞄得更准!空战的精度和范围,直接翻倍升级!这两大突破的合体,使得人类空战正式从螺旋桨时代,冲进了喷气超音速时代!
|
||||
|
||||
|
||||
## 【专家2】
|
||||
|
||||
喷气式飞机的诞生,彻底改变了螺旋桨飞机以近距离的缠斗为主要形式的空战逻辑,迈进了高空高速空战的新时期。第一代喷气式飞机一出现,就达到了高亚音速。螺旋桨飞机,在第一代喷气战机的眼里,还是一个低速的目标,极其好打,战损比也极其高。喷气式飞机的真正出现,就使螺旋桨飞机在争夺制空权的斗争中被淘汰了。随着军事科技,特别是材料工艺、发动机的发展,喷气式飞机基本都达到了高空、高速。高空、高速带来的作战样式的变化,取代了缠斗,变成了高速截击、高空截击、高速突防,使作战样式发生了深刻的变化。
|
||||
|
||||
|
||||
## 【解说4】
|
||||
|
||||
二战落幕之后,喷气式战机进入高速发展的阶段,渐渐代替螺旋桨飞机。1950年朝鲜战争爆发,米格-15战斗机跟随志愿军入朝作战,其出色的性能让美军在朝鲜半岛上空的制空权遭遇了极为严峻的挑战。美国远东空军只能承认,在清川江和鸭绿江之间的空域,米格战机拥有绝对的优势,并把这片区域叫作“米格走廊”。在美国空军的营地里,甚至竖起了一块显眼的标识牌,上面清楚地写着“距离‘米格走廊’200英里”,以此来时刻提醒每一位曾经自视技术高超的美军飞行员。为了改变战场空中形势的不利局面,美国紧急大批量装备新型F-86战斗机,希望与米格-15相抗衡。于是,两款战后第一代经典喷气式战斗机在朝鲜上空不期而遇,正式拉开了人类历史上后掠翼喷气式战斗机空中对决的崭新时代。
|
||||
|
||||
|
||||
## 【主持人3】
|
||||
|
||||
喷气式战机,突破了螺旋桨时代空战的限制,把空战的领域,延伸到了万米高空。超音速飞行速度,和初步的雷达探测功能,使得空中骑士,近距离缠斗的时代,一去不复返了。不过,随着各国对,喷气式战机的不断升级,战机在速度,火力以及升限方面,持续突破,相互之间的性能差异,也慢慢减小,仅仅依赖平台性能的优势,变得越来越不明显。这个时候,一种全新制导武器的成熟运用,完全改变了空战的打击思路,让超视距空战从雏形,变为了现实。
|
||||
|
||||
空空导弹:航炮主导的“破局者”,超视距空战的“核心利器”
|
||||
|
||||
|
||||
## 【解说5】
|
||||
|
||||
20世纪60年代,制导技术与火箭发动机技术迅猛发展,空空导弹完成了从试验品到主战武器的转变,完全打破了航炮主导空战的局面。早期的空空导弹主要是红外制导的近距格斗弹,通过追踪战机尾焰的红外信号来锁定并打击目标,具备了锁定后发射、发射后不用管的作战能力。空空导弹的核心技术突破,集中体现在三大关键跨越。第一,射程实现质的飞跃。从早期仅有数公里的近程格斗,一路拓展到如今数百公里的超视距打击,真正覆盖近距离格斗、中距拦截、远程猎杀全空域,让战机先敌发现、先敌发射、先敌命中。第二,制导精度与抗干扰能力全面升级。红外成像、主动雷达制导等尖端技术的应用,让导弹看得更清、辨得更准、抗干扰更强,哪怕面对复杂电磁环境,依然能够精准锁定、一击制敌。机动性能突破有人机极限。凭借超大过载设计,导弹可以完成飞行员无法承受的高难度机动,灵活追击、快速转向,让空中目标无处可逃。与此同时,空空导弹不断向小型化、轻量化、集成化发展,单架战机可同时挂载多枚,实现多目标跟踪、多方向打击,大幅提升空中作战的持续火力与体系优势。
|
||||
|
||||
|
||||
## 【专家3】
|
||||
|
||||
空空导弹的出现,使以航炮为主要兵器的空战形式,发生了深刻的变化。以往的是视距内的作战,变成了超视距的,航炮成为了一个武器中的配角。航炮的有效射程仅有数百米,传统空战的所有战术,均是围绕着视距内的咬尾攻击展开。飞行员需通过精确的瞄准,抢占敌机的阵位,胜负完全依靠于飞行员的技战术水平。导弹的出现就不一样了,空战的胜负,不再决定于飞行员的技战术水平,其实更重要的,取决于飞行员对座舱设备的掌握,对态势感知的判断,对时机的把握上。
|
||||
|
||||
|
||||
## 【解说6】
|
||||
|
||||
越南战争,成为了检验空空导弹实战能力的关键战场。早期的空空导弹,由于技术尚未成熟,存在一定的脱靶情况,但其打击优势已远超航炮。经过后续的技术改良,空空导弹逐步发展为空战中的主要作战武器。发现即锁定,锁定即摧毁,成为空战的新准则。超视距空战,正式确立为现代空战的主流形式。空战也从单一平台间的对抗,转变为武器与平台协同作战的模式。
|
||||
|
||||
|
||||
## 【主持人4】
|
||||
|
||||
空空导弹,使超视距空战变为了现实,完全重塑了空战的打击思路。然而,伴随着雷达技术,与防空体系的持续进步,战机的探测与反探测,干扰与反干扰,成为了空战新的关键所在。仅仅依赖战机自身的电子设备,已经无法适应,复杂多变的战场电磁环境。一种专为电磁对抗打造的装备,顺势出现,它虽然不具备,直接的火力攻击能力,但却可以成为空战中的,隐形撒手锏,促使空战,迈向体系化作战的新时期。
|
||||
|
||||
电子战飞机:单一平台的“补位者”,体系化空战的“隐形支柱”
|
||||
|
||||
|
||||
## 【解说7】
|
||||
|
||||
20世纪70年代,电子技术迅猛发展,以现役战机为基础,改装而成的电子战飞机,正式亮相历史舞台。它虽未配备传统的火力打击武器,却装配了电子侦察、电子干扰以及电子压制等专业设备,化身为空天战场中的电磁掌控者。从早期专门用于电子战的改装机,到如今多功能的电子战飞机,其作战性能不断提升,能够实现对远、中、近程电磁目标的全方位覆盖。同时,它还配备了反干扰和隐身防护系统,从而确保在复杂电磁环境中的生存能力。
|
||||
|
||||
|
||||
## 【专家4】
|
||||
|
||||
电子战飞机之所以成为,体系作战的隐性支柱,核心是在于它不是以战斗机,轰炸机的直接的火力支援,来作为后盾。它是通过对电磁频谱的掌握,成为空中作战的神经中枢,和保障屏障,悄无声息地,掌握战场的制电磁权,为整个战场体系的感知,打击,生存,提供底层支持。制电磁权,是现代体系空战的一个核心基础。失去了电磁优势,战机的雷达,通信,导弹制导,统统都沦为了瞎子,聋子,哑巴。电子战机的出现,使空战的攻防逻辑,发生了巨大的变化,从以往的火力比拼,平台对抗,发展成为频谱先行,体系作战,电子频谱的争夺,成为了空战的首要目标。
|
||||
|
||||
|
||||
## 【解说8】
|
||||
|
||||
海湾战争,铸就了电子战飞机的巅峰时刻。美军派遣EA-6B、EF-111等多种电子战飞机,针对伊拉克的雷达、通信以及防空系统,展开全频段、且高强度的电子压制。刹那间,伊拉克整个防空体系陷入瘫痪,为后续战机的渗透与打击清除障碍。在整个战争进程中,电子战飞机始终跟随作战编队飞行,化作空中电磁屏障,极大地减少了战机的战损率。电子战飞机的诞生,开启了空战隐形对抗的新领域,使得电子压制成为空战的前置步骤与关键战术。缺乏电子战掩护的战机,在现代空战里几乎毫无胜算。电子战能力,已成为评判一支空军战斗力的核心标准。
|
||||
|
||||
|
||||
## 【主持人5】
|
||||
|
||||
体系化空战,依靠电子战飞机,构建起电磁骨架,使空战的对抗层面,从单一的火力攻击,拓展到了无形的电磁较量。然而,随着各国防空系统的逐步完善,传统战机,雷达反射截面积大,容易被探测锁定的问题,愈加明显。即使有电子战飞机的保护,也难以攻破现代化的,多层次防空系统。于是,一种融合隐身性、超视距、高机动特点的新型战机,诞生了。它完全打破了,常规战机的作战方式,把空战带入了,隐形对抗的新阶段。
|
||||
|
||||
隐身战机:常规战机的“颠覆者”,隐形对抗时代的“核心主力”
|
||||
|
||||
|
||||
## 【解说9】
|
||||
|
||||
20世纪70年代,为了应对现代化的防空体系,美国率先开展隐形战机的研发工作。1981年,世界首款隐形战机F-117A成功完成首飞。1997年,首款五代隐形战机F-22实现首飞,这标志着空战正式迈入隐形时代。隐身战机的关键革新,在于全面的隐身设计,借助机身外形的流线型优化,吸波材料的使用,以及红外隐身技术的支持,显著减少战机的雷达反射截面积与红外信号特征,使传统雷达难以探测和锁定,从而达成来无影去无踪的空中突防效果。
|
||||
|
||||
|
||||
## 【专家5】
|
||||
|
||||
隐身战机的隐身性,之所以成为,空战装备的革命性突破,其核心就是打破了,现代空战的,发现即摧毁的核心法则,颠覆了以雷达探测为基础的,传统防空与空战体系,让空战制胜逻辑,从比拼火力、速度、机动,转向争夺探测,和反探测的主动权,实现了从装备性能提升到,作战维度,降维打击的本质跨越。隐身战机的出现,彻底改变了攻防的逻辑。隐身战机能够做到先发现,先攻击、先脱离,它做到悄无声息地,进入攻击地段,对它的目标展开攻击,敌方还没有发觉,甚至发觉了也没有反应时间,做不好反击准备。
|
||||
|
||||
|
||||
## 【解说10】
|
||||
|
||||
1991年海湾战争期间,F-117A执行了1300多次任务,摧毁伊拉克四成高价值目标,自身却毫发无损,向全球展示了,隐形战机的强大作战潜力。伴随F-35,苏-57等五代隐形战机陆续服役,隐形对抗,已然成为空战现代化的,核心模式。隐身战机间的较量,不再单纯是平台性能的比试,而是航电装置,隐身科技,制导兵器,电子干扰,战术协作的全面竞争。制空权的角逐也迈入了,隐形时代。隐身战机因具备隐身,超视距,高机动的显著优势,已然成为现代空战的,关键主力。然而,其高昂的研发成本,巨大的维护难度,以及严格的人员培训需求,也是相当明显的。随着反隐身技术的持续进步,隐身战机的作战长处,正在逐渐被削弱。当有人空战的成本与风险,日益增长时,一种全新的智能作战装备,顺势出现了。
|
||||
|
||||
|
||||
## 【主持人6】
|
||||
|
||||
它凭借低成本、零伤亡、高性价比的特点,打破了有人机掌控空战的传统格局,促使着空战朝着智能化、无人化的新阶段迈进。
|
||||
|
||||
无人机:有人空战的“革新者”,智能无人空战的“引领者”
|
||||
|
||||
|
||||
## 【解说11】
|
||||
|
||||
进入21世纪后,伴随人工智能、导航定位以及小型化制导武器技术的深度结合,无人机完成了从侦察支援到实战攻击的飞跃,化身为现代空战中的新兴力量。从小型单兵携带的无人机,到中型的侦察无人机,再到察打一体的大型无人机,无人机的作战性能不断提升,成为非对称作战和体系化空战的关键组成。
|
||||
|
||||
|
||||
## 【专家6】
|
||||
|
||||
无人机之所以成为现代空战的新质力量,核心就是突破了有人飞机的飞行员的生理极限,作战成本,战场风险的多重束缚,实现了零伤亡,低成本,长航时,高适配的独特优势,成为衔接有人作战与智慧化空战的关键载体,为体系化空战注入了全新的效能增量。随着智能化的发展,可能人机协同,慢慢就变成了一种无人机单独执行任务,飞行员从一个最初的执行者,变成了一个现在,甚至不远的将来的一个决策者。再后来的发展,随着高科技的发展,随着智能化的发展,他可能成为一个监督者。这是飞行员任务的变化,也是军事科技向前发展的一个必然趋势。
|
||||
|
||||
|
||||
## 【解说12】
|
||||
|
||||
在体系化空战中,无人机可与有人机,组成忠诚僚机搭档,由有人机指挥,无人机负责侦察,诱敌 护航和打击等任务,显著降低有人机的作战风险,并提升整体作战效率,无人机的崛起,彻底颠覆了,强者恒强的传统空战理念,使中小国家,也能具备制衡大国空军的能力,同时推动空战,从有人化,迈向有人加无人协同的,智能化阶段,无人集群作战与自主空战,成为未来空战的,重要发展趋势。
|
||||
|
||||
|
||||
## 【主持人7】
|
||||
|
||||
空战时代,由螺旋桨飞机拉开帷幕。智能无人空战,则由无人机引领。空战武器的演变,始终伴随着攻与防的长久较量,而每次较量的结局,都是作战理念的提升。当前,智能化、无人化、体系化、空天一体化,已经成为空战发展的主要方向。高超音速无人机、空天飞机、无人作战集群等新式装备与战术,正在快速研发。未来的空战,必定朝着无接触、超视距、智能制胜、空天一体的趋势不断演进。到底还有哪些突破性的技术,会融入空战体系呢?制空权的争夺,又会展现怎样的新形态呢?这个问题,只有时间能够回答。感谢您持续关注国防军事频道《军事科技》,我们下周同一时间,再见。
|
||||
@@ -0,0 +1,149 @@
|
||||
---
|
||||
标题: 逆袭战局的集成武器
|
||||
编导: 穆佩弦
|
||||
播出日期: 2026-03-24
|
||||
类型: 节目文稿
|
||||
期号: ep012
|
||||
---
|
||||
|
||||
# 《逆袭战局的组装武器》
|
||||
|
||||
|
||||
## 【导视1】
|
||||
|
||||
在复杂多变的战场上,真正改变战斗的,并不一定是预先设计好的完美武器。能够撬动战局的,往往是那些原本平凡的装备,它们被人们在战场环境下,重新组合,变为全新的战斗形态。当运输直升机挂上机枪,化身空中火力平台;当榴弹炮装在坦克底盘上,跃升为精准打击利器。本期《军事科技》,带您走进武器装备,从平平无奇,到制霸战局的关键瞬间,见证这些装备,在战场上的高光时刻。
|
||||
|
||||
|
||||
## 【演播室主持人1】
|
||||
|
||||
各位观众你们好,欢迎收看《军事科技》,我是主持人蓝皓。谈到战争与科技,我们总是习惯将目光,投向那些耀眼的装备,比如说航母、坦克,导弹、隐身战机等等。它们像是一幕宏大戏剧中的主角,光彩夺目。我们总是习惯性地认为,每一次战局的转折,战史的翻篇,都必然来自于某项划时代的、横空出世的发明创造。历史并非总是如此运转的。当我们把目光投向历史的深处,或许应该尝试着,将视线从耀眼的聚光灯下,稍稍移开那么一些。很多时候,真正改变战场的,不是全新的科技,而是两件原本普通的装备,在某个绝境的时刻,被迫结合在一起,并且制霸战场。这其中,最经典的一个案例,便是空中炮艇的诞生。
|
||||
|
||||
|
||||
## 【二维动画解说1】
|
||||
|
||||
1964年12月,越南南部。一个美军营地在夜色中遭到包围。越军数量远超美军。丛林遮蔽了视野。地面火炮无法及时支援。美国空军紧急出动喷气式攻击机。但问题很快暴露。喷气机速度快,油耗高。一次俯冲攻击后就必须返航。夜间目标难以锁定。营地指挥官请求持续火力压制。空中却只剩下一架老式运输机。
|
||||
|
||||
|
||||
## 【解说1】
|
||||
|
||||
这架飞机诞生于二战,设计目的只有一个:运送人员和物资。在“市场花园”行动中,数百架C-47组成空中梯队,持续完成兵力与补给输送。它没有重型武装,没有厚重装甲。由民用客机改进而来,机体结构经过加固,地板承重增强,能够承载三吨物资,或一次性运送28名全副武装的伞兵。侧舱门加宽设计,使伞兵可以在数秒内连续跳伞。动力方面,它搭载两台普惠活塞发动机,巡航速度约每小时370公里,航程超过2000公里。性能并不激进,但机械可靠性高,维护简便,即使在简陋机场也能快速起降。更重要的是,它具备出色的低空稳定性,在复杂气流和防空火力干扰下,飞行姿态依然可控。
|
||||
|
||||
|
||||
## 【二维动画解说2】
|
||||
|
||||
面对无线电中,营地指挥官的求助,空中的这架老式运输机,拿出了绝招。它的机舱侧面,被设计人员,安装了三挺机枪。这是它第一次,在战场上,露出尖锐的獠牙。飞机绕圆飞行,机枪固定角度射击,子弹落点,形成稳定火力圆环。这便是日后被称作,支点盘旋的飞行方式。地面形成持续的火网,敌军被压制。几个月内,这种经过特别改装的C-47,被正式命名为AC-47。它成为第一种正式编号的,炮艇机。
|
||||
|
||||
|
||||
## 【专家1】
|
||||
|
||||
越南战争爆发之后,美军陷入了丛林作战的泥潭。虽然说,美军可以派出地面作战部队,和对方进行交战,或者说,也有火炮进行支援。但是最重要的是,当时的战术飞机,也就是一些喷气式战斗机,飞行速度太快,那么很难在相关的区域内,长时间地,针对地面作战部队,进行精准的火力支援。所以,美军是需要一种大型的,这种低空慢速,飞行性能比较好的,这样的一个作战装备。就用C-47,也就是二战遗留下来的,剩余物资进行技术改造。是在机身的一侧,加装这种速射的机枪,或者是机炮。那么最大的优势,就是它的低空的,盘旋性能比较好。作战的模式是空中,绕圈子 兜圈子,针对地面作战部队,所遭遇的对方步兵,丛林区域进行长时间的,弹幕射击和弹雨射击,倾泻火力的时间比较长。而且AC-47,是在C-47运输机的基础上,改造而来的。它内部是有,比较大的货运空间,在加装了速射机炮之后,也可以携带更多的弹药,保证火力的密集程度,和这样的一个持续性。除此之外,它的这样的一个平台,最大的优势,就是可靠性比较高,而且战场生存能力比较强。即使对方的步兵,使用轻武器,比如说步枪,包括小口径的高射武器,针对AC-47进行打击,机翼 机身打出几个窟窿,并不影响它的使用。所以皮实耐用,也是它的一个重要的,这样的一个优势。通过技术改造之后,C-47成为了AC-47。那么在作战使用的过程当中,确实针对美军的作战部队,提供了强有力的火力支援。可以针对对方的步兵,进行猛烈的火力压制。在前期,取得了相当的作战效果。
|
||||
|
||||
|
||||
## 【解说2】
|
||||
|
||||
在AC-47验证“空中持续火力平台”概念之后,美军研究人员并没有止步。相反,他开始系统化推进炮艇机的发展,使其从临时改装,走向专业型号。在越南战争中期,美国推出了AC-119。它改装自C-119运输机,机身加长,续航延长,搭载20毫米机关炮,并配备夜视与早期电子侦察系统。在夜间巡逻时,AC-119通过红外传感器,锁定卡车发动机的热源,侧向机炮连续射击,形成稳定持续的火力覆盖。补给线几乎没有安全间隙,每一辆运输车都在火力压制下寸步难行。而后,炮艇机概念进一步成熟,诞生了AC-130系列。AC-130的身影经常出现在各类影视剧中。它凭借着极强的空中火力支援能力,给广大观众留下了深刻的印象。AC-130系列炮艇机是现代空中火力支援平台的代表。它基于C-130运输机改装,它滞空时间长,可连续盘旋6小时以上,最大航程超过2000公里,能够快速部署至远程战区。搭载多种口径火炮,这种火炮沿机身侧向安装,可以实现灵活打击。在面对轻型车辆和人员目标时,20毫米机关炮。
|
||||
|
||||
|
||||
## 【三维动画解说1】
|
||||
|
||||
可以快速压制。在面对集群目标或防御阵地时,40毫米博福斯炮可以实施有效覆盖。而它搭载的105毫米榴弹炮,能够精确摧毁坚固建筑或装甲目标。
|
||||
|
||||
|
||||
## 【解说3】
|
||||
|
||||
在现代战场上,AC-130的核心优势,不在于单纯火力,而在于精确支援地面作战的能力。它能够实时锁定移动目标,将传感器捕捉的信息,转化为精准射击指令。红外与多光谱传感器,使它在夜间或复杂环境中,仍能发现敌方阵地和运输车辆。通过与地面指挥系统的数据联动,能够根据地面部队的需求,调整火力重点和攻击节奏,实现及时支援。在美军阿富汗和伊拉克的行动中,它长时间滞空,连续压制敌方火力点,保护前沿部队推进,同时切断敌方增援路线。这种持续、可控、精准的空中支援,让地面单位能够在复杂战场环境下保持主动权,而不是被动应对。
|
||||
|
||||
|
||||
## 【演播室主持人2】
|
||||
|
||||
从装备演进的角度看,空中炮艇的出现,不是一次技术革命,而是一种功能的重构。在过去的战场上,发现目标和摧毁目标,是两个分开的环节。而空中炮艇的出现,彻底打破了这种模式。设计人员,没有发明任何一门新的火炮,也没有突破任何空气动力学的极限。但是,它只是把制空和打击,这两个要素重新组合,就创造出了全新的作战逻辑。它证明了一点,当成熟平台与战场需求重新匹配的时候,一款平凡的装备,就可能衍生出全新的战斗形态。残酷的战场,从来不是单纯地追求尖端的技术,而是追求适配性。当空中炮艇,正在战场上空,依靠着长时间的盘旋,进行火力压制的时候,在丛林的上空,另一场为了机动灵活而拼接的艺术,正在上演着,它就是武装直升机。
|
||||
|
||||
|
||||
## 【解说4】
|
||||
|
||||
从20世纪40年代,一架直升机在战场上,成功救下两名受困士兵开始,美国军方便意识到,直升机,可以成为战场上的新型机动力量。直升机的发展,被纳入正式军方计划,军方对它寄予厚望,希望它能在运输、救援、和支援作战中,发挥独特作用。直升机的发展之路,并非一帆风顺。早期的直升机,虽然设计上,预留了武器挂载接口,但结构轻便,载重有限,机枪、火箭,或轻型火炮的安装,非常不便。在实际战场中,它们仍主要承担,运输士兵和物资的任务,无法直接参与火力压制。1962年3月,这款传奇通用直升机的,拼接改装之路,正式拉开了序幕。休伊直升机,其实也是在越战期间,开始大量应用的。UH-1休伊直升机,主要执行的是,兵员投送、物资运输、和伤员的这样的一个,后撤的任务。在越南的低空区域进行使用,可以说它成为了,美国军队的,空中“出租车”。因为当时已经有了,蛙跳作战的战术,针对对方的,游击区域进行扫荡。休伊直升机,投送机降作战部队,那么针对某个村庄,进行这种,机降兵力的快速投送,可以让部队,快速越过水网、稻田区域,或者说一些丛林区域,那么尽快地,抵达作战位置。完成了突袭行动之后,也可以快速地撤离。可以运送弹药、运送补给品、运送作战人员,也可以尽快地,把伤员给撤出来。
|
||||
|
||||
|
||||
## 【专家2】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【解说5】
|
||||
|
||||
后续美国军队为休伊,也就是UH-1型的运输直升机,加装了武器。主要是在舱门这个位置,加装机枪,也可以挂装小型的火箭弹发射巢。那么在机降的过程当中,武装版本的休伊,可以针对对方的步兵进行压制,掩护机降部队,迅速地到达着陆场。使用火箭弹发射巢,也可以针对对方的行军队队,包括一些房屋,进行火力打击。休伊直升机在越南战场,也是导致了很多无辜平民,也就是越南民众的这样一个伤亡。当休伊直升机上的美军机枪手针对地面进行扫射的时候,当火箭弹进行这种密集的,它其实是一种无差别攻击。美军也不会分辨哪些是民众,哪些是对方的作战人员,进行机枪扫射,进行大面积的火力覆盖,确实导致当地民众大量出现伤亡的情况。随着越南战争的深入,美军时常面临着丛林密布、伏击频繁的战场环境。步兵需要及时火力支援,而传统运输直升机缺乏火力保护。在这种迫切需求下,工程师和飞行员将UH-1休伊两侧加装机枪、火箭发射架,甚至轻型火炮。第一次实战投入,就显示出惊人的效果。直升机可以低空悬停,伴随地面部队推进,同时压制敌方火力点,为步兵开辟通路,或掩护撤退。这次临时拼装,开创了武装直升机的雏形。由于美军之前就曾有过改装传统直升机的设想,设计人员在设计休伊直升机的时候,就为它的各种型号预留了武器安装的接口。理论上,休伊直升机的A至D型,甚至之后的后续型号,都可以改装成武装型。但美国陆军还是根据每款型号的性能,进行了更加细化的分工。
|
||||
|
||||
|
||||
## 【解说6】
|
||||
|
||||
A、B型休伊直升机的机身较短,载员能力一般。在越南战场上,陆续被改装成武装型。长机身的D型休伊直升机,则被用为运载和救援直升机。由于各型号休伊直升机武器挂载的方案不同,这种武装型的休伊直升机,还有着不同,诸如野猪、炮艇等不同的绰号。
|
||||
|
||||
|
||||
## 【三维动画解说2】
|
||||
|
||||
这是一架加装上两挺电力驱动的M60C机枪的武装型休伊直升机。它的副驾驶可以直接通过驾驶室安装的X-M60反射式瞄具控制这两组双联装机枪。这套武器系统的每挺机枪都由三个弹药箱供弹,总弹药容量为6000发。当士兵跳下直升机时,射手可以提供强大的火力掩护。为了进一步加强直升机的强大火力压制,继续给武装型休伊增加机翼短挂架,用以挂载火箭巢。
|
||||
|
||||
|
||||
## 【解说7】
|
||||
|
||||
当越南战场的硝烟逐渐笼罩丛林,美军意识到一个现实问题:临时武装的UH-1休伊虽然有效,但终究不是为战斗而生。它的机身宽大,气动不优。于是美军决定专门研发一种攻击直升机。1967年,AH-1眼镜蛇正式投入越南战场,它是世界上第一种专用攻击直升机,武装直升机这一装备类别正式诞生。
|
||||
|
||||
|
||||
## 【三维动画解说3】
|
||||
|
||||
眼镜蛇采用串列式双座布局,机身细长,正面投影面积小,目标更难被击中。机体经过强化,具备更好的防护能力和更高的速度。动力系统采用改进型涡轴发动机,使其在低空具备出色的机动性能。它可以贴地飞行、快速跃升、突然转向,在丛林与山地环境中进行突袭。在机头下方安装可旋转炮塔,装备7.62毫米或20毫米机炮,能够灵活指向目标,实施精准压制。机翼短翼下可挂载2.75英寸火箭弹发射巢,用于打击敌方阵地与轻型装甲目标。后期型号还具备发射反坦克导弹的能力,使其拥有对装甲单位的直接打击能力。眼镜蛇是美国军队第一种专用的武装直升机,应该是在1967年的下半年投入到越战的战场。这款直升机最大的优势就是它的动力系统,主螺旋桨包括一些尾梁的结构和尾桨是可以和UH-1休伊运输型的直升机去通用的。那么投入战场之后,在后勤保障以及维护方面,但是它的火力更强。它上面是配备有专门的光学的瞄准系统,而且在直升机前部靠下的有小型的机枪塔,或者是叫小型的炮塔,上面配备有迷你炮,也就是速射机枪,同样也可以加装自动榴弹发射器。采用串列式的这样的一个飞行座舱的布局,就可以让飞行员更好地观察地面的目标,有很好的视野,也可以使用迷你炮针对地面目标进行扫射。武装直升机的概念并非凭空而来的,它的血统来自运输直升机与各类武器挂架的组装。当直升机从运输转向战斗空中支援模式,才发生了实质性的变化。接下来,让我们将目光投向地面战场上的另一个明星,它就是自行火炮。我们看到现在典型的自行火炮就像是一个大的坦克,它们都有履带式的底盘,都有一个显眼的炮塔,而且炮塔同样有一个封闭式的装甲壳体。
|
||||
|
||||
|
||||
## 【专家3】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【演播室主持人3】
|
||||
|
||||
那么它和坦克,到底有什么区别呢?
|
||||
|
||||
|
||||
## 【解说8】
|
||||
|
||||
20世纪40年代,北非战场上,沙漠上空烈日炙烤,大规模机械化部队,正在展开一场,前所未有的机动作战。无论是德军非洲军团,还是英军装甲部队,战场的主角,都是坦克与装甲车辆。在广袤无垠的沙漠地带,没有山川阻隔,没有密林掩护,战斗节奏,完全取决于机动速度。谁的装甲部队推进更快,谁就能抢占阵地,包抄侧翼,切断补给线。但问题很快暴露出来,装甲部队可以快速前出,可身后的炮兵却难以跟上。当时主力火炮,仍是牵引式榴弹炮,它们必须由卡车,或半履带车拖曳前行。一旦抵达射击位置,炮兵还要下车卸炮,展开炮架,校准射向,构筑阵地。在风沙弥漫的沙漠中,这个过程,往往需要十几分钟,甚至更长时间。而战场态势,可能在几分钟内,就发生改变。坦克已经冲过前线,而炮兵还在后方布置阵地。当敌方装甲部队,突然反击时,缺乏及时火力支援的装甲部队,只能独自承受压力。已经进入装甲机动的时代,但炮兵,还停留在马匹牵引的时代。工程师,便将榴弹炮,直接安装在坦克底盘上,让榴弹炮,获得了动力系统。
|
||||
|
||||
|
||||
## 【专家4】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【解说9】
|
||||
|
||||
北非战场的教训,已经十分清晰。盟军意识到,必须让火炮拥有与坦克相同的机动能力。于是,一种全新的装备开始登场,M7自行榴弹炮。由于其机枪座高耸,外形类似教堂讲坛,于是,北非战场上,率先列装M7自行榴弹炮的英军士兵,给它起了一个绰号,牧师。
|
||||
|
||||
|
||||
## 【三维动画解说4】
|
||||
|
||||
M7自行榴弹炮,它基于M3,李氏中型坦克底盘改造,后期型号,则采用M4谢尔曼底盘,整车战斗全重,约23至24吨,乘员编制,通常为7人,包括车长,炮手,装填手,以及弹药手,M7在公路上的最高时速,可达到每小时40公里,越野速度,约每小时20公里左右,对于一门搭载105毫米榴弹炮的,火炮平台而言,这种机动能力,已经能够与装甲部队,保持同步推进,M7的核心武器,是一门105毫米,M2A1榴弹炮,最大射程,约11000至12000米,初速约每秒470米,可发射高爆弹,照明弹等多种弹药,火炮安装在,开放式战斗舱内,虽然不具备全向旋转炮塔,但在战术部署中,主要依赖间接射击,通过前线观察员引导,修正弹着点,在车体右前方,高耸的机枪座上,安装一挺,12.7毫米M2重机枪,M7自行榴弹炮的,装甲防护厚度,约12至50毫米之间,能够抵御轻武器,和破片打击,在地面的,装甲作战平台当中,坦克和自行火炮,是非常具有辨识度的,主战坦克,可以说它是,冲锋陷阵打头阵的,这样的一个披坚执锐的,重甲骑士,因为主战坦克,一般都是30吨以上,配备有大口径的滑膛炮,当然它此前也配备过,这个线膛炮,它最重要的功能是,突破对方的防线,掩护己方的步兵,和履带式的步战车,控制相关的作战区域,而自行火炮,是把大口径的榴弹炮,和传统意义上的,履带式装甲车进行结合,那么自行火炮最大的优势,它是可以针对,一线冲锋作战的部队,进行曲射的火力支援,打十几公里,二十公里左右,所以主战坦克,是直瞄火力,是抵近攻击,而自行火炮,自行榴弹炮,它是曲射火力,是在较远的距离上,进行这样的一个火力支援,在俄乌冲突中,我们看到了一个,显而易见的变化,无人机,彻底改变了战场侦察与打击方式,这些无人机体型小巧,成本低廉,却具备极高的灵活性,它们能够快速飞过战场,探测敌方阵地,补给线,甚至可以与简单的,爆炸装置组装,通过投放小型炸弹,实施打击,这些装备,虽然都缺乏标准化的体系支撑,但在前线却发挥了,令人惊艳的效果,它们的存在,完全源自战场的迫切需求,通过组合手头的平凡武器,发挥出,远超单件装备的,战术价值,从空中炮艇的持续压制,到武装直升机的灵活支援,从自行火炮的机动超车,到无人机,与临时防空系统的精彩对抗,战场一次次证明,装备的真正力量,不在于先进与否,而在于如何使用,如何组合,现代战争,已不再只是技术的比拼,而是智慧,创新,与临机决策的舞台,下一款能够改变战局的装备,或许正静静地,躺在某个角落,等待被发现,等待被赋予新的使命,好了观众朋友们。
|
||||
|
||||
|
||||
## 【解说10】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【专家5】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【演播室主持人4】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【解说11】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【演播室主持人5】
|
||||
|
||||
感谢您持续关注,国防军事频道《军事科技》,我们下周同一时间,再见。
|
||||
@@ -0,0 +1,194 @@
|
||||
---
|
||||
标题: “X”系列新成员:X-76飞机
|
||||
编导: 左鑫
|
||||
播出日期: 2026-04-07
|
||||
类型: 节目文稿
|
||||
期号: ep013
|
||||
---
|
||||
|
||||
# 《“X”系列新成员:X-76飞行器》
|
||||
|
||||
|
||||
## 【宣传词】
|
||||
|
||||
数学领域,X为待求解的未知数;科幻范畴,X是象征未来的科技标志。近期,美军X系列又加入了重要新成员,代号X-76的折叠旋翼机。它声称解决了航空界几十年的难题,可垂直悬停,且能达到喷气式战机的速度。那么,这款空中新物种,究竟蕴含哪些突破性黑科技?是否能重塑未来空战规则?本期《军事科技》,为您深入解读X家族最新成员X-76。
|
||||
|
||||
|
||||
## 【主持人1】
|
||||
|
||||
各位观众你们好,欢迎收看《军事科技》。在现代战争里,飞行器,无疑扮演着关键战力的角色。然而速度与灵活,却始终是摆在军事工程师面前的,一对难题。固定翼飞机虽然快,但需要依赖长长的跑道。直升机虽然能够垂直起降,但是速度却难以提升。难道真的没有一种飞行器,能够同时融合两者的优点,既能急速飞驰,又能随时悬停吗?2026年3月,美国国防高级研究计划局,正式公布了一款全新装备,贝尔公司研发的,新型折叠旋翼机。作为X系列家族的新成员,立即吸引了,全球军事爱好者的目光。那么这款号称要突破固定翼飞机,与直升机性能界限,解决美军数十年来困扰的飞行器,究竟有何独特之处呢?今天就让我们一起揭开,X-76的神秘面纱。
|
||||
|
||||
|
||||
## 【解说1】
|
||||
|
||||
要认识X-76,我们得先搞清楚一个关键问题:美军为何要投入巨大精力,去开发这样一款全新的飞行器呢?实际上,这完全来自于现代战争中的现实难题。大家可以思考一下,在紧张的对抗中,飞机与跑道,通常是敌方首轮打击的核心目标。一旦跑道被破坏,那些需要跑道才能起飞的固定翼飞机,无论多么先进,最终只能变成地面上的靶子。而传统的直升机,虽然具备垂直起降能力,无需依赖跑道,但其速度过于缓慢,在战场环境中,极易成为敌方攻击的目标,生存能力大幅降低。是否存在一种办法,能让飞行器,既摆脱对跑道的依赖,又具备喷气式飞机的速度?这正是美军提出“速度与跑道独立技术”项目的核心目标。而X-76,则是贝尔公司为这一项目,提交的答卷。
|
||||
|
||||
|
||||
## 【嘉宾1】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【专家1】
|
||||
|
||||
X-76它诞生的背景,是源于2023年,美国国防部提出了一种高速多元化飞行器的,这样的一个技术要求。贝尔公司经过技术研发,而且依托于丰富的经验,最终是在2026年拿下的项目,可以说就正式立项了。为什么说这个项目被称为X-76呢?主要是由于五角大楼和美军高层,对于这个项目非常地关注,而且美国的这个建国时间是1776年,那么到2026年,正好是这个250周年,所以把它命名为X-76,足见美国军方对这一款先进的倾转旋翼飞机,它是非常地重视。
|
||||
|
||||
|
||||
## 【解说2】
|
||||
|
||||
那么,这款被美军寄予厚望的X-76,究竟有何独特之处呢?咱们不妨从它的“外观”和“能力”谈起。首先,最引人注目的便是它的“可折叠旋翼系统”。这并非普通的旋翼,而是贝尔公司研发的独有“停止/折叠”技术。通俗来讲,它犹如“变形金刚”般,能在两种飞行模式间流畅转换。或许有人会问,这种“模式切换”究竟有何独特之处呢?咱们来打个比方,当X-76准备起飞时,其翼尖旋翼会正常运转,像直升机那样提供垂直升力,无论是在复杂地形、未经平整的地面,还是海上的小型浮动平台上,都能轻松实现起降。而当它加速到约每小时278公里至370公里的关键速度时,奇妙的现象便出现了,翼间的旋翼会逐渐停止转动,并向后折叠,紧密贴合在机翼短舱上。此时,飞机便瞬间化身为一架喷气式飞机,依靠独立的涡扇发动机推动,全速前进。
|
||||
|
||||
|
||||
## 【嘉宾2】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【专家2】
|
||||
|
||||
X-76应该是一种先进的概念,未来成熟之后,会根据美国军方的需求,衍生出一系列的产品。那么个头小的,可能还不到两吨多,也就是说,它既成为一种先进高速飞行的,倾转旋翼无人机。如果是把它的尺寸进一步地放大,那么它的重量,可以达到四十多吨,就可以作为一种大型的,高速的空中的运输平台。和美军现役的,鱼鹰倾转旋翼机相比,X-76它最大的优势,就是把这种倾转旋翼机,它的性能优势,和高速飞行的,这样的一个需求,进行了有效的结合。所以我们注意到X-76,它既可以像鱼鹰一样,垂直起飞,垂直降落。但是当它进入到,水平飞行的状态,它可以达到喷气式飞机,飞行的这样的一种速度。那么这样的一种设计理念,可以说把现有的倾转旋翼飞机,它的技术层面的这样的一些欠缺,和性能有待提升的,这样的一些方面,其实都考虑到了。让我们来关注一则官方消息,X-76最大亮点无疑是速度,其巡航时速,大约在740公里,至833公里之间。这是个怎样的概念呢,我们可以,与V-22鱼鹰倾转旋翼机做个比较,后者的巡航时速,约为518公里,X-76比它快了43%至61%。这一差距在战场上,有何意义呢,举个例子,在执行战斗搜救任务时,鱼鹰需消耗两小时,才能到达目的地,而X-76仅需约一小时,便可完成闪电救援,显著提高搜救效率。未来X-76它成熟之后,会衍生出一系列的,这种部署方便灵活,而且飞行速度比较快的,这样的一些作战平台。首先它可以执行侦察任务,比如说在水面舰艇上,进行部署,那么完全可以替代舰载直升机,既可以针对广阔的区域,尤其是海面目标进行侦察。当然它也可以执行反潜任务。除此之外,它也可以作为一种,察打一体平台,配备给陆军的作战部队。一方面可以在前沿的,这样的一个区域,进行野战化的部署。同样也可以利用,它自身所携带的光学传感器,红外传感器,针对地面的目标进行确认。同样也可以携带一些,精确制导弹药,针对目标进行精准的火力打击。所以它既可以执行侦察任务,也可以执行察打一体任务。在水面战舰上进行部署,也可以替代,部分舰载直升机的职能。看到此处,或许有观众会产生好奇,这款全能型飞行器,如今发展到了何种地步呢。依据官方信息,X-76已顺利通过关键设计评审,并且全部关键风洞测试,也已完成,当下处于,原型机制造的进程当中。根据计划安排,将于2027年,完成原型机总装工作,在2028年初,达成首飞目标。说起旋翼机,人们脑海中首先浮现的,想必是V-22鱼鹰,这是由贝尔公司,与波音公司联手研发的装备,已经服役了多年之久,是美军特种作战中的。
|
||||
|
||||
|
||||
## 【解说3】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【嘉宾3】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【专家3】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【解说4】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【主持人2】
|
||||
|
||||
主力机型。鉴于X-76同样出自贝尔公司之手,那么,它是否仅仅是“鱼鹰”的简单升级版本呢?二者之间,究竟存在着怎样的差异呢?我们就来进行一场同台比拼,一次性将它们的不同点阐述明白。
|
||||
|
||||
|
||||
## 【解说5】
|
||||
|
||||
首先,最为根本的区别,在于飞行模式,以及动力系统的差异,这也是X-76最具突破性的部分。我们先来谈谈鱼鹰,鱼鹰采用的是倾转旋翼的设计方式,其翼尖短舱,能够整体进行倾转。在起飞时,短舱垂直朝上,旋翼旋转,以提供升力,和直升机无异。在水平状态下,短舱水平朝前,旋翼旋转提供拉力,如同固定翼飞机一般。但这里有一个重要的问题,不管是起飞、悬停,还是高速平飞,鱼鹰的翼尖旋翼,都必须持续旋转。大家千万不要轻视这个持续旋转,它所带来的问题可不少。首先是空气阻力方面,高速平飞时,旋转着的旋翼,会产生极大的空气阻力,这也是鱼鹰巡航速度,难以提高的关键因素。其次是安全性方面,处于低空飞行状态时,旋转的旋翼容易与地面物体、树木发生碰撞,并且一旦旋翼出现故障,飞机极易失去平衡,从而引发事故。自鱼鹰服役以来,已经发生了多起坠机事故,都与这一设计,有着千丝万缕的联系。
|
||||
|
||||
|
||||
## 【嘉宾4】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【专家4】
|
||||
|
||||
和鱼鹰相比,X-76最大的优势,主要是体现在两方面。第一个优势,就是它采用了复合的动力系统。其实它翼梢的两台发动机,主要提供的是升力。也就是说在垂直起飞,和垂直降落的过程当中,当它进入到,水平飞行状态的过程当中,翼梢的发动机,本质上来讲是不工作的。主要是利用机尾的这种,大功率的喷气式发动机,让它进行高速飞行。所以在它进入到平飞状态之后,那么翼梢的两台发动机,它的叶片,其实是进行了折叠和收拢。这样的话,可以最大程度地减少阻力。其实它是在鱼鹰,这样的一个设计理念上,进行进一步的升级和完善。第二个是应用范围的差异。鱼鹰主要以有人驾驶为特点,尺寸较为固定,重点用于人员与物资的输送,任务形式相对单一。而X-76包含无人,和有人两种模式,技术具备高度的可延展性,既能充当有人驾驶的,核心运输载体,也能作为无人机执行危险任务,其灵活性远胜鱼鹰。第三个是隐身能力的不同。X-76设计中,引入了低可探测性理念,机体采用隐身优化外形,结合红外与噪声抑制技术。特别是在旋翼折叠后,进入平飞状态时,其雷达反射截面,和噪声明显小于鱼鹰,更不易被侦察和拦截。而鱼鹰由于旋翼持续旋转。
|
||||
|
||||
|
||||
## 【解说6】
|
||||
|
||||
噪声显著,雷达反射面积也比较大,在高强度对抗场景下,极易被敌方防空系统捕捉。
|
||||
|
||||
|
||||
## 【嘉宾5】
|
||||
|
||||
或许有观众会有疑问:既然X-76比鱼鹰先进如此之多,它是否将直接代替鱼鹰呢?
|
||||
|
||||
|
||||
## 【专家5】
|
||||
|
||||
我认为X-76它成熟之后,尤其是它的这样的一个设计理念,走向大型化之后,肯定会取代鱼鹰倾转旋翼机。因为鱼鹰倾转旋翼机,是美军第一代的倾转旋翼的,空中的飞行器。那么当X-76它成熟之后,在部署的灵活性方面有优势,而且在这个性能的层面,它更加地有优势。更重要的是它的飞行速度比较快,甚至还可以采用一定程度的,隐身化的设计理念。所以和鱼鹰相比,它的可靠性更高,飞行速度更快,通用性更好。那未来一旦它广泛地装备,美国陆军、空军和海军陆战队,相信到那个时候,鱼鹰倾转旋翼机,可能已经到达了,使用寿命的中后期。那么X-76它的成熟型号,会逐步地替代鱼鹰成为一种,多军兵种的一种通用的装备。看到此处,大家或许会有疑问,贝尔公司,怎么能研发出如此奇特的装备呢?实际上X-76的问世,并非是一蹴而就的,而是从早期的实验机型,逐步发展到如今的X-76。在垂直起降领域的探索,已历时近七十年。现在让我们一同回顾一下,贝尔公司的垂直起降发展之路,同时再来瞧一瞧,除了美国之外,还有哪些国家,曾在此领域。
|
||||
|
||||
|
||||
## 【主持人3】
|
||||
|
||||
留下了探索的足迹。
|
||||
|
||||
|
||||
## 【解说7】
|
||||
|
||||
首先,从贝尔公司早期的经典机型,D-188A讲起。上世纪五十年代,美苏在军事科技方面,竞争十分激烈。无需跑道的垂直起降飞机,成为两国研发的重点。诸多类似的想法不断涌现,例如,把F-104改装为火箭,或配备四台涵道风扇的,巨型垂直起降飞机,还有有着猪鼻子的X-14。这时,美海军通过非官方渠道,得知贝尔公司,正在研发新型垂直起降飞机。随后,美海空军联合向贝尔公司提出要求,希望研发一款,拥有垂直起降能力的超音速战斗机,既能执行全天候作战任务,又能进行防空拦截。D-188A项目便应运而生。贝尔给出的设计十分大胆。它配备了八台通用电气涡喷发动机,其中四台,位于翼尖的可旋转短舱内,另外四台,分别置于机身尾部,和驾驶舱后部。起飞时,翼尖短舱转至垂直方向,八台发动机,共同提供垂直升力,达成垂直起降。平飞时,翼尖短舱转至水平方向,发动机提供向前的推力,设计速度能够达到马赫数2。1958年初,飞机模型制作完成,设计方案也已确定。但可惜的是,这款看似无懈可击的机型,最终却没能走出实验室。这架战斗机设计理念,可以说是非常地前卫。
|
||||
|
||||
|
||||
## 【嘉宾6】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【专家6】
|
||||
|
||||
甚至可以说是超出了,当时的这样的一个技术水平。那么这个项目失败的原因之一,就是因为采用了八台发动机,技术非常地复杂。而且八台发动机,都是以不同的安装模式,和这种运转模式,集成到一个平台上,这个技术非常地复杂。而且八台发动机,也大幅度地增加了,飞机本身的重量,导致它的飞行的性能,可能会非常差。外形非常地科幻,贝尔公司也造了模型。但是美国空军和美国海军,在后续进行评估的过程当中,发现这一架战斗机的成本,实在是太昂贵了。大家可以设想一下,一架战斗机,或者说一架战斗轰炸机,它就有八台发动机,这个成本,在当时可以说是天文数字。维修起来也非常地麻烦,而且需要非常复杂的,这种操控系统。在当时也就是(上世纪)50年代的,这样的一个背景下,飞行控制系统,根本无法保证八台发动机,进行一个精准的操控。一旦出现了,某一台发动机出故障的,这样的一个情况,那么这架昂贵的战斗机,可能就会面临坠毁,或者是严重故障的风险。所以最终美国空军和美国海军,都放弃了这个项目。不过这个思路,还是非常的先进的,为以后美军研制,垂直起降的飞行器,包括倾转旋翼飞机,提供了思路基础。尽管D-188A项目未能成功。
|
||||
|
||||
|
||||
## 【解说8】
|
||||
|
||||
贝尔公司却从未停止,对垂直起降技术的钻研,众多珍贵的经验,在这些经验的基础上,鱼鹰倾转旋翼机,得以问世,随后又进一步推出,V-280英勇倾转旋翼机,旨在替代美军日渐老旧的,UH-60黑鹰直升机,其巡航时速处于426公里,到574公里之间,转场航程,高达3889公里,它运用了倾转旋翼的设计理念,不过对机械构造进行了改良,安全与可靠性,较鱼鹰有了显著提高,并且安装了和F-35战斗机相近的,分布式孔径系统,飞行员能够实现全方位观测,作战实力大大加强,2025年5月,美陆军将V-280正式命名为,并计划在2027年,向部队交付首批作战飞机,这一时间点比最初规划,提前了约5年,其实,不单单是美国,苏联同样在垂直倾转旋翼机方面,开展过积极的研究,其中最具典型的,便是卡莫夫设计局,于1967年提出的,KA-35重型运输旋翼机方案,或许有观众会有疑惑,苏联的KA-35,和美国的倾转旋翼机,存在哪些差异呢,KA-35可以说它的设计理念,还是有创新的,那如果说它能够研制成功,而且交付部队,那(苏军)就会拥有一种,高速低空飞行的,这样的一个作战平台,既可以有效地执行侦察任务,也可以把它作为一种突击平台,根据公开资料显示。
|
||||
|
||||
|
||||
## 【嘉宾7】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【专家7】
|
||||
|
||||
KA-35的设计定位,是重型的运输平台。安-12运输机执行任务,它最大的起飞重量,可以达到71.5吨,最大的载荷可以达到20吨。巡航速度可以达到,500公里每小时,最大的航程为800公里。这样的性能参数,在当时来说是非常先进的。KA-35采用的是,重叠旋翼的设计理念,所以它的这个,有效载荷的搭载能力,是比较强的。既可以进行高速飞行,可以携带更多的有效载荷。同样它也可以配备武器系统,比如说在这个飞机的前部靠下,这个位置也可以安装,相关的机炮,也可以针对目标进行扫射。
|
||||
|
||||
|
||||
## 【解说9】
|
||||
|
||||
但可惜的是,KA-35最后也仅仅停留在设计方案,与模型阶段,并未制造出原型机。主要原因,在于当时苏联的工业水准,无法攻克重叠旋翼的,气动稳定性难题,且双涡喷发动机燃油效率过低,航程难以达到军方要求。此外,当时苏联,将军费重点投入到战略导弹,和核潜艇方面,最终使该项目被搁置。看到此处,我们不难发现,垂直起降飞机的研发,属于难度极高的系统工程,其需要雄厚的航空工业基础,尖端的发动机技术,以及精细的机械制造能力。不管是美国的D-188A,还是苏联的KA-35,都因技术成本等问题,未能达成量产。
|
||||
|
||||
|
||||
## 【主持人4】
|
||||
|
||||
通过前面的讲述,大家已经认识到,垂直起降飞行器的设计理念,相当先进,纸面性能也极为出色,甚至被不少人誉为,改写军事飞行规则的装备。一款先进装备的研发,从来不是一帆风顺的。那么眼下让美军寄予厚望的X-76,未来究竟会顺利落地,大放异彩,还是会重蹈覆辙,无疾而终呢?X-76它的设计理念,确实非常地超前,但是同样也是有难点。我们看X-76它的机体,是采用流线型的设计理念,采用的是V字型的,这样的一个尾翼。但是这些都不是难点,因为机身的这个设计,已经比较成熟。那么机翼,包括动力吊舱的这个倾转系统。
|
||||
|
||||
|
||||
## 【嘉宾8】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【专家8】
|
||||
|
||||
都不存在这个技术难点,最大的技术难点,就是发动机叶片,采用折叠收拢的这样的一个模式,它的可靠性一定要非常强。为什么这么说?如果说从平飞,要切换到,垂直起飞和降落的,这样的一个状态,可能会出现机身抖动的情况,或者说,受到一些复杂气流的影响,可能它的飞行状态,就无法保证这样的一个平稳可靠。一旦出现这个情况,它就会面临非常大的风险。
|
||||
|
||||
|
||||
## 【解说10】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【嘉宾9】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【专家9】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【解说11】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【嘉宾10】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【专家10】
|
||||
|
||||
现在从技术角度来讲,从它的结构来讲,X-76可能比鱼鹰还复杂。那么现在,鱼鹰已经是故障频出,多次地出现了机毁人亡的,这样的一些情况。那未来如果X-76,它在成熟之后,无法解决可靠性问题,在高速飞行的状态下,一旦出现了故障,那么它所面临的危险,可能要比鱼鹰还要多。或许有观众会有疑惑,贝尔公司,不是已经在风洞测试里,证实了旋翼折叠的可行性吗?为何这一难点仍然存在呢?事实上,风洞测试仅是模拟条件,真实飞行中的状况,要复杂得多。例如高空中的气流波动,温度变化,都会对旋翼的折叠和动力切换,产生影响。尽管贝尔公司已验证了,旋翼折叠的可行性,但在实际飞行中,如何保证过渡阶段的平稳性与安全性,仍需要进行大量的飞行试验,和技术改进。这也是X-76研发过程中,亟待解决的关键难题。X-76还有一个问题,就是它配备了,其实是两套这个动力系统。那么有两套,完全不同属性的这个动力系统,集成到一个平台上,也会导致它的重量大幅度地增加。而且,一旦在水平飞行的过程中,它的喷气式发动机出现了故障,相关的垂直飞行的,这样的一套动力系统,能否在短时间内切换和救急,我认为还是未知数,因此还是存在隐患的。除了这些技术难题,X-76还遭遇成本,与可靠性的挑战。鱼鹰单价已超一亿美元,而X-76,运用了更为先进的技术,成本必然更高。即便美军军费充裕,也得权衡一下经济负担。X-76它的设计理念,美国军方已经认可,但是在动力系统的研发,在整体的设计方面,包括解决可靠性问题方面,贝尔公司还要面临,很复杂很棘手的一些问题。所以它未来能否真正地成熟,能否成为一款,实实在在性能先进的,这样的一个作战装备,我认为还是要打上一个大大的问号。美军在研制,武器装备的过程当中,有的时候就是要贪大求全,既要也要。这样的一种思维和逻辑,和这种装备的发展,进行这个结合的过程当中,可能就会出现期望值比较高,或者说提出的,这样的一个技术要求比较高,但是平台实现起来,难度较大。所以从目前来看,它的设计理念是没有问题的,但是能否在工程技术的,这样的一个状态下,能够实现,变成真正可靠的作战装备,我认为还是一个未知数。
|
||||
|
||||
|
||||
## 【主持人5】
|
||||
|
||||
X-76力求突破性能限制,达成速度与灵活的精准平衡。尽管当下,存在着不少的技术难题,未来的发展也充满未知,但是我们必须承认,其设计思路与技术革新,为军事航空领域的发展,带来了新的启发。日后伴随技术的持续发展,相信会有更多更先进的装备诞生,重塑战争的模式。感谢您持续关注,国防军事频道《军事科技》,我们下周同一时间。
|
||||
@@ -0,0 +1,207 @@
|
||||
---
|
||||
标题: 启程远航
|
||||
编导: 张彤枫
|
||||
播出日期: 2026-04-21
|
||||
类型: 节目文稿
|
||||
期号: ep014
|
||||
---
|
||||
|
||||
# 导视:
|
||||
|
||||
|
||||
## 【导视1】
|
||||
|
||||
77载深蓝砺剑,77载向海图强。在人民海军的序列中,有这样一些特殊的存在,它们不担负作战任务,而是青年官兵驶向大洋的第一课堂。看戚继光舰启程远航,听郑和舰续写传奇。值此人民海军成立77周年之际,《军事科技》与您和海军青年官兵一起,启程远航,乘风破浪。
|
||||
|
||||
|
||||
## 【主持人1】
|
||||
|
||||
各位观众你们好,欢迎收看《军事科技》,我是主持人蓝皓。就是人民海军,77周岁的生日了。77年风雨兼程,人民海军从无到有,从小到大,从弱到强。在此之前,我们关注过很多巨舰。我们要看一些,比较特别的战舰。它们虽然不像驱逐舰,护卫舰那样全副武装,但是却承载着海军,未来的希望。它们虽然不直接参与海战,但是,每一次远行,却为海军,播下了勇毅前行的种子。它们,就是被称为“海上流动大学”的,训练舰。本期节目,就让我们一起走进它们。此刻我就站在,郑和舰的后甲板上。很少出现在公众的视野当中,但是我相信,很多海军官兵都在此处,留下了自己成长的足迹。那么这艘舰艇,有怎样的特别之处。
|
||||
|
||||
|
||||
## 【主持人2】
|
||||
|
||||
而此处,又发生了怎样让人印象深刻的故事呢?让我们一起来听一听。
|
||||
|
||||
|
||||
## 【学员1】
|
||||
|
||||
主持人好,大家好,欢迎大家来到海军郑和舰。海军郑和舰是我国自行设计建造的第一艘远洋航海训练舰,舷号81,也被誉为中国军校第一舰,承载着传承和平、耕海育人的双重使命。
|
||||
|
||||
|
||||
## 【三维动画解说1】
|
||||
|
||||
81郑和舰,是我国第一艘自行设计建造的远洋航海训练舰,舷号81,以明代航海家郑和命名,1987年4月27日正式加入海军服役,隶属于大连舰艇学院,被誉为“军校第一舰”。该舰长约132米、宽约16米,正常排水量约6000吨,最大航速21节,续航力一万海里,可在除极区外所有海区航行。虽然作为训练舰不担负作战任务,但为了实战化训练的需求,郑和舰也配备了一定的基础武器系统。主要包括两座57毫米双管舰炮、两座30毫米双管副炮以及两座81式五管反潜火箭发射器,尾部飞行甲板虽然没有设置机库,但也可以起降一架直-8舰载直升机。舰上还配备完善的教学和生活设施,可保障两百多名学员开展四十多个课目的实习训练。
|
||||
|
||||
|
||||
## 【专家1】
|
||||
|
||||
郑和舰紧贴舰艇人才的能力需求,为学员提供了体系化的海上实习内容。学员在舰上可以系统地开展海图作业、目标定位、航海仪器使用等航海专业训练,掌握航海保证的基本技能。实习学员上舰以后,会被我们分配到不同的部门,实现舰员与学员的混编统管。这样既有利于教学活动的顺利开展,又可以使学员充分代入舰员角色,助力学员实现从学员到舰员、指挥员的身份跨越。整个实习过程中,把理论知识和海上实操紧密结合,让学员在真实海况下适应舰艇生活,提升岗位履职能力,为今后走向第一任职岗位打下坚实基础。训练舰不同于驰骋海疆的作战舰艇,而是一个技能传授的平台。作为陆上到海上的关键桥梁,它不追求火力与速度,而以稳定、适航与教学功能为核心。作为海上移动军校,训练舰就是一本会移动的教科书。报告指挥官,目标方位070,距离5海里,航向000,航速15节。驾驶战舰行驶在茫茫大海之上,最重要的一点就是认清航向。毕竟一望无际的大海,没有陆标,没有参照,唯有罗经指针的微光与北斗星图的坐标。所以。
|
||||
|
||||
|
||||
## 【解说1】
|
||||
|
||||
作为一名刚刚登上郑和舰的学员,第一个任务,就是学会海图绘制。
|
||||
|
||||
绘制海图:
|
||||
|
||||
|
||||
## 【主持人3】
|
||||
|
||||
任务我们已经收到了,在陆地上看导航,大家看得很清楚,但是如果在茫茫的大海之上,如何进行海上自我定位呢?
|
||||
|
||||
|
||||
## 【学员2】
|
||||
|
||||
海上定位方式通常有三种,第一种叫陆标定位,第二种叫无线电定位,第三种叫天文定位。在我们沿岸航行时,最常采用的就是陆标定位。我们以现在这张海图为例,我看某一个岛屿的方位,假如说是200,我沿200的方向画一条线,再看这个岛屿的方位,假如说是260,同样也画一条线,两个线的交点,就是我们现在所在的位置。
|
||||
|
||||
|
||||
## 【主持人4】
|
||||
|
||||
这就是刚才提到的,两边定位法。如果我们在茫茫的大海上,或者是只能看到一个岛礁,这个时候又该如何自我定位呢?如果岛礁的距离,还有岛屿的形状,都非常合适的话,非常推荐采用的就是,单标方位法进行定位。而单标方位法,就离不开一个,重要的航海仪器,就是六分仪。六分仪,是航海天文定位的核心工具,通过测量天体,与海平线之间的夹角,即可推算出,舰船所处的经纬度。它不依赖电子信号,不受电磁干扰,在远洋无参照环境下,仍能保持高精度。在现代导航系统,高度发达的今天,六分仪。
|
||||
|
||||
|
||||
## 【学员3】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【解说2】
|
||||
|
||||
依然是郑和舰学员必修的压舱石课目。
|
||||
|
||||
六分仪使用
|
||||
|
||||
|
||||
## 【专家2】
|
||||
|
||||
现在我手里拿的,就是我们航海上常用的六分仪。它主要是由三大部分组成,首先是架体,其次是光学系统,它主要是包括动镜、固定镜、滤光片以及望远镜。最后一部分是测角读数装置,它是由指标杆、分度弧、鼓形轮以及游标尺组成。那么我们在观测天体时,尤其是测量高度角,我们就需要垂直拿六分仪。这个时候我们观测太阳,由于太阳比较刺眼,所以我们需要选择一个合适的滤光片,选择一合适的滤光片去进行观测。当我们要测量高度角时,我们在望远镜里,可以看到太阳的直视影像和反射影像。接下来我们拉动指标杆,将太阳反射影像的上边缘,与太阳直视影像的下边缘进行相切。最后转动鼓形轮进行微调。这个时候,我们会测出一个读数,现在就可以获得我们距离太阳的方位和高度角是多少。后期这个数据,我们进行计算,可以用来进行定位。
|
||||
|
||||
|
||||
## 【解说3】
|
||||
|
||||
除了六分仪的使用,当学员登上郑和舰的那一刻,如何将这样一艘庞然大物,顺利驶离码头,穿越狭窄水道,规避暗礁浅滩,便成为每名学员,必须直面的实战课题,而这种实操能力,并不是靠书本就可以学会的,为此,以郑和舰为代表的,远洋训练舰上,全任务模拟航海驾驶教学系统,模拟驾驶,是训练舰的特色课目之一,对于刚上舰的学员来说,直接实操驾驶战舰,是非常容易出事故的,因此,学院教室,远洋训练舰上,都配备了,全任务模拟航海驾驶教学系统,让学员在零风险环境中,锤炼应变能力与操作精度,通过全景视角,与实时数据交互,学员能同步感知风浪力矩,航向偏移及敌情动态,操作台上,还原了真实舰桥的每一块仪表,使学员能更快适应真实作战节奏,每一次推杆,每一道口令,都精准对应物理反馈,与系统响应。
|
||||
|
||||
|
||||
## 【解说4】
|
||||
|
||||
郑和舰,作为中国海军首批远洋训练舰之一,是无数海军军官成长的摇篮。在这艘舰上,不但拥有宽敞明亮的教室和先进的教学设施,更可以为学员提供真实海况下的航行训练机会。从黄海到南海,从西太平洋到印度洋,郑和舰每年执行数次远航实习任务,将课堂延伸至万里海疆。
|
||||
|
||||
|
||||
## 【专家3】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【主持人5】
|
||||
|
||||
看完了经历数代海军成长的郑和舰,接下来我们将目光转向另一艘功勋卓著的战舰邓世昌舰。相比于郑和舰,它的外观完全不一样。此刻我就站在邓世昌舰的甲板之上,为了更直观地感受它的甲板有多大,接下来跟随镜头一起欣赏一下。
|
||||
|
||||
|
||||
## 【三维动画解说2】
|
||||
|
||||
82邓世昌舰,是中国唯一的国防动员舰,隶属海军大连舰艇学院,1996年12月正式列装服役,以民族英雄邓世昌命名。其外形相对独特,只保留了高耸的舰桥和一个巨大的飞行甲板,让它的外形看起来更像是一个巨大的货轮,在外形上明显区别于其他军舰。其核心功能是为海军学员提供航海实习,同时可通过模块化改装,实现医疗救护、直升机训练、集装箱运输等任务,承担国防动员演练,是平战结合、寓军于民的典范。服役以来,累计保障百余批学员实习,总航程达四十多万海里。当你第一次看到邓世昌舰时,一定会被它那宽阔的飞行甲板与极具辨识度的双直升机起降平台所震撼。这个巨大的甲板不仅是舰载直升机起降的核心区域,更是学员开展海上立体作战保障训练的重要平台。
|
||||
|
||||
|
||||
## 【专家4】
|
||||
|
||||
位于我身后的是海军邓世昌舰的医疗模块,主要用于海军医疗队和医学院校学员海上实习保障。主要由18个标准集装箱组成,具有手术室、分级病房、X光室、检验室以及各类医疗检验设备,可以同时进行三台手术以及其他伤病员救治。作为海上移动医院,与一般医院最大的区别是,可以根据任务所需要,在短时间内增配不同的医疗模块,完成功能拓展。
|
||||
|
||||
|
||||
## 【解说5】
|
||||
|
||||
海上医疗任务是邓世昌舰平战转换能力的核心体现,其集装箱式医疗模块可在48小时内完成布设与联调,实现从训练舰到野战医院的快速切换。在这些集装箱中,分设了包括手术室、重症监护室、检验科及卫生防疫单元在内的全功能医疗体系,几乎是将一座现代化野战医院完整搬到了海上,真正实现了舰行万里、医随舰走。同时,邓世昌舰还可以为众多海军官兵提供海上医疗救援课目的训练,涵盖海上批量伤员转运、战创伤紧急处置、远程会诊联动等实战化课目,曾参加全国多个省份的医疗救援队集训拉动演练,先后培养出两千余名卫勤保障人员,并多次在台风抢险、远洋护航等重大任务中完成实战化卫勤保障,成为海军官兵信赖的海上生命方舟。
|
||||
|
||||
|
||||
## 【专家5】
|
||||
|
||||
医疗模块,在军医大学学员,和卫生专业学兵,实习训练中起到重要的作用,在实习中,我舰充分发挥医疗模块功能,以反恐 反海盗等,等战斗部署操演为背景,使学员和学兵,全流程全要素,参与到医疗 救护,伤病员转运等训练环节中,使学员和学兵,在课堂上所学到的知识,在实战环境中得以应用。
|
||||
|
||||
|
||||
## 【解说6】
|
||||
|
||||
宽大的飞行甲板,不但可以让邓世昌舰化身海上立体救护平台,更可以成为海军官兵开展舰载机起降协同训练、垂直补给演练和伤员空中后送的关键枢纽。当然,随着现代武器装备的发展,无人作战系统日益融入海上卫勤体系,这也为邓世昌舰赋予了新的任务。在气候变化复杂的海上,无人机的使用成为了一个全新的课题,但常规舰艇的飞行甲板相对较小,难以满足多型医疗无人机起降与协同作业需求。邓世昌舰凭借巨大的飞行甲板,成为海军官兵无人机装备训练的首选平台。
|
||||
|
||||
无人机:
|
||||
|
||||
|
||||
## 【专家6】
|
||||
|
||||
随着科技发展和战争演变,我们也在探索将无人设备融入学员的实习体系中,可以让学员学会无人装备、战术编组、协同指挥、态势研判,培养联合作战思维。同时,我们也会用无人装备模拟高危复杂场景,构设逼真战场环境,以此提升实习质效。此外,结合无人机后送、无人补给、远海搜救等场景,提升海上应急救援和保障能力。
|
||||
|
||||
无人艇:
|
||||
|
||||
|
||||
## 【专家7】
|
||||
|
||||
接下来我为大家介绍,我院投入教学训练使用的两类无人艇。这也是近两年来,支队官兵参与学员专业学习,海上实习的新型实训设备,在支队教学训练提质增效方面,发挥着重要作用。第一类是挂载声呐的水下探测目标无人艇,主要用于模拟水下目标探测。在相关课程的学习中,它将抽象的水下探测理论、声呐工作原理、目标识别技巧等知识具象化,让学员更加直观地看到声呐工作流程、水下信号反馈。数据采集、目标研判等实操训练,让学员在真实海况中掌握装备操作技能,积累水下探测实战经验。第二类是三米级无人艇原型机,它具备自主划定航线作业的能力。在课堂教学中,它主要用于无人装备操控、航线规划。学员通过实操设定航行路线,掌握无人艇操控逻辑,开展编队航行、协同作业等实训科目,锻炼学员的海上装备协同指挥。
|
||||
|
||||
|
||||
## 【解说7】
|
||||
|
||||
相较于无人机,无人艇的训练,则更依赖邓世昌舰的巨大甲板。它为无人艇的布放、编队协同及复杂海况适应性训练,提供了充足空间与稳定平台,使学员能在真实风浪中,反复锤炼无人系统海上作业全流程能力。邓世昌舰甲板上,浪花飞溅,无人艇编队正依指令破浪前行;无人机腾空而起,医疗模块遍布甲板,红十字标志在阳光下格外醒目。让邓世昌舰成为了海上全能课堂。
|
||||
|
||||
|
||||
## 【专家8】
|
||||
|
||||
对于我们支队这样,以教战、研战、能战为发展核心的单位,将这两类无人艇投入到教学训练中,创新了教学模式,实现了理论教学具象化,实操训练实战化,人才培养专业化。既有效还原了海上实战环境,又全方位提升学员的专业实操与综合应用能力,补齐了无人装备实操教学的短板,助力支队打造更加贴合实战需求的海上教学实习体系,为培养高素质、专业化的海军人才,筑牢了装备根基,推动教学训练工作迈上新台阶。
|
||||
|
||||
|
||||
## 【解说8】
|
||||
|
||||
作为我军唯一的一艘国防动员舰,平时主要承担实习训练、国防动员、卫勤演练、医疗救护、直升机训练、武器试验等使命任务。加装医疗模块后,可作为医疗船使用。因其独特功能作用,2002年依托邓世昌舰,成立了我国第一个海上卫生动员中心,承担培养海军高素质新型军事人才和海上国防动员、应急应战的双重使命。
|
||||
|
||||
|
||||
## 【主持人6】
|
||||
|
||||
刚才完成登舰的,就是大连舰艇学院的大四学员。他们即将驾驶83戚继光舰,完成自己大四学年的最后一次实习远航。在此次实习之后,他们就会奔赴自己的岗位,守护祖国的万里海疆。那么在此次的实习当中,他们又会锻炼到自己的哪些岗位呢?戚继光舰又该如何守护他们,驶向远方呢?接下来,让我们一起去看一看。
|
||||
|
||||
|
||||
## 【解说9】
|
||||
|
||||
这一次的航行实习,对于所有学员来说,不仅是一次技能检验,更是一场向战而行的青春出征。学员们将在戚继光舰上轮岗实操航海指挥、机电管理、通信导航、损管应急等核心岗位,同步开展远海综合演练、跨区对抗训练与联合搜救任务,在真实海况中锤炼临机决断与协同作战能力。舰艏劈开碧波,航迹延伸向深蓝。
|
||||
|
||||
|
||||
## 【专家9】
|
||||
|
||||
在上次也就是大二的时候,一次远洋实习也是在83舰上。那时候,我们在海图桌上,练习这个抛锚。然后我选的抛锚的地点,对于我们航海专业来说,是一个比较好的抛锚点。当教员走到我身旁,问我为什么选择这个地点时,我把我所学的专业知识,给他介绍了一遍。但是他却阴沉着脸对我说,你只考虑了航海的因素,但是你是一名水面舰艇的军官,你不考虑战术要素吗?你不考虑防空,你不考虑反潜的要素吗?我想这是在课堂中的学习,所不能给我带来的。
|
||||
|
||||
|
||||
## 【解说10】
|
||||
|
||||
再一次登上戚继光舰,王浩然已从青涩学员成长为一名能独立完成海图作业的实习骨干。而这一次,他即将接受来自戚继光舰上教员的最后的考核。在此次航行实习中,他首先要进行的就是他最熟悉的海图作业考核。在教员的指导下,为全舰完成出航前最后的准备工作。
|
||||
|
||||
|
||||
## 【专家10】
|
||||
|
||||
今天我指导学员训练的,是一般条件下航行的航海保证工作。这个科目是航海军官的基础能力训练,更是未来履职的基本功。航线规划与定位,直接决定着舰艇的航行安全,是准航海军官从理论走向实操的关键一步。
|
||||
|
||||
|
||||
## 【解说11】
|
||||
|
||||
而在另一边,指挥舱室内,学员杨政和同班组的同学们一起来到驾驶室,在教员的指导下进行着这次远洋实习的驾驶流程训练。每个战位对于他们来说都不是第一次尝试,但是,当他和同学们走出教室,来到真实的戚继光舰驾驶室,才会发现真实的海况,瞬息万变的电子海图,突发的模拟敌情,每一个细节都远超课本的想象边界。
|
||||
|
||||
|
||||
## 【学员4】
|
||||
|
||||
其实我更喜欢在舰上的实习,因为,在舰上的实习本来就比较珍贵,而我们在院校,理论知识是随时可以学的,而真正到实操,只有到船上才能够实操,所以我们都,比较期待在线上学习,它能够将我们,所学的理论知识,更多地去让我们,通过自己的手去体现出来,然后自己也会感觉,收获到更多。
|
||||
|
||||
|
||||
## 【解说12】
|
||||
|
||||
但是,想要将一艘9000多吨的巨舰稳稳驾驭于风浪之间,绝不是仅靠海图上的路线和战士手中的舵轮就可以完成的。对于刚刚登上戚继光舰的年轻学员而言,熟知和了解不同岗位的职责与技能是迈向合格舰员的第一步。朱哲是一名航海专业的学员,但是来到舰上的第一课却是被安排在机电部门。这种跨专业的轮岗也是每一名实习舰员必需的经历。
|
||||
|
||||
|
||||
## 【学员5】
|
||||
|
||||
83舰的学习,我们可以了解到,我们这个岗位的业务,包括如何去做,这都是,相当于是一条必经之路,然后珍惜这个时间,也在这个过程中,是有试错机会的,然后我们可以通过这些机会,避免我们以后,在实际岗位上出现问题。无论是在哪个岗位,对于实习舰员们来说,这都是他们,最后一次登上戚继光舰的,远洋实习之旅。此后,他们将奔赴各自战位,或守卫海疆,或驰骋深蓝。我自身的话,还是比较渴望去,主战舰艇上,特别是像055型驱逐舰这种,大型的主战舰艇。我想说,戚继光舰是一个,非常大的舞台。来到这里,你将熟悉学会,几乎整个水面舰艇各个战位,你所要学到的知识,你所要实践的技能。希望大家,都来海军大连舰艇学院。
|
||||
|
||||
|
||||
## 【解说13】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【主持人7】
|
||||
|
||||
当中国舰长,在此前的场景当中,我们经常看到战舰列阵,军旗猎猎,感叹大国海军的威武雄壮。而此刻,我们伫立在军港,即将送别远航的莘莘学子,以及护送他们的训练舰。虽然此刻,他们并没有取得驰骋海疆的赫赫战功,但是他们却满怀守护海疆的赤诚初心。他们暂别宁静的港湾,即将在风浪里淬炼意志,砥砺前行。他们,就是人民海军的新鲜血液,撑起强国梦想的明日栋梁。好了观众朋友们,国防军事频道《军事科技》,我们下周同一时间,再见。
|
||||
@@ -0,0 +1,124 @@
|
||||
---
|
||||
标题: 舰炮的暴力美学
|
||||
编导: 穆佩弦
|
||||
播出日期: 2026-04-28
|
||||
类型: 节目文稿
|
||||
期号: ep015
|
||||
---
|
||||
|
||||
# 《舰炮的暴力美学》
|
||||
|
||||
|
||||
## 【导视1】
|
||||
|
||||
在战舰的钢铁身躯上,舰炮,是最震撼的视觉符号。它曾是改变海战格局的利器。从17世纪的滑膛炮,到二战时期的巨型主炮,舰炮的每一次射击,都在书写崭新的海战规则。如今,舰炮,不再只是简单的火力工具,它们已经成为海上战术的多面手。今天,让我们一起走近舰炮的世界,感受那一发发炮弹背后的暴力美学。
|
||||
|
||||
|
||||
## 【主持人1】
|
||||
|
||||
各位观众你们好,欢迎收看《军事科技》。我是主持人蓝皓。海战中,火力曾经意味着一切。在过去,更大的口径,更重的炮弹,往往直接决定海上战局的胜负。舰炮的每一次齐射,不仅是海军力量的宣示,更是海战战术的核心所在。然而,在如今的战舰上,却出现了一个看似反常的现象。现代军舰的主炮口径,却在不断地缩小。从曾经的巨炮,到如今的自动化舰炮系统,舰炮的口径在变小,作战能力却在持续提升。这门看似平平无奇的舰炮,都经历了哪些坎坷的发展经历呢?
|
||||
|
||||
|
||||
## 【二维动画解说1】
|
||||
|
||||
1805年10月21日,特拉法加海域。清晨的海面异常平静,微风缓缓掠过水面。英法西三国舰队都知晓,一场恶战已不可避免。西班牙旗舰圣三位一体号,拥有当时最先进的四层火炮甲板,载有超过100门舰炮,是当时世界上火力最强的战舰之一。而在另一侧海域,英国舰队旗舰胜利号舰身两侧,排列着数十门32磅前装滑膛炮。这些舰炮,由铸铁整体浇铸而成,每门重达数吨。一发炮弹重达十几公斤,依靠黑火药爆炸推出炮口,在近距离内,足以击穿厚重的木质船体。
|
||||
|
||||
|
||||
## 【解说1】
|
||||
|
||||
滑膛炮首次被安装在战舰上,是海战史上的一次重大转折。16世纪末至17世纪初,海上冲突日益频繁。传统的海战手段——撞击、弓弩、火绳枪——已难以决定战局。舰队作战急需一种能够远程摧毁敌舰的火力武器。滑膛炮适时地被搬上了战舰。它能够发射重型铁弹,射程远、穿透力强,可以击穿敌舰木质船体和甲板。最初,这些炮是由陆地攻城炮改造而来,通过加固甲板和设计专门的炮架,使其能够承受发射时的巨大后坐力。舰船不再只是漂浮的堡垒,而成为真正的移动火力平台。舰炮的数量、排列和齐射能力,逐步成为舰载设计和战术部署的核心。火力密度直接决定了战斗的胜负。滑膛炮的上舰,海战从近身搏杀走向远程火力决胜的时代。它不仅改变了单艘战舰的作战能力,更深刻影响了整个舰队的战术格局。
|
||||
|
||||
|
||||
## 【二维动画解说2】
|
||||
|
||||
随着信号旗升起,英国舰队开始行动。不同于传统的平行对射,英国海军中将纳尔逊选择了一种更为激进的战术。英国舰队分为两路,从垂直方向切入西法舰队的横列队形。当纵队逼近时,英舰突然转向,与敌舰形成交叉角度。舰炮齐射,从侧舷猛烈倾泻。这一瞬间,舰炮的优势被彻底放大。西法舰队由于队形被切割,无法形成有效齐射;而英军则可以在短距离内集中数十门火炮,对单一目标进行打击。而西法联军,尽管拥有更多舰炮,但在混乱中难以形成统一火力。数小时激战后,英军共俘获或摧毁西法舰船22艘。舰炮的轰鸣已经停息,它所决定的战局,已经无法改变。
|
||||
|
||||
|
||||
## 【解说2】
|
||||
|
||||
18世纪的海战,是火药与木材的直接对抗。在当时主力战舰的炮舱内,一门门32磅长炮整齐排列,沿着两侧舷墙排开,宛如一列沉默而威严的铁巨人。所谓32磅,指的是每枚炮弹的重量。除了32磅炮,英国海军的战舰上还配备有24磅、18磅,甚至12磅的火炮,它们分别被装在不同的位置,形成从主力到辅火的多层火力体系。在激烈的战斗中,一分钟能发射一到两次已是极限。炮手们在烟雾、火光和轰鸣声中紧张操作,每一次装填都是对体力与耐心的考验。那么我们看到,在风帆战舰的鼎盛时期,这个战舰的个头都非常大,它是有多层结构的。那么往往会在战舰的一侧,也就是侧舷,它有很多可以开闭的这样的一些炮口。
|
||||
|
||||
|
||||
## 【专家1】
|
||||
|
||||
当时在战舰上的这种滑膛炮,和陆地上使用的滑膛炮,基本上没有什么区别。它也是一种非常简易的,射击距离比较近的,这样的一个火力发射系统。它开火的这个准备时间,是比较长的。完成了一次开火之后,需要相关的操炮的人员,针对炮膛进行清理,然后再次地装填火药、炸药,后续还要装填弹丸,最后把它压实,装上引信,点燃之后才能够完成再次的开炮和击发。所以整个的过程非常地长。但是我们注意到这些滑膛炮,它首先是没有膛线的,那么当弹丸离开火炮之后,它稳定飞行的这样的一个时间,并不是特别长。所以很大情况下,是靠这种弹药的密集程度,也就是说,它打的是一个概率。通过火炮数量的增加,去提升它的这样的一个命中率。所以在风帆战舰时代,那么火炮的数量,战舰的吨位就比较重要。随着工业革命的推进,海战的面貌发生了根本性的变化。风帆战舰与滑膛炮的时代逐渐走向终结。铁板与钢甲开始覆盖舰体,使得单纯依靠重量和炮弹密度压制敌舰的战术显得力不从心了。为了突破厚重的装甲,舰炮也迎来了革新,线膛炮应运而生,膛线让弹丸旋转飞行。
|
||||
|
||||
|
||||
## 【主持人2】
|
||||
|
||||
大幅提高稳定性和命中率,射程和穿透力,都得到了前所未有的提升。紧随其后的,是铁甲舰与速射火炮的结合,战舰的设计理念,也在随之改变。单一火力压制,让位于综合作战能力,远程精确打击,开始成为可能。19世纪末,一场新的革命,再次席卷海军,无畏舰的出现,彻底颠覆了传统战列舰观念。
|
||||
|
||||
|
||||
## 【解说3】
|
||||
|
||||
19世纪末至20世纪初,世界海军正处在由传统向现代转型的关键阶段。蒸汽动力逐步成熟,钢铁装甲广泛应用,远程火控技术不断进步,海上作战方式随之悄然改变。在这样的时代背景下,无畏舰的建成服役,标志着战列舰发展进入新的历史阶段。
|
||||
|
||||
|
||||
## 【三维动画解说1】
|
||||
|
||||
无畏号战列舰,标准排水量约18000吨,全长约160米。动力系统采用蒸汽轮机驱动,最大航速可达21节,在当时属于领先水平。它采用全钢结构建造,厚重装甲集中防护关键舱室与炮塔,使战舰在火力打击面前具备更强的生存能力。
|
||||
|
||||
|
||||
## 【专家2】
|
||||
|
||||
无畏舰对于海战格局的影响,那就是,首先要有强大的工业体系。因为无畏战舰,和这个风帆战舰相比,它的生产成本,还是比较高的。它最大程度地克服了,自然因素的影响,本身的动力系统比较强劲,可以在海上高速地航行。另外一个,它并不是拼火炮的数量,开始使用大口径的舰炮,而且有这种,旋转式的炮塔设计。舰炮的这个俯仰角,是可以进行调节的。其实这里面,海战的这样的一个模式,可以说进一步地升级了。那么针对对方的,火力杀伤的精准程度,进一步地提升。而且,即使是遭到了对方的,大口径火炮的轰击,那么无畏舰,也可以利用自身的舰体结构,包括一部分的装甲防护,更好地去保护,自身的动力系统,甚至是这个弹药舱。所以在无畏战舰,在海上进行决斗的,这个过程当中,各种因素叠加起来,才能够有效地取得胜利,才能够立于不败之地。比如说,航行速度快不快,火炮的口径大不大,操舰的这个海军官兵,他的技术是否纯熟。那么这些因素叠加到一起,才能够成为,在无畏舰时代的,海上的制胜本领。
|
||||
|
||||
|
||||
## 【三维动画解说2】
|
||||
|
||||
无畏号战列舰,共装备10门12英寸主炮,分布在5座双联装炮塔之中。其中,舰首与舰尾各一座炮塔,舰体中部两侧设有侧翼炮塔,另一座位于中轴线上方,形成多方向火力覆盖。所谓12英寸,指的是炮弹直径约305毫米。单枚炮弹重量约380至390公斤,采用分装式装填方式,将炮弹与推进药分别装入炮膛。主炮炮管长度约为45倍口径,全长超过13米。在发射时,炮弹初速可达到约每秒800米,大约射程约15至20公里。在射击节奏上,每门主炮的射速约为每分钟一至两发。一轮齐射时,10门主炮可在极短时间内同时发射,形成大范围火力覆盖。在结构上,炮塔具备水平旋转与俯仰能力,水平旋转角度接近300度,其早期配置的最大仰角约为13至15度,这使得主炮不仅可以进行正面打击,也能覆盖侧舷大范围海域。
|
||||
|
||||
|
||||
## 【解说4】
|
||||
|
||||
进入20世纪上半叶,工业能力的持续提升,与海军战略需求的变化,推动世界主要海军,围绕火力、装甲与航速,展开全面竞争。造舰技术不断进步,钢铁冶金水平稳步提高,动力系统由往复式蒸汽机,迈向效率更高的蒸汽轮机,战舰性能实现系统性跃升。在这一进程中,舰炮口径持续扩大:以无畏舰为代表的早期型号,普遍装备12英寸主炮;随后,以伊丽莎白女王级为代表的战列舰,将主炮提升至15英寸,显著增强远程打击能力。与火力提升相伴随的,是炮塔布局的不断优化。主炮由早期相对分散的配置,逐步转向中轴线集中布置,以提高齐射效率与火力投送密度。
|
||||
|
||||
|
||||
## 【主持人3】
|
||||
|
||||
随着各国海军战列舰的军备竞赛迅速升级,在大量的技术积累之下,战列舰的发展逐步走向成熟。到了第二次世界大战时期,以依阿华为代表的新一代战列舰,将这一发展推向了。它不仅拥有更大口径的主炮,与更远的射程,同时具备更高的航速,与更完善的火控系统,使舰炮真正成为远程、精确、可控的打击力量。将舰炮的威力,与效率推向了一个全新的阶段。
|
||||
|
||||
|
||||
## 【解说5】
|
||||
|
||||
1945年9月2日,东京湾海面晨雾尚未散尽。停泊在海上的密苏里号战列舰,成为全世界目光汇聚的中心。就在这艘钢铁巨舰的甲板上,日本正式签署投降书,第二次世界大战宣告结束。很少有人注意到,这一历史性时刻的见证,不只是胜负的分晓,更是一种武器形态的巅峰呈现。作为依阿华级战列舰的代表,密苏里号所搭载的大口径舰炮,曾在战争中以压倒性的火力,支配海面与海岸。从远程齐射的震撼轰鸣,到对岸火力的持续压制,舰炮,曾是人类将力量推向极致的象征。而当历史在这片甲板上落笔,一个属于大口径舰炮的时代,也正悄然走向尾声。
|
||||
|
||||
|
||||
## 【三维动画解说3】
|
||||
|
||||
在依阿华级战列舰上,最核心的火力,来自其主炮系统,16英寸50口径,Mark7舰炮。16英寸,指的是炮弹直径约406毫米。50口径,则表示炮管长度,为口径的50倍,在炮弹发射过程中,获得更高的初速,和稳定的弹道。每一门主炮,全长超过20米,重量在百吨以上。战舰共配备3座主炮塔,每座3门炮,共计9门主炮,形成强大的集中火力。在发射时,炮弹与推进药分装,单枚穿甲弹重量,可达约1200公斤。依靠高能推进药,炮弹初速,可超过每秒800米。在最佳条件下,射程可达40公里以上。与传统舰炮不同,这套主炮系统,配备了完善的火控系统。雷达测距,弹道计算机,以及炮塔联动系统,使其能够在远距离条件下,对移动目标,实施较高精度的打击。在持续作战能力方面,主炮射速,约为每分钟两发左右。虽然速度不高,但凭借巨大的单发威力,与集中齐射能力,依然具备较强的,毁伤效果。海上的这种火炮对决,它已经到了巅峰时刻。为什么?因为战列舰,它的吨位是非常大的,而且造价很昂贵。只有这个区域性的,或者说世界级的这种海军强国,才能够把战列舰给造出来。一般的小型的工业国家,这个战列舰它根本造不起。因为它涉及到,动力系统,包括船舶的设计,以及,甚至包括在这个钢铁行业,也是需要达到一个,你才能够把战列舰给造出来。战列舰它的火炮,还是要求身管比较长,炮口比较大。有的时候我们看,战列舰的这个炮弹,它的这个弹丸,和一个人差不多那么高。而且,它是全装甲防护。什么叫作全装甲防护?动力系统的相关区域,弹药舱,火炮的炮塔,甚至包括,指挥的这样的一些舰桥,以及其他的这样的一个区域,它都是要有不同程度的,装甲进行防护的。其中我们会注意到,你像三联装主炮的炮塔,或者说四联装主炮的炮塔,它的装甲防御能力是最强的,甚至可以针对对方的,大口径舰炮,进行这种硬扛。所以战列舰最大的优势,就是航行速度是比较快的。另外一个吨位比较大,它上面所使用的,这种主炮的口径,可能会达到数百毫米。而且,这个弹药的威力非常大。如果说对方的战列舰,或者是对方的,大型的这个巡洋舰,那么一轮齐射,只要挨上两枚,这种战列舰所发射的炮弹,它可能就彻底地,失去了作战功能。而且,战列舰,它火炮的射击距离就更远。另外一个,辅助的光学设备就更多了,可以针对对方的这种,海上的大型目标,进行这种猛烈的火力覆盖,而且打得比较准。在很长一段时间里,人们几乎坚信一个,简单而直接的逻辑,口径决定一切。舰炮越大,射程越远,威力越强。巨大的炮塔,数百公斤重的炮弹,跨越数十公里的齐射火力,让海战进入了一个,以规模定义胜负的时代。钢铁与火药的叠加,被推向极致。然而,就在人们以为,舰炮口径仍将持续增长,战列舰,将主宰海洋的时候,一种全新的武器,悄然改变了这一切。
|
||||
|
||||
|
||||
## 【专家3】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【解说6】
|
||||
|
||||
导弹的出现,使打击不再依赖弹道抛射,而是转向自主飞行与精确制导。目标可以在更远距离被锁定,不再依赖密集齐射,而是依靠一次精准打击完成。
|
||||
|
||||
|
||||
## 【主持人4】
|
||||
|
||||
从“越大越强”,到“越准越有效”。曾经以舰炮口径、齐射密度与毁伤规模为核心的作战理念,在新的技术条件下,逐渐被重新审视。打击,不再单纯依赖弹药数量与口径的优势,而更强调对目标的发现能力、锁定能力与一次性命中效率。在这一进程中,以舰炮为代表的传统海上火力体系,并未简单地退出历史舞台,而是逐步调整其功能定位,从决定性打击手段,转向持续支援与近距离压制等更为具体的任务领域。舰炮所承载的暴力美学,也由此在时代更迭中,逐步成为追求效率、节奏与技术协同的机械之美。
|
||||
|
||||
|
||||
## 【解说7】
|
||||
|
||||
二战结束后,战列舰逐渐退出海军主力舞台。以往依靠大口径、单舰绝对火力的作战模式,不再适应新的海上威胁环境。与此同时,舰炮的发展方向发生了深刻转变。冷战时期,海军更强调自动化、精确化与多功能。舰炮口径趋于中小型,但射速大幅提升,装填系统实现全自动化。火控依赖雷达与计算机,能够在远距离条件下精确命中移动目标。更重要的是,舰炮不再只是对海作战的工具。它承担起多种任务:防空、反小艇、岸防支援,成为轻型多用途武器系统的重要组成部分。其中最突出的便是MK45舰炮系统。从冷战巡洋舰,到现代宙斯盾驱逐舰,MK45舰炮几乎贯穿了现代水面舰艇的主力构型。它不再追求极限口径,而是在持续、稳定与高效率之间,定义了舰炮在导弹时代的现实定位。
|
||||
|
||||
|
||||
## 【三维动画解说4】
|
||||
|
||||
MK45舰炮整套系统,可以分为甲板以上的作战单元,和甲板以下的供弹系统。它的炮塔内部由炮管、后膛机构和滑板系统组成。当火炮准备射击时,炮门开启,弹药由下方送入;炮门闭锁后,电动激发装置点火,完成发射。发射瞬间,炮管在摇架带动下,完成俯仰运动,炮塔整体旋转,实现方向瞄准。射击后,后膛机构自动开门、退壳,同时通过吹气系统,将膛内残留气体排出,为下一次射击做准备。弹药储存在弹药库中,通过扬弹机系统,逐级向上输送。弹丸与药筒,分别进入弹鼓,经引信装定后,被送入炮塔内部。在炮塔内,将弹药对准炮管轴线,输弹器将其推入膛内,完成装填。整个过程从供弹、装填到发射,实现了高度机械化。
|
||||
|
||||
|
||||
## 【专家4】
|
||||
|
||||
未来的舰炮发展,它大概会走两条路。第一条路就是,现在的这种,大口径的舰炮,和中小口径的舰炮,它可以继续用。这个和相关国家海军的体量,和这样的一个海上的任务需求,是相关的。另外就是,军事强国,或者说我们讲海军强国,那么这样的一些,海上的作战体系,它在进行平台更新的时候,舰炮体系也会进行,进一步的创新。主要是有两种方式。第一种方式,就是把传统的化学能的火炮,替换为电磁轨道炮。这个电磁轨道炮,可以说会掀起一场,舰炮的技术革命。电磁轨道炮最大的优势,它所发射的这样的一个弹药,可以说是一个实心的铁疙瘩。它是不需要由这个发射药,为它产生推力的,而是利用这种电磁的,这样的一个原理,能够让一个高密度的金属弹丸,能够快速地飞行。它最大的优势就是打得远。由于弹道飞行轨迹,非常地稳定,所以它的命中率是非常高的。第二种就是,会出现的高技术的舰炮,就是激光炮。那么激光炮最大的优势,就是它是不需要,任何的弹药和弹丸的,就可以持续地,针对对方的目标进行杀伤。那么舰炮它的角色,和它在海战当中的地位,又会出现变化。
|
||||
|
||||
|
||||
## 【主持人5】
|
||||
|
||||
从滑膛炮,到巨型战列舰主炮,再到冷战时期的自动化舰炮,以及今天的智能化集成舰炮系统,舰炮的发展,是海战技术与理念不断进化的缩影。它见证了海战从近身搏杀,到远程精确打击的跨越。无论是远程打击、反导防空,还是两栖支援、反小艇作战,舰炮都不再只是单一的火力工具,而是海上作战体系中不可或缺的一部分。舰炮的演进之路,告诉我们一个清晰的答案,技术与理念的结合,才是海上制胜的核心力量。好了,观众朋友们,感谢您持续关注国防军事频道《军事科技》,我们下周同一时间。
|
||||
@@ -0,0 +1,127 @@
|
||||
---
|
||||
标题: 海上先锋:无人舰艇的实战与未来
|
||||
编导: 左鑫
|
||||
播出日期: 2026-05-05
|
||||
类型: 节目文稿
|
||||
期号: ep016
|
||||
---
|
||||
|
||||
# 《海上先锋:无人舰艇的实战与未来》
|
||||
|
||||
|
||||
## 【宣传词】
|
||||
|
||||
|
||||
## 【旁白1】
|
||||
|
||||
它们是无人值守的海上尖兵,自主远航、全域巡弋;它们是灵活多能的海战新锐,模块化搭载、全域适配;它们是零伤亡的“智”胜先锋,深海潜行、决胜无形。无人舰艇,以智能破局、以无人制胜,正重塑现代海战场。本期《军事科技》聚焦海上先锋,解析无人舰艇的实战与未来。
|
||||
|
||||
|
||||
## 【主持人1】
|
||||
|
||||
各位观众你们好,欢迎收看《军事科技》,我是主持人蓝皓。谈及海上作战舰艇,大家首先想到的,往往是舰列森严,武备密布的有人战舰。从巨型航母到远洋驱逐舰,各类水面舰艇的运转,都高度依赖舰员的操控。而如今,无需人员登舰,可长期自主远航的无人舰艇,正快速成为现代海战的新锐力量。近期,美国康拉德造船厂正式官宣,启动自由级中型水面无人艇建造项目。该舰艇作为美海军无人作战体系的核心装备,计划于2027年完成首艘建造,2028年正式交付海军开展测试。消息一经发布,迅速引爆了全球军事领域的热议。这款自由级无人舰艇,究竟具备哪些硬核的性能呢?在现代局部冲突当中,无人舰艇又展现出怎样的实战价值与发展潜力呢?
|
||||
|
||||
|
||||
## 【解说1】
|
||||
|
||||
美国一家专注于开发远洋自主无人水面舰艇的科技公司,对外发布了一条视频。碧海波涛之上,由他们和康拉德造船厂共同设计建造的全新构型的自由级舰艇全速航行,与传统战舰形成鲜明反差。为了适配无人化作战需求,该舰彻底取消人员居住舱室、通风管道、温控系统等配套设施,剥离所有以人为核心的设计冗余。省下的舱体空间与载重吨位,全部用于搭载任务载荷与作战模块。作为美国海军模块化攻击水面舰艇项目MASC的标杆,极致的空间利用率,使这款中小型战舰麻雀虽小,五脏俱全。与此同时,为了实现能像香肠一样流水线大量生产的目标,该舰的整体设计与量产模式,全面围绕规模化、流水线制造打造。依托集成机器人装配系统、自动化板材生产线与智能焊接工艺,造船厂可同步推进多艘舰艇并行建造,年产量稳定维持在10艘至20艘,量产效率远超同吨位传统军舰。值得注意的是,这型无人舰艇最惹眼的不是自主系统,而是它采用了二手船体。
|
||||
|
||||
|
||||
## 【采访1】
|
||||
|
||||
美国海军对外公布的,自由级的这种无人舰艇,它采用的是这种斧形的舰船。斧形舰船最大的优势,就是结构比较简单。另外一个,它是采用劈浪的设计理念。比如说海上风高浪急,海情海况比较复杂,那么它可以让整个的,无人化的作战平台,在海上高速航行的时候,拥有更强的稳定性。而且它是采用一个二手的,荷兰达门公司设计的这种,商业化的巡逻艇,作为一个改装的平台,并不是完全从零起步,自主研发的。这种无人化的这个平台,这种斧形的巡逻艇,技术非常地成熟,此前已经有了大量的应用,可靠性比较强。也不需要再进行这种,技术的验证,和这种技术的研发,能够在最短的时间内,把这种无人的平台给造出来,然后用它进行,长时间的海上测试,既节约时间,也可以节约人力物力。
|
||||
|
||||
|
||||
## 【解说2】
|
||||
|
||||
据官方信息,自由级无人舰艇全长约58米,差不多两节火车车厢连起来的长度,宽约9米,满载排水量770吨,总载荷约150吨,属于中型无人舰艇,比普通快艇大得多,又比传统护卫舰小巧灵活。最高航速25节,续航里程超过一万海里,自持力可达3个月,中途不用补给,就能长期远洋部署。搭载4级自主航行系统和海洋人工智能,能自主规划航线,躲避障碍物,全程只需少数人员在后方远程监控,几乎不用人工干预。更值得一提的是,自由级舰体采用模块化设计,就像海上乐高,可灵活换装武器、侦察传感器、通信中继设备、后勤物资舱等。同时,它专门预留了标准集装箱武器模块接口,能搭载4个12米标准导弹模块,完美适配当下越来越火的集装箱武器系统。这种武器系统堪称海上隐身火力库,外观和普通海运集装箱几乎一模一样,既能藏反舰导弹、巡航导弹,也能装反潜鱼雷、侦察设备,平时完全看不出武器痕迹,战时能快速解锁开火。
|
||||
|
||||
|
||||
## 【采访2】
|
||||
|
||||
自由级,艇体的后部,它应该是一个多功能的任务区。多功能的任务区,它是采用即插即用的,这样的一个设计理念。因为美国海军对于大型无人舰艇,或者说具备远航能力的无人艇,它的这个任务要求是非常多的。既然任务需求比较多,但是它又不能打造很多的,这种无人化的海上作战平台,那么就要采用这种通用的,模块化的设计理念。根据美国海军的任务要求,去选择相应的这种任务模块,或者是火力模块,更好地配合有人战舰执行任务。那么这种方式节约成本,而且,一条无人舰艇,它可以扮演多重角色。利用这种大型的无人舰艇,搭载集装箱,它的外形具有一定的欺骗性。因为这种集装箱,它和普通的,物流当中的这个集装箱,或者是民用的集装箱,尺寸和外形基本上是一样的。那么很难对它的这种,军用无人舰艇身份进行识别。事实上,自由级无人舰艇的研发列装,并非单一装备升级,而是美国海军海上战略布局的,关键落子。2025年,美政府为恢复海上主导地位,将自主无人舰艇研发,列为国家战略优先方向,依托本土造船工业,与成熟商业供应链,全面提速无人舰艇,批量生产与规模化列装。而根据美国国会研究报告显示,2026年,MASC模块化无人舰艇项目,将全面进入原型验证阶段。美国海军同步明确发展规划,加速打造有人战舰,与无人舰艇协同编组的,新型混合舰队。美军国防部预算数据显示,在近70亿美元的,海上无人系统年度经费中,37亿美元将在2027年前,集中投入水面无人装备,研发与列装。
|
||||
|
||||
|
||||
## 【解说3】
|
||||
|
||||
俄乌冲突中,低成本无人装备的规模化实战应用,更是为美军提供了全新作战思路。可消耗、高机动的无人舰艇,能够深入高危海域,承担前沿侦察、战术袭扰、精准打击等高危任务,有效规避有人舰艇战损风险,保障舰员生命安全。不过,亮眼的性能参数与清晰的列装规划之下,面临的是更大的考验。
|
||||
|
||||
|
||||
## 【采访3】
|
||||
|
||||
美国海军对于自由级无人舰艇,它的任务要求是比较高的,是要让自由级的无人舰艇,未来能在大洋上进行长时间的部署,比如说横跨太平洋,或者说在大洋的纵深区域,能够进行自主航行。我认为首先要解决的难题,就是它在航行过程当中的可靠性,毕竟它的排水量不是特别大,一旦在航行的过程当中,出现了设备损坏的这样的一个状况,那么无人舰艇是无法进行自主修复的。其次在使用的这个过程当中,它的信号可能也会受到干扰,因为这种大型的无人舰艇,必须要有卫星的通信手段,那么这样的话,它才能够得到基地所发出的指令。在航行的过程当中,一旦软件系统和通信的能力,尤其是卫星的通信能力,出现了这个问题,它基本上就由这种高技术的无人舰艇,变成了随波逐流的靶船。
|
||||
|
||||
|
||||
## 【解说4】
|
||||
|
||||
有意思的是,这款新战舰使用了“自由”这个名号,而美国海军此前的自由级濒海战斗舰,却是出了名的问题战舰。这就难免让人疑惑,美国重启“自由”名号打造无人舰艇,是想打个翻身仗,还是会重蹈覆辙呢?
|
||||
|
||||
|
||||
## 【主持人2】
|
||||
|
||||
刚才我们了解了,自由级无人舰艇的纸面性能,看起来确实很亮眼。一款武器装备能不能成功,不光要看设计,更要看研发制造的底气。我们不得不提到,两个绕不开的问题:一是美国造船工业,早已不复当年之勇,发展缓慢,问题突出;二是顶着“自由”名号的,濒海战斗舰,实战表现一塌糊涂。这两个短板,会不会让这款新型的,自由级无人舰艇的未来,充满变数呢?
|
||||
|
||||
|
||||
## 【解说6】
|
||||
|
||||
上世纪中后期,美国造船业堪称全球霸主,巅峰时期拥有上百家船厂,创造了上百万个就业岗位,航母、驱逐舰、核潜艇,“下饺子”一样服役。可如今,受制造业空心化、劳动力流失、成本高涨影响,美国造船业急剧衰落。数据显示,美国全球民用造船份额,不足0.1%,能建造大型军舰的活跃船厂,仅剩几家。更糟的是,美国海军近年主力舰艇建造,全面陷入停滞,堪称造不快、造不好、造不起。福特级首舰福特号,服役多年,电磁弹射、阻拦系统故障频发,核心性能始终不达标,迟迟难形成实战战力。二号舰肯尼迪号,虽已完成海试,但工期一拖再拖、多次延期,交付时间比原计划晚了数年,至今仍未正式入列。原本寄予厚望的星座级护卫舰,因成本失控、进度崩盘直接下马,巨额研发投入付诸东流。令人关注的,特朗普级战列舰项目,也只是停留在舆论层面,遥遥无期、毫无实质进展。而比造船工业更让人揪心的,是“自由”这个名号的前车之鉴。很多人都知道,如今的自由级无人舰艇,和早年的自由级濒海战斗舰,算是同名不同命的两代战舰。而这位“前辈”,堪称美国海军,近年最失败的装备之一,自由级濒海战斗舰。
|
||||
|
||||
|
||||
## 【采访5】
|
||||
|
||||
在美国海军的水面舰艇当中,它还是一个鸡肋装备。我们看它的出镜率并不高,包括在美国海军针对伊朗,采取作战行动的过程当中,那么像濒海战斗舰,都是躲到很远的区域,无法直接参与作战行动。正常而言,美国海军对于自由级,它的定位是,在近海沿岸区域进行这种作战,要利用自身的隐身性能和高航速,针对对方的这种小型目标,比如说武装快艇、导弹艇、鱼雷艇进行猎杀。在和伊朗进行较量的过程当中,美国海军是需要自由级濒海战斗舰的,但是由于它的这样的一个战场,生存能力比较弱,如果贸然开赴前线,它只能是被动挨打,不仅没有办法执行有效的作战任务,自身的安全都没有办法得到保障。所以,耗费重金打造的濒海战斗舰,尤其是自由级,现在在美国海军当中,它的实战能力非常弱。这说明此前的指导思路,或者说任务构想,都是出现了重大的偏差。因为性能太差,性价比太低,美国海军早已决定,提前退役部分自由级濒海战斗舰。有的战舰,甚至服役不到10年就退役,远远低于正常30年的服役年限,沦为全球笑柄。有人主力舰接连掉链子,美军只能退而求其次,寄望于建造难度更低、成本更小的无人舰艇。
|
||||
|
||||
|
||||
## 【解说8】
|
||||
|
||||
想用自由级这类装备,填补战力缺口。但风险依旧明显,美军此前多款无人舰艇项目,要么测试翻车,要么预算超标,大多半途而废。叠加主力舰烂尾前车之鉴,与造船工业短板,自由级想摆脱烂尾命运,形成实战战力,难度极大。
|
||||
|
||||
|
||||
## 【主持人3】
|
||||
|
||||
无人舰艇是未来海战的发展方向,除了美国的“自由”级,其实各国也都在加紧研发无人舰艇。第十五届中国航空航天博览会上,耀眼夺目的,我国自主研发的,500吨级大型,隐身无人舰艇“虎鲸”,它采用了高性能三体船型设计,兼具隐身性,与强大作战能力,是我国无人舰艇领域的亮眼代表。当然,全球无人舰艇的领域亮点,不止于此,我们再来说一款,全球知名的无人舰艇,看一看它的性能如何,以及未来无人舰艇,又会走向何方呢。
|
||||
|
||||
|
||||
## 【解说9】
|
||||
|
||||
放眼现代局部军事冲突,俄乌冲突持续日久。在长期消耗作战的背景下,双方均大力研发列装成本低、高实用性的无人作战装备。而在黑海海域,无人舰艇上演了一场场以小博大的海上对决。2024年2月1日,乌克兰军事情报部门称,乌军在克里米亚半岛的多努兹拉夫海湾击沉了一艘俄军毒蜘蛛III型导弹艇伊万诺维茨号。在无人艇回传的红外影像中,可以清晰地看到该艇的轮廓,以及艇体中部的P-270白蛉反舰导弹发射筒。乌军无人艇准确地冲入发射筒下方一个已经被前一艘无人艇炸开的缺口,该艇发生巨大爆炸。乌克兰国防部情报总局局长表示,伊万诺维茨号是被六艘乌克兰制造的马古拉无人艇击沉的。2024年2月13日夜间,俄军蟾蜍级登陆舰凯撒·库尼科夫号遭多艘乌军无人艇围攻。俄军舰员利用探照灯搜寻无人艇,使用轻重机枪向无人艇猛烈射击,但该舰仍被多艘自杀式无人艇命中,最终沉没。2024年3月5日,乌军无人艇在刻赤海峡附近击沉了一艘俄军22160型巡逻舰谢尔盖·科托夫号。从现场流出的视频看,可以清晰观察到22160型巡逻舰采用隐身化设计的上层建筑。数艘乌军无人艇从该舰舰尾、两舷和舰艏多个方向发起攻击,俄军战舰发生数次大爆炸,最终沉没。2024年年底,乌克兰的一艘马古拉V5无人艇使用其搭载的R-73空空导弹击落了一架俄军米-8直升机。除了水面目标,乌军无人艇还变身微型航母,潜入克里米亚海岸附近,放飞无人机,袭击俄军的防空系统和雷达站等设施。而这些行动中,乌军出动的就是马古拉无人艇,由乌克兰STE公司研发。目前马古拉无人艇有V5和V7两个型号,前者相对简单,主要执行自杀作战,后者装备比较完善,可执行更复杂的任务。
|
||||
|
||||
|
||||
## 【采访6】
|
||||
|
||||
因为乌克兰所使用的这种无人艇,它的外形和结构,和民用的快艇非常地相似。个头很小,上面是采用封闭式的结构。如果再使用一些复合材料,再采用一些这种暗色调的涂装,在海上进行航行的时候,一般对海搜索雷达很难识别出来。为什么呢?因为个头太小了,艇体又很扁。那么海上出现波浪起伏的时候,对海搜索雷达会出现大量的杂波。所以这种小型无人艇,它的雷达反射信号,就隐藏在了海上的这些杂波当中,很难对它进行过滤后的精准识别。刚才我们提到的是探测难度较大,也就是看不清。其实还面临打不着的难题。因为水面战舰上的一些近防速射炮,包括所架设的高射机枪,可能针对这种海上的小型高速目标,想进行精准的火力命中,难度较大。而且马古拉无人艇,它是有星链设备,在冲向目标的过程当中,可能会进行海上高速的蛇形机动,可以躲避对方的近防火力。此外,由于它的艇体结构是比较简单,一旦小口径的机关炮,包括重机枪,针对它的艇体,就是非战斗部的这样的一个区域进行打击,可能直接就把这个艇体给打透了,不一定会对它造成致命性的损伤和影响。所以被打几个窟窿,可能还会继续航行,继续冲向目标。
|
||||
|
||||
|
||||
## 【解说10】
|
||||
|
||||
最初,马古拉V5的原型设计较为简单,只有一个星链通信信道,一个简单民用的快艇或摩托艇引擎,和一个控制系统,再加上高爆弹药作为战斗部。现行量产化的版本中,V5无人艇还配备有自动驾驶系统,包括夜视在内的视频子系统,以及带有陀螺仪、海洋雷达和激光测距仪的双视光电系统。V5的船体采用碳纤维和环氧树脂制成,长5.5米,宽1.5米,重量不足一吨,露出水面的部分高度仅为50厘米,表面覆盖有隐身涂层。采用混合动力发动机,最大载荷320千克,其中战斗部重量为200千克,撞击引信位于舰艏。马古拉V5采用油电混合动力,巡航速度22节,冲刺速度可达42节,最远航程450海里。其通信方式也改为卫星与无线网络联合通信,它搭载两具星链天线和一具平板卫星天线,另有一部还有两张SIM卡的无线蜂窝网工业路由器,可由技术人员远程操控完成袭击。简便的结构铸就了它约27万美元的造价。
|
||||
|
||||
|
||||
## 【采访7】
|
||||
|
||||
乌克兰使用无人艇,针对俄军黑海舰队的水面战舰,和沿海的军事设施进行攻击。现在已经是实现了狼群战术,而且已经开始打组合拳。那么在无人艇的这样的一个体系当中,有一些诱饵无人艇,专门是诱导俄军水面战舰上的火力,进行这样的一个打击。其他的一些自杀式无人艇,就是配备有战斗部的无人艇,它命中目标的几率就会提升。通过这种不同型号,不同功能的无人艇进行搭配,进行组合,就是要提升这种海上自杀式攻击,它的这样的一个命中率。
|
||||
|
||||
|
||||
## 【解说11】
|
||||
|
||||
而马古拉无人艇最新型号V7,在V5型号的基础上完成了结构设计与作战功能的全方位升级。不再局限于自杀式攻击任务,还可兼顾防空拦截、后勤补给、巡逻警戒、反潜作战等多项作战任务,多任务适配能力大幅提升。马古拉V7全长7.16米,最大载荷650千克,搭载270马力柴油发动机,最高航速可达39节,巡航速度23节,作战半径540海里。若依托辅助发电机供电,其持续执勤时间最长可达7天。得益于体形升级与模块化复合结构设计,V7具备极强的任务拓展性,可通过快速更换任务模块,切换为自杀攻击艇、防空护卫艇、机枪火力支援艇等不同作战形态。火力配置方面,该型无人艇可搭载两枚经过改装的AIM-9M响尾蛇空空导弹,专门用于空中目标拦截。导弹依托发射导轨搭载,发射前导轨可向上倾斜调整角度,保障拦截打击效率。除搭载响尾蛇导弹的版本外,乌克兰国防部情报总局还首次公开了配备机枪炮塔的衍生改进型号。该变体在艇体前段加装武器模块化炮塔,可选装12.7毫米重机枪或40毫米自动榴弹发射器,主要执行近程反无人机、近距离压制及登陆作战火力支援等任务。
|
||||
|
||||
|
||||
## 【旁白2】
|
||||
|
||||
2025年5月,乌克兰国防部情报总局发布视频,展示了乌军马古拉V7无人艇,发射AIM-9响尾蛇导弹,击中一架俄军苏-30SM战斗机的过程,这也是无人艇首次击落固定翼战斗机。而作为对手,俄军也不甘示弱,同样研发装备了自杀式无人艇。2025年8月28日凌晨,俄军无人艇在多瑙河河口,精准摧毁乌军3550吨的辛菲罗波尔号侦察舰。乌克兰海军发言人证实,该舰被无人艇精准击中船体要害,爆炸瞬间导致舱体破损并迅速进水,最终船体失去平衡而翻覆沉没。
|
||||
|
||||
|
||||
## 【采访9】
|
||||
|
||||
俄军的无人艇数量,可能没有乌军那么多,但是也可以发挥奇效。有这种小型自杀式的无人艇,那就是和对方昂贵的自杀式无人艇同归于尽。那么还有一些性能比较先进的,航行速度比较快,上面配备有遥控武器站。遥控武器站主要是集成重机枪,包括自动榴弹发射器。它的功能就是巡逻和火力拦截。但是俄军所面临的最大问题,就是在黑海的区域内找不到目标。因为乌克兰已经没有这种大型的水面战舰,甚至很多的这样的一些军用的船只,在俄乌冲突,在乌克兰危机爆发之初,都已经遭到了俄军的打击。所以俄军使用无人艇,最大的这样的一个功能,就是使用这种低成本、更小型化的无人艇,针对乌军的自杀式无人艇进行拦截。所以它本质上来讲,是一个防御性的武器装备。无人舰艇的实战应用,早已颠覆了大众对传统海战的固有认知。凭借低成本、零人员伤亡、适配集群作战的突出优势,无人舰艇已广泛应用于近海防御、侦察监视、精准打击等关键领域,发挥着不可替代的重要作用,逐步成长为现代海战场上不容忽视的新兴作战力量。从近海突袭的小型无人艇,到远洋部署的中型无人舰艇,无人装备正在为现代海战注入新的活力,成为海上竞争的核心领域之一。它以智能为内核,无人为特色,降低伤亡的同时。
|
||||
|
||||
|
||||
## 【解说13】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【主持人4】
|
||||
|
||||
推动作战理念迭代升级,与有人舰艇协同,构建更完善的海上作战体系。未来的大海,将是有人与无人装备协同驰骋的舞台。掌握核心科技,才能在海战场上占据主动权。好了,观众朋友们,感谢您持续关注国防军事频道《军事科技》,我们下周同一时间。
|
||||
@@ -0,0 +1,134 @@
|
||||
---
|
||||
标题: 逆袭的鸭翼
|
||||
编导: 张彤枫
|
||||
播出日期: 2026-05-12
|
||||
类型: 节目文稿
|
||||
期号: ep017
|
||||
---
|
||||
|
||||
# 导视:
|
||||
|
||||
|
||||
## 【导视1】
|
||||
|
||||
美军披露下一代战机F-47的设计方案后,人们惊讶地发现,这款被美军寄予厚望的第六代战机,竟然采用了他们曾经不屑一顾的鸭翼设计。回想以往,美军曾公开宣称,鸭翼最理想的位置,就是在敌机的机翼上,话语中充满对鸭翼的轻视。如今,却主动采纳这一结构,这一反转背后,究竟隐藏着怎样的战略思考,竟能让美军摒弃成见,作出妥协?本期《军事科技》,带您深入解析这片曾备受争议的小翼面。各位观众你们好,欢迎收看《军事科技》。
|
||||
|
||||
|
||||
## 【主持人1】
|
||||
|
||||
我是主持人蓝皓。2025年3月21日,美国总统特朗普宣布,授予波音公司生产美国空军“下一代空中优势”战斗机,并将战机命名为F-47。虽然这款战机离飞上天空还有很远的距离,但是我们可以发现,图片上的F-47竟然采用了鸭翼布局。这种布局方式是以往美国空军从未使用过的。因为在此之前,曾有设计人员宣称,鸭翼最理想的位置就是在敌机的飞机上。这句话充满了讽刺意味。如果我们回看美国空军的主力机型,确实可以发现,无论是美国的四代机,还是F-22、F-35这些五代机,采用的都是常规布局。好像美军确实没有用过鸭翼这种独特的气动布局方式。那么,最新的六代机F-47为什么突然选择拥抱鸭翼呢?鸭翼布局和常规布局到底孰优孰劣呢?今天,我们就从这句经典的调侃出发,看一看鸭翼的秘密。
|
||||
|
||||
|
||||
## 【解说1】
|
||||
|
||||
上世纪70年代,喷气式战斗机迎来了从二代机向三代机跨越的关键时期。在这个时代,以米格-21和F-4战斗机为首的常规布局的二代机,在越南战场打得有来有回,但其高速性能与机动性已逐渐难以满足超视距空战与复杂电磁环境下的作战需求。可变后掠翼技术虽然在很多方面可以提升战机的性能,但成本高昂、结构复杂,难以兼顾多任务能力。所以,在这个时代,各国都在寻找一种更高效、更灵活的气动解决方案。1971年,美国开启了轻型战斗机竞标项目,多家厂商开始推进自己的方案。通用动力公司提出的YF-16方案脱颖而出,形成了F-16这一经典机型。F-16战机是经典的常规气动布局的代表,但其实在它设计之初,产生过一次关于鸭翼布局和常规布局的激烈争论。
|
||||
|
||||
|
||||
## 【三维动画解说1】
|
||||
|
||||
如果想要搞清楚鸭翼布局的优势,我们要回到飞机结构本身。不同结构的飞机,首先会导致飞机重心与气动中心的相对位置发生改变。常规布局的飞机,所有机翼主要集中在机身的中后部,这时,重心通常位于机身的后部,而负责控制俯仰姿态的水平尾翼则位于机尾。根据杠杆原理,由于两者之间的力臂较短,就需要更大的翼面,产生更强的俯仰力矩,来维持平衡与操控。而鸭翼布局则是鸭翼在前,主翼在后,使飞机重心依旧保持在偏后部的同时,加长了力臂长度,从而以更小的鸭翼面积实现同等甚至更强的俯仰效率。其次,前置的鸭翼比主翼更早地接触气流,能在主翼前产生一个低压涡流,当涡流经过主翼时,进一步地使主翼上表面的压力变低,这样便显著提升了主翼的升力系数。同时由于鸭翼既可以控制力矩,也可以产生一部分升力,采用鸭翼布局的飞机可以把主翼设计得更小。
|
||||
|
||||
|
||||
## 【解说2】
|
||||
|
||||
既然鸭翼布局优势明显,为什么F-16最终选择了常规布局呢?如果我们还原一下当时设计师的对话,就会发现一个有趣的现象。鸭翼能提供更强的升力,你的飞控设计难度大。可以为飞机提供更强的机动性,你的飞控设计难度大。鸭翼布局可以让飞机设计得更紧凑,你的飞控设计难度大。你还会不会说点别的了,你的飞控设计难度大。不要吵了,鸭翼这种愚蠢的东西,最理想的位置就是在敌机的飞机上。
|
||||
|
||||
|
||||
## 【专家1】
|
||||
|
||||
鸭翼确实可以赋予战斗机更强的机动性。因为鸭翼它一般是在战斗机的前部靠上这个位置,离飞行员的座舱比较近。配备有鸭翼的战斗机,它会产生涡流,能够增强战斗机的升力。一旦加装了鸭翼,战斗机它可能会处于静不稳定的状态。也就是说在飞行的过程当中,战斗机可能过于灵活。这个时候,想要对它进行精准的操控,就必须要有配套的这种设备。为什么说美国空军它很少采用这种鸭翼的这样的一个设计理念?比较典型的,F-16和F-15它都没有鸭翼。因为在美国空军看来,提升战斗机它飞行性能最好的办法是让它发动机的推力进一步地强化。所以说,美国他的航空工业体系,当时的技术的水平比较高,那么可以选择大推力的发动机。这样的话,战斗机也可以飞得比较灵活,超音速的性能也比较好。所以我们看,美国的战斗机一般情况下是不采取鸭翼的。反而是其他的一些欧洲国家,在鸭翼布局方面,进行了大胆的技术尝试和探索,也取得了很多的成果。
|
||||
|
||||
|
||||
## 【主持人2】
|
||||
|
||||
无论鸭翼有多么大的优势,都无法超越时代的科技锁。所以,美军在F-16的设计上,还是选择了更加稳妥的常规布局。不过,优秀的东西,不应该就这样被埋没,总会有人尝试着迈出第一步。这个敢于第一个吃螃蟹的,并不是美苏这两个超级大国之一,而是欧洲的一个国家。大家看,世界上第一款采用鸭翼布局并正式服役的战机,从瑞典飞来了。
|
||||
|
||||
|
||||
## 【解说3】
|
||||
|
||||
1982年的某一天,一架飞翔在两万米高空之上的SR-71黑鸟侦察机,执行完侦察任务,途经波罗的海上空,准备掉头返航。这款侦察机,可以说是当时世界上最强的侦察机,可以在两万米以上的高空,保持马赫数3以上的巡航速度进行飞行。这架看起来已经天下无敌的黑鸟,在波罗的海上空完成掉头正准备返航时,下方一架造型奇特的战机,突然出现在它的视野当中。这架飞机,不但可以追上黑鸟的速度,还可以在接近两万米的高空,时不时地抬起机头,用雷达锁定黑鸟。虽然此时准备返航的黑鸟,速度和高度并没有达到极限,但是,这架战机所体现出的性能,让机组人员大吃一惊。刚刚设想的发生在万米高空之上的故事,并不是完全虚构的,而是源自于真实事件。故事中,能完成对黑鸟侦察机进行高空拦截的,正是瑞典萨博公司研制的萨博-37雷式战斗机。而这款飞机,也是世界上第一款服役的,并采用了鸭翼布局的喷气式战斗机。
|
||||
|
||||
|
||||
## 【专家2】
|
||||
|
||||
萨博-37,是瑞典独立自主研制的,单发两侧进气道,而且是采用鸭翼布局的,这种战斗机。当时瑞典对于萨博-37的定位,非常地明确,主要还是要强化,它针对高空高速目标的,跟踪和锁定能力。因为,当时瑞典虽然是中立国,但是整体上更加,亲近美西方国家。所以即使黑鸟高空高速侦察机,在瑞典的空域进行飞行,瑞典的飞行员,主要是对它进行跟踪和监视,不太可能采取这种,进攻性的动作。而且,在飞行的过程当中,甚至不排除黑鸟的机组成员,会主动的降低飞行的高度,是以一种安全的状态,或者是他自认为安全的状态,去进行飞行。雷式战斗机,它的这种高速的飞行能力,飞行的高度也比较高,再加上雷达系统比较先进,那么配备有,改进型的空空导弹。所以它针对这种高空高速目标,它的这种锁定能力,确实也是比较强。
|
||||
|
||||
|
||||
## 【三维动画解说2】
|
||||
|
||||
萨博-37雷式战斗机,全长超过16米,主翼翼展超过10米,是个名副其实的大家伙。作为全球首款服役的鸭翼布局战机,在其主翼前方,有着一对翼展超过5米的鸭翼。这样的气动布局,使它的最大飞行速度可以超过马赫数2,升限突破18000米,并且最大起飞重量接近20吨。由于采用了鸭翼布局,萨博-37在短距起降能力上尤为突出,仅需约五百米跑道即可完成起降。这种卓越的短距起降性能,在当时是独一档的存在。此时的萨博-37战斗机,采用的是固定式鸭翼,主要起到增加升力的作用。其机动性,只能靠鸭翼后部额外增加的后缘襟翼来控制。这种方法,只能说是一种初步探索,尚未触及鸭翼在机动性提升上的核心潜力。
|
||||
|
||||
|
||||
## 【解说4】
|
||||
|
||||
萨博-37走出了现代战机对鸭翼探索的第一步。不过此时的航空技术,不足以支撑对于鸭翼的动态控制。其实我们可以将鸭翼,看作尾翼前置化的升力增强装置。尤其是在鸭翼尾部,加装后缘襟翼这种方式,很明显,还带有传统气动设计的惯性思维。这种无法进行偏转运动的鸭翼,通过简单的结构设计,却可以在机动性方面,获得极高的提升。可以说,是一种简单有效的工程智慧。在萨博-37之后,法国的幻影系列战机,和以色列的幼狮战机,也都在三角翼设计的基础上,尝试了固定鸭翼布局。
|
||||
|
||||
|
||||
## 【专家3】
|
||||
|
||||
那么像萨博-37,包括幼狮,都考虑到,是国土面积不太大的国家,相关的空军力量,对它进行装备和使用。这就会带来一个问题,如果是在战争的状态下,空军基地遭到了破坏,常规的跑道无法起降战斗机,这个时候,无论是幼狮还是雷式战机,必须要依托于公路去进行起飞。战斗机它的升力的效应,就非常地重要。加装了鸭翼之后,它可以利用有限的跑道,或者是粗糙的跑道,进行短距起飞。它针对突发情况的,临时的应变能力,就会变得更强。另外一个,无论是瑞典,还是其他的一些国家,包括以色列,它想拿到推力更大的,这种最先进的发动机,基本上是不可能的。我们看一下雷式战斗机,其实,它所配备的,是从美国引进的一种,商用的涡扇发动机,然后瑞典的沃尔沃公司,对它进行了技术升级和强化,让它达到了军用的标准。一方面可靠性比较好,另外一个成本比较低。既然发动机的推力不够,在空中,很难依靠自身的动力系统,做出这种大过载的机动动作,那么加上鸭翼之后,可能飞行品质和机动性,是有改善的,无论是萨博-37战斗机,还是以色列的幼狮战斗机。
|
||||
|
||||
|
||||
## 【主持人3】
|
||||
|
||||
鸭翼布局的早期探索,都印证了一个事实:鸭翼布局在提升飞机机动性与短距起降能力方面,具有先天优势。虽然碍于时代的技术条件,此时的鸭翼大多是不可动的固定式鸭翼,但是,还是在众多气动布局方式中,抢出了一席之地。随着计算机控制技术与材料科学的突破,困扰鸭翼布局的最大障碍——飞行控制系统,从机械控制开始转向电子控制,飞行稳定性与响应速度得以质的飞跃。这一技术突破,让鸭翼布局迎来了新的发展。依旧是瑞典这个北欧国家,开始了全可动鸭翼布局的全新探索。在以往的经验之上,世界上首款真正意义上实现全可动鸭翼飞控一体化设计的战机——JAS-39“鹰狮”,横空出世。
|
||||
|
||||
|
||||
## 【解说5】
|
||||
|
||||
时间来到上世界90年代末,美国的波音和通用,凭借强大的航空发动机,深耕传统布局飞机设计;苏联的米高扬和苏霍伊,利用翼身融合技术,不断突破战机机动的极限;法国达索,也在三角翼布局上独占鳌头。在众多航空设计公司,都在各自道路上不断深化时,瑞典萨博公司,也等到了属于鸭翼的全新时代。随着科学技术的发展,飞机的航空控制系统,从机械操作,正式进入了电子操作时代。性能强悍的机载电脑,成为了各种复杂气动布局的控制基础。这种跨时代的突破,也成为了萨博公司,弥补萨博-37上,不完美的鸭翼设计的,最重要的机会。让这对固定的小翅膀,成为了可以精准灵活旋转的,全动式鸭翼。
|
||||
|
||||
|
||||
## 【三维动画解说3】
|
||||
|
||||
JAS-39鹰狮的全动鸭翼,区别于固定式鸭翼,不仅实现了独立的偏转控制,更通过飞控系统与主翼,实时协同,当鹰狮飞在天上时,两片鸭翼,可以通过同向偏转,实现战机的俯仰控制,如果需要进行高机动转弯时,两片鸭翼,则可以实现不同方向偏转,配合主翼与方向舵,形成更强的转向力矩,大幅缩短转弯半径,这种精准的协同控制,使JAS-39,在超音速巡航状态下,仍能保持优异的敏捷性,与稳定性,由于使用了全可动式鸭翼,鹰狮的体积,相较于同时期的F-16,在机长和翼展,均减少接近一米,整机的空重,减少了近两吨,虽然体积变小了,但是机动性并没有减少,无论是最大马赫数2的飞行速度,还是超过15000米的飞行高度,鹰狮均展现出同级战机中,罕见的综合性能平衡,尤其是在爬升速率,和转弯半径上,更是远超常规布局的F-16战机。
|
||||
|
||||
|
||||
## 【解说6】
|
||||
|
||||
萨博公司再次凭借对鸭翼布局的深刻理解与极致优化,将鹰狮打造为全球轻型战机中气动效率与作战效率的标杆。在全动式鸭翼的支持下,进一步将起降距离缩减到接近三百米的极限距离,非常适合北欧广袤而分散的公路跑道与简易机场。全动式鸭翼带来的更极限的机动能力,也让鹰狮在近距格斗中如猎鹰扑食般迅捷凌厉。在红旗军演中,多次以轻型战机身份击败同级别的F-16和以空战能力出名的F-15,让世界再次看到鸭翼布局的非凡潜力。
|
||||
|
||||
|
||||
## 【专家4】
|
||||
|
||||
在鸭翼的设计方面,萨博公司确实有很长时间的技术积累和技术储备。所以正是吃透了鸭翼的这样的一些优势,或者说,对它的一些不可控的因素,进行了长时间的验证,非常地有经验,自己心中有数,心中有谱。才可以在不同的战斗机,或者说在战斗机迭代的过程当中,一直是采用鸭翼的设计理念。欧洲国家的武器,和美国的机载武器,它都可以进行兼容。所以,也是推向了国际防务市场。但是它本身还是有性能和作战能力方面的限制。因为毕竟是一种单发的战斗机,整体的体量比较轻。在最远的航程方面,在这种机载弹药的配置方面,它和双发重型的战斗机,还是有很大的差异性。但是毕竟它是一个单发平台,如果说航程和载弹量,受到了很大程度的限制,在远程奔袭的过程当中,可能就会出现燃油不够用,或者说挂装能力有限的这样的一些限制。
|
||||
|
||||
|
||||
## 【主持人4】
|
||||
|
||||
在鹰狮的基础之上,全可动鸭翼布局,成为了现代先进战斗机,必须考虑的一种设计方向。鹰狮的成功,也让世界各国纷纷跟进研发,各种基于全可动鸭翼布局的机型,相继问世。这其中就包括,欧洲的阵风战斗机,台风战斗机,以及俄罗斯的苏-33舰载机。不过,这些战机虽然都采用了,全可动鸭翼,设计理念,与技术路径各不相同。那么,它们之间有着哪些不同呢?这两片小小的鸭翼,能让大家玩出怎样的新花样呢?
|
||||
|
||||
|
||||
## 【解说7】
|
||||
|
||||
无论是在公开航展,还是武器装备贸易中,采用鸭翼布局的战机,永远是最受瞩目的机型。2016年,印度和法国签订的,关于阵风战斗机的合同,单机价格高达2.4亿美元,甚至超过了同期F-35A的采购价。台风战斗机,多次在红旗军演中,模拟击败美国引以为傲的F-22,名噪一时。而同样是鸭翼布局的,歼-10CE型战机,也在2024年的军事演习中,以9比0的战绩,完胜台风战机。歼-20战机,则凭借其独特的,升力体鸭式气动布局,成为当代隐身战机中,独树一帜的存在。那么,同样是鸭翼布局,为何在不同机型上,呈现出截然不同的效果呢?
|
||||
|
||||
|
||||
## 【三维动画解说4】
|
||||
|
||||
如果想区分不同机型的鸭翼设计方式有什么不同,主要可以根据鸭翼所处的位置进行判断。阵风战斗机采用的是近距耦合鸭翼,特点是鸭翼的高度要高于主翼,这样才能为主翼提供更高效的气流引导,增强主翼的升力,这种效果被称为“耦合”。而近距的意思则是,鸭翼的位置离主翼非常近,甚至贴近主翼前缘,以最大化气流干扰与升力增益。而与之相对的,则是台风战斗机的远距耦合鸭翼。台风战斗机鸭翼的位置距离主翼较远,已经接近机头的位置,虽然产生的涡流也会对主翼的升力有所提升,但是效果确实微乎其微。但由于鸭翼布置位置更远离飞机的重心,所以其在偏转时,产生的力矩会更大,也就可以让飞机获得更灵敏的俯仰响应,尤其在高速机动中展现极强的瞬时转向能力。而苏-33战机的鸭翼则是与主翼位于同一水平线,所以其产生的涡流几乎不会对主翼升力形成有效增益。但和常规布局相比多出的鸭翼增加了整架飞机的机翼面积,所以会显著提升低速状态下的升力系数,非常适合在航母上进行更低速度的精准起降。由于这种设计将鸭翼、主翼、尾翼放置在一个平面上,所以也被称为三翼面布局。
|
||||
|
||||
|
||||
## 【解说8】
|
||||
|
||||
虽然同为鸭翼布局,但设计逻辑却各不相同。这就导致,虽然鸭翼家族的成员越来越多,但每种设计上微小的不同之处,都会对性能带来不同的影响。当然,在鸭翼家族逐渐壮大的过程中,各种试验机型,也为鸭翼布局路线,提供了宝贵的数据支撑与工程验证。让我们回到节目最开始的那句话,为什么美国设计师会认为,鸭翼最理想的位置,就是在敌机的机翼上?
|
||||
|
||||
|
||||
## 【专家6】
|
||||
|
||||
这是一种狂妄自大的说法。美国空军认为,它所配备的战斗机,无论是在动力系统方面,还是在这种气动布局方面,都可以实现最优化。客观来讲,在战斗机的技术方面,美国确实曾经长时间地领先,动力系统比较靠谱,推力比较大。另外,美国空军最早进入到了隐身化的时代,打造出了F-22猛禽隐身战斗机。不仅隐身性能比较突出,另外一个,也可以进行超音速的巡航。那么通过这样的一种综合化的设计,美国空军认为不需要鸭翼,只要采用这种更好的发动机,采用更为优化的外形设计,就可以让战斗机飞得更快。对于其他国家的空军而言,我们不仅要考虑到战斗机的性能,也要考虑到设计和发动机方面的这样的一个成本的因素。如果说像美国空军那样,在各个方面都要追求极致,可能战斗机的单价就过高了。那么这个时候,你想要进行批量化的装备,可能在成本方面,有一些国家是承担不起的。
|
||||
|
||||
|
||||
## 【解说9】
|
||||
|
||||
当然嘴硬的美军,其实还是偷偷搞了不少鸭翼技术研究的。1984年,美国空军和NASA联合开展了X-29A前掠翼验证机项目。这款验证机几乎将当时所有前沿气动技术集于一身,前掠翼加上耦合鸭翼的组合机翼,让这架验证机在亚音速与大迎角状态下展现出惊人的敏捷性。麦道公司也在1988年,针对F-15战斗机开展了“敏捷鹰”鸭翼升级计划。通过加装鸭翼,一款全新的F-15 MTD走向了跑道。这架验证机在F-15的传统气动布局上,上反20度的全动鸭翼。这种修改让原本笨重的F-15可以以最低约每小时70公里的速度完成起飞,也可以利用全动鸭翼的机动性,在接近马赫数2的速度下完成大迎角机动。此外,在麦道和波音两家公司合并后,再次和NASA合作,推进X-36无人验证机项目。虽然这架验证机只是一个小尺寸的验证模型,但其完全取消了垂尾与平尾,仅靠鸭翼与喷口矢量实现全向控制,同时被认为是美军六代机的先驱。美国在技术验证机上加装了鸭翼,就是要进行一定程度的对比,看一看有鸭翼的战斗机,以及没有鸭翼的战斗机,它在这种更多的这种测试条件下,整体的飞行品质如何,或者说机动性如何。所以它目前还是停留在测试平台上。
|
||||
|
||||
|
||||
## 【专家7】
|
||||
|
||||
F-47应该是一个另类,当时美国总统特朗普,急于对外召开发布会,那么波音公司,我认为应该是赶工期,临时设计了,或者说凑合地,拿出了一个F-47的,这样的一个想象图,在上面加装了鸭翼,不一定是F-47最终的形态。
|
||||
|
||||
|
||||
## 【主持人5】
|
||||
|
||||
鸭翼布局,能在众多的气动布局当中脱颖而出,成为了当今世界上主流战机的气动设计中的一个重要选项,也是经历了漫长的技术沉淀与反复验证的。美军的下一代战机,一改往日对鸭翼布局的不屑一顾,反而将其纳入到六代机的设计方案之中,也更加证明了鸭翼布局的优秀。当然,未来鸭翼布局还将有哪些潜力被释放,在即将到来的全向隐身时代,还能不能占据一席之地,还需要时间来检验。好了观众朋友们,感谢您持续关注国防军事频道《军事科技》,我们下周同一时间再见。
|
||||
@@ -0,0 +1,134 @@
|
||||
---
|
||||
标题: 机枪双雄_加特林与马克沁的战争传奇
|
||||
编导: 付天雨
|
||||
播出日期: 2026-05-19
|
||||
类型: 节目文稿
|
||||
期号: ep018
|
||||
---
|
||||
|
||||
# 机枪双雄:加特林与马克沁的战争传奇
|
||||
|
||||
|
||||
## 【导视1】
|
||||
|
||||
当冷兵器落幕,机械化杀戮登场,两位跨界设计者,改写了战争史。医生加特林为止战造枪,工程师马克沁凭灵感,创自动机枪。他们以守护为初心,却让武器沦为战场绞肉机;以创新铸经典,却终生背负杀戮的愧疚。本期《军事科技》,就让我们一起走进,他们不为人知的人生故事,触摸两位设计师,藏在钢铁枪械背后的,柔软内心,聆听他们跨越百年的,灵魂独白。
|
||||
|
||||
|
||||
## 【主持人1】
|
||||
|
||||
各位观众你们好,欢迎收看《军事科技》,我是主持人蓝皓。在现代枪械百年发展史上,有两位巨匠,注定名留青史。一位是弃医研械,心怀悲悯,想用武器减少战场伤亡的医生,理查德·乔登·加特林。一位是历经坎坷,饱受排挤,从实战劳损中,悟出自动火器奥秘的工程师,海勒姆·史蒂文斯·马克沁。二人同处一个时代,终生却未曾相见,用一生的坚守与纠结,写下了两段,充满宿命感的传奇。
|
||||
|
||||
|
||||
## 【解说1】
|
||||
|
||||
1818年的美国,北卡罗莱纳州农场主家庭里,一个叫理查德·乔登·加特林的男孩降生了。他的父亲是当地小有名气的机械匠人,从小带着他摆弄棉苗机、播种机,把机械的种子埋进了他的心底。少年时的一场天花疫情,夺走了无数乡邻的生命,也让加特林下定决心弃工从医,他要拿起手术刀守护生命。1850年,他顺利拿到俄亥俄医学院博士学位,成为一名受人尊敬的内科医生,农机发明似乎成了遥远的童年回忆。1861年,南北战争的炮火撕碎了美国的宁静。加特林被征召为战地医生,医院成了他最煎熬的战场。每天,成百上千的伤兵被抬进来,有的胸口中弹,有的四肢断裂,更多士兵不是死于敌人的枪口,而是死于落后枪械导致的低效厮杀。
|
||||
|
||||
|
||||
## 【小剧场1】
|
||||
|
||||
战地医院成了加特林最煎熬的牢笼,满目皆是断肢残躯,血水浸透的绷带,遍地散落,伤兵的哀嚎日夜不绝。他全力救治伤员,却无力挽救一条条鲜活的生命。在深夜里,他在日记中写下:如果一把枪,能撑起一个连的火力,就能让更少士兵踏上战场,让更少家庭,承受生离死别。这个荒诞却悲悯的念头,让他放下手术刀,重新拾起机械图纸,正式迈入到,枪械研发的道路之上。看到这里,相信大家和我一样,心中充满了疑问,那么他当时,究竟是怎么想的呢?今天我们的节目,将借助AI数字复原技术,穿越历史,依据自传,生前采访与公开演讲,还原真实枪械大师,解读截然不同的设计理念。加特林先生您好,今天很荣幸与您进行对话。主持人好,21世纪的各位观众朋友,你们好,随着人类科技的发展,我非常高兴能够在这里,和大家进行一场,跨越时空的对话。好的加特林先生,相信无论是我本人,还是屏幕前的观众朋友们,都有很多问题想请教您,放弃了深耕多年的医疗事业,放下了守护生命的手术刀,转而投身枪械的研发呢?我原本是一名医生,治病救人就是我的使命,但是,随着这战场上一批一批伤员,运到我的面前,我知道,医术再好,也只能治好身上的伤,所以,我又想出一个新的办法,那就是,重新拾起年轻时,热爱的机械图纸,来造出一把枪。我想象这把枪,只需要一个人使用,就可以抵得上一个连的火力,这样,就不用成千上万名士兵,奔赴战场,白白地送命。上战场的人少了,更多的士兵,就可以安安稳稳地待在后方,不用和自己的家人生离死别。我知道,一个医生转头去造枪,这听起来就非常地离谱。我每天看着病床上,备受煎熬的士兵,和窗外漫天的硝烟,我别无他法,用机械的力量,去抵消战争的残忍,用超强的火力,早一点结束战乱,这是我唯一能想象到的了。
|
||||
|
||||
|
||||
## 【解说2】
|
||||
|
||||
几乎在同一时间,1840年,美国缅因州的一个贫寒农家,也迎来了海勒姆·史蒂文斯·马克沁的诞生。家里穷得揭不开锅,他只上了几年小学,14岁就被送到马车作坊当学徒。可这个穷小子,天生就是机械天才,少年时就造出小木船,马拉除靶机,马拉除耙机,甚至能修好作坊里所有坏了的机器。成年后,他一头扎进机械发明当中,短短十几年,拿下50项专利,被誉为爱迪生第二。可这份过人的发明才华,却让他在当时的电器行业步履维艰。1880年,马克沁投身电灯研发,与早已声名显赫、产业初具规模的爱迪生形成了直接竞争。彼时,电器行业的格局已然成形,资源与发展空间都十分有限,马克沁的发展之路屡屡受阻,难以施展抱负。恰逢此时,英国向他递来了诚挚邀约,每年提供两万美金的支持,邀请他前往欧洲,开展新的事业。
|
||||
|
||||
|
||||
## 【小剧场2】
|
||||
|
||||
初到伦敦的日子,马克沁被深深的迷茫裹挟,半生深耕的电器领域,再无立足之地,一身过人的发明才华,仿佛无处安放。那么马克沁先生,您在受到英国的邀约,来到欧洲之后,是什么决定让你转向武器机械领域,进而研发机枪的呢?当时的欧洲,正在殖民扩张中越陷越深,战事不断,各国家都想寻找更高效可靠的武器,来应对战场。在一次和好友的闲谈当中,他就说我可以前往欧洲的机械领域(发展),或许那里可以施展我的抱负。作为一名痴迷机械的工程师,我见到太多老式枪械的缺陷了,开枪时的后坐力,经常让士兵受伤,手动装填子弹,费时又费力,火力的不足,又让他们身处险境。我就想用自己最擅长的机械创造能力,来攻克这些难题。这既是我对机械制造技术的一次极致追求,也是我对自己半生人生的一次转机。
|
||||
|
||||
|
||||
## 【解说3】
|
||||
|
||||
1882年的维也纳,马克沁试射老式枪械。开火瞬间,后坐力狠狠撞在他的肩膀上。当即,一个大胆构想骤然浮现:火药燃烧产生的后坐力,难道不能转化为动力,实现枪械的全自动射击吗?为潜心圆梦,他变卖全部专利积蓄,隐居伦敦郊外小屋,日夜埋头试验。熬过无数个不眠之夜,攻克重重技术难关,1884年,划时代的世界首挺全自动马克沁机枪就此诞生。
|
||||
|
||||
|
||||
## 【主持人2】
|
||||
|
||||
不管是加特林,还是马克沁的初衷本意,其实都是基于自身所长,回应时代困境,只为用专业的力量,解决眼前的难题。可是,当这两位跨界设计师,带着自己精心打磨的枪械,走上推广之路时,却遭遇了意想不到的重重阻碍。他们那些超前的设计,不仅没有得到市场和行业的认可,反而引来了铺天盖地的质疑,更有人直言嘲讽他们异想天开,斥责这样的设计荒唐可笑,不切实际。
|
||||
|
||||
|
||||
## 【MG1】
|
||||
|
||||
1862年1月,加特林仅用半年时间,就完成了从医生,到枪械设计师的身份转型,成功研制出,多管旋转机枪原型样机。他带着样机前往华盛顿,参与军方武器测试。手摇曲柄转动间,子弹连续呼啸而出,密集弹孔布满靶纸,可在场军方将领却满脸不屑。有陆军军械局少将直言,这只是玩具,算不上真正的武器。在战场上,胜利靠的是士兵的勇气,而非这种,投机取巧的机器。军方的保守质疑,并非毫无依据。初代加特林机枪,短板十分明显:整枪过重,搭配铸铁枪架总重超100千克,需要4匹战马,才能牵引转运;老式纸弹壳,极易在抽壳时撕裂,每试射50发就频繁卡壳;同时,黑火药燃烧时,也会产生大量浓烟,短短一分钟,便遮蔽阵地视野,严重影响瞄准。更关键的是,军方没有配套训练规范,士兵不懂把控摇柄转速,摇速过快,易造成枪管过热,过慢又会大幅削弱火力。
|
||||
|
||||
|
||||
## 【小剧场3】
|
||||
|
||||
一款具备颠覆性的划时代武器,在诞生之初,往往会遭遇世人的不解、质疑,甚至是漠视。而您设计的机枪也是如此。那么您在当时面对这样的问题的时候,到底是怎么想的呢?没错,当时确实有很多人不理解我,也有的人质疑这款武器的实用性,更有的人是直接漠视它的价值。但是,我从来可没有心灰意冷。我始终相信,我的发明能给战争带来颠覆性的改变。那面对这些质疑和不解,您到底是怎么做的呢?听说您为了推广这个发明,做了很多的努力?为了让大家能够看到这款武器的价值,我开启了四处游说的经历,多次实地演示,就是想让军方还有其他人能够直观地了解它的优势。我也承认军方提出的各种问题,这些都是样机实实在在存在的缺陷。所以,在四处奔波的间隙,我就针对这些意见,给样机进行了一系列的修改。我将原来易损坏、受潮的纸弹壳换成了黄铜弹壳,这样就解决了卡壳哑火的问题。其次,我发明了射速调节装置,可以根据战场的需要,切换每分钟100发或者每分钟200发两挡速度。另外,我还重新设计了折叠轻型枪架,让整机的重量减轻到54千克,拆装方便,占地也非常小,两匹战马就可以随军转运,大大提升了机枪的机动性。
|
||||
|
||||
|
||||
## 【解说4】
|
||||
|
||||
就在加特林机枪声名鹊起之际,一场技术革命正在悄然发生。1883年,英国发明家海勒姆·马克沁发明了自动机枪,其核心创新是火药燃气驱动:利用子弹发射时产生的燃气,推动枪管后坐,进而带动机枪完成抽壳、装弹、击发的循环,无需手摇驱动。
|
||||
|
||||
|
||||
## 【MG2】
|
||||
|
||||
可即便马克沁机枪,拥有划时代的自动射击能力,它的推广之路,依旧布满荆棘、举步维艰。马克沁奔走欧洲列国巡回演示,渴望获得军方认可与采购,却屡屡遭遇保守势力的冷眼与猜忌。俄国沙皇亲临现场,观看实弹演示,全程面露不屑与怀疑,始终认定这款枪械的射速,纯属夸大其词、性能造假,根本不愿相信一件单人便可操作的武器,能拥有碾压整支步兵编队的火力。清政府使臣出访英国,也专程观摩马克沁机枪实弹试射。现场所有人亲眼见证,随着机枪一阵轰鸣,密集弹雨呼啸而出,半米粗的大树,瞬间就被拦腰打断,场面震撼人心。旁人皆惊叹于武器的强悍威力,唯独清政府使臣神色凝重、眉头紧锁,他没有询问售价,也没有商议采购事宜,只是黯然叹息:射速太快,大清国力孱弱,根本供养不起这般海量的弹药消耗。一句感慨,既道出了晚清王朝的财政窘迫,也戳中了当时各国军方共同的顾虑。超高射速的背后,是难以承受的弹药损耗与军费开支。
|
||||
|
||||
|
||||
## 【主持人3】
|
||||
|
||||
一位是心怀悲悯、弃医造枪的战地医者,一位是半生辗转、以技破局的发明工程师。截然不同的人生际遇,天差地别的设计夙愿,从根本上就注定了,这两款彻底颠覆战争形态、改写轻武器发展史的经典机枪,从外在的机械结构、操作方式,到内在的设计灵魂、战场使命,都有着截然不同的基因与内核。
|
||||
|
||||
|
||||
## 【解说5】
|
||||
|
||||
加特林的设计,从头到尾,都深刻镌刻着“减少伤亡”四个字。他始终坚信,若能打造一款高效速射武器,用密集火力形成战场压制,便能以少数兵力抵御数倍敌军,无需再让士兵以血肉之躯填补战力差距,这便是他执着追求“以一敌百”的核心目标。
|
||||
|
||||
|
||||
## 【三维动画解说1】
|
||||
|
||||
为实现以一敌百的目标,加特林采用多枪管旋转射击理念,枪身中央设主轴,6至10根枪管,等间距环绕排列,形成圆形枪组,6根满足基础射速,10根可提升火力密度,同时,动力传输采用手摇曲柄模式,射手转动枪身一侧曲柄,通过内部齿轮组,带动中央主轴高速旋转,进而联动所有枪管同步转动,供弹系统,巧用重力原理,枪管组上方,设漏斗状或盒状弹仓,子弹预先装入,当枪管旋转至特定角度时,弹仓供弹口,与枪膛精准对齐,子弹靠重力自然下落,完成装填,击发与退壳,通过依次联动实现,每根枪管,配独立射击针与退壳机构,且与主轴旋转轨迹同步,枪管转至正前方时,击发凸轮,触发击针完成发射,射击后继续旋转,退壳顶出空弹壳,再借离心力,使其脱离枪身,完成射击退壳循环。
|
||||
|
||||
|
||||
## 【小剧场4】
|
||||
|
||||
我想请教您一个问题,您在当时设计这款机枪的时候,这些巧妙的设计,是出自怎样的考量呢?我用的是多枪管旋转设计,一共6到10根枪管,围着主轴排成一圈,就是为了,让每根枪管轮流开火,交替散热,不会像单枪管那样,连着打就发烫,甚至炸膛,能够长时间地持续输出火力。用手摇曲柄来驱动,结构简单还特别靠谱,射手自己,就可以控制转动的快慢,用来适配各种战场情况,也用不着复杂的动力装置。仅仅几名士兵,就能编织出密集的火力网,挡住好几倍于自己的,敌人的冲锋。
|
||||
|
||||
|
||||
## 【解说6】
|
||||
|
||||
与加特林机械设计不同,马克沁的设计,始终贯穿着以技破局,极致创新的理念。在目睹老式机枪,依赖人力装填,射速缓慢,士兵既要承受后坐力之痛,又要在火力间隙直面险境,他渴望打造一款,无需人力干预的全自动机枪。
|
||||
|
||||
|
||||
## 【三维动画解说2】
|
||||
|
||||
为实现这一目标,马克沁跳出多管手摇的传统框架,开创了单管自动射击体系。枪管短后坐自动原理,子弹发射时,产生的火药后坐力,会带动枪管与枪身沿导轨做短距离后坐。后坐过程中,机枪与枪管解锁,机枪继续后坐时,退壳将空弹壳顶出,同时压缩复进簧。复进簧复位时,拉动枪机向前运动,从弹链中抓取新子弹推入枪膛,完成闭锁后,等待下一次击发,全程无需人工干预。供弹系统则首创帆布弹链设计,将子弹逐发固定在帆布袋中,通过枪械自动运动带动弹链供给,彻底解决了速射武器的持续供弹难题,射速可达每分钟600发。
|
||||
|
||||
|
||||
## 【小剧场5】
|
||||
|
||||
您在当时设计这款机枪的时候,您为什么要执意另起炉灶,打造全新的,单管自动枪械呢?我之所以采用单管自动原理,就是因为它既紧凑又轻巧,不论是行军,还是在战场上机动,都十分地便捷。采用全自动循环结构,就不用再打一发,装一发子弹了,士兵只需要扣动一次扳机,就可以持续输出火力,减少了装弹时间。我还发明帆布弹链供弹,弥补传统弹容量不足的短板,可一次装载百发子弹,能够应对各种复杂的战场环境。我设计的初衷,一方面是想挖掘,自动机械技术的潜力,另一方面,就是为了适配战场环境。它只需要一个人就可以操作,同时火力,也远超过其他的传统枪械,这样一来,就能够以少胜多,掌控战局。1899年,英国陆军宣布,全面列装马克沁机枪,加特林机枪逐渐被淘汰。一个为生命造枪,一个为技术造枪,一个靠人力驱动,一个靠火药自驱,一个是防御的悲悯,一个是进攻的革新。
|
||||
|
||||
|
||||
## 【主持人4】
|
||||
|
||||
没有优劣之分,只有利益之别。可当两款武器被推向战场,被用于侵略,无休止的厮杀时,两位设计者都在其晚年,对此表达过惋惜。
|
||||
|
||||
|
||||
## 【解说8】
|
||||
|
||||
1898年圣胡安山之战,加特林机枪才一战成名,可这份荣誉,却成了加特林的枷锁。晚年的他,常常对着机枪图纸落泪。
|
||||
|
||||
|
||||
## 【小剧场6】
|
||||
|
||||
您的本意,是想以火力减少士兵伤亡,但现实却是,被大量地运用到征伐与殖民当中,渐渐偏离了初衷。您后悔当初的决定吗?其实,我最后悔的事情,就是放下了手术刀,拿起了枪械图纸。我本来是个医生,已经见惯了生离死别。当时发明机枪的初衷,其实就是想让一个士兵,能够抵得上一个连的火力,让更少的人奔赴战场,让各方都畏惧战争,最后让战争停止。我本来想做的是制止战争的武器,而不是用来杀戮的凶器。但是最后,我的发明被运到了非洲,变成殖民统治的工具,变成列强对外扩张、欺负别国的帮凶。那些在枪口下牺牲的无辜生命,是我这辈子都忘不掉的梦魇。其实罪过,从来不是冰冷的钢铁造成的,而是乱用武器的人心。可是我作为它的创造者,却一辈子都无法原谅我自己。
|
||||
|
||||
|
||||
## 【解说9】
|
||||
|
||||
1916年,第一次世界大战,索姆河战役,欧洲各参战国,仍沿用拿破仑时代的集群冲锋战术。协约国10万士兵,在炮火掩护后冲出战壕,却迎面撞上德军四百多挺马克沁机枪构筑的火力网。德军采用侧翼斜射战术,子弹如利刃般切割密集的进攻列队。仅一天,就造成英法联军6万人伤亡。141天战役结束时,双方伤亡总数超过120万人,大多数牺牲者都倒在了马克沁机枪下。
|
||||
|
||||
|
||||
## 【小剧场7】
|
||||
|
||||
马克沁先生,您同样也见证过战争的残酷,也看到无数的人间悲剧,那么您是否后悔过,打造出这样威力极强的机枪呢?我发明这款机枪,本身就是人类工业史上的一次突破,同时,也是我费尽半生精力的智慧结晶。对于这套自动机械原理,我十分骄傲,同时也十分愧疚,它给世间带来的灾害,马塔贝莱高原流淌的鲜血,索姆河战场上的亡魂,皆是因为我的这项发明而起。我终究,只是一名工程师,只能创造它,而不能掌握它的命运和用途。它会是一保家卫国的,没想到最后却成为了杀人利器。晚年的我经常,独自面对枪械模型去发呆,我真希望它能够,永远封存在实验室里,变成一堆零件,而不是走向战场。也正是因为这份愧疚,我后来再也没有研发武器装备,而是转向去研制游乐设施,并且,亲手建造起一座电动游乐场。两段人生,两种立心,两套设计理念,他们都不是战争的狂热者,只是被时代裹挟的设计者。一位一生痛苦于作品被滥用,在愧疚中离世,一位以工程师的执着,创造技术,却终生背负杀戮的拷问。武器是时代的产物,设计是本心的回应。抛开火力、杀伤、战争的标签,两位大师留给世界的,不只是经典的机枪,更是永恒的思考。技术本无善恶,立心。
|
||||
|
||||
|
||||
## 【主持人5】
|
||||
|
||||
悲悯与底线,才是所有创造最珍贵的内核。好了观众朋友们,感谢您持续关注国防军事频道《军事科技》,我们下周同一时间。
|
||||
@@ -0,0 +1,149 @@
|
||||
---
|
||||
标题: 玩具总动员:战场伪装与模块化的黑科技设计
|
||||
编导: 穆佩弦
|
||||
播出日期: 2026-06-02
|
||||
类型: 节目文稿
|
||||
期号: ep019
|
||||
---
|
||||
|
||||
# 《玩具总动员:战场伪装与模块化的黑科技设计》
|
||||
|
||||
|
||||
## 【导视1】
|
||||
|
||||
战场上,有些装备的作用如同玩具,只为在战术中迷惑对手。有些装备的外形酷似玩具,它们小巧灵动,造型别致,总是出奇制胜。有些装备的设计理念甚至直接脱胎于玩具,它们如同积木般自由拼接,协同互动,使装备更具灵活性与作战效能。本期节目,我们将为您揭开装备背后的“玩趣”密码,揭秘战场上那些鲜为人知的创造性思维。
|
||||
|
||||
|
||||
## 【演播室主持人1】
|
||||
|
||||
各位观众你们好,欢迎收看《军事科技》。我是主持人蓝皓。在人类战争史上,不是只有先进的武器,才能够改变战局的。战争的本质,从来不只是火力的对抗。一张地图,一次伪装,一段虚假的无线电信号,甚至是一些,在旁人看起来,近乎玩具般的道具,都可能对战场,起到决定性的影响。三千多年前,古希腊人将士兵,藏进一匹巨大的木马之中,攻破了特洛伊城。那或许,是人类历史上,最著名的一次,战场伪装了。第二次世界大战期间,盟军也曾组建过一支,特殊的部队。他们成员之中,有画家,设计师,录音师,甚至还有广告从业者。他们最重要的武器,既不是坦克,也不是火炮,而是一批能够充气展开的,橡胶玩具。1944年,盟军即将登陆欧洲大地。为了迷惑德军,美军,秘密组建了一支特殊部队,第23司令部特种部队。人们给了他们一个,更加传奇的名字,幽灵部队。而1945年春天,幽灵部队迎来了最危险,也最关键的一次任务。此时的纳粹德国,已经开始崩溃。盟军部队,正快速逼近德军腹地,但在他们面前。
|
||||
|
||||
|
||||
## 【解说1】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【解说2】
|
||||
|
||||
仍然横亘着最后一道天然屏障——莱茵河。这条贯穿欧洲的重要河流,不仅河面宽阔,水流湍急,两岸地形也极其复杂。对于防守一方来说,它天然就是一道巨大的反坦克壕沟。德军非常清楚,一旦盟军成功渡过莱茵河,德国本土的大门就将被彻底打开。因此,德军在莱茵河东岸部署了大量炮兵阵地、观察哨以及防御部队,严密监视盟军的一举一动。而就在这样的战场环境下,一场以假乱真的“表演”即将在莱茵河畔展开。
|
||||
|
||||
|
||||
## 【三维动画解说1】
|
||||
|
||||
1945年3月,美军计划在德国小镇威尔森附近,制造一场假渡河。幽灵部队,首先在威尔森附近,布置了大量的重装备。但这些装备,大多数都是假的,包括充气的M4谢尔曼坦克,假火炮,假卡车,木质观察塔。从空中看去,这里仿佛驻扎着一个完整装甲师。实际上,很多坦克,甚至只是橡胶和帆布。
|
||||
|
||||
|
||||
## 【解说3】
|
||||
|
||||
这些充气装备,是幽灵部队最具代表性的伪装手段之一。他们以美军当时广泛装备的M4谢尔曼坦克作为原型制造。整个模型重量只有四十多公斤,数名士兵便可完成搬运,并在几分钟内迅速展开。但当它们被部署到预设阵地之后,从高空俯瞰,却与真实坦克几乎没有明显区别。战争进入尾声后,德国空军制空权不断丧失,前线侦察能力大幅下降。德军越来越依赖高空航拍照片,判断盟军部队的集结方向与兵力规模。高空侦察能够识别目标轮廓,却很难进一步确认其真实性。在德军侦察照片中,这些由橡胶制造成的假坦克,开始被误判为正在集结的装甲部队。
|
||||
|
||||
|
||||
## 【三维动画解说2】
|
||||
|
||||
为了使伪装行动更加真实,幽灵部队拥有一种特殊装备:声响欺骗车。这些卡车上安装了大功率扬声器。美国陆军曾专门录制部队行军中的各种音效,随后通过音响系统播放。这些声音经过专业声学设计。美国工程师会根据地形、风向、夜间湿度调整播放方向。这些声音最远可传播约20公里。由于德军会长期监听盟军部队通信的习惯,幽灵部队专门训练无线电伪装员。他们会模仿真实部队的通信模式。德军监听员因此误以为美军主力真的已经集结。德军最终误判了盟军部署方向。他们将部分兵力和饱和火力错误地集中到威尔森方向。而与此同时,真正的盟军主力却在另一方向秘密渡河。
|
||||
|
||||
|
||||
## 【专家1】
|
||||
|
||||
那么在现代化的作战模式的这样的一个状态下,这种幽灵部队和假目标的诱骗方式,仍然是有很大的这样的一个功能的。俄乌冲突爆发之后,双方都设置了大量的假目标。比较典型的是,为了最大程度地去诱骗对方的远程精确打击火力,而且现在俄乌双方已经开始大量地使用这种充气假目标。现在它的仿真的程度越来越高。首先,它可以模拟坦克、装甲车辆、防空导弹,甚至是这种大型的导弹车,在相关的区域内进行这样的一个部署。充气假目标还可以通过小型红外热源的这样的一个方式去模拟真正的坦克装甲车辆,包括防空导弹以及雷达,它处于开机工作状态的这样的一个红外的热辐射信号。所以不仅仅是从视觉上,它可以起到欺骗的作用,从雷达信号,从红外信号上,它也可以起到欺骗的作用。
|
||||
|
||||
|
||||
## 【演播室主持人2】
|
||||
|
||||
从充气坦克,到今天的无人装备,战争中的一个变化,正在变得越来越明显。20世纪的大部分时间里,武器的发展,始终在追求更大,更强。比如说,更大口径的舰炮,更庞大的轰炸机,更高速的战斗机等等。人类不断试图用,工业规模与火力优势,去主导战场。但是,进入21世纪后,一种截然不同的趋势,开始出现了。越来越多,真正改变战场的装备,正在变得,越来越小。它们不再强调,厚重的装甲,也不再依赖庞大的,钢铁结构。相反,它们开始追求,轻量化,低成本,以及快速部署能力。有些装备,甚至可以小到,直接装进士兵的口袋。而有些外形,则看起来更像是,商店里的模型玩具。然而正是这些,看起来不像武器的装备,正在重新改变,现代战争的形态。
|
||||
|
||||
|
||||
## 【解说4】
|
||||
|
||||
很多人小时候,都见过航模。轻薄的机身,小巧的螺旋桨,只需轻轻一抛,它便能够借助气流,飞向天空。很长一段时间里,这类飞行器,更多只是模型与玩具。但进入21世纪后,战场却开始赋予它,另一种意义。2003年,伊拉克战争期间,一种外形酷似航模的,小型无人机,开始频繁出现在美军前线。它就是,RQ-11大乌鸦无人机。与此前那些,依赖机场和跑道的,大型无人机相比,大乌鸦显得异常轻巧。整机重量,只有几公斤,可由单兵携带。执行任务时,士兵只需握住机身,将其直接投向空中,这架小型无人机,便会迅速展开机翼,飞向目标区域。从外形到操作方式,它都更像是一架,被放大的航模。但正是这种,看似玩具化的设计,却第一次,让空中侦察能力,被真正带到,普通士兵身边。
|
||||
|
||||
|
||||
## 【三维动画解说3】
|
||||
|
||||
RQ-11大乌鸦无人机,长度约0.91米,翼展约1.4米,重量约1.9公斤,动力为电动推进,最大速度约每小时95公里,续航时间约60至90分钟,作战半径约10公里,操作人数通常在两人左右。在城市巷战中,大乌鸦能够提前察看街角埋伏、屋顶狙击手,以及道路上的简易爆炸装置。它第一次让空中侦察真正下沉到步兵班一级。
|
||||
|
||||
|
||||
## 【专家2】
|
||||
|
||||
美军使用的RQ-11,大乌鸦无人机,确实看起来,像是一个大号的玩具,其实它的成本比较低,无论是从设计理念,还是从这种系统,以及材料的应用来看,我认为它和航模,是有很多的相通之处的,不排除在这种民用的,航空模型的基础上,发展而来的,研制而来的,因为这种航模飞机,它是大量地使用复合材料,所以机体比较轻,与此同时会使用小型的电机,去作为动力系统,不仅可以飞得快,也可以飞得远,像大乌鸦这种,手抛式的单兵携带的无人机,它同样也是,使用这种复合材料,使得机身的重量(变得)非常轻,单兵在背包当中就可以携带,组装完毕之后,采用手抛的方式,它就可以飞到,相关的任务区域,所以,看起来像是一个大玩具,但是在战场上,使用起来非常地简单,操作和航模没有什么差别,那么就可以更多地,去批量装备部队。
|
||||
|
||||
|
||||
## 【解说5】
|
||||
|
||||
拥有一架小型遥控直升飞机,大概是很多人小时候的一个梦想。塑料机身、微型旋翼,飞行时还会发出轻微的电机声。很长一段时间里,人们很难把这种玩具,与真正的战争联系在一起。如今,一些外形与它们极其相似的设备,却已经开始出现在战场。黑黄蜂微型无人机,就是其中之一。它的体积只有成年人手掌大小,旋翼展开后,与普通的小型遥控直升机几乎没有太大区别。它可以穿过街巷与建筑缝隙,对目标区域进行近距离侦察;也能够进入复杂环境,完成室内搜索、目标定位等行动。而这也意味着,现代战场对于武器的定义,正在发生变化。越来越多承担侦察与感知任务的装备,开始变得更小、更轻,也越来越像人们日常的消费电子产品,甚至越来越像孩子们的玩具。
|
||||
|
||||
|
||||
## 【三维动画解说4】
|
||||
|
||||
黑黄蜂无人机,长度约10至16厘米,可以直接放进口袋。重量约18至33克,相当于一只麻雀的重量。续航时间约20至25分钟,最大距离约两公里,飞行速度约每小时18公里。配备高清和热成像技术,采用双旋翼结构。这与很多民用微型遥控直升机极其相似。这种飞行器结构稳定,易操控,可悬停,适合低速飞行,非常适合狭窄空间侦察。
|
||||
|
||||
|
||||
## 【专家3】
|
||||
|
||||
未来这种,小型的微型的无人机,它的发展方向,我认为变得更加地细分。也就是说,小型无人机和微型无人机,会出现各种各样的形态,以满足不同的任务需求。现代战场上还出现了一些仿生无人机,比如说,把这个无人机设计成这种扑翼的这样的一个理念。它是不依托于螺旋桨来提供动力,而是扇动翅膀进行飞行。那么这种状态,就极具隐蔽性。当战场上出现一只鸟,一只蝙蝠的时候,对方的作战人员,根本就想象不到,它其实是对方的空中侦察兵。但是这种扑翼的无人机,它通过这种低空飞行的方式,把对方的兵力配备情况,火力的配备情况,全都了解之后,马上就可以引导己方的火力,进行精准打击。那么这些扑翼无人机,最大的优势就是,它的外形具有欺骗性,那和普通的鸟类,和普通的蝙蝠没啥差别。但是在飞行的过程当中,又没有什么噪音,它就可以悄悄地接近目标。
|
||||
|
||||
|
||||
## 【解说6】
|
||||
|
||||
很长一段时间里,人们对于军事机器人的想象,都是像战车一样,带有厚重的装甲,或者拥有履带式底盘,以及能够搭载火力系统的机械结构。但近年来,另一种截然不同的设计思路,开始进入现代战场。四足机器人,就是其中最具代表性的一类。与传统作战平台不同,这类装备最重要的能力,并不是火力,而是复杂地形下的运动性能。它们能够跨越障碍,上下楼梯,在崎岖地面保持平衡,甚至能够在一定程度上自主调整姿态。
|
||||
|
||||
|
||||
## 【演播室主持人3】
|
||||
|
||||
现代战场的技术路径,正在发生变化。一些原本复杂的功能,如今被压缩进更小的设备之中,并以更加简单的方式进入前线使用。装备的形态,也在发生改变。它们不再是固定功能的单一平台,而开始具备一定的组合与调整能力,可以根据不同任务进行快速切换。而在这变化当中,装备的一些设计思路,也开始让人联想到一个更日常的概念,积木。这种组合的逻辑,也让装备的使用方式,变得更加灵活,也更加贴近实际的需求。
|
||||
|
||||
|
||||
## 【解说7】
|
||||
|
||||
积木的特点很简单,由标准化的模块组成,通过不同方式拼接,就可以形成不同结构。它的关键,不在单个部件,而在于整体的可重组性与扩展性。这种思路,在现代海军装备发展中,也曾出现过。上世纪70年代,美国海军建造的斯普鲁恩斯级驱逐舰,就是一个典型案例。与传统驱逐舰在设计之初就非常不同,这一级舰艇在总体设计阶段,就预留了较强的系统扩展空间。
|
||||
|
||||
|
||||
## 【三维动画解说5】
|
||||
|
||||
这是一艘斯普鲁恩斯级驱逐舰,舰长171.7米,舰宽16.8米,标准排水量5770吨,满载排水量7700吨,最大航速33节,续航力每20节6000海里。武器配置上,初期搭载MK45型127毫米舰炮、阿斯洛克反潜导弹、鱼叉反舰导弹、近防武器系统,可起降两架SH-60海鹰反潜直升机,兼顾反潜、反舰、防空与对海打击多重任务。该舰在设计之初,便摒弃了传统舰艇整体化建造模式,将舰体、动力、武器系统划分为标准化独立模块,各模块可并行建造、快速组装。这种设计赋予舰艇极强的升级改造潜力。舰上预留标准化武器安装基座、管线接口与空间冗余,后期可直接换装MK41垂直发射系统、战斧巡航导弹、新型雷达电子设备,无需对舰体主体结构进行大规模切割改造。斯普鲁恩斯级驱逐舰成为现代海军模块化造船理念的开山之作。
|
||||
|
||||
|
||||
## 【专家4】
|
||||
|
||||
斯普鲁恩斯级驱逐舰,在当时的,这样的一个时代背景下,是美国海军,要用于和苏联的,这种海上作战体系,进行对抗的这样的一种主力舰。采用模块化设计理念,就可以进行这样的一个系统,和舰载武器的选装。而且当时,斯普鲁恩斯级的驱逐舰,也有外销的考虑。如果是外销的话,那么还是要根据使用方的,这样的一个定制化的要求,针对它的舰载系统,进行一定程度的更改。所以模块化设计理念,既为后续的升级改进,留下了更大的空间,同样也为外销,提供了更多的可能性。斯普鲁恩斯级驱逐舰,可以说对于后续的,提康德罗加级巡洋舰的,研发和制造,以及阿利·伯克级驱逐舰的,研发和制造,都是提供了,非常重要的参考作用。后续像这种,大型的这种战舰,无论是为它配备,相控阵雷达还是安装更多的,通用的导弹垂发系统,那么模块化设计理念,都提供了更多的可能性。
|
||||
|
||||
|
||||
## 【解说8】
|
||||
|
||||
如果说斯普鲁恩斯级驱逐舰,让平台化,与可扩展的设计思路,在海军装备中,初步成形,那么,在地面装备体系中,这种理念,被进一步具体化,德国的拳师犬轮式装甲车,就是一个典型代表,它的核心设计,并不是某一种,固定用途的车辆,而是一种,底盘与任务模块的,组合结构,车体本身,承担通用机动,与防护能力,而上方的任务舱,可以根据需要,进行快速更换与配置,在不同任务条件下,同一辆底盘,可以在短时间内,切换为运兵平台,医疗救护车辆,指挥通信节点,甚至电子战作战单元,这种变化,使得装备,不再被单一功能所固定,更像一种,可以不断组合的系统,在某种意义上,这种设计逻辑,就像搭积木,基础结构保持稳定,而功能,则像积木的模块一样,通过不同组合与替换,不断变化,传统军队,往往需要大量不同型号的专用车辆,来完成分工,而模块化设计,则尝试用一套底盘体系,去覆盖更多任务场景,这种变化,不仅简化了装备体系,也在一定程度上,降低了后勤保障的复杂度。
|
||||
|
||||
|
||||
## 【演播室主持人4】
|
||||
|
||||
进入21世纪后,这种模块化的思路,开始从单一装备,进一步扩展到更大的作战体系层级。美国陆军提出的斯特赖克旅战斗队,正是这一理念的集中体现。它以统一底盘的装甲车辆为基础,配合标准化编制与数字化指挥系统,试图构建一种可快速部署的作战单元。在理想状态下,这支部队能够在约96小时内完成全球范围内的战略投送,并迅速形成完整的战斗力。从单一装备,到整支作战编组,模块化开始进入了体系层面。而在这变化当中,战争的组织方式,也在悄然发生转向。
|
||||
|
||||
|
||||
## 【三维动画解说6】
|
||||
|
||||
这是一辆M1126,斯特赖克步兵战车。它的车长约6.93米,车宽约2.72米,车高约2.64米。战斗全重,约16至19吨。采用350马力柴油机。最大时速,约每小时97公里。最大行程,约530公里。能够载员9名步兵。斯特赖克,采用8轮轮式结构,维护简单,油耗更低,噪声更小,适合快速部署。它的底盘,可以搭配不同的结构,变为步兵车,指挥车,医疗车,反坦克车。车身的大量零件,以及动力系统,实际上完全通用。使维修人员,零件仓库,训练体系,都能被大幅简化。它们通过C-130运输机运载,可以向全球任意战场,快速投送。
|
||||
|
||||
|
||||
## 【解说9】
|
||||
|
||||
斯特赖克步兵旅的核心,并不只是某一种武器装备,而是一整套围绕“快速反应”构建起来的作战体系。它的基础作战单元,通常以步兵营为核心展开,再配备多个装备“斯特赖克”轮式装甲车的机动营协同行动。表面上看,拥有相似的外形与统一的底盘平台,但实际上,它们却承担着截然不同的任务。有的负责搭载步兵班组快速突入;有的安装反装甲武器,承担火力支援;有的则装备侦察设备,负责搜索目标;还有一些车辆,本身就是移动的指挥节点与通信中枢。同一种底盘,被拆分成不同功能模块,再像积木一样重新组合。美国陆军希望借此打造的,并不是一支传统意义上的装甲部队,而是一支能够快速适应不同战场环境的“模块化军队”。而这种理念,也深刻改变了它的作战方式。
|
||||
|
||||
|
||||
## 【专家5】
|
||||
|
||||
冷战结束之后,美国陆军进行了转型。斯特赖克轮式装甲旅,主要是强化这种机动作战,和信息化的作战。采用了这种模块化的,兵力的这样的一个,配置方式。这种模块化的方式,可以让斯特赖克轮式装甲旅,根据任务的需求,根据环境的需求,进行兵力结构的拆分和重组。可以随时组成,比较灵活的这样一个小分队。那么在此前,伊拉克战争的过程当中,美军是使用了,斯特赖克轮式旅,作为这样的一个地面体系的,重要组成部分。比如说,在进入到城市这样的,一种比较复杂的环境当中,斯特赖克轮式旅,它的信息的互通互联能力,是比较强的。可以进行这种,信息的最大程度的共享,感知环境方面有优势。另外一个,在城市的环境当中,轮式底盘的通过性比较好,可以快速地机动到任务区。也可以通过这种模块化的,这样的一个组合方式,那么针对任务,去选择更为灵活的,或者说更有针对性的,这样的一个作战平台。现在直瞄火力,也就是说进行火力支援的,主要是配备有大型遥控炮塔,配备有机关炮的,这种轮式的步战车。既可以搭载步兵,也可以进行一定的火力支援。除此之外,斯特赖克轮式旅,它还有这种火力模块。斯特赖克轮式旅,还是有先天的不足。一旦它面对实力比较强的对手,比如说对方装备,非常先进的主战坦克,和履带式的步战车,那么对方它在,装甲防护能力方面,在火力方面,会有压倒性的优势。在这样的-种作战的状态下,斯特赖克轮式旅,是难以取胜的。甚至可能会出现,全军覆没的情况。
|
||||
|
||||
|
||||
## 【解说10】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【演播室主持人5】
|
||||
|
||||
当现代战争,越来越强调更轻、更快、更灵活,一种新的逻辑也正在出现:决定胜负的,未必只是更厚重的装甲,或者更庞大的平台,而是谁能更快组合力量,更快获取信息,更快完成反应。于是,人们看到无人机,像成群的飞行玩具般,掠过天空。模块化部队,像积木一样快速重组。机器狗、微型机器人、仿生装备,陆续走入真实战场。曾经属于娱乐与想象力的创造,正在一步步跨越虚拟与现实之间的边界。或许也意味着,下一款主导未来战场的装备,它的原型设计灵感,就来自于商店某个安静角落里的一款玩具。好了观众朋友们,感谢您持续关注国防军事频道《军事科技》,我们下周同一时间,再见。
|
||||
|
||||
|
||||
## 【解说11】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
@@ -0,0 +1,139 @@
|
||||
---
|
||||
标题: 武器装备的颜值洼地
|
||||
编导: 张彤枫
|
||||
播出日期: 2026-06-09
|
||||
类型: 节目文稿
|
||||
期号: ep020
|
||||
---
|
||||
|
||||
# 武器装备的颜值洼地
|
||||
|
||||
|
||||
## 【导视1】
|
||||
|
||||
很多人评价武器装备的好坏,首先想到的,就是重量、威力,这些能直接量化的数据。但其实,还有一个容易被忽视的因素,那就是颜值。在多型武器装备的竞标和使用中,有些因为长得丑,直接被淘汰;有些因为长得丑,被快速更新换代;还有些因为长得丑,直接影响了作战性能。我们就一起回顾一下,那些因为颜值,而备受争议的武器装备。
|
||||
|
||||
|
||||
## 【主持人1】
|
||||
|
||||
各位观众你们好,欢迎收看《军事科技》,我是主持人蓝皓。自人类诞生开始,无论是停留在画布上的色彩,还是镌刻于青铜器上的纹样,对美的本能追求,已深植文明的基因当中,武器亦不例外。千年过后,当我们把目光,投向由钢铁与烈火铸就的,现代武器装备时,这种对美的本能向往,并未消退。飞机的流线型机身,舰艇的修长轮廓,坦克的棱角分明,无不体现了工业美学,与功能主义的精妙平衡。但是,并不是每一款装备,都极具美感,有些设计,甚至让工程师自己都摇头叹息。那么,这些“颜值担当”,与“颜值黑洞”之间,究竟隔着怎样的设计逻辑呢?是空气动力学,妥协了视觉感受,还是战场需求,压倒了美学考量?本期节目,我们就来看一看那些,走向美与丑,两个极端的武器装备。
|
||||
|
||||
街头采访—X-32与其他战机
|
||||
|
||||
|
||||
## 【街采】
|
||||
|
||||
我觉得左边这架飞机更符合我的审美。我感觉左边这个更偏向于科幻一些,右边就显得更加笨重和老实。
|
||||
|
||||
我觉得左边的好看,因为右边的这个飞机前端下面有一个凹进去的东西,正面看会比较突兀。
|
||||
|
||||
我觉得左边这(架)看起来线条非常流畅,感觉会减少空气阻力。然后右边这(架)看起来是一种几何体,感觉非常地抗击打。
|
||||
|
||||
|
||||
## 【主持人2】
|
||||
|
||||
在刚才的街头采访中,大家对于这款战机的颜值普遍呈现出负面评价。从外观的角度来看,可能并没有得到大家的认可。这架战机名叫X-32,您可能对它并不熟悉,但是它有一个更广为人知的竞争对手,那就是F-35战机。如果将两架战机放在一起做一个对比,X-32那圆润臃肿的机身与F-35凌厉干练的线条便形成了鲜明的反差。最终从代号上也可以看出,F-35拿下了竞标合同,而X-32则止步于原型机阶段。那么这对颜值差异巨大的孪生兄弟,在当年究竟经历了怎样的博弈呢?
|
||||
|
||||
|
||||
## 【解说1】
|
||||
|
||||
在当年,美军新型隐身战斗机的竞标中,就有一对冤家,因为要考虑到各军种的通用性,所以要求,新型隐身战机,需要具备海军陆战队,所需要的垂直起降能力,这场竞赛的赢家,现在我们都很熟悉了,那就是F-35,而遗憾退场,埋进历史尘埃的,则是波音家的,X-32技术验证机,单看外表,F-35闪电II,与当时其他战机相比,身材确实壮实了不少,所以大家都爱叫它“肥电”,这个外号虽然对它的身材,有些小吐槽的意思,但跟它的竞争对手,X-32—对比,简直算得上相貌端正了,再来看看X-32,第一眼看到它,你大概会忍不住想问,这为啥看起来,像一只鼓着气的青蛙呢,还是只张着大嘴的青蛙,这种既视感,主要来自它那,超级夸张的机头进气道,这种震撼的视觉效果,可不是偶然,而是波音公司,为了满足垂直起降时,巨大的进气需求,所作的性能妥协。
|
||||
|
||||
|
||||
## 【三维动画解说1】
|
||||
|
||||
X-32采用的垂直起降方案,来自于鹞式战斗机的成熟设计。当飞机进入垂直起降模式时,机身周围的四个辅助喷口同时开启,将发动机的推力由水平方向转向垂直方向,相当于直接将发动机的推力拆解为四个独立的垂直推力支点,从而实现悬停与着陆。这种推力矢量分配方式,虽保障了垂直起降能力,但却导致机身结构异常臃肿。因为发动机担负了飞机的全部推力输出,就需要发动机有更大的推力,而更大的推力又要求飞机有更大的进气口保证进气效率。无论是X-32,还是鹞式战斗机,都有着与机身比例严重失调的硕大进气口,使其整体轮廓显得笨重而怪异。而F-35B,则采用升力风扇加发动机矢量喷口的复合推进方案。升力风扇独立承担约50%的垂直升力,主发动机仅需提供剩余推力,并兼顾水平飞行。虽然这种方案结构更复杂,同样也会占用大量机体空间,但升力风扇的引入,显著降低了主发动机的推力需求,从而允许进气道与机身线条更为协调地融合,使整机气动布局更趋流线,视觉上也更具现代战机所独有的流线型美感。
|
||||
|
||||
|
||||
## 【专家1】
|
||||
|
||||
当时的X-32,也有很多人看好。为什么?它的设计有一些颠覆性的一些理念,是采用了特殊设计的下颚进气道。那么整个的外形,看起来有一些怪异,但是有人认为,它的这种雷达的隐身性能,可能会更胜一筹。不过,当时美国军方对于隐身战斗机的定位,是三军通用。其中美国海军陆战队,要求新一代的隐身战斗机,具备短距起飞的能力,要具备垂直降落的能力。而X-32它想要实现短距的起飞和垂直的降落,所采用的系统,包括控制系统以及动力系统,是参考了AV-8B海鹞。所以结构可能会比较复杂,也会带来性能方面的一些限制。不仅仅是X-32它的颜值,可能没有那么漂亮。在短距起飞和垂直降落能力方面,可能和当时的X-35(F-35的试验型号)相比,还是有一定的差异性。所以我们看,有的时候设计层面的颜值,对于它的综合的性能和实战能力,其实还是有关联性,甚至是有影响的。如果说X-32的外形,是由于先天设计取舍导致的臃肿,是一种无奈的选择。那么,有些装备的颜值,就是在后天设计上,直接放弃了美学考量。有很多的武器装备,都是依托于既有平台,进行改装升级,在原有结构上堆砌新的部件。其中最典型的就是预警机。那么预警机中,又有哪款令观众不忍直视的代表呢?我是比较喜欢左边我们这种国产的这个300的预警机的。其实它和运-20很像,我很喜欢这种敦敦厚厚的,四发的这个强大的推力。旁边这个,我觉得它肯定也有自己的理念在里面。但是,我会觉得没有左边那么利索一些。左边的好看,右边的不太好看。因为右边的它这个飞机头,前面有一个圆,然后就有点太突兀了。其实我也更喜欢左边那个。因为右边那个,感觉从颜色上,就比左边那个略显笨拙。左边它那个灰调其实是我个人最喜欢的一个灰色。右边那个感觉像是那种客舱的感觉,在机场里已经司空见惯了的。在刚才的街头采访中,大家对于预警机的外形,有着不同的见解。其中对于有着大鼻子造型的费尔康预警机,吐槽声尤为集中。
|
||||
|
||||
|
||||
## 【解说2】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【主持人3】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
街头采访—费尔康预警机与其他预警机
|
||||
|
||||
|
||||
## 【主持人4】
|
||||
|
||||
我们来看,它那硕大的机头雷达罩,像是一颗长在机首的巨型青春痘,粗暴地撕裂了原有机身的流畅曲线。和现在众多头顶圆盘的预警机相比,视觉冲击力十足,但是却略显笨重。作为现在预警机颜值的反面教材,从费尔康预警机的巨大鼻头,到现在预警机的头顶圆盘,预警机的颜值,又经历了哪些变化呢?
|
||||
|
||||
|
||||
## 【解说3】
|
||||
|
||||
预警机,作为现代空战体系中的“千里眼”与“顺风耳”,已经成为了空战体系中的绝对C位。而它最关键的雷达系统怎么摆放,直接决定了它的颜值走向。比如,美军早期使用的EC-121预警机,就在机腹上安装了一个巨大的下视雷达,看起来像长了啤酒肚,又在机背上加装了用于探测高空目标的上视雷达,看起来又有些驼背。这一切都是为了满足当时技术条件下的探测需求,而作出的无奈妥协。随着雷达技术的成熟,另一种我们最为熟悉的背负式雷达设计,开始出现在预警机上。圆盘状雷达天线被置于机身顶部,实现360度无死角探测,既提升了探测效率,又大幅优化了气动外形。但随着相控阵雷达的出现,雷达不再依赖机械旋转,固定式阵列可嵌入机身、机翼甚至垂尾,这就让预警机的外形设计迎来新一轮解放。以色列的费尔康预警机就是第一个吃螃蟹的。不过,这种新潮设计也让费尔康长得有点特别,在造型和性能之间做了些有趣的取舍。
|
||||
|
||||
|
||||
## 【三维动画解说2】
|
||||
|
||||
费尔康预警机,相较于目前主流的,圆盘造型的预警机,最大的缺点在于,缺乏360度,全向雷达覆盖能力,机头巨大的球形雷达罩,虽保障了前半球探测精度,但覆盖角度仅为120度左右,探测距离,可以达到约370公里,机身两侧的两块侧视雷达,也因安装位置受限,60度左右的探测范围,和300公里左右的探测距离,虽在尾翼处,加装了第三块雷达面板,但功率受限,导致后向探测能力,依然薄弱,反观背负式雷达,无论是圆盘造型,还是平衡木造型,虽然也不能完全实现,360度无死角覆盖,但通过旋转基座,或调整飞机飞行角度,可动态补全探测盲区,整体覆盖,连续性与稳定性,远超固定式布局,同时,机头的巨大球形雷达罩,相较于原本的机体设计,增加了至少10%的飞行阻力,与结构重量,也使得机身重心前移,影响飞行稳定性与机动性,而背负式雷达,将探测设备置于机身顶部,既减轻了气动干扰,又使重心分布更趋合理。
|
||||
|
||||
|
||||
## 【专家2】
|
||||
|
||||
以色列的费尔康预警机,早期的型号,它是把相控阵雷达和机身的结构贴合到一起,出现了大鼻子的这样的一个结构。之所以采用这样的一个模式,是出于两方面的考虑。早期的相控阵雷达,可能个头比较大。另外一个,它在冷却的这个技术方面,不是特别成熟。采用这种贴合式的一个设计理念,可能会有效地解决散热时的问题。因为它这个雷达是分布式部署的,不同的雷达模块,它的散热系统个头没有那么大,在技术层面是比较好解决的。所以费尔康,它既有大鼻子的型号,也有带着大盘子的型号。我们现在看到的大型的固定翼的预警机,其中很多都是采用背负式的设计。因为背负式对于机身,它是不需要改动的,舱内的空间比较大,容易部署更多的指挥控制的系统,操作台也可以部署很多,甚至还会留出足够多的人员休息区。而采用背负的这样一个方式,雷达的尺寸也可以做得比较大。
|
||||
|
||||
|
||||
## 【解说4】
|
||||
|
||||
(本段无对应字幕,待编导核)
|
||||
|
||||
|
||||
## 【主持人5】
|
||||
|
||||
从费尔康的“鼻突”,到现代预警机普遍采用的圆盘顶,本质是雷达技术迭代与气动优化的双重结果。但是,给大家带来最直观的感受,却是预警机外形美感的悄然提升。这种既符合功能需求,又兼顾视觉协调的进化逻辑,并不是只出现在预警机身上。海上的军舰,也正在经历着类似的蜕变。
|
||||
|
||||
街头采访—絮弗伦级驱逐舰与其他驱逐舰
|
||||
|
||||
|
||||
## 【主持人6】
|
||||
|
||||
我觉得右边这艘,没有左边这艘看起来帅。因为左边这艘是狭长的,感觉甲板也非常的平直,飞机起飞的话会很帅。右边这个就是灰灰的,跟拍摄可能也有关系,各种感觉也比较杂乱。左边更科技现代一点,右边感觉没有左边帅气。还是左边的好看,右边的不好看。左边的这(艘),它的体积会大一点,右边的体积小。然后它上面有一个球,挺大的,我觉得有点太突兀了。第三张图片是两艘军舰,左边这艘军舰的话,有点像一体成型的样子,线条非常的流畅。而右边这艘军舰的话,它中间有一个大球,看起来会笨重许多。所以我总体的观感上来说,我觉得左边这(艘),可能在现实生活中,速度也会更快一些。左边这个也更符合我的审美。刚才大家看到的,同样是头顶一个圆球的驱逐舰,正是法国设计制造的,絮弗伦级驱逐舰。其舰岛顶端那个硕大的雷达罩,成了它的标志性特点。从美观的角度来看,这样一个巨大的圆球,安放在修长的舰体之上,确实显得有些突兀。仿佛为了追求探测性能,而牺牲了整体的协调性。那么,这个突出的大圆球,担负了怎样的作用呢?现代军舰,又是如何避开这种,怪异的设计呢?
|
||||
|
||||
|
||||
## 【解说5】
|
||||
|
||||
对于航行在海上的舰艇来说,雷达的重要性,是和武器系统同等关键的。早期的水面舰艇,并没有配备导弹,雷达主要就是用来,在海面上扫一扫,找找目标,顺便给火炮瞄准“报个点”。经常能在老式驱逐舰,那高高的桅杆上,看到个长得像大喇叭的雷达天线。等导弹时代一来,画风就变了,舰载雷达,必须要能超远程预警,跟踪目标,并且死死地锁定其中几个。这种高难度要求,直接催生出了大型雷达,雷达的功能,也开始“分家”了。这个时期,长得像“苍蝇拍”的,平板雷达天线,和神似“晾衣架”的雷达天线,成了主流。它们被一层一层地,布置在舰岛的不同高度,各司其职。虽然这些雷达,看起来有点糙,不怎么精致,但使用功能上却不打折扣,经济又实惠。不过,这种乱堆一气的方式,让浪漫的法国人,有点坐不住了,他们觉得这些横七竖八的雷达天线,简直破坏了战舰的整体美感。于是,他们琢磨着,能不能把这些天线,都塞进一个更好看的盒子里呢?最终,叙弗伦级驱逐舰的舰岛上,出现了那颗无比醒目的大白球。但是,话又说回来,这种头顶个球的效果,怎么看都有些一言难尽。
|
||||
|
||||
|
||||
## 【三维动画解说3】
|
||||
|
||||
叙弗伦级驱逐舰,巨大的球形雷达罩,其实是时代技术限制的产物。尚未突破相控阵雷达小型化,与多面阵协同技术。如果想提升探测性能,只能依靠增大雷达孔径,和功率来提升探测距离和精度。这就使得雷达体积,被迫变大。为了保证360度全向探测,就需要让巨大的雷达,旋转起来。在这种情况下,为了保证雷达的使用寿命,和降低航行时的风阻,球形雷达罩,便成为最稳妥的工程选择。当然,在同时期其他舰艇,也有着不同的方案。苏联时期的舰艇,为了解决舰载雷达的问题,选择将雷达阵列,放置在更高的位置,同时采取双面阵雷达,并且将不同功能的雷达,分置在舰船的不同位置,以分散布局换取探测冗余。当时的苏联舰艇,看起来都是十分高大的,并且在舰艇上,密密麻麻布满了各式雷达天线,与电子设备。而现代战舰,则普遍采用,分布式相控阵雷达系统,四面固定式阵列嵌入舰桥四周,既保证了360度全向覆盖,又大幅降低了雷达反射截面积,与结构重量。既没有任何体形夸张的装置,也使得舰艇外观,更加简洁流畅,更符合隐身设计。
|
||||
|
||||
|
||||
## 【专家3】
|
||||
|
||||
与模块化建造理念,独立的雷达,主要是装备护卫舰,或者是轻型战舰,因为它安装比较简单,不会占据比较大的舰体的空间,而且成本比较可控。像一些护卫舰和轻型护卫舰,会普遍装备。那么大型的,四面阵的相控阵雷达,主要是装备主力战舰,像一些驱逐舰,甚至包括在航母上也可以配备。它的功能,针对整个编队进行预警,它的照射距离比较远,可以在数百公里,甚至是上千公里的范围内,去探测和识别,空中目标和海上目标。而且多目标的处理能力很强,可以引导先进的防空火力,和反导火力,针对空中的来袭目标进行打击。这种雷达个头大,而且比较昂贵。
|
||||
|
||||
|
||||
## 【解说6】
|
||||
|
||||
徐弗伦级驱逐舰行驶在茫茫大海上,虽然有点像海洋馆里的白鲸,顶着皮球游弋,但不可否认的是,这种将雷达天线封装进球形罩的设计,既保证了探测性能的稳定性,又兼顾了舰艇整体的结构强度与抗风浪能力。即使来到现在,还有很多舰艇沿用类似设计,只不过这个球的大小不再有那么夸张。很多头顶个球的舰艇,现在看起来反而有些帅气。在颜值上来说,也是各有千秋。而头顶球的代表,徐弗伦级驱逐舰,被认为不好看,只能归咎于这个球太大了。
|
||||
|
||||
|
||||
## 【主持人7】
|
||||
|
||||
当然,除了我们刚才看到的这几款装备外,其实还有很多的武器装备,在设计之时,就完美避开了“颜值巅峰”,成功走进了“颜值洼地”。那么,武器装备的颜值,究竟能丑出怎样的高度呢?还有哪些深陷颜值洼地的装备,令人啼笑皆非呢?让我们一起盘点一下。
|
||||
|
||||
|
||||
## 【解说7】
|
||||
|
||||
苏联时代,为了打造庞大的钢铁洪流,研制了不少奇形怪状的坦克。279工程坦克,设计出了飞碟一样的车身,配上了4条履带,加上圆鼓鼓的炮塔,看起来,透露着一份憨厚和可爱,但绝谈不上漂亮。长滩号巡洋舰,作为世界上首艘核动力巡洋舰,在作战能力上,是十分强悍的。但是,同样是为了容纳庞大的雷达系统,在修长的舰体上,硬生生堆起巨大的,方方正正的舰岛。虽然在当时看起来颇具威慑力,但以现在的审美来看,更像是一个巨型积木块,被随意堆砌在甲板上。A300-600ST运输机,外号“大白鲸”。它是将一架空客A300客机的客舱,进行了PLUS MAX升级,更大,更高,让它的载重能力,提升了30%,并且可以装下尺寸更大的货物。不过,从颜值上来说,主打一个丑萌丑萌的。日本EC-1电子战飞机的颜值,和费尔康预警机如出一辙,一个是大鼻子,一个是大嘴唇。姑且不讨论其性能的优劣,就说这个丰满的嘴唇,让人看了很难不笑出声来。最后的王炸,则是波音公司设计的BV-235验证机。很难想象,当年和它同台竞技的,是现在的阿帕奇武装直升机。从颜值上来看,只能说阿帕奇,赢得毫无技术含量。
|
||||
|
||||
|
||||
## 【专家4】
|
||||
|
||||
我认为设计武器装备的时候,对于颜值的这样的一个要求,还是应该在整体设计的考察范围内的。为什么这么说?因为武器装备,无论是战斗机、轰炸机,包括大型的战舰,那么它都是一个工业化的这样的一个产品。如果说在设计的层面,没有进行优化,或者说外观看起来很怪异,它在使用的过程中,肯定会有一些功能层面的限制。所以有的时候,颜值确实代表一定程度的作战能力。而且现在我们也注意到,武器装备它的外形和它的颜值,可能也会影响到它的外销和海外防务市场上的订单。这种漂亮的武器装备,看起来很顺眼的武器装备,在进行表演的过程当中,在进行展示的过程当中,也容易吸引人,可能也适合拿到更多的订单。所以我认为颜值比较重要,但是并不是说所有的武器装备都是把颜值作为一个首要考虑的标准。
|
||||
|
||||
|
||||
## 【主持人8】
|
||||
|
||||
虽然说颜值,并不是衡量一款武器装备强弱的核心标准,但是,令人大跌眼镜的外观设计,往往成为公众认知的第一道门槛。X-32验证机,那夸张的下颌式进气道,与臃肿的机身轮廓,预警机和军舰上那颗硕大突兀的圆球,无一不是功能优先逻辑下的无奈选择。虽然外观看起来不太聪明,但它们为各自赛道的后续装备设计,确立了一定的技术范式与审美基准。所以说,人不可貌相,装备亦是如此。感谢您持续关注,国防军事频道《军事科技》,我们下周同一时间,再见。
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 814 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 351 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
@@ -0,0 +1,313 @@
|
||||
# Prompt 1: 题材分类判别 v0.3
|
||||
|
||||
## SYSTEM
|
||||
|
||||
你是央视《军事科技》栏目的内容分析助手。你的任务是判断一期节目的题材分类,输出 4 个字段。
|
||||
|
||||
### 一、4 个字段定义
|
||||
|
||||
**字段 1: `program_format` 主题材(单选)** — 节目内容切口,不是播出形态
|
||||
|
||||
| 枚举值 | 含义 | 判定要领 |
|
||||
|--------|------|----------|
|
||||
| `装备深解` | 围绕一款/一型具体装备(或一个功能体系)深度剖析 | 全片聚焦同一款装备或同一个功能体系(如防空体系、航母编队、训练舰体系),从多个方面(性能/历史/技术/战例)拆解它 |
|
||||
| `横切类比` | 用某**非技术维度**的切口跨装备类比 | 全片用一个主题角度(颜值/尺寸/命名/价格/国别…)串联不同装备,换一批装备、保留同一切口,节目结构仍然成立 |
|
||||
| `历史纵深` | 沿时间线讲装备/战术演进 | 全片按时间先后推进(代际/年代/战争阶段),打乱时间顺序则逻辑崩塌 |
|
||||
| `前沿科技` | 新兴技术在军事装备上的应用 | 全片核心命题是"某项新兴技术如何改变军事装备",**技术本身是主角**,各装备案例只是技术的应用实例 |
|
||||
| `事件战例` | 围绕某次行动、战役、冲突 | 全片围绕一场或一组具体军事行动展开,战役本身是叙事主轴 |
|
||||
| `人物牵引` | 用人物作叙事入口和牵引线 | 用人物生平/对决/成就作为组织全片的骨架,去掉人物则节目不成立 |
|
||||
|
||||
**字段 2: `equipment_domain` 装备域(多选,至少 1 项)**
|
||||
|
||||
`陆战装备` / `海军舰船` / `航空航天` / `导弹与反导` / `信息化` / `后勤保障` / `轻武器` / `跨域` / `其他`
|
||||
|
||||
- `轻武器` = 手枪/步枪/冲锋枪/机枪/狙击步枪/单兵火箭筒等个人携行武器
|
||||
- `导弹与反导` = 弹道导弹/巡航导弹/防空导弹/反舰导弹/反导拦截系统
|
||||
- `信息化` = 雷达/电子战/C4ISR/数据链/网络作战
|
||||
|
||||
**⚠ 显著篇幅门槛(严格执行)**:只选**节目真正花篇幅展开讨论**的装备域。对每个候选域做三问检验:
|
||||
1. 该装备域是否有**专门段落**(≥2 段)独立展开?
|
||||
2. 如果删掉这部分内容,节目是否有**明显缺失**?
|
||||
3. 还是只在另一个主装备域的讨论中**顺带提及**?
|
||||
|
||||
前两问答"是" → 标注。第三问答"是" → **不标**。
|
||||
|
||||
**⚠ 从属零件不独立标域** — 以下情况**不要**额外标域:
|
||||
- 防空节目提到预警雷达/指挥系统 → **不标**信息化(雷达是防空体系的组成部分)
|
||||
- 航母节目提到舰载机 → **不标**航空航天(舰载机是航母战力的组成部分)
|
||||
- 空战史提到空空导弹 → **不标**导弹与反导(导弹是空战的武器手段)
|
||||
- 舰艇节目提到舰载无人机 → **不标**航空航天(无人机是舰艇作战体系的附属)
|
||||
- 前沿科技节目中某段介绍一款激光/拦截武器作为 AI 应用案例 → **不标**导弹与反导(该武器是技术应用的示例,不是独立的装备域主题)
|
||||
|
||||
**⚠ `跨域` 的用法**:当节目涉及 **3 个及以上**具体装备域且各域都有显著篇幅时,在标注各具体域的同时**额外加选** `跨域`。`跨域` 是一个分类收纳标签——表示"这期节目天上、水里、地上都讲了,没法归到单一装备域"。不需要节目主题是"跨域协同作战"才标。
|
||||
|
||||
**字段 3: `scene_tags` 场景标签(多选,可空)** — 回答"为什么这周播"
|
||||
|
||||
| 枚举值 | 含义 | 文本证据示例 |
|
||||
|--------|------|-------------|
|
||||
| `节点策划` | 纪念日/周年/节庆/装备节点 | "今天是农历大年初一""值此人民海军成立77周年" |
|
||||
| `热点跟进` | 突发新闻/近期军事事件 | "近期,美以两国协同向伊朗发起军事打击" |
|
||||
| `装备首发` | 新装备首次公开亮相,**且该首发事件是本期节目的策划动因** | "2026年3月,DARPA正式公布了…X-76" |
|
||||
| `展会` | 军事展会/航展/防务展 | "在本届珠海航展上" |
|
||||
|
||||
**判定规则**:只有在文稿中**找到明确文本证据**时才标注。没有证据则留空数组 `[]`。大部分节目没有场景标签,宁缺勿滥。
|
||||
|
||||
**⚠ `装备首发` 判定要点**:不是"节目讲了一款新装备"就标,而是要判断**该装备的公开亮相/发布事件是否构成本期节目的策划动因**。问自己:如果这款装备没有在近期首发/亮相,这期节目还会不会做?不会 → 标装备首发。
|
||||
|
||||
判定细则:
|
||||
- **直接做这款装备的节目**(如 X-76 专题)→ 查文稿中是否有"近期发布/公布/首飞/亮相"的时间锚点 → 有则标
|
||||
- **因某款新装备的发布而引发的延伸讨论**(如 F-47 首发带动讨论鸭翼布局历史)→ 即使节目主体不是讲这款装备本身,只要**策划动因**来自首发事件 → 也要标
|
||||
- **外国项目官宣/合同签订**不等于装备首发 → 造船厂宣布启动建造计划、签署采购合同等属于项目进展,不是装备公开亮相 → **不标**
|
||||
|
||||
**字段 4: `tech_tags` 技术线标签(多选,可空)** — 新质新域追踪
|
||||
|
||||
`无人作战` / `智能化`(AI/算法/智能决策)/ `有人无人协同`
|
||||
|
||||
**⚠ 显著篇幅门槛(严格执行)**:只有当该技术是节目的**核心议题之一**时才标注。做两问检验:
|
||||
1. 该技术是否有**专门段落**(≥2 段)集中讨论其原理、应用或影响?
|
||||
2. 删掉这部分内容,节目的核心论述是否受损?
|
||||
|
||||
两问答"是" → 标注。否则 → **不标**。
|
||||
|
||||
**⚠ 以下情况不标 tech_tags**:
|
||||
- 专家在结尾展望中一句话提到"未来将走向智能化/有人无人协同" → **不标**(展望 ≠ 讨论)
|
||||
- 装备的某个功能参数碰巧涉及无人模式 → **不标**(如 X-76 兼具有人/无人模式只是参数,不是节目主题)
|
||||
- 盘点式节目中无人机作为众多展品之一出现 → **不标**(如"玩具总动员"讲微型无人机只是诸多小装备中的一个)
|
||||
- 历史演进类节目在最后一章讲到无人机时代,顺带一句提到"有人无人协同是趋势" → 只标 `无人作战`,**不标** `有人无人协同`(无人机时代是独立段落展开的,有人无人协同只是附带提及)
|
||||
|
||||
**⚠ tech_tags 的正确标注对比**:
|
||||
- ep005「硅基大脑」:有人无人协同在文稿中有专门段落——暴风雨战斗机指挥 8 架僚机、格里芬装甲车地空协同——这些是实质讨论 → **标**
|
||||
- ep011「空战演进」:无人机部分有独立段落 → 标无人作战。但"有人无人协同"只在专家评述中一句话带过 → **不标**
|
||||
|
||||
### 二、关键边界(最容易判错的地方)
|
||||
|
||||
1. **装备深解 vs 横切类比 — 做"换装测试" + "从属关系测试"**
|
||||
|
||||
**换装测试**:如果把节目中的所有具体装备换成另一批完全不同的装备,但保留同一个主题角度,节目结构是否仍然成立?
|
||||
- 成立 → `横切类比`(装备是可替换的素材,主题切口才是骨架)
|
||||
- 不成立 → `装备深解`(装备本身才是骨架)
|
||||
|
||||
**同类体系测试(v0.3 更新)**:节目中出现的多种装备之间,是否存在**功能从属关系**或**同属一个窄类别**?
|
||||
- 是(如防空体系的远/中/近程层级;航母编队的舰/机/护航;同一用途的多艘训练舰;同一军种的多款驱逐舰)→ `装备深解`(全片在拆解一个体系或一个窄类别)
|
||||
- 否(如马年图鉴的各种"马"装备、颜值洼地的各种丑装备,彼此无功能关联、跨越多个不相关类别)→ `横切类比`
|
||||
|
||||
**关键**:判断"可不可以换装"时,要区分"换成同类别的其他型号"和"换成完全不同类别的装备"。如果只能换成同类(三艘训练舰只能换成另外三艘训练舰),说明类别本身是骨架 → `装备深解`。如果能换成完全不同类别的装备(马装备换成虎装备),说明主题切口才是骨架 → `横切类比`。
|
||||
|
||||
**典型陷阱**:
|
||||
- 「潜艇的仿生之路」虽然用"仿生"作为叙事线索,但全片只讲潜艇这一类装备的各个子系统(浮力/隐身/声呐/诱饵),换成"坦克的仿生之路"节目结构就不成立 → `装备深解`
|
||||
- 「防空反导大对决」按远程/中程/近程拦截层级展开,看似"三类装备",但它们共同构成一个防空反导体系,体系内各层级存在功能从属 → `装备深解`,不是横切类比
|
||||
- 「启程远航」介绍三艘训练舰各自的功能和训练故事,看似"三款装备并列",但三艘舰都属于"海军训练舰"这一窄类别、承担相同使命,换装只能换成另外三艘训练舰 → `装备深解`(在拆解训练舰这个类别),不是横切类比
|
||||
- 「舰证不凡」用"体形大/拳头硬/责任大"三个维度比较多艘不同的舰艇,各舰之间无功能从属关系,换一批舰艇同一框架仍可套用 → `横切类比`
|
||||
|
||||
2. **前沿科技 vs 横切类比 — 看串联各案例的角度本身是否就是一项新兴技术**
|
||||
|
||||
**核心判据**:串联全片各案例的那个"主题角度",它本身是不是一项新兴/前沿技术?
|
||||
- 是新兴技术(AI、无人化、仿生、隐身、智能化…)→ `前沿科技`
|
||||
- 是非技术维度(颜值、尺寸、命名、价格、国别、战绩…)→ `横切类比`
|
||||
|
||||
**关键区分**:横切类比的"切口"是**观察维度**(从什么角度看装备),前沿科技的"切口"是**被考察的技术本身**(技术是主角,装备是它的应用载体)。
|
||||
|
||||
**典型案例**:
|
||||
- 「硅基大脑」— 用"AI 如何改变武器装备"串联多个装备案例 → AI 本身就是新兴技术,技术是主角 → `前沿科技`
|
||||
- 「无人舰艇」— 考察"无人技术在海军的应用",各型无人艇是技术的载体 → `前沿科技`
|
||||
- 「射速决定论」— 用"射速"串联不同枪械 → 射速是观察维度,不是新兴技术 → `横切类比`
|
||||
- 「武器颜值洼地」— 用"外形丑"串联不同装备 → 颜值是观察维度 → `横切类比`
|
||||
|
||||
3. **横切类比 vs 历史纵深 — 看时间轴是骨架还是佐料**
|
||||
|
||||
文稿中出现历史背景描述很常见。关键看:**全片大章节之间**是否按时间先后排列、且必须按此顺序才讲得通。
|
||||
- 是 → `历史纵深`
|
||||
- 否(各章节有自己的历史背景但彼此不构成时间链)→ `横切类比`
|
||||
|
||||
4. **人物牵引 — 人物必须是组织骨架**
|
||||
|
||||
只是在某一段提到发明者或设计师不算。人物必须贯穿全片、作为叙事的组织轴线。
|
||||
判断方法:去掉人物的名字和生平,节目是否还能成立?能 → 不是人物牵引。
|
||||
|
||||
5. **scene_tags 宁缺勿滥,但装备首发不要漏**
|
||||
|
||||
不要因为节目提到"近年来""当今世界"就标热点跟进——必须有具体的新闻事件或时间节点。
|
||||
|
||||
**但注意**:如果节目的策划动因是某款新装备近期首发/亮相(如某新型战斗机首飞、某新概念飞行器正式公布),即使文稿中提及该事件的篇幅不大,也应标注 `装备首发`——因为判定依据是**策划动因**,不是**篇幅占比**。
|
||||
|
||||
### 三、强制判断流程(必须按这四步走)
|
||||
|
||||
**第一步:提取节目主题一句话概括**
|
||||
用一句话回答"这期节目在讲什么"。
|
||||
|
||||
**第二步:识别组织骨架**
|
||||
列出全片大章节(3-5 个),问"是什么把这些章节串在一起"——同一款装备(或同一体系)的不同侧面?同一主题角度下的不同装备?时间线?人物?一项新兴技术的不同应用?
|
||||
|
||||
**第三步:做鉴别测试**
|
||||
- **换装测试**:把所有具体装备换成另一批完全不同的装备,保留同一个主题角度,节目结构是否仍成立?
|
||||
- **同类体系测试**:节目中的多种装备之间是否存在功能从属关系或同属一个窄类别?
|
||||
- **技术主角测试**:串联全片的"角度"本身是不是一项新兴技术?
|
||||
|
||||
**第四步:逐字段判定(带门槛检验)**
|
||||
按 program_format → equipment_domain → scene_tags → tech_tags 顺序输出。**对每个多选字段的每一项,都要过一遍"三问检验"或"两问检验",不达标的项目剔除。**
|
||||
|
||||
### 四、7 个示例
|
||||
|
||||
**示例 1:枪械射速决定论(横切类比)**
|
||||
|
||||
主题概括:用"射速"这个技术切口,比较不同枪械类型如何提升射速
|
||||
大章节:手枪射速演进 / 步枪射速演进 / 机枪射速演进
|
||||
组织骨架:"射速"主题串联不同枪械
|
||||
换装测试:换成"火炮射速""导弹发射速率"也能用同一结构 → 成立 → 横切类比
|
||||
技术主角测试:射速是观察维度,不是新兴技术 → 横切类比(非前沿科技)
|
||||
```json
|
||||
{
|
||||
"program_format": "横切类比",
|
||||
"equipment_domain": ["轻武器"],
|
||||
"scene_tags": [],
|
||||
"tech_tags": []
|
||||
}
|
||||
```
|
||||
|
||||
**示例 2:"X"系列新成员:X-76 飞机(装备深解 + 装备首发)**
|
||||
|
||||
主题概括:深度剖析美军新型折叠旋翼机 X-76 的研发背景、核心技术与战场意义
|
||||
大章节:研发背景与需求 / 核心技术(折叠旋翼) / 性能参数对比 / 面临的设计挑战
|
||||
组织骨架:围绕 X-76 这一款装备展开
|
||||
换装测试:换成其他飞行器,所有技术细节和分析框架都不成立 → 不成立 → 装备深解
|
||||
场景证据:文稿提到"2026年3月,美国国防高级研究计划局正式公布" → 装备首发
|
||||
tech_tags 检验:X-76 兼具有人/无人模式,但这只是性能参数的一部分,没有专门段落讨论无人技术本身 → **不标** tech_tags
|
||||
```json
|
||||
{
|
||||
"program_format": "装备深解",
|
||||
"equipment_domain": ["航空航天"],
|
||||
"scene_tags": ["装备首发"],
|
||||
"tech_tags": []
|
||||
}
|
||||
```
|
||||
|
||||
**示例 3:武器进化论——空战格局的演进之路(历史纵深)**
|
||||
|
||||
主题概括:沿时间线讲述空战从螺旋桨时代到无人机时代的装备演进
|
||||
大章节:螺旋桨时代 / 喷气时代 / 导弹空战时代 / 隐身时代 / 无人机时代
|
||||
组织骨架:严格按技术代际的时间线推进,打乱顺序则因果链断裂
|
||||
换装测试:不适用——时间轴本身就是骨架
|
||||
equipment_domain 检验:全片以战斗机/轰炸机为主线 → 标航空航天。虽然提到空空导弹、电子战飞机,但它们是空战装备演进中的配角元素,不是独立展开的装备域 → **不标**导弹与反导、**不标**信息化
|
||||
tech_tags 检验:最后一章专门讨论无人机时代对空战的颠覆性影响,有独立段落展开 → 标无人作战。专家在结尾一句话提到"智能化趋势" → **不标**智能化(无专门段落)
|
||||
```json
|
||||
{
|
||||
"program_format": "历史纵深",
|
||||
"equipment_domain": ["航空航天"],
|
||||
"scene_tags": [],
|
||||
"tech_tags": ["无人作战"]
|
||||
}
|
||||
```
|
||||
|
||||
**示例 4:枪王对决——卡拉什尼科夫与尤金斯通纳的设计传奇(人物牵引)**
|
||||
|
||||
主题概括:通过两位枪械设计大师的生平与对决,讲述 AK-47 与 M16 背后的设计哲学之争
|
||||
组织骨架:两位人物的成长背景 / 各自的设计理念 / 两款武器的实战较量 / 历史评价
|
||||
换装测试:去掉卡拉什尼科夫和斯通纳的名字与生平,节目不成立 → 人物是骨架
|
||||
```json
|
||||
{
|
||||
"program_format": "人物牵引",
|
||||
"equipment_domain": ["轻武器"],
|
||||
"scene_tags": [],
|
||||
"tech_tags": []
|
||||
}
|
||||
```
|
||||
|
||||
**示例 5:马年装备图鉴(横切类比 + 节点策划)⚠️**
|
||||
|
||||
主题概括:马年新春,盘点以"马"命名的经典军事装备
|
||||
大章节:野马气垫登陆艇 / 运-5"马驹"运输机 / 其他"马"装备
|
||||
组织骨架:"与马相关的命名"主题串联不同装备
|
||||
换装测试:换成"虎年图鉴"→ 虎式坦克/黑虎直升机… 结构完全成立 → 横切类比
|
||||
技术主角测试:"马命名"是观察维度,不是技术 → 横切类比
|
||||
场景证据:文稿开头明确说"今天是农历大年初一""2026年是农历马年" → 节点策划
|
||||
equipment_domain 检验:海军舰船(野马气垫艇,有专门段落)✓、航空航天(运-5,有专门段落)✓、陆战装备(悍马车,有专门段落)✓。三个域均有独立段落展开 → 全标。涉及 ≥3 域 → 额外加 `跨域`
|
||||
```json
|
||||
{
|
||||
"program_format": "横切类比",
|
||||
"equipment_domain": ["海军舰船", "航空航天", "陆战装备", "跨域"],
|
||||
"scene_tags": ["节点策划"],
|
||||
"tech_tags": []
|
||||
}
|
||||
```
|
||||
|
||||
**示例 6:硅基大脑——AI 如何改变未来战场(前沿科技)⚠️ 易错**
|
||||
|
||||
主题概括:考察 AI 技术如何渗透进各类军事装备,改变未来作战模式
|
||||
大章节:AI 在空中作战的应用 / AI 在地面装备的应用 / AI 在指挥决策中的应用
|
||||
组织骨架:"AI 技术"串联不同装备案例
|
||||
换装测试:换一批装备(把无人机换成潜艇、把坦克换成军舰),只要它们也在用 AI,节目结构仍成立 → 看似横切类比
|
||||
**技术主角测试**:串联全片的"AI"本身就是一项新兴技术 → **前沿科技**(不是横切类比!)
|
||||
equipment_domain 检验:航空航天(AI 空战有专门段落)✓、陆战装备(AI 地面有专门段落)✓ → 标这两个
|
||||
tech_tags 检验:智能化(AI 是全片主题,贯穿始终)✓。有人无人协同(有专门段落讨论人机协作模式)✓。无人作战(只是在 AI 空战段落里作为一个应用案例提了一下)→ **不标**
|
||||
```json
|
||||
{
|
||||
"program_format": "前沿科技",
|
||||
"equipment_domain": ["航空航天", "陆战装备"],
|
||||
"scene_tags": [],
|
||||
"tech_tags": ["智能化", "有人无人协同"]
|
||||
}
|
||||
```
|
||||
**这个示例教你最关键的一课**:当换装测试通过时,不要直接判横切类比——还要过"技术主角测试"。如果串联角度本身是新兴技术,答案是前沿科技。
|
||||
|
||||
**示例 7:防空反导大对决(装备深解 + 热点跟进)⚠️ 易错**
|
||||
|
||||
主题概括:剖析现代防空反导体系的远程/中程/近程三层拦截架构
|
||||
大章节:远程预警与拦截 / 中程区域防空 / 近程末端防御 / 实战案例
|
||||
组织骨架:围绕同一个防空反导体系的不同层级展开
|
||||
换装测试:换成其他防空系统?——远/中/近三层是同一体系内的功能分层,不是三类无关装备,换掉一层则体系不完整 → 不成立
|
||||
**从属关系测试**:远程、中程、近程拦截互为补充,共同构成一个完整的防空反导体系 → 存在功能从属 → `装备深解`
|
||||
场景证据:文稿提到近期伊朗遭受的防空作战事件 → 热点跟进
|
||||
equipment_domain 检验:导弹与反导(全片主题)✓。虽然提到预警雷达和指挥系统,但那是防空体系的组成部分 → **不标**信息化
|
||||
```json
|
||||
{
|
||||
"program_format": "装备深解",
|
||||
"equipment_domain": ["导弹与反导"],
|
||||
"scene_tags": ["热点跟进"],
|
||||
"tech_tags": []
|
||||
}
|
||||
```
|
||||
**这个示例教你**:当节目中出现多种装备但它们同属一个功能体系的不同层级时,判装备深解而非横切类比。
|
||||
|
||||
### 五、输出格式
|
||||
|
||||
只输出一个 JSON,严格按以下 schema:
|
||||
|
||||
```json
|
||||
{
|
||||
"program_format": "枚举值之一",
|
||||
"equipment_domain": ["至少一项"],
|
||||
"scene_tags": [],
|
||||
"tech_tags": [],
|
||||
"format_reasoning": {
|
||||
"one_sentence_summary": "这期节目在讲什么,一句话",
|
||||
"chapter_list": ["大章节1", "大章节2", "大章节3"],
|
||||
"backbone": "什么把这些章节串在一起",
|
||||
"swap_test": "换装测试结论,一句话",
|
||||
"same_system_test": "同类体系测试:各装备之间有无功能从属或同属窄类别,一句话",
|
||||
"tech_protagonist_test": "技术主角测试:串联角度本身是否为新兴技术,一句话"
|
||||
},
|
||||
"domain_evidence": "装备域判定依据,列出关键装备名称。对每个标注的域说明其独立段落证据;对考虑过但未标的域说明排除理由",
|
||||
"scene_evidence": "场景标签的文本证据原文引用(如无则 null)",
|
||||
"tech_evidence": "技术标签的证据:标注项列出段落证据,排除项说明理由(如无候选则 null)",
|
||||
"confidence": "高 或 中 或 低",
|
||||
"confidence_reason": "为什么这个置信度,1 句话"
|
||||
}
|
||||
```
|
||||
|
||||
### 六、置信度自评标准
|
||||
|
||||
- 高:program_format 判定明确,三项测试结论一致,equipment_domain 和 tech_tags 每项都有清晰的段落级证据
|
||||
- 中:program_format 在两个选项间犹豫,或 equipment_domain/tech_tags 有 1-2 项在门槛边界上
|
||||
- 低:无法确定 program_format,或文稿信息严重不全
|
||||
|
||||
## USER
|
||||
|
||||
请判断以下文稿的题材分类:
|
||||
|
||||
```
|
||||
{transcript}
|
||||
```
|
||||
|
||||
【重要:输出约束】
|
||||
你的回复必须是且只是一个合法的 JSON 对象,不要有任何前置说明、后置说明、markdown 代码块包裹(不要 ```json 标记)、思考过程描述。
|
||||
直接输出 { 开头,} 结尾的纯 JSON。
|
||||
@@ -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。
|
||||
@@ -0,0 +1,201 @@
|
||||
# Prompt 3: 开篇钩子强度判别 v0.3
|
||||
|
||||
## SYSTEM
|
||||
|
||||
你是央视《军事科技》栏目的内容分析助手。你的任务是判断一期节目的**开篇钩子强度**——即节目前 1-2 分钟对观众的抓取力。
|
||||
|
||||
**阅读范围**:文稿的**前三个段落**——通常是 `【导视】` + `【主持人1】` + **紧跟的第一个 `【解说】` 或 `【动画解说】`**。有些节目的钩子力量在主持人段之后才爆发(如主持人铺垫后,解说段抛出真实战事或战场故事),因此必须看到第三段才能准确判定。如果第三段已经进入正文技术细节,则钩子判定只基于前两段。
|
||||
|
||||
### 一、三档定义
|
||||
|
||||
开篇钩子决定的是"观众会不会换台"。电视观众在前 1-2 分钟内决定去留,钩子的作用是制造"必须继续看"的理由。
|
||||
|
||||
**强 (`强`)** — 前 1-2 分钟就制造出强烈的"不能换台"理由
|
||||
|
||||
以下任一路径均可独立触发"强"(不需要叠加多条):
|
||||
|
||||
| 路径 | 判据 | 典型文本信号 |
|
||||
|------|------|-------------|
|
||||
| **新闻现场感** | 开篇直接抛出一个正在发生或刚刚发生的真实军事事件/冲突,让观众觉得"这是新闻,错过就没了" | 具体日期 + 具体国家/战场("2026年2月28日,美以两国协同向伊朗发起军事打击") |
|
||||
| **画面震撼 + 悬念** | 用密集的视觉化语言制造临场感,同时抛出一个未解的核心问题 | 爆炸/追击/深海/黑暗 + 设问句("它们是如何被精确拦截的?") |
|
||||
| **反差/颠覆** | 开篇抛出一个违反直觉的命题,制造认知冲突 | "平凡装备逆袭制霸战局""最酷的未投入使用枪支" |
|
||||
| **具体战场故事** | 直接进入一个有时间、地点、人物/困境的战场叙事 | "1964年12月,越南南部。一个美军营地在夜色中遭到包围" |
|
||||
| **节日/文化共鸣** | 利用节日气氛制造仪式感和"此刻独有"的观看理由,并将节日元素与装备巧妙关联 | "今天是农历大年初一""2026年是农历马年" + 马装备盘点 |
|
||||
| **自然/生存意象** | 用生物世界的生死竞争意象类比军事装备,制造原始紧张感 | "深海是最残酷的竞技场""生存法则只有两条:隐藏自己,发现敌人" |
|
||||
| **零门槛切口** | 开篇用日常生活人人都懂的概念(形状、美丑、大小、速度快慢等)作为军事科技话题的入口,瞬间消除专业门槛,让观众觉得"这期我看得懂" | 以形状/颜值/生活常识为切口进入装备分析("武器装备里的形状规律""武器装备的颜值洼地") |
|
||||
|
||||
**⚠ 重要:紧迫感不是判"强"的唯一门槛。** 以下效果中任何一种足够强烈,即可判强:
|
||||
- 时间紧迫感(新闻/正在发生)
|
||||
- 情感冲击力(画面震撼/生死意象/节日共鸣)
|
||||
- 认知冲突(反差/颠覆常识)
|
||||
- 叙事吸引力(战场故事让人想知道"后来呢")
|
||||
- 零门槛亲近感(用日常概念消除军事科技专业壁垒,"这期我看得懂")
|
||||
|
||||
**中 (`中`)** — 有一定吸引力,但缺少"立刻抓住"的力量
|
||||
|
||||
典型特征:
|
||||
- **背景铺陈**:先讲一段大背景、概述性介绍,再引入具体内容("在瞬息万变的战场上……")
|
||||
- **装备亮相**:主持人手持/介绍一款装备的外观特征,制造了一定好奇心但没有情感冲击
|
||||
- **历史溯源**:从某个历史背景缓缓展开("提到潜艇,想必大家并不陌生")
|
||||
- **概念式引入**:先抛出一个有趣但不紧迫的主题角度("有一个看似基础却至关重要的设计——长度")
|
||||
- **常规装备发布**:引用了某款装备发布的新闻,但只是作为引子带入常规介绍,没有制造紧张感或认知冲突
|
||||
|
||||
中的核心特征:**观众觉得"挺有意思",但可以先去倒杯水再回来看也没关系**。
|
||||
|
||||
**弱 (`弱`)** — 平铺直叙,观众没有"必须留下"的理由
|
||||
|
||||
典型特征:
|
||||
- **直接讲技术概念**:开篇就进入技术参数、定义、指标的介绍,没有故事、没有画面冲击、没有悬念
|
||||
- **课堂式开场**:像上课一样介绍要讲的知识点,主持人直接进入"首先我们来看……"
|
||||
- **信息性提问**:导视提出的问题是"哪些枪最快""这个指标是什么"这类百科式问答,不构成悬念
|
||||
|
||||
弱的核心特征:**观众从一开始就知道这是一堂技术课,看不看都行**。
|
||||
|
||||
### 二、关键边界
|
||||
|
||||
1. **强 vs 中的分界线:多路径判断,不只看紧迫感**
|
||||
|
||||
不要把"有无紧迫感"当成判强的唯一标准。问自己:**看完开篇三段后,作为一个普通电视观众,你是什么感受?**
|
||||
- "天呐,这在打仗/这个太颠覆了/后来怎么样了/这个节日好特别" → **强**
|
||||
- "嗯挺有意思,但也不急" → **中**
|
||||
- "哦,今天讲这个啊" → **弱**
|
||||
|
||||
2. **中 vs 弱的分界线:悬念式提问 vs 信息性提问**
|
||||
|
||||
导视几乎都会提问,但提问有质量之分:
|
||||
- **悬念式提问**(想知道答案,有探索欲):"为何长度取舍各不相同?""它们是如何被精确拦截的?" → 至少是**中**
|
||||
- **信息性提问**(像百科搜索,无探索欲):"射速最快的枪械都是哪些?""这个指标有什么意义?" → 倾向**弱**
|
||||
|
||||
区分标准:如果这个问题可以用一句话或一个列表回答,它就是信息性提问;如果回答需要讲一个故事或揭示一个原理,它就是悬念式提问。
|
||||
|
||||
3. **不要被导视的花哨语言迷惑**
|
||||
|
||||
导视(预告片)天然会用吸引人的语言,这是它的本职工作。判断钩子强度时,要看**导视 + 主持人 + 首段解说的整体效果**——三段合在一起是什么感受。
|
||||
|
||||
特别注意:有些节目的钩子在主持人段之后才引爆(主持人铺垫 → 解说段抛出真实战事/战场故事)。不要因为主持人段偏平就急着判中——看完第三段再定。
|
||||
|
||||
4. **常规装备发布 ≠ 新闻现场感**
|
||||
|
||||
"某国发布了一款新装备" 只是一个引子,如果后续直接进入该装备的常规技术介绍,属于**中**(装备亮相),不是**强**。只有当装备发布本身构成了震撼性新闻(如改变地缘格局、颠覆技术认知),或开篇围绕发布事件制造了悬念/冲突/紧迫感时,才算**强**。
|
||||
|
||||
### 三、强制判断流程
|
||||
|
||||
**第一步:定位开篇段落**
|
||||
找到文稿中 `【导视】`(或 `【导视1】`)、`【主持人1】`(或 `【演播室主持人1】`)、以及紧跟的第一个 `【解说1】`(或 `【二维动画解说1】`)。这三段合在一起是前 1-2 分钟的内容。
|
||||
|
||||
**第二步:提取钩子元素**
|
||||
在这三个段落中,找出所有可能构成钩子的元素:新闻事件?画面震撼?悬念问题?反差命题?战场故事?节日仪式?自然意象?装备亮相?技术概念?
|
||||
|
||||
**第三步:做"换台测试"**
|
||||
想象你是一个普通电视观众(不是军迷),刚打开电视看到这个开头。你的第一反应是:
|
||||
- "天呐!"(震撼/紧迫/颠覆/被故事抓住)→ **强**
|
||||
- "嗯有点意思"(好奇但不急)→ **中**
|
||||
- "哦,今天讲这个"(无感)→ **弱**
|
||||
|
||||
### 四、6 个示例
|
||||
|
||||
**示例 1:防空反导大对决(强 — 画面震撼 + 新闻现场感)**
|
||||
|
||||
导视:"导弹呼啸而过,战机低空穿梭,无人机如蜂群般袭来……它们是如何被精确拦截的?"
|
||||
主持人:介绍防空系统从二战到现代的演变框架
|
||||
解说1:"2026年2月28日,美以两国协同向伊朗发起军事打击,中东地区顿时战火纷飞"
|
||||
钩子元素:导视的画面震撼语言 + 解说1抛出的真实新闻事件(具体日期+国家+战事)
|
||||
换台测试:观众看到密集战场画面+真实战事 → "天呐,这是真打起来了" → **强**
|
||||
注意:虽然主持人段偏背景铺陈,但解说1紧接着引爆了新闻现场感,三段整体效果是强
|
||||
```json
|
||||
{ "opening_hook": "强" }
|
||||
```
|
||||
|
||||
**示例 2:潜艇的仿生之路(强 — 自然/生存意象)**
|
||||
|
||||
导视:"在浩瀚海洋深处,数亿年的进化,塑造了无数神奇的攻防利器……当潜艇披上鲨鱼皮消声瓦,当声呐模仿海豚看穿黑暗,当乌贼的墨汁化作潜艇的电子替身"
|
||||
主持人:"深海就是地球上最残酷的竞技场……生存的法则只有两条:隐藏自己,发现敌人"
|
||||
钩子元素:极具画面感的自然意象(鲨鱼皮、海豚、乌贼墨汁) + 生死竞争意象("最残酷的竞技场""生存法则") + 悬念(如何把生物本领变成潜艇技术?)
|
||||
换台测试:观众被深海生死意象抓住 → "好酷,想知道潜艇怎么学的" → **强**
|
||||
```json
|
||||
{ "opening_hook": "强" }
|
||||
```
|
||||
|
||||
**示例 3:逆袭战局的集成武器(强 — 反差 + 具体战场故事)**
|
||||
|
||||
导视:"真正改变战斗的,并不一定是预先设计好的完美武器……当运输直升机挂上机枪,化身空中火力平台"
|
||||
主持人:"我们总是习惯将目光投向那些耀眼的装备……很多时候,真正改变战场的,不是全新的科技,而是两件原本普通的装备,在某个绝境的时刻,被迫结合在一起"
|
||||
二维动画解说1:"1964年12月,越南南部。一个美军营地在夜色中遭到包围……空中却只剩下一架老式运输机"
|
||||
钩子元素:反差命题(平凡装备 vs 制霸战局) + 具体战场叙事(1964年越南,被包围的营地,只剩一架运输机)
|
||||
换台测试:反差抓住好奇心 + 战场故事抓住情感 → "只剩一架运输机?后来怎么了?" → **强**
|
||||
注意:主持人段是铺垫,动画解说1才是钩子引爆点——必须看到第三段
|
||||
```json
|
||||
{ "opening_hook": "强" }
|
||||
```
|
||||
|
||||
**示例 4:马年装备图鉴(强 — 节日/文化共鸣)**
|
||||
|
||||
导视:"金戈铁马,龙马精神。2026年,是农历马年。那么在武器装备领域中,有哪些与马相关,或以马命名的经典装备?"
|
||||
主持人:"各位观众你们好……今天是农历大年初一,万家灯火,共庆新春。……2026年是农历马年,马象征着力量、速度与进取。本期节目,我们就来回顾一下,那些以马命名的武器装备"
|
||||
钩子元素:节日仪式(大年初一+马年) + 文化联想(马=力量/速度 → 马装备) + "此刻独有"的观看理由
|
||||
换台测试:大年初一看马年特别节目 → "过年就该看点应景的" → **强**(节日/文化共鸣路径)
|
||||
```json
|
||||
{ "opening_hook": "强" }
|
||||
```
|
||||
**这个示例教你**:节日+文化关联制造的"此刻独有"感,和新闻现场感一样有效——都让观众觉得"错过就没了"。
|
||||
|
||||
**示例 5:枪械设计中的小尺寸大讲究(中 — 概念引入 + 装备亮相)**
|
||||
|
||||
导视:"藏于方寸的敌后利器,锋芒毕露的火力先锋……同为枪械家族成员,为何长度取舍各不相同?"
|
||||
主持人:"有一个看似基础,却至关重要的设计,始终左右着枪械的实战性能,它就是长度……首先出场的是,短到可以塞进火柴盒里的,美国FP-45解放者手枪。我手里的这款模型……"
|
||||
钩子元素:有趣的主题角度("长度"切口) + 装备亮相(实物模型) + 悬念式提问("为何取舍不同")
|
||||
换台测试:观众觉得"嗯有点意思"但不急 → **中**
|
||||
```json
|
||||
{ "opening_hook": "中" }
|
||||
```
|
||||
|
||||
**示例 6:枪械射速决定论(弱 — 直接讲技术概念)⚠️ 易误判为中**
|
||||
|
||||
导视:"非常重要的一个指标,就是射速。在众多类型的枪械中,射速最快的枪械都是哪些?"
|
||||
主持人:"在瞬息万变的战场上,火力的压制与覆盖,往往能决定战斗的走向。枪械射速的快慢,直接关系到,单位时间内,弹药投送的密度……" → 然后直接进入"首先我们来看看手枪"
|
||||
解说1:直接进入前膛枪/火绳枪的装填方式和射速数据
|
||||
钩子元素:技术概念铺陈(射速的定义和重要性)+ 信息性提问("射速最快的是哪些")
|
||||
换台测试:观众知道这是一堂射速科普课 → "哦,今天讲射速" → **弱**
|
||||
**注意**:导视的"射速最快的枪械都是哪些"看似是提问,但这是**信息性提问**——可以用一个排行榜回答,不构成悬念。不要因为有问号就判中。
|
||||
```json
|
||||
{ "opening_hook": "弱" }
|
||||
```
|
||||
|
||||
### 五、输出格式
|
||||
|
||||
只输出一个 JSON,严格按以下 schema:
|
||||
|
||||
```json
|
||||
{
|
||||
"opening_hook": "强 或 中 或 弱",
|
||||
"opening_segments": {
|
||||
"daoshi_summary": "导视段的一句话概括",
|
||||
"host_summary": "主持人段的一句话概括",
|
||||
"first_narration_summary": "首段解说的一句话概括(如已进入正文技术细节则填'已进入正文')",
|
||||
"overall_effect": "三段合在一起的整体效果,一句话"
|
||||
},
|
||||
"hook_elements": ["列出在开篇中找到的所有钩子元素"],
|
||||
"channel_switch_test": "做换台测试:普通观众看完这个开头的第一反应是什么?一句话",
|
||||
"reasoning": "判断理由,2-3 句话。必须说明走的是哪条路径(新闻/画面/反差/故事/节日/意象/概念/技术科普)",
|
||||
"confidence": "高 或 中 或 低",
|
||||
"confidence_reason": "为什么这个置信度,1 句话"
|
||||
}
|
||||
```
|
||||
|
||||
### 六、置信度自评标准
|
||||
|
||||
- 高:钩子元素清晰,强/中/弱归档明确
|
||||
- 中:在两档之间犹豫(如导视激烈但后续段落转平),或开篇段落边界不清
|
||||
- 低:文稿格式异常,无法确定哪部分是开篇
|
||||
|
||||
## USER
|
||||
|
||||
请判断以下文稿的开篇钩子强度(看导视 + 主持人1 + 紧跟的首段解说):
|
||||
|
||||
```
|
||||
{transcript}
|
||||
```
|
||||
|
||||
【重要:输出约束】
|
||||
你的回复必须是且只是一个合法的 JSON 对象,不要有任何前置说明、后置说明、markdown 代码块包裹(不要 ```json 标记)、思考过程描述。
|
||||
直接输出 { 开头,} 结尾的纯 JSON。
|
||||
@@ -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()
|
||||
@@ -0,0 +1,849 @@
|
||||
"""
|
||||
L3 分析引擎原型 — 第一层"纯算的"
|
||||
读 Excel 收视数据 + ground-truth AI 标签,计算 5 个分析模块,输出 HTML 可视化。
|
||||
|
||||
模块:
|
||||
1. 核心指标卡(年度均值、达标率、最高/最低、期数)
|
||||
2. 走势图(逐期份额 + 基础/摸高目标线 + 滚动3期均值)
|
||||
3. 阶段对比(Q1 vs Q2 均值、达标率变化)
|
||||
4. 题材对比(各 program_format 均值份额排名)
|
||||
5. 编导对比(各编导均值、期数、最高/最低)
|
||||
"""
|
||||
|
||||
import json
|
||||
import openpyxl
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
|
||||
# ── 配置 ──────────────────────────────────────────────
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
EXCEL_PATH = BASE_DIR / "example" / "2026收视update.xlsx"
|
||||
GT_PATH = BASE_DIR / "benchmark-set" / "ground-truth.json"
|
||||
OUTPUT_PATH = BASE_DIR / "output" / "l3_report.html"
|
||||
|
||||
TARGET_BASE = 0.6448
|
||||
TARGET_HIGH = 0.8989
|
||||
|
||||
# 三色判定(基于 audience_share vs yearly_targets)
|
||||
# 红=份额 > 摸高 = 优秀;蓝=基础 ≤ 份额 ≤ 摸高 = 达标;绿=份额 < 基础 = 待提升
|
||||
COLOR_EXCELLENT = "#c0584f" # 红
|
||||
COLOR_STANDARD = "#5b8db8" # 蓝
|
||||
COLOR_IMPROVE = "#7aa874" # 绿
|
||||
|
||||
# 题材配色(饼图用)
|
||||
FORMAT_COLORS = {
|
||||
"装备深解": "#5b8db8",
|
||||
"横切类比": "#c0584f",
|
||||
"历史纵深": "#e8a838",
|
||||
"前沿科技": "#7aa874",
|
||||
"事件战例": "#9b7eb8",
|
||||
"人物牵引": "#d4816b",
|
||||
}
|
||||
|
||||
|
||||
def rating_color(share):
|
||||
if share > TARGET_HIGH:
|
||||
return COLOR_EXCELLENT
|
||||
elif share >= TARGET_BASE:
|
||||
return COLOR_STANDARD
|
||||
else:
|
||||
return COLOR_IMPROVE
|
||||
|
||||
|
||||
def rating_label(share):
|
||||
if share > TARGET_HIGH:
|
||||
return "优秀"
|
||||
elif share >= TARGET_BASE:
|
||||
return "达标"
|
||||
else:
|
||||
return "待提升"
|
||||
|
||||
|
||||
# ── 1. 读 Excel ──────────────────────────────────────
|
||||
def read_excel():
|
||||
wb = openpyxl.load_workbook(EXCEL_PATH, data_only=True)
|
||||
ws = wb[wb.sheetnames[0]]
|
||||
|
||||
episodes = []
|
||||
for row in ws.iter_rows(min_row=4, values_only=True):
|
||||
date_str = row[1]
|
||||
period = row[2]
|
||||
title = row[3]
|
||||
editor = row[4]
|
||||
share = row[5]
|
||||
rating = row[6]
|
||||
|
||||
if date_str is None or period is None:
|
||||
continue
|
||||
|
||||
period_num = int("".join(c for c in str(period) if c.isdigit()))
|
||||
editor_clean = str(editor).replace(" ", "").strip() if editor else ""
|
||||
|
||||
if isinstance(date_str, str):
|
||||
date_str = date_str.strip().replace(" ", "-")
|
||||
air_date = datetime.strptime(date_str, "%Y-%m-%d")
|
||||
else:
|
||||
air_date = date_str
|
||||
|
||||
episodes.append({
|
||||
"period": period_num,
|
||||
"title": str(title).strip(),
|
||||
"editor": editor_clean,
|
||||
"air_date": air_date,
|
||||
"share": float(share) if share else 0,
|
||||
"rating": float(rating) if rating else 0,
|
||||
})
|
||||
|
||||
return sorted(episodes, key=lambda e: e["period"])
|
||||
|
||||
|
||||
# ── 2. 读 ground-truth + 匹配 ────────────────────────
|
||||
def read_gt_and_match(episodes):
|
||||
with open(GT_PATH, "r", encoding="utf-8") as f:
|
||||
gt = json.load(f)
|
||||
|
||||
gt_episodes = gt["episodes"]
|
||||
|
||||
# 用关键词匹配 GT episode → Excel episode
|
||||
# GT 标题是缩写,Excel 标题是全称
|
||||
TITLE_KEYWORDS = {
|
||||
"防空反导大对决": "防空",
|
||||
"潜艇仿生": "潜艇",
|
||||
"枪械小尺寸大讲究": "小尺寸",
|
||||
"枪王对决": "枪王",
|
||||
"硅基大脑": "硅基",
|
||||
"舰证不凡": "舰",
|
||||
"大国巨舰反差": "巨舰",
|
||||
"马年军事图鉴": "马年",
|
||||
"重金造神器": "重金",
|
||||
"射速决定论": "射速",
|
||||
"空战演进": "空战",
|
||||
"逆袭战局": "逆袭战局",
|
||||
"X-76飞行器": "X76",
|
||||
"启程远航": "启程",
|
||||
"舰炮暴力美学": "舰炮",
|
||||
"无人舰艇": "无人舰艇",
|
||||
"逆袭的鸭翼": "鸭翼",
|
||||
"机枪双雄": "机枪双雄",
|
||||
"玩具总动员": "玩具",
|
||||
"武器颜值洼地": "颜值",
|
||||
}
|
||||
|
||||
for gt_ep in gt_episodes:
|
||||
gt_title = gt_ep["title"]
|
||||
keyword = TITLE_KEYWORDS.get(gt_title, gt_title)
|
||||
matched = False
|
||||
for ep in episodes:
|
||||
if keyword in ep["title"]:
|
||||
ep["program_format"] = gt_ep.get("program_format")
|
||||
ep["equipment_domain"] = gt_ep.get("equipment_domain", [])
|
||||
ep["scene_tags"] = gt_ep.get("scene_tags", [])
|
||||
ep["tech_tags"] = gt_ep.get("tech_tags", [])
|
||||
ep["narrative_structure"] = gt_ep.get("narrative_structure")
|
||||
ep["opening_hook"] = gt_ep.get("opening_hook")
|
||||
matched = True
|
||||
break
|
||||
if not matched:
|
||||
print(f" ⚠ GT ep{gt_ep['ep']} '{gt_title}' 未匹配到 Excel 期次")
|
||||
|
||||
matched_count = sum(1 for e in episodes if "program_format" in e)
|
||||
print(f" GT 匹配结果:{matched_count}/{len(episodes)} 期有 AI 标签")
|
||||
return episodes
|
||||
|
||||
|
||||
# ── 3. 计算分析模块 ──────────────────────────────────
|
||||
def compute_analysis(episodes):
|
||||
shares = [e["share"] for e in episodes]
|
||||
n = len(shares)
|
||||
|
||||
# ── 模块 1: 核心指标卡 ──
|
||||
avg_share = sum(shares) / n
|
||||
max_ep = max(episodes, key=lambda e: e["share"])
|
||||
min_ep = min(episodes, key=lambda e: e["share"])
|
||||
above_base = sum(1 for s in shares if s >= TARGET_BASE)
|
||||
above_high = sum(1 for s in shares if s > TARGET_HIGH)
|
||||
|
||||
metrics = {
|
||||
"total_episodes": n,
|
||||
"avg_share": round(avg_share, 4),
|
||||
"avg_rating": round(avg_share / TARGET_BASE, 2),
|
||||
"base_completion": round(avg_share / TARGET_BASE * 100, 1),
|
||||
"high_completion": round(avg_share / TARGET_HIGH * 100, 1),
|
||||
"max_share": max_ep["share"],
|
||||
"max_title": max_ep["title"],
|
||||
"max_period": max_ep["period"],
|
||||
"min_share": min_ep["share"],
|
||||
"min_title": min_ep["title"],
|
||||
"min_period": min_ep["period"],
|
||||
"above_base_count": above_base,
|
||||
"above_base_pct": round(above_base / n * 100, 1),
|
||||
"above_high_count": above_high,
|
||||
"above_high_pct": round(above_high / n * 100, 1),
|
||||
}
|
||||
|
||||
# ── 模块 2: 走势图数据 ──
|
||||
trend = []
|
||||
for i, ep in enumerate(episodes):
|
||||
rolling_3 = None
|
||||
if i >= 2:
|
||||
rolling_3 = round(sum(shares[i-2:i+1]) / 3, 4)
|
||||
trend.append({
|
||||
"period": ep["period"],
|
||||
"title": ep["title"],
|
||||
"share": ep["share"],
|
||||
"rating": ep["rating"],
|
||||
"editor": ep["editor"],
|
||||
"air_date": ep["air_date"].strftime("%m-%d"),
|
||||
"rolling_3": rolling_3,
|
||||
"color": rating_color(ep["share"]),
|
||||
"label": rating_label(ep["share"]),
|
||||
})
|
||||
|
||||
# ── 模块 3: 阶段对比(按季度)──
|
||||
quarters = defaultdict(list)
|
||||
for ep in episodes:
|
||||
q = (ep["air_date"].month - 1) // 3 + 1
|
||||
quarters[f"Q{q}"].append(ep["share"])
|
||||
|
||||
stages = {}
|
||||
for q_name in sorted(quarters.keys()):
|
||||
q_shares = quarters[q_name]
|
||||
q_avg = sum(q_shares) / len(q_shares)
|
||||
q_above_base = sum(1 for s in q_shares if s >= TARGET_BASE)
|
||||
stages[q_name] = {
|
||||
"count": len(q_shares),
|
||||
"avg_share": round(q_avg, 4),
|
||||
"base_completion": round(q_avg / TARGET_BASE * 100, 1),
|
||||
"high_completion": round(q_avg / TARGET_HIGH * 100, 1),
|
||||
"above_base_count": q_above_base,
|
||||
"above_base_pct": round(q_above_base / len(q_shares) * 100, 1),
|
||||
}
|
||||
|
||||
# ── 模块 4: 题材对比 ──
|
||||
format_groups = defaultdict(list)
|
||||
for ep in episodes:
|
||||
fmt = ep.get("program_format")
|
||||
if fmt:
|
||||
format_groups[fmt].append(ep["share"])
|
||||
|
||||
topic_stats = {}
|
||||
for fmt, fmt_shares in format_groups.items():
|
||||
topic_stats[fmt] = {
|
||||
"count": len(fmt_shares),
|
||||
"avg_share": round(sum(fmt_shares) / len(fmt_shares), 4),
|
||||
"max_share": max(fmt_shares),
|
||||
"min_share": min(fmt_shares),
|
||||
}
|
||||
topic_stats = dict(sorted(topic_stats.items(), key=lambda x: x[1]["avg_share"], reverse=True))
|
||||
|
||||
# ── 模块 5: 编导对比 ──
|
||||
editor_groups = defaultdict(list)
|
||||
for ep in episodes:
|
||||
editor_groups[ep["editor"]].append(ep)
|
||||
|
||||
editor_stats = {}
|
||||
for editor, ed_eps in editor_groups.items():
|
||||
ed_shares = [e["share"] for e in ed_eps]
|
||||
ed_max = max(ed_eps, key=lambda e: e["share"])
|
||||
ed_min = min(ed_eps, key=lambda e: e["share"])
|
||||
editor_stats[editor] = {
|
||||
"count": len(ed_eps),
|
||||
"avg_share": round(sum(ed_shares) / len(ed_shares), 4),
|
||||
"max_share": ed_max["share"],
|
||||
"max_title": ed_max["title"],
|
||||
"min_share": ed_min["share"],
|
||||
"min_title": ed_min["title"],
|
||||
"above_base_count": sum(1 for s in ed_shares if s >= TARGET_BASE),
|
||||
"above_base_pct": round(sum(1 for s in ed_shares if s >= TARGET_BASE) / len(ed_shares) * 100, 1),
|
||||
}
|
||||
editor_stats = dict(sorted(editor_stats.items(), key=lambda x: x[1]["avg_share"], reverse=True))
|
||||
|
||||
return {
|
||||
"metrics": metrics,
|
||||
"trend": trend,
|
||||
"stages": stages,
|
||||
"topic_stats": topic_stats,
|
||||
"editor_stats": editor_stats,
|
||||
}
|
||||
|
||||
|
||||
# ── 4. 生成 HTML ─────────────────────────────────────
|
||||
def generate_html(data, episodes):
|
||||
metrics = data["metrics"]
|
||||
trend = data["trend"]
|
||||
stages = data["stages"]
|
||||
topic_stats = data["topic_stats"]
|
||||
editor_stats = data["editor_stats"]
|
||||
|
||||
# 走势图数据
|
||||
periods = [t["period"] for t in trend]
|
||||
titles = [f'第{t["period"]}期 {t["title"]}' for t in trend]
|
||||
shares = [t["share"] for t in trend]
|
||||
rolling3 = [t["rolling_3"] for t in trend]
|
||||
colors = [t["color"] for t in trend]
|
||||
editors = [t["editor"] for t in trend]
|
||||
labels = [t["label"] for t in trend]
|
||||
dates = [t["air_date"] for t in trend]
|
||||
|
||||
# 题材对比数据
|
||||
topic_names = list(topic_stats.keys())
|
||||
topic_avgs = [topic_stats[k]["avg_share"] for k in topic_names]
|
||||
topic_counts = [topic_stats[k]["count"] for k in topic_names]
|
||||
|
||||
# 题材饼图数据
|
||||
topic_pie_data = []
|
||||
topic_pie_colors = []
|
||||
for k in topic_names:
|
||||
color = FORMAT_COLORS.get(k, "#999")
|
||||
topic_pie_data.append({"name": k, "value": topic_stats[k]["count"]})
|
||||
topic_pie_colors.append(color)
|
||||
|
||||
# 编导对比数据
|
||||
editor_names = list(editor_stats.keys())
|
||||
editor_avgs = [editor_stats[k]["avg_share"] for k in editor_names]
|
||||
editor_counts = [editor_stats[k]["count"] for k in editor_names]
|
||||
|
||||
# 阶段对比数据
|
||||
stage_names = list(stages.keys())
|
||||
stage_avgs = [stages[k]["avg_share"] for k in stage_names]
|
||||
stage_base_pcts = [stages[k]["above_base_pct"] for k in stage_names]
|
||||
|
||||
# ── 摸高目标完成率卡片动态样式 ──
|
||||
hc = metrics["high_completion"]
|
||||
if hc >= 95:
|
||||
high_card_class = "metric-card metric-card-shimmer"
|
||||
high_card_value_style = "background: linear-gradient(90deg, #c0584f 40%, #f0a050 50%, #c0584f 60%); background-size: 200%; -webkit-background-clip: text; -webkit-text-fill-color: transparent; animation: shimmer 2s infinite;"
|
||||
elif hc >= 85:
|
||||
high_card_class = "metric-card"
|
||||
high_card_value_style = "color:#5b8db8"
|
||||
elif hc >= 80:
|
||||
high_card_class = "metric-card metric-card-pulse-warn"
|
||||
high_card_value_style = "color:#7aa874"
|
||||
else:
|
||||
high_card_class = "metric-card metric-card-pulse-danger"
|
||||
high_card_value_style = "color:#333"
|
||||
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>军事科技 2026 收视分析 — L3 原型</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
|
||||
<style>
|
||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||
body {{
|
||||
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
|
||||
background: linear-gradient(135deg, #f5f0e8 0%, #e8e4d8 30%, #f0ece2 60%, #ebe5d5 100%);
|
||||
min-height: 100vh;
|
||||
color: #333;
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: 32px 24px;
|
||||
}}
|
||||
h1 {{
|
||||
text-align: center;
|
||||
color: #4a6741;
|
||||
font-size: 22px;
|
||||
margin-bottom: 4px;
|
||||
}}
|
||||
.subtitle {{
|
||||
text-align: center;
|
||||
color: #888;
|
||||
font-size: 13px;
|
||||
margin-bottom: 24px;
|
||||
}}
|
||||
|
||||
/* 指标卡 */
|
||||
.metrics-grid {{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 28px;
|
||||
}}
|
||||
.metric-card {{
|
||||
background: rgba(255,255,255,0.55);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
border: 1px solid rgba(255,255,255,0.6);
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.06);
|
||||
}}
|
||||
.metric-card .value {{
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
}}
|
||||
.metric-card .desc {{
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}}
|
||||
.metric-card .sub {{
|
||||
font-size: 11px;
|
||||
color: #aaa;
|
||||
margin-top: 4px;
|
||||
}}
|
||||
|
||||
/* 图表容器 */
|
||||
.chart-section {{
|
||||
background: rgba(255,255,255,0.55);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
border: 1px solid rgba(255,255,255,0.6);
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.06);
|
||||
}}
|
||||
.chart-section h2 {{
|
||||
font-size: 15px;
|
||||
color: #4a6741;
|
||||
margin-bottom: 14px;
|
||||
padding-left: 8px;
|
||||
border-left: 3px solid #6b8e6b;
|
||||
}}
|
||||
.chart-section h3 {{
|
||||
margin: 0;
|
||||
}}
|
||||
.chart-section ul {{
|
||||
margin: 0;
|
||||
list-style-type: disc;
|
||||
}}
|
||||
.chart-container {{
|
||||
width: 100%;
|
||||
height: 380px;
|
||||
}}
|
||||
.chart-container-sm {{
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
}}
|
||||
|
||||
/* 双列布局 */
|
||||
.two-col {{
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
}}
|
||||
|
||||
/* 阶段对比表格 */
|
||||
.stage-table {{
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}}
|
||||
.stage-table th {{
|
||||
background: #f5f3eb;
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
}}
|
||||
.stage-table td {{
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}}
|
||||
|
||||
/* 图例 */
|
||||
.legend {{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 12px;
|
||||
}}
|
||||
.legend-item {{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}}
|
||||
.legend-dot {{
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}}
|
||||
|
||||
/* 摸高卡片动效 */
|
||||
@keyframes shimmer {{
|
||||
0% {{ background-position: -200% center; }}
|
||||
100% {{ background-position: 200% center; }}
|
||||
}}
|
||||
@keyframes pulse-danger {{
|
||||
0%, 100% {{ box-shadow: 0 0 8px rgba(180,30,30,0.3); }}
|
||||
50% {{ box-shadow: 0 0 20px rgba(180,30,30,0.6); }}
|
||||
}}
|
||||
@keyframes pulse-warn {{
|
||||
0%, 100% {{ opacity: 1; }}
|
||||
50% {{ opacity: 0.75; }}
|
||||
}}
|
||||
.metric-card-shimmer .value {{
|
||||
display: inline-block;
|
||||
}}
|
||||
.metric-card-pulse-danger {{
|
||||
border: 2px solid rgba(180,30,30,0.4);
|
||||
animation: pulse-danger 1.5s infinite;
|
||||
}}
|
||||
.metric-card-pulse-warn {{
|
||||
animation: pulse-warn 2s infinite;
|
||||
}}
|
||||
|
||||
.footer {{
|
||||
text-align: center;
|
||||
color: #bbb;
|
||||
font-size: 11px;
|
||||
margin-top: 20px;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>军事科技 2026 年度收视分析</h1>
|
||||
<p class="subtitle">数据范围:第1–25期 | 基础目标 {TARGET_BASE} | 摸高目标 {TARGET_HIGH} | 生成时间 {datetime.now().strftime("%Y-%m-%d %H:%M")}</p>
|
||||
|
||||
<div class="legend">
|
||||
<div class="legend-item"><div class="legend-dot" style="background:{COLOR_EXCELLENT}"></div>优秀(>摸高)</div>
|
||||
<div class="legend-item"><div class="legend-dot" style="background:{COLOR_STANDARD}"></div>达标(基础~摸高)</div>
|
||||
<div class="legend-item"><div class="legend-dot" style="background:{COLOR_IMPROVE}"></div>待提升(<基础)</div>
|
||||
</div>
|
||||
|
||||
<!-- 指标卡 -->
|
||||
<div class="metrics-grid">
|
||||
<div class="metric-card">
|
||||
<div class="value" style="color:{rating_color(metrics['avg_share'])}">{metrics['avg_share']}</div>
|
||||
<div class="desc">平均份额</div>
|
||||
<div class="sub">基础完成率 {metrics['base_completion']}%</div>
|
||||
</div>
|
||||
<div class="{high_card_class}">
|
||||
<div class="value" style="{high_card_value_style}">{metrics['high_completion']}%</div>
|
||||
<div class="desc">摸高目标完成率</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="value" style="color:{COLOR_EXCELLENT}">{metrics['max_share']}</div>
|
||||
<div class="desc">最高份额</div>
|
||||
<div class="sub">第{metrics['max_period']}期 {metrics['max_title'][:8]}…</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="value" style="color:{COLOR_IMPROVE}">{metrics['min_share']}</div>
|
||||
<div class="desc">最低份额</div>
|
||||
<div class="sub">第{metrics['min_period']}期 {metrics['min_title'][:8]}…</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="value">{metrics['above_base_count']}<span style="font-size:16px;color:#888">/{metrics['total_episodes']}</span></div>
|
||||
<div class="desc">达标期数</div>
|
||||
<div class="sub">达标率 {metrics['above_base_pct']}% | 优秀 {metrics['above_high_count']} 期</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 走势图 -->
|
||||
<div class="chart-section">
|
||||
<h2>收视走势</h2>
|
||||
<div id="trendChart" class="chart-container"></div>
|
||||
</div>
|
||||
|
||||
<!-- AI 诊断报告 -->
|
||||
<div class="chart-section">
|
||||
<h2>AI 诊断报告</h2>
|
||||
<div style="display:flex;gap:20px;">
|
||||
<div style="flex:1;padding:16px;background:rgba(192,88,79,0.06);border-radius:12px;border-left:3px solid #c0584f;">
|
||||
<h3 style="font-size:14px;color:#c0584f;margin-bottom:10px;">核心发现</h3>
|
||||
<ul style="font-size:13px;line-height:1.8;color:#444;padding-left:16px;">
|
||||
<li><b>摸高目标完成率 83.7%</b>,距达标差 16.3 个百分点。Q2 连续 4 期低于基础线(第16-19期),是全年最大失速段。</li>
|
||||
<li>收视<b>两极分化加剧</b>:最高 1.227(第23期)与最低 0.343(第22期)相差 3.6 倍,说明选题质量波动远大于制作水平波动。</li>
|
||||
<li><b>近 3 期强势反弹</b>(第23-25期均值 1.105),但需警惕"福建舰效应"消退后能否维持。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div style="flex:1;padding:16px;background:rgba(91,141,184,0.06);border-radius:12px;border-left:3px solid #5b8db8;">
|
||||
<h3 style="font-size:14px;color:#5b8db8;margin-bottom:10px;">病因与提振建议</h3>
|
||||
<ul style="font-size:13px;line-height:1.8;color:#444;padding-left:16px;">
|
||||
<li><b>轻武器/枪械题材连续拖累</b>:4 期枪械相关节目平均份额仅 0.573,远低于基础线。建议控制同类选题间隔至少 6 期。</li>
|
||||
<li><b>"横切类比"形态两极分化</b>:好的横切(马年图鉴 0.95、颜值洼地 1.227)收视极高,差的(重金造神器 0.52)极低。关键变量是"切口话题感"。</li>
|
||||
<li><b>建议增加"装备深解+热点跟进"组合</b>:防空网 0.875、X-76 0.873 证明该组合稳定高产。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<p style="font-size:11px;color:#bbb;margin-top:12px;text-align:right;">* 以上为 AI 模拟诊断草稿,仅供版面预览,非真实分析结论</p>
|
||||
</div>
|
||||
|
||||
<!-- 双列:阶段对比 + 题材对比 -->
|
||||
<div class="two-col">
|
||||
<div class="chart-section">
|
||||
<h2>季度对比</h2>
|
||||
<table class="stage-table">
|
||||
<tr><th>季度</th><th>期数</th><th>平均份额</th><th>基础完成率</th><th>达标率</th></tr>
|
||||
{''.join(f'<tr><td>{q}</td><td>{stages[q]["count"]}</td><td style="color:{rating_color(stages[q]["avg_share"])};font-weight:600">{stages[q]["avg_share"]}</td><td>{stages[q]["base_completion"]}%</td><td>{stages[q]["above_base_pct"]}%</td></tr>' for q in stage_names)}
|
||||
</table>
|
||||
<div id="stageChart" class="chart-container-sm" style="margin-top:12px"></div>
|
||||
</div>
|
||||
|
||||
<div class="chart-section">
|
||||
<h2>题材对比</h2>
|
||||
<div style="display:flex;gap:16px">
|
||||
<div id="topicPieChart" class="chart-container-sm" style="flex:1;height:280px"></div>
|
||||
<div id="topicBarChart" class="chart-container-sm" style="flex:1;height:280px"></div>
|
||||
</div>
|
||||
<div style="font-size:11px;color:#aaa;margin-top:8px;text-align:center">仅含已标注的 {sum(topic_counts)} 期(共 {metrics['total_episodes']} 期,{metrics['total_episodes'] - sum(topic_counts)} 期待标注)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编导对比 -->
|
||||
<div class="chart-section">
|
||||
<h2>编导对比</h2>
|
||||
<div id="editorChart" class="chart-container-sm"></div>
|
||||
</div>
|
||||
|
||||
<div class="footer">L3 分析引擎原型 v0.1 — TPS 工作台 | 数据草稿,仅供内部讨论</div>
|
||||
|
||||
<script>
|
||||
// ── 走势图 ──
|
||||
var trendTitles = {json.dumps(titles, ensure_ascii=False)};
|
||||
var trendChart = echarts.init(document.getElementById('trendChart'));
|
||||
trendChart.setOption({{
|
||||
tooltip: {{
|
||||
trigger: 'axis',
|
||||
formatter: function(params) {{
|
||||
var p = params[0].dataIndex;
|
||||
var editors = {json.dumps(editors, ensure_ascii=False)};
|
||||
var labels = {json.dumps(labels, ensure_ascii=False)};
|
||||
var dates = {json.dumps(dates, ensure_ascii=False)};
|
||||
var shares = {json.dumps(shares)};
|
||||
var s = '<b>' + trendTitles[p] + '</b><br/>';
|
||||
s += dates[p] + ' | ' + editors[p] + '<br/>';
|
||||
s += '份额:<b>' + shares[p] + '</b>(' + labels[p] + ')';
|
||||
if (params.length > 1 && params[1].value != null) {{
|
||||
s += '<br/>滚动3期均值:' + params[1].value;
|
||||
}}
|
||||
return s;
|
||||
}}
|
||||
}},
|
||||
grid: {{ left: 50, right: 30, top: 80, bottom: 40 }},
|
||||
xAxis: {{
|
||||
type: 'category',
|
||||
data: {json.dumps([f'{t["period"]}' for t in trend])},
|
||||
axisLabel: {{ fontSize: 11 }}
|
||||
}},
|
||||
yAxis: {{
|
||||
type: 'value',
|
||||
min: 0.2,
|
||||
max: 1.4,
|
||||
axisLabel: {{ fontSize: 11 }}
|
||||
}},
|
||||
series: [
|
||||
{{
|
||||
name: '收视份额',
|
||||
type: 'bar',
|
||||
data: {json.dumps([{"value": s, "itemStyle": {"color": c}} for s, c in zip(shares, colors)])},
|
||||
barWidth: '50%',
|
||||
label: {{
|
||||
show: true,
|
||||
position: 'top',
|
||||
rotate: 45,
|
||||
align: 'left',
|
||||
fontSize: 10,
|
||||
color: 'inherit',
|
||||
opacity: 0.7,
|
||||
formatter: function(params) {{
|
||||
var t = trendTitles[params.dataIndex];
|
||||
var name = t.replace(/^第\\d+期\\s*/, '');
|
||||
return name.length > 4 ? name.substring(0, 4) + '..' : name;
|
||||
}}
|
||||
}},
|
||||
}},
|
||||
{{
|
||||
name: '滚动3期均值',
|
||||
type: 'line',
|
||||
data: {json.dumps(rolling3)},
|
||||
smooth: true,
|
||||
lineStyle: {{ color: '#e8a838', width: 2 }},
|
||||
itemStyle: {{ color: '#e8a838' }},
|
||||
symbolSize: 4,
|
||||
}},
|
||||
{{
|
||||
name: '基础目标',
|
||||
type: 'line',
|
||||
data: {json.dumps([TARGET_BASE] * len(trend))},
|
||||
lineStyle: {{ color: '{COLOR_STANDARD}', type: 'dashed', width: 1.5 }},
|
||||
symbol: 'none',
|
||||
tooltip: {{ show: false }},
|
||||
}},
|
||||
{{
|
||||
name: '摸高目标',
|
||||
type: 'line',
|
||||
data: {json.dumps([TARGET_HIGH] * len(trend))},
|
||||
lineStyle: {{ color: '{COLOR_EXCELLENT}', type: 'dashed', width: 1.5 }},
|
||||
symbol: 'none',
|
||||
tooltip: {{ show: false }},
|
||||
}},
|
||||
]
|
||||
}});
|
||||
|
||||
// ── 阶段对比 ──
|
||||
var stageChart = echarts.init(document.getElementById('stageChart'));
|
||||
stageChart.setOption({{
|
||||
tooltip: {{ trigger: 'axis' }},
|
||||
grid: {{ left: 50, right: 30, top: 20, bottom: 30 }},
|
||||
xAxis: {{ type: 'category', data: {json.dumps(stage_names)} }},
|
||||
yAxis: {{ type: 'value', min: 0.4, max: 1.0 }},
|
||||
series: [
|
||||
{{
|
||||
type: 'bar',
|
||||
data: {json.dumps([{"value": v, "itemStyle": {"color": rating_color(v)}} for v in stage_avgs])},
|
||||
barWidth: '40%',
|
||||
label: {{ show: true, position: 'top', fontSize: 12, fontWeight: 'bold',
|
||||
formatter: function(p) {{ return p.value.toFixed(4); }} }}
|
||||
}},
|
||||
{{
|
||||
type: 'line',
|
||||
data: {json.dumps([TARGET_BASE] * len(stage_names))},
|
||||
lineStyle: {{ color: '{COLOR_STANDARD}', type: 'dashed', width: 1 }},
|
||||
symbol: 'none',
|
||||
}},
|
||||
{{
|
||||
type: 'line',
|
||||
data: {json.dumps([TARGET_HIGH] * len(stage_names))},
|
||||
lineStyle: {{ color: '{COLOR_EXCELLENT}', type: 'dashed', width: 1 }},
|
||||
symbol: 'none',
|
||||
}}
|
||||
]
|
||||
}});
|
||||
|
||||
// ── 题材对比 - 饼图(期数占比)──
|
||||
var topicPieChart = echarts.init(document.getElementById('topicPieChart'));
|
||||
topicPieChart.setOption({{
|
||||
tooltip: {{
|
||||
trigger: 'item',
|
||||
formatter: function(params) {{
|
||||
return params.name + '<br/>期数:' + params.value + ' 期(' + params.percent + '%)';
|
||||
}}
|
||||
}},
|
||||
series: [{{
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['50%', '55%'],
|
||||
data: {json.dumps(topic_pie_data, ensure_ascii=False)},
|
||||
color: {json.dumps(topic_pie_colors)},
|
||||
label: {{
|
||||
fontSize: 11,
|
||||
formatter: '{{b}}\\n{{c}}期 ({{d}}%)'
|
||||
}},
|
||||
emphasis: {{
|
||||
label: {{ fontSize: 14, fontWeight: 'bold' }}
|
||||
}}
|
||||
}}]
|
||||
}});
|
||||
|
||||
// ── 题材对比 - 柱图(平均份额排名)──
|
||||
var topicBarChart = echarts.init(document.getElementById('topicBarChart'));
|
||||
var formatColors = {json.dumps(FORMAT_COLORS, ensure_ascii=False)};
|
||||
var topicColorData = {json.dumps([{"value": v, "itemStyle": {"color": FORMAT_COLORS.get(k, "#999")}} for k, v in zip(reversed(topic_names), reversed(topic_avgs))])};
|
||||
topicBarChart.setOption({{
|
||||
tooltip: {{
|
||||
trigger: 'axis',
|
||||
formatter: function(params) {{
|
||||
var counts = {json.dumps(topic_counts)};
|
||||
return params[0].name + '<br/>平均份额:' + params[0].value + '<br/>期数:' + counts[params[0].dataIndex];
|
||||
}}
|
||||
}},
|
||||
grid: {{ left: 80, right: 30, top: 10, bottom: 30 }},
|
||||
xAxis: {{ type: 'value', min: 0.4, max: 1.0 }},
|
||||
yAxis: {{
|
||||
type: 'category',
|
||||
data: {json.dumps(list(reversed(topic_names)), ensure_ascii=False)},
|
||||
axisLabel: {{ fontSize: 12 }}
|
||||
}},
|
||||
series: [{{
|
||||
type: 'bar',
|
||||
data: topicColorData,
|
||||
barWidth: '50%',
|
||||
label: {{ show: true, position: 'right', fontSize: 11,
|
||||
formatter: function(p) {{ return p.value.toFixed(3); }} }}
|
||||
}}]
|
||||
}});
|
||||
|
||||
// ── 编导对比 ──
|
||||
var editorChart = echarts.init(document.getElementById('editorChart'));
|
||||
var editorAvgs = {json.dumps(editor_avgs)};
|
||||
var editorCounts = {json.dumps(editor_counts)};
|
||||
var editorDetails = {json.dumps({k: v for k, v in editor_stats.items()}, ensure_ascii=False)};
|
||||
editorChart.setOption({{
|
||||
tooltip: {{
|
||||
trigger: 'axis',
|
||||
formatter: function(params) {{
|
||||
var name = params[0].name;
|
||||
var d = editorDetails[name];
|
||||
var s = '<b>' + name + '</b>(' + d.count + '期)<br/>';
|
||||
s += '平均份额:<b>' + d.avg_share + '</b><br/>';
|
||||
s += '最高:' + d.max_share + '(' + d.max_title.substring(0,10) + ')<br/>';
|
||||
s += '最低:' + d.min_share + '(' + d.min_title.substring(0,10) + ')<br/>';
|
||||
s += '达标率:' + d.above_base_pct + '%';
|
||||
return s;
|
||||
}}
|
||||
}},
|
||||
grid: {{ left: 70, right: 30, top: 10, bottom: 30 }},
|
||||
xAxis: {{ type: 'value', min: 0.4, max: 1.0 }},
|
||||
yAxis: {{
|
||||
type: 'category',
|
||||
data: {json.dumps(list(reversed(editor_names)), ensure_ascii=False)},
|
||||
axisLabel: {{ fontSize: 13 }}
|
||||
}},
|
||||
series: [{{
|
||||
type: 'bar',
|
||||
data: {json.dumps([{"value": v, "itemStyle": {"color": rating_color(v)}} for v in reversed(editor_avgs)])},
|
||||
barWidth: '40%',
|
||||
label: {{ show: true, position: 'right', fontSize: 12, fontWeight: 'bold',
|
||||
formatter: function(p) {{ return p.value.toFixed(4); }} }}
|
||||
}},
|
||||
{{
|
||||
type: 'line',
|
||||
data: {json.dumps([TARGET_BASE] * len(editor_names))},
|
||||
lineStyle: {{ color: '{COLOR_STANDARD}', type: 'dashed', width: 1 }},
|
||||
symbol: 'none',
|
||||
}}]
|
||||
}});
|
||||
|
||||
// 响应窗口变化
|
||||
window.addEventListener('resize', function() {{
|
||||
trendChart.resize();
|
||||
stageChart.resize();
|
||||
topicPieChart.resize();
|
||||
topicBarChart.resize();
|
||||
editorChart.resize();
|
||||
}});
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
return html
|
||||
|
||||
|
||||
# ── 主流程 ────────────────────────────────────────────
|
||||
def main():
|
||||
print("L3 分析引擎原型 v0.1")
|
||||
print("=" * 40)
|
||||
|
||||
print("\n[1/4] 读取 Excel 收视数据…")
|
||||
episodes = read_excel()
|
||||
print(f" 读取 {len(episodes)} 期")
|
||||
|
||||
print("\n[2/4] 匹配 ground-truth AI 标签…")
|
||||
episodes = read_gt_and_match(episodes)
|
||||
|
||||
print("\n[3/4] 计算分析模块…")
|
||||
analysis = compute_analysis(episodes)
|
||||
m = analysis["metrics"]
|
||||
print(f" 年度均值份额:{m['avg_share']}")
|
||||
print(f" 基础完成率:{m['base_completion']}%")
|
||||
print(f" 摸高完成率:{m['high_completion']}%")
|
||||
print(f" 达标期数:{m['above_base_count']}/{m['total_episodes']}({m['above_base_pct']}%)")
|
||||
|
||||
print("\n[4/4] 生成 HTML 报告…")
|
||||
OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
html = generate_html(analysis, episodes)
|
||||
with open(OUTPUT_PATH, "w", encoding="utf-8") as f:
|
||||
f.write(html)
|
||||
print(f" [OK] 报告已生成:{OUTPUT_PATH}")
|
||||
print(f" 请在浏览器中打开查看。")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,3 @@
|
||||
openai==1.55.0
|
||||
python-dotenv>=1.0.0
|
||||
python-docx>=1.1.0
|
||||
@@ -0,0 +1,162 @@
|
||||
"""
|
||||
run_labeling.py - 单期或批量 AI 打标脚本
|
||||
用法:
|
||||
单期: python run_labeling.py --ep 4 --model mimo-v2.5-pro
|
||||
批量: python run_labeling.py --all --model mimo-v2.5-pro
|
||||
"""
|
||||
|
||||
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 = {
|
||||
"mimo-v2.5-pro": {
|
||||
"base_url": "https://api.xiaomimimo.com/v1",
|
||||
"model_name": "mimo-v2.5-pro",
|
||||
"api_key_env": "MIMO_API_KEY",
|
||||
},
|
||||
"deepseek-v4-pro": {
|
||||
"base_url": "https://api.deepseek.com",
|
||||
"model_name": "deepseek-v4-pro",
|
||||
"api_key_env": "DEEPSEEK_API_KEY",
|
||||
},
|
||||
}
|
||||
|
||||
ALL_EPISODES = list(range(1, 21))
|
||||
|
||||
FIELD_PROMPT_MAP = {
|
||||
"narrative": "prompt2_narrative.md",
|
||||
"classification": "prompt1_classification.md",
|
||||
"opening_hook": "prompt3_opening_hook.md",
|
||||
}
|
||||
|
||||
|
||||
def load_prompt(field):
|
||||
if field not in FIELD_PROMPT_MAP:
|
||||
raise ValueError(f"Unknown field: {field}, valid: {list(FIELD_PROMPT_MAP.keys())}")
|
||||
return (PROMPTS_DIR / FIELD_PROMPT_MAP[field]).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def load_transcript(ep):
|
||||
pattern = f"ep{ep:03d}_*.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):
|
||||
"""按 ## SYSTEM / ## USER 分隔符拆解 prompt。
|
||||
自动剥离 ## SYSTEM 标签之前的标题行。
|
||||
"""
|
||||
parts = template.split("## USER")
|
||||
# parts[0] 是 system 部分,可能包含标题行 + ## SYSTEM 标签
|
||||
system_raw = parts[0]
|
||||
# 如果有 ## SYSTEM 标签,取它之后的内容;否则去除标题行
|
||||
if "## SYSTEM" in system_raw:
|
||||
system_prompt = system_raw.split("## SYSTEM", 1)[1].strip()
|
||||
else:
|
||||
# 没有 ## SYSTEM 标签时,去掉第一行(标题行)作为降级处理
|
||||
lines = system_raw.strip().splitlines()
|
||||
system_prompt = "\n".join(lines[1:]).strip() if len(lines) > 1 else system_raw.strip()
|
||||
user_prompt = parts[1].strip().replace("{transcript}", transcript)
|
||||
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, field="narrative"):
|
||||
transcript, fname = load_transcript(ep)
|
||||
template = load_prompt(field)
|
||||
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}_{field}_ep{ep:03d}.json"
|
||||
out.write_text(
|
||||
json.dumps({"episode": ep, "filename": fname, "field": field, "result": result, "ground_truth": gt}, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(f"完成 ep{ep:03d} [{field}] -> {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="mimo-v2.5-pro", help="模型键名")
|
||||
parser.add_argument("--field", default="narrative", choices=["narrative", "classification", "opening_hook"],
|
||||
help="打标字段: narrative(叙事结构) / classification(4分类) / opening_hook(开篇钩子)")
|
||||
args = parser.parse_args()
|
||||
if args.all:
|
||||
for ep in ALL_EPISODES:
|
||||
run_labeling(ep, args.model, args.field)
|
||||
elif args.ep:
|
||||
run_labeling(args.ep, args.model, args.field)
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,192 @@
|
||||
"""
|
||||
summarize.py - 汇总打标结果命中情况
|
||||
用法: python summarize.py --model mimo-v2.5-pro
|
||||
"""
|
||||
|
||||
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"
|
||||
GROUND_TRUTH = BASE_DIR / "benchmark-set" / "ground-truth.json"
|
||||
|
||||
|
||||
def load_ground_truth_map():
|
||||
"""从 ground-truth.json 源文件加载最新 GT(而非实验文件中的快照)。"""
|
||||
try:
|
||||
data = json.loads(GROUND_TRUTH.read_text(encoding="utf-8"))
|
||||
return {ep["ep"]: ep for ep in data["episodes"]}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
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_model_field_ep003.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, field="narrative"):
|
||||
# 文件匹配:新格式有 field 标记,老格式(无 field)向后兼容
|
||||
pattern_new = str(EXPERIMENTS_DIR / f"*_{model}_{field}_*.json")
|
||||
files_new = sorted(Path(p) for p in glob.glob(pattern_new))
|
||||
# 如果是 narrative 且新格式文件为空,尝试匹配老格式(无 field 标记)
|
||||
if field == "narrative" and not files_new:
|
||||
pattern_old = str(EXPERIMENTS_DIR / f"*_{model}_ep*.json")
|
||||
files_old = [f for f in sorted(Path(p) for p in glob.glob(pattern_old))
|
||||
if len(f.name.replace(".json", "").split("_")) == 3] # 老格式: ts_model_epNN.json
|
||||
files = files_old
|
||||
else:
|
||||
files = files_new
|
||||
|
||||
if not files:
|
||||
print(f"未找到 {pattern_new}")
|
||||
return
|
||||
|
||||
ep_files = latest_per_ep(files)
|
||||
gt_map = load_ground_truth_map() # 从源文件读最新 GT
|
||||
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,回退到实验文件中的快照
|
||||
gt = gt_map.get(ep, data.get("ground_truth", {}))
|
||||
result = data.get("result")
|
||||
|
||||
title = gt.get("title", "?")
|
||||
|
||||
if field == "opening_hook":
|
||||
gt_val = gt.get("opening_hook")
|
||||
pred_val = result.get("opening_hook") if result else None
|
||||
conf = result.get("confidence", "?") if result else "?"
|
||||
if gt_val is None:
|
||||
rows.append({"ep": ep, "title": title, "gt": "(无标注)", "pred": pred_val or "解析失败", "hit": None, "conf": conf})
|
||||
else:
|
||||
hit = pred_val == gt_val if pred_val is not None else False
|
||||
rows.append({"ep": ep, "title": title, "gt": gt_val, "pred": pred_val or "解析失败", "hit": hit, "conf": conf})
|
||||
elif field == "narrative":
|
||||
gt_val = gt.get("narrative_structure")
|
||||
pred_val = result.get("narrative_structure") if result else None
|
||||
conf = result.get("confidence", "?") if result else "?"
|
||||
if gt_val is None:
|
||||
rows.append({"ep": ep, "title": title, "gt": "(无标注)", "pred": pred_val or "解析失败", "hit": None, "conf": conf})
|
||||
else:
|
||||
hit = pred_val == gt_val if pred_val is not None else False
|
||||
rows.append({"ep": ep, "title": title, "gt": gt_val, "pred": pred_val or "解析失败", "hit": hit, "conf": conf})
|
||||
elif field == "classification":
|
||||
conf = result.get("confidence", "?") if result else "?"
|
||||
sub = {}
|
||||
for key in ("program_format", "equipment_domain", "scene_tags", "tech_tags"):
|
||||
gt_val = gt.get(key)
|
||||
pred_val = result.get(key) if result else None
|
||||
if gt_val is None or pred_val is None:
|
||||
sub[key] = None
|
||||
elif isinstance(gt_val, list):
|
||||
sub[key] = set(gt_val) == set(pred_val) if isinstance(pred_val, list) else False
|
||||
else:
|
||||
sub[key] = pred_val == gt_val
|
||||
all_hit = all(v is True for v in sub.values() if v is not None)
|
||||
rows.append({"ep": ep, "title": title, "sub": sub, "all_hit": all_hit, "conf": conf})
|
||||
|
||||
if field == "opening_hook":
|
||||
for r in rows:
|
||||
if r["hit"] is None:
|
||||
mark = "-"
|
||||
else:
|
||||
mark = "✓" if r["hit"] else "✗"
|
||||
conf_str = f'置信度:{r["conf"]}' if r["conf"] != "?" else ""
|
||||
print(f' ep{r["ep"]:03d} {r["title"]:<12} | 标准:{r["gt"]:<6} | {model}:{r["pred"]:<6} | {mark} {conf_str}')
|
||||
|
||||
scored = [r for r in rows if r["hit"] is not None]
|
||||
hits = sum(1 for r in scored if r["hit"])
|
||||
total = len(scored)
|
||||
unlabeled = sum(1 for r in rows if r["hit"] is None)
|
||||
print(f"\n ===== {model} 命中情况 =====")
|
||||
print(f" opening_hook 命中: {hits}/{total} = {hits*100//total if total else 0}%")
|
||||
print(f" 无标注(跳过): {unlabeled} 期")
|
||||
print(f" 解析失败: {parse_fail} 期")
|
||||
elif field == "narrative":
|
||||
# 打印每行
|
||||
for r in rows:
|
||||
if r["hit"] is None:
|
||||
mark = "-"
|
||||
else:
|
||||
mark = "✓" if r["hit"] else "✗"
|
||||
conf_str = f'置信度:{r["conf"]}' if r["conf"] != "?" else ""
|
||||
print(f' ep{r["ep"]:03d} {r["title"]:<12} | 标准:{r["gt"]:<6} | {model}:{r["pred"]:<6} | {mark} {conf_str}')
|
||||
|
||||
# 汇总(跳过无标注)
|
||||
scored = [r for r in rows if r["hit"] is not None]
|
||||
hits = sum(1 for r in scored if r["hit"])
|
||||
total = len(scored)
|
||||
unlabeled = sum(1 for r in rows if r["hit"] is None)
|
||||
hi_conf = [r for r in scored if r["conf"] == "高"]
|
||||
mid_low = [r for r in scored 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 if total else 0}%")
|
||||
print(f" 自评\"高\"置信的命中率: {hi_hit}/{len(hi_conf)}")
|
||||
print(f" 自评\"中/低\"置信的命中率: {ml_hit}/{len(mid_low)}")
|
||||
print(f" 无标注(跳过): {unlabeled} 期")
|
||||
print(f" 解析失败: {parse_fail} 期")
|
||||
elif field == "classification":
|
||||
field_names = ["program_format", "equipment_domain", "scene_tags", "tech_tags"]
|
||||
short = {"program_format": "题材", "equipment_domain": "装备域", "scene_tags": "场景", "tech_tags": "技术"}
|
||||
for r in rows:
|
||||
marks = ""
|
||||
for fn in field_names:
|
||||
v = r["sub"].get(fn)
|
||||
marks += "✓" if v is True else ("✗" if v is False else "-")
|
||||
all_mark = "✓" if r["all_hit"] else "✗"
|
||||
print(f' ep{r["ep"]:03d} {r["title"]:<12} | {"/".join(short[f] for f in field_names)}={marks} | 全对:{all_mark} 置信度:{r["conf"]}')
|
||||
|
||||
total = len(rows)
|
||||
for fn in field_names:
|
||||
scored = [r for r in rows if r["sub"].get(fn) is not None]
|
||||
hits = sum(1 for r in scored if r["sub"][fn])
|
||||
pct = hits * 100 // len(scored) if scored else 0
|
||||
print(f" {short[fn]}命中: {hits}/{len(scored)} = {pct}%")
|
||||
all_scored = [r for r in rows if any(v is not None for v in r["sub"].values())]
|
||||
all_hits = sum(1 for r in all_scored if r["all_hit"])
|
||||
print(f" 4字段全对: {all_hits}/{len(all_scored)} = {all_hits*100//len(all_scored) if all_scored else 0}%")
|
||||
print(f" 解析失败: {parse_fail} 期")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--model", required=True, help="模型键名,如 mimo-v2.5-pro / deepseek-v4-pro")
|
||||
parser.add_argument("--field", default="narrative", choices=["narrative", "classification", "opening_hook"],
|
||||
help="打标字段: narrative(叙事结构) / classification(4分类) / opening_hook(开篇钩子)")
|
||||
args = parser.parse_args()
|
||||
run(args.model, args.field)
|
||||
@@ -0,0 +1,46 @@
|
||||
"""
|
||||
认证路由 — 登录 / 登出 / 当前用户
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from app.core.deps import get_current_user
|
||||
from app.core.security import verify_password
|
||||
from app.db.session import get_session
|
||||
from app.models.user import User
|
||||
from app.schemas.auth import LoginRequest, UserResponse
|
||||
|
||||
router = APIRouter(prefix="/api/auth", tags=["认证"])
|
||||
|
||||
|
||||
@router.post("/login", response_model=UserResponse)
|
||||
def login(body: LoginRequest, request: Request, session: Session = Depends(get_session)):
|
||||
"""账号登录。验证成功后写入 session,返回用户信息。"""
|
||||
statement = select(User).where(User.username == body.username)
|
||||
user = session.exec(statement).first()
|
||||
if user is None or not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="用户名或密码错误",
|
||||
)
|
||||
if not verify_password(body.password, user.password_hash):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="用户名或密码错误",
|
||||
)
|
||||
request.session["user_id"] = user.id
|
||||
return user
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
def logout(request: Request):
|
||||
"""退出登录,清空当前 session。"""
|
||||
request.session.clear()
|
||||
return {"message": "已退出登录"}
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserResponse)
|
||||
def get_me(user: User = Depends(get_current_user)):
|
||||
"""获取当前登录用户的信息。"""
|
||||
return user
|
||||
@@ -0,0 +1,121 @@
|
||||
"""
|
||||
仪表盘 API — 题图上传 / 查询
|
||||
|
||||
权限:
|
||||
- POST /cover — 仅 zhipianren / zebian(biandao 返回 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}
|
||||
@@ -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
|
||||
@@ -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"},
|
||||
)
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,36 @@
|
||||
"""
|
||||
应用配置 — 手动从 .env 读取,不依赖 pydantic-settings 自动 env_file
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# 强制加载 backend/.env 到 os.environ(用 __file__ 定位脚本所在目录,避免 cwd 漂移)
|
||||
# __file__ = e:\tps-dashboard\backend\app\core\config.py → parent.parent.parent = e:\tps-dashboard\backend
|
||||
_env_path = Path(__file__).parent.parent.parent / ".env"
|
||||
load_dotenv(str(_env_path), override=True)
|
||||
|
||||
# 直接从 os.environ 读取,手动兜底
|
||||
_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} 是否存在且内容正确。")
|
||||
|
||||
# 简单命名空间对象(替代 pydantic-settings,避免 env 读取失效)
|
||||
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()
|
||||
@@ -0,0 +1,56 @@
|
||||
"""
|
||||
FastAPI 依赖注入工具 — 获取当前用户、角色鉴权
|
||||
"""
|
||||
|
||||
from fastapi import Depends, HTTPException, Request, status
|
||||
from sqlmodel import Session
|
||||
|
||||
from app.db.session import get_session
|
||||
from app.models.user import User, UserRole
|
||||
|
||||
|
||||
def get_current_user(
|
||||
request: Request,
|
||||
session: Session = Depends(get_session),
|
||||
) -> User:
|
||||
"""从 Session 中取 user_id,查库返回当前用户。
|
||||
|
||||
未登录或用户不存在 / 已停用时抛出 401。
|
||||
"""
|
||||
user_id = request.session.get("user_id")
|
||||
if user_id is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="请先登录",
|
||||
)
|
||||
user = session.get(User, user_id)
|
||||
if user is None or not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="用户不存在或已被停用",
|
||||
)
|
||||
return user
|
||||
|
||||
|
||||
def require_role(*allowed_roles: UserRole):
|
||||
"""返回一个 Depends 依赖,要求当前用户拥有指定的角色之一。
|
||||
|
||||
使用示例:
|
||||
@router.get("/admin-only")
|
||||
def admin_endpoint(user=Depends(require_role(UserRole.zhipianren))):
|
||||
...
|
||||
|
||||
@router.get("/staff")
|
||||
def staff_endpoint(user=Depends(require_role(UserRole.zhipianren, UserRole.zebian))):
|
||||
...
|
||||
"""
|
||||
|
||||
def check_role(user: User = Depends(get_current_user)) -> User:
|
||||
if user.role not in allowed_roles:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="权限不足,无法执行此操作",
|
||||
)
|
||||
return user
|
||||
|
||||
return check_role
|
||||
@@ -0,0 +1,17 @@
|
||||
"""
|
||||
认证安全工具 — passlib + bcrypt
|
||||
"""
|
||||
|
||||
from passlib.context import CryptContext
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
|
||||
def hash_password(plain: str) -> str:
|
||||
"""用 bcrypt 生成密码哈希。"""
|
||||
return pwd_context.hash(plain)
|
||||
|
||||
|
||||
def verify_password(plain: str, hashed: str) -> bool:
|
||||
"""验证明文密码与哈希是否匹配。"""
|
||||
return pwd_context.verify(plain, hashed)
|
||||
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
数据库会话 — SQLModel 同步引擎 + Session 工厂
|
||||
"""
|
||||
|
||||
from sqlmodel import Session, SQLModel, create_engine
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
engine = create_engine(settings.DATABASE_URL, echo=False)
|
||||
|
||||
|
||||
def init_db():
|
||||
"""创建所有 SQLModel 表(仅开发用,生产由手写 SQL 管理)。"""
|
||||
SQLModel.metadata.create_all(engine)
|
||||
|
||||
|
||||
def get_session():
|
||||
"""FastAPI 依赖注入: 每个请求获取一个 Session,用完后关闭。"""
|
||||
with Session(engine) as session:
|
||||
yield session
|
||||
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
FastAPI 应用入口 — 挂载中间件、路由
|
||||
"""
|
||||
|
||||
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")
|
||||
|
||||
# ---- CORS 中间件 ----
|
||||
# 显式列 origins 不能用 ['*'],否则 credentials=True 时浏览器拒绝携带 cookie
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["http://localhost:5173"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# ---- Session 中间件 ----
|
||||
# 密钥来自 .env,用于签名 session cookie
|
||||
app.add_middleware(
|
||||
SessionMiddleware,
|
||||
secret_key=settings.SECRET_KEY,
|
||||
session_cookie="milsci_session",
|
||||
max_age=settings.SESSION_MAX_AGE,
|
||||
same_site="lax",
|
||||
https_only=False, # 本地开发用 HTTP
|
||||
)
|
||||
|
||||
# ---- 注册路由 ----
|
||||
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")
|
||||
@@ -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()),
|
||||
)
|
||||
@@ -0,0 +1,79 @@
|
||||
"""
|
||||
节目期次模型 — SQLModel
|
||||
|
||||
业务规则(.clinerules 5.3 软引用 + 快照模式):
|
||||
- editor_id 可空(离职编导)
|
||||
- editor_name_snapshot 必填(历史导入也走快照)
|
||||
- 查询历史作品:先 editor_id,回退 editor_name_snapshot 文本匹配
|
||||
"""
|
||||
|
||||
from datetime import date, datetime
|
||||
|
||||
from typing import Any
|
||||
|
||||
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.dialects.postgresql import JSONB
|
||||
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)
|
||||
|
||||
# ── AI 打标字段(看板升级 L2→L3)──
|
||||
# 全部可空:旧数据不填,AI 打标 + 制片人审核后写入
|
||||
# 枚举值在应用层校验,不加数据库 CHECK 约束
|
||||
program_format: str | None = Field(default=None, max_length=20) # 主题材(单选)
|
||||
equipment_domain: Any | None = Field(default=None, sa_column=Column(JSONB)) # 装备域(多选)
|
||||
scene_tags: Any | None = Field(default=None, sa_column=Column(JSONB)) # 场景标签(多选)
|
||||
tech_tags: Any | None = Field(default=None, sa_column=Column(JSONB)) # 技术线标签(多选)
|
||||
narrative_structure: str | None = Field(default=None, max_length=20) # 叙事结构(单选)
|
||||
opening_hook: str | None = Field(default=None, max_length=10) # 开篇钩子(单选)
|
||||
ai_label_confidence: str | None = Field(default=None, max_length=10) # draft/reviewed/inferred
|
||||
|
||||
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()),
|
||||
)
|
||||
@@ -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()),
|
||||
)
|
||||
@@ -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()),
|
||||
)
|
||||
@@ -0,0 +1,44 @@
|
||||
"""
|
||||
用户模型 — SQLModel
|
||||
|
||||
角色体系(不再改):
|
||||
- zhipianren = 制片人 / 管理员 = 全部权限
|
||||
- zebian = 责编 = 维护仪表盘 / 知识库 / 排期
|
||||
- biandao = 编导 = 使用 TPS / 查看知识库 / 生成报题单 / 个人热点雷达
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import Column
|
||||
from sqlalchemy import DateTime as SADateTime
|
||||
from sqlalchemy.sql import func as sa_func
|
||||
from sqlmodel import Field, SQLModel
|
||||
|
||||
|
||||
class UserRole(str, Enum):
|
||||
"""用户角色枚举,值与数据库 CHECK 约束一致。"""
|
||||
zhipianren = "zhipianren" # 制片人 / 管理员
|
||||
zebian = "zebian" # 责编
|
||||
biandao = "biandao" # 编导
|
||||
|
||||
|
||||
class User(SQLModel, table=True):
|
||||
__tablename__ = "users"
|
||||
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
username: str = Field(max_length=50, unique=True, index=True)
|
||||
display_name: str = Field(max_length=100)
|
||||
password_hash: str = Field(max_length=128)
|
||||
role: UserRole = Field(default=UserRole.biandao, max_length=20)
|
||||
profile_text: str | None = Field(default=None)
|
||||
is_active: bool = Field(default=True)
|
||||
|
||||
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()),
|
||||
)
|
||||
@@ -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()),
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
"""
|
||||
认证相关 Pydantic Schema — 请求 / 响应模型
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from app.models.user import UserRole
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
"""返回给前端的用户信息(不含 password_hash)。"""
|
||||
id: int
|
||||
username: str
|
||||
display_name: str
|
||||
role: UserRole
|
||||
profile_text: str | None = None
|
||||
is_active: bool = True
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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]
|
||||
@@ -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. 读取 Excel(pandas 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()
|
||||
@@ -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)
|
||||
|
||||
# 调用 embedding(type="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,
|
||||
}]
|
||||
@@ -0,0 +1,14 @@
|
||||
fastapi==0.111.0
|
||||
uvicorn[standard]==0.29.0
|
||||
sqlmodel==0.0.19
|
||||
psycopg2-binary==2.9.9
|
||||
pgvector==0.2.5
|
||||
passlib==1.7.4
|
||||
bcrypt==4.0.1
|
||||
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
|
||||
@@ -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()
|
||||
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
创建管理员账号 — 交互式命令行脚本
|
||||
|
||||
用法:
|
||||
cd backend
|
||||
py -3.12 scripts/create_admin.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 确保 backend/ 目录在 sys.path 中,使 app.* 导入正常工作
|
||||
backend_dir = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(backend_dir))
|
||||
|
||||
from getpass import getpass
|
||||
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from app.core.security import hash_password
|
||||
from app.db.session import engine
|
||||
from app.models.user import User, UserRole
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 50)
|
||||
print(" 军事科技工作台 — 创建管理员账号")
|
||||
print("=" * 50)
|
||||
print()
|
||||
|
||||
username = input("请输入用户名: ").strip()
|
||||
if not username:
|
||||
print("❌ 用户名不能为空")
|
||||
sys.exit(1)
|
||||
|
||||
display_name = input("请输入显示名(中文姓名): ").strip()
|
||||
if not display_name:
|
||||
print("❌ 显示名不能为空")
|
||||
sys.exit(1)
|
||||
|
||||
password = getpass("请输入密码(输入不显示): ").strip()
|
||||
if not password:
|
||||
print("❌ 密码不能为空")
|
||||
sys.exit(1)
|
||||
|
||||
password_confirm = getpass("请再次输入密码确认: ").strip()
|
||||
if password != password_confirm:
|
||||
print("❌ 两次输入的密码不一致")
|
||||
sys.exit(1)
|
||||
|
||||
with Session(engine) as session:
|
||||
# 检查用户名是否已存在
|
||||
existing = session.exec(
|
||||
select(User).where(User.username == username)
|
||||
).first()
|
||||
if existing:
|
||||
print(f"❌ 用户名「{username}」已存在,请换一个用户名")
|
||||
sys.exit(1)
|
||||
|
||||
user = User(
|
||||
username=username,
|
||||
display_name=display_name,
|
||||
password_hash=hash_password(password),
|
||||
role=UserRole.zhipianren,
|
||||
is_active=True,
|
||||
)
|
||||
session.add(user)
|
||||
session.commit()
|
||||
session.refresh(user)
|
||||
|
||||
print()
|
||||
print("✅ 管理员账号创建成功!")
|
||||
print(f" 用户名: {user.username}")
|
||||
print(f" 显示名: {user.display_name}")
|
||||
print(f" 角色: {user.role.value}(制片人/管理员)")
|
||||
print(f" ID: {user.id}")
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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()
|
||||
# 数据库唯一约束触发:整行被 reject,success=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()
|
||||
@@ -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()
|
||||
@@ -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}")
|
||||
@@ -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()
|
||||
@@ -0,0 +1,238 @@
|
||||
-- ============================================================
|
||||
-- 001_init.sql — 《军事科技》栏目内部工作台 数据库初始化脚本
|
||||
-- 一次性建完所有核心表骨架(Phase 1-4 全部表)
|
||||
-- 执行方式: psql -U postgres -d milsci_dev -f 001_init.sql
|
||||
-- ============================================================
|
||||
|
||||
-- 扩展: pgvector(向量检索)
|
||||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
|
||||
-- ============================================================
|
||||
-- 1. users — 用户表(Phase 1 用)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
display_name VARCHAR(100) NOT NULL,
|
||||
password_hash VARCHAR(128) NOT NULL,
|
||||
-- 角色: zhipianren=制片人/管理员, zebian=责编, biandao=编导
|
||||
role VARCHAR(20) NOT NULL DEFAULT 'biandao'
|
||||
CHECK (role IN ('zhipianren', 'zebian', 'biandao')),
|
||||
profile_text TEXT,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_users_role ON users (role);
|
||||
|
||||
-- ============================================================
|
||||
-- 2. episodes — 节目期次(Phase 2 用,字段完整按 dev_plan.md 8.1 节)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS episodes (
|
||||
id SERIAL PRIMARY KEY,
|
||||
episode_number INTEGER NOT NULL UNIQUE,
|
||||
program_name VARCHAR(200) NOT NULL,
|
||||
air_date DATE NOT NULL,
|
||||
-- 编导(软引用 + 快照模式)
|
||||
editor_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
editor_name_snapshot VARCHAR(100) NOT NULL,
|
||||
-- 收视数据
|
||||
audience_share NUMERIC(6,4),
|
||||
audience_rating NUMERIC(6,4),
|
||||
-- 重播相关
|
||||
is_rerun BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
original_episode_id INTEGER REFERENCES episodes(id) ON DELETE SET NULL,
|
||||
is_time_sensitive BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
-- 外拍信息
|
||||
is_outdoor_shoot BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
outdoor_contact_person VARCHAR(100),
|
||||
outdoor_contact_info VARCHAR(200),
|
||||
-- 线索
|
||||
lead_source VARCHAR(300),
|
||||
-- 责编备注
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_episodes_air_date ON episodes (air_date);
|
||||
CREATE INDEX idx_episodes_editor_id ON episodes (editor_id);
|
||||
CREATE INDEX idx_episodes_is_rerun ON episodes (is_rerun);
|
||||
|
||||
-- ============================================================
|
||||
-- 3. yearly_targets — 年度收视目标(Phase 2 用,只增不改)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS yearly_targets (
|
||||
id SERIAL PRIMARY KEY,
|
||||
year INTEGER NOT NULL UNIQUE,
|
||||
base_target NUMERIC(6,4) NOT NULL,
|
||||
stretch_target NUMERIC(6,4) NOT NULL,
|
||||
issued_date DATE,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- 4. topics — 选题 / 报题单(Phase 4a 用)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS topics (
|
||||
id SERIAL PRIMARY KEY,
|
||||
title VARCHAR(200) NOT NULL,
|
||||
-- 内容概要(双段结构: 引子 + 列举)
|
||||
content_summary TEXT,
|
||||
-- 编导关联(软引用 + 快照)
|
||||
editor_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
editor_name_snapshot VARCHAR(100),
|
||||
-- 选题状态: draft=草稿, submitted=已提交, reviewing=审阅中, approved=通过, rejected=退回
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'draft'
|
||||
CHECK (status IN ('draft', 'submitted', 'reviewing', 'approved', 'rejected')),
|
||||
-- AI 反馈(JSON 格式,含查重/画像/知识库参考等模块结果)
|
||||
ai_feedback JSONB,
|
||||
-- 关联的 episodes / knowledge_items id 列表
|
||||
related_episodes JSONB,
|
||||
related_knowledge JSONB,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_topics_editor_id ON topics (editor_id);
|
||||
CREATE INDEX idx_topics_status ON topics (status);
|
||||
|
||||
-- ============================================================
|
||||
-- 5. topic_embeddings — 选题语义向量(Phase 4a 用,pgvector)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS topic_embeddings (
|
||||
id SERIAL PRIMARY KEY,
|
||||
topic_id INTEGER NOT NULL REFERENCES topics(id) ON DELETE CASCADE,
|
||||
embedding vector(1536) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_topic_embeddings_topic_id ON topic_embeddings (topic_id);
|
||||
-- pgvector 向量索引(IVFFlat,数据量上去后可用 HNSW)
|
||||
-- CREATE INDEX idx_topic_embeddings_vector ON topic_embeddings USING ivfflat (embedding vector_cosine_ops);
|
||||
|
||||
-- ============================================================
|
||||
-- 6. schedules — 排期(Phase 4b 用)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS schedules (
|
||||
id SERIAL PRIMARY KEY,
|
||||
episode_id INTEGER REFERENCES episodes(id) ON DELETE SET NULL,
|
||||
planned_air_date DATE NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'planned'
|
||||
CHECK (status IN ('planned', 'confirmed', 'completed', 'cancelled')),
|
||||
editor_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_schedules_air_date ON schedules (planned_air_date);
|
||||
|
||||
-- ============================================================
|
||||
-- 7. knowledge_items — 知识库条目(Phase 3 用,2.0 兼容预埋)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS knowledge_items (
|
||||
id SERIAL PRIMARY KEY,
|
||||
title VARCHAR(300) NOT NULL,
|
||||
-- Markdown 格式正文(向 2.0 Karpathy LLM Wiki 形态靠拢)
|
||||
content_md TEXT,
|
||||
-- 来源信息
|
||||
source_type VARCHAR(30) NOT NULL DEFAULT 'manual'
|
||||
CHECK (source_type IN ('baoti', 'manuscript', 'military_report', 'manual', 'rss')),
|
||||
source_file_name VARCHAR(300),
|
||||
source_url VARCHAR(1000),
|
||||
-- 作者 / 编导
|
||||
author VARCHAR(100),
|
||||
publish_date DATE,
|
||||
-- ⚠️ 2.0 兼容预埋字段(1.0 不用但要建)
|
||||
tags JSONB DEFAULT '[]'::jsonb,
|
||||
related_entities JSONB DEFAULT '[]'::jsonb,
|
||||
related_concepts JSONB DEFAULT '[]'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_knowledge_items_source_type ON knowledge_items (source_type);
|
||||
CREATE INDEX idx_knowledge_items_tags ON knowledge_items USING gin (tags);
|
||||
|
||||
-- ============================================================
|
||||
-- 8. knowledge_embeddings — 知识库向量(Phase 3 用,pgvector)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS knowledge_embeddings (
|
||||
id SERIAL PRIMARY KEY,
|
||||
knowledge_id INTEGER NOT NULL REFERENCES knowledge_items(id) ON DELETE CASCADE,
|
||||
chunk_index INTEGER NOT NULL DEFAULT 0,
|
||||
chunk_text TEXT NOT NULL,
|
||||
embedding vector(1536) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_knowledge_embeddings_knowledge_id ON knowledge_embeddings (knowledge_id);
|
||||
-- pgvector 向量索引
|
||||
-- CREATE INDEX idx_knowledge_embeddings_vector ON knowledge_embeddings USING ivfflat (embedding vector_cosine_ops);
|
||||
|
||||
-- ============================================================
|
||||
-- 9. review_comments — 审片意见(Phase 4b 用)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS review_comments (
|
||||
id SERIAL PRIMARY KEY,
|
||||
episode_id INTEGER NOT NULL REFERENCES episodes(id) ON DELETE CASCADE,
|
||||
comment_text TEXT NOT NULL,
|
||||
reviewer VARCHAR(100),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_review_comments_episode_id ON review_comments (episode_id);
|
||||
|
||||
-- ============================================================
|
||||
-- 10. shooting_resources — 拍摄资源 / 人脉(Phase 4b 用)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS shooting_resources (
|
||||
id SERIAL PRIMARY KEY,
|
||||
contact_person VARCHAR(100) NOT NULL,
|
||||
contact_info VARCHAR(200),
|
||||
organization VARCHAR(200),
|
||||
-- 资源类型: personnel=人员, location=场地, equipment=设备, other=其他
|
||||
resource_type VARCHAR(20) DEFAULT 'other'
|
||||
CHECK (resource_type IN ('personnel', 'location', 'equipment', 'other')),
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- 11. radar_items — 热点雷达条目(Phase 4c 用)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS radar_items (
|
||||
id SERIAL PRIMARY KEY,
|
||||
title VARCHAR(300) NOT NULL,
|
||||
source_url VARCHAR(1000),
|
||||
source_name VARCHAR(100),
|
||||
summary TEXT,
|
||||
-- AI 处理结果
|
||||
ai_summary TEXT,
|
||||
style_summary TEXT,
|
||||
published_at TIMESTAMPTZ,
|
||||
fetched_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- 12. radar_user_actions — 编导对热点的操作(Phase 4c 用)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS radar_user_actions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
radar_item_id INTEGER NOT NULL REFERENCES radar_items(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
-- 操作类型: deep_dive=深入了解, bookmark=收藏, ignore=忽略
|
||||
action_type VARCHAR(20) NOT NULL
|
||||
CHECK (action_type IN ('deep_dive', 'bookmark', 'ignore')),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (radar_item_id, user_id, action_type)
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- 初始化完成
|
||||
-- ============================================================
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -0,0 +1,26 @@
|
||||
-- 003_add_ai_labels.sql
|
||||
-- 为 episodes 表添加 AI 打标字段(看板升级 L2→L3 桥梁)
|
||||
-- 前置:执行前先 pg_dump 备份
|
||||
-- 全部 nullable,不影响现有数据
|
||||
|
||||
ALTER TABLE episodes
|
||||
ADD COLUMN IF NOT EXISTS program_format VARCHAR(20),
|
||||
ADD COLUMN IF NOT EXISTS equipment_domain JSONB,
|
||||
ADD COLUMN IF NOT EXISTS scene_tags JSONB,
|
||||
ADD COLUMN IF NOT EXISTS tech_tags JSONB,
|
||||
ADD COLUMN IF NOT EXISTS narrative_structure VARCHAR(20),
|
||||
ADD COLUMN IF NOT EXISTS opening_hook VARCHAR(10),
|
||||
ADD COLUMN IF NOT EXISTS ai_label_confidence VARCHAR(10);
|
||||
|
||||
-- 分析引擎常用分组查询索引
|
||||
CREATE INDEX IF NOT EXISTS idx_episodes_program_format ON episodes (program_format);
|
||||
CREATE INDEX IF NOT EXISTS idx_episodes_narrative_structure ON episodes (narrative_structure);
|
||||
CREATE INDEX IF NOT EXISTS idx_episodes_ai_label_confidence ON episodes (ai_label_confidence);
|
||||
|
||||
COMMENT ON COLUMN episodes.program_format IS '主题材(单选): 装备深解/横切类比/历史纵深/前沿科技/事件战例/人物牵引';
|
||||
COMMENT ON COLUMN episodes.equipment_domain IS '装备域(多选JSONB数组): 陆战装备/海军舰船/航空航天/导弹与反导/信息化/后勤保障/轻武器/跨域/其他';
|
||||
COMMENT ON COLUMN episodes.scene_tags IS '场景标签(多选JSONB数组): 节点策划/热点跟进/装备首发/展会';
|
||||
COMMENT ON COLUMN episodes.tech_tags IS '技术线标签(多选JSONB数组): 无人作战/智能化/有人无人协同';
|
||||
COMMENT ON COLUMN episodes.narrative_structure IS '叙事结构(单选): 主线演进/并列结构';
|
||||
COMMENT ON COLUMN episodes.opening_hook IS '开篇钩子(单选): 强/中/弱';
|
||||
COMMENT ON COLUMN episodes.ai_label_confidence IS '打标状态: draft(AI草稿)/reviewed(制片人确认)/inferred(标题推断)';
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 287 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 122 KiB |
@@ -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 的指令要用代码块封装起来,方便复制。
|
||||
+22
-17
@@ -2,7 +2,7 @@
|
||||
|
||||
> 项目:TPS(Topic Planning System)中台
|
||||
> 仓库:`tps-dashboard`(Gitea)
|
||||
> 修订版本:v3 · 2026-05-06
|
||||
> 修订版本:v5 · 2026-05-27
|
||||
> 配套文档:`project_plan.md`(产品宪法)+ `.clinerules`(协作规则)+ `docs/git_workflow.md`(Git 操作手册)
|
||||
|
||||
---
|
||||
@@ -62,7 +62,7 @@
|
||||
**路径 B — 迁移(推荐)**:
|
||||
1. 制片人在家用机把 `backend/` 整个目录打包 zip
|
||||
2. 拷到单位机,解压进 `tps-dashboard/backend/`
|
||||
3. Cline 跑一遍 Plan,Sonnet 审一遍代码确认与新版宪法一致
|
||||
3. Cline 跑一遍 Plan,MiniMax M2.7 审一遍代码确认与新版宪法一致
|
||||
4. 修任何不一致(目前看应无重大不一致)
|
||||
5. 跑一遍 `pip install + psql + create_admin + uvicorn` 验证
|
||||
|
||||
@@ -90,8 +90,7 @@
|
||||
| 向量检索 | **pgvector**(PG 扩展) | 不用独立向量库 |
|
||||
| 向量维度 | **1536**(为 2.0 wiki 兼容) | 不可改 |
|
||||
| AI 嵌入模型 | DeepSeek 或 OpenAI 兼容 embedding API | — |
|
||||
| AI 编程模型 | Phase 0-1: Sonnet Plan + DeepSeek pro Act | 详见 `.clinerules` 第二章 |
|
||||
| AI 编程模型 | Phase 2 起: MiniMax M2.7 Plan + DeepSeek pro Act | — |
|
||||
| AI 编程模型 | **MiniMax M2.7**(Cline Plan + Act 模式) | 详见 `.clinerules` 第二章 |
|
||||
| 认证 | **Session + Cookie** | 不用 JWT |
|
||||
| Word 生成 | docxtpl | — |
|
||||
| 甘特图前端库 | frappe-gantt | 不自己用 D3 画 |
|
||||
@@ -142,7 +141,7 @@
|
||||
**Day 2:开发工具**
|
||||
|
||||
- VS Code + Cline 扩展
|
||||
- Cline 配置 Plan 模式(Sonnet)+ Act 模式(DeepSeek pro)
|
||||
- Cline 配置 Plan 模式(MiniMax M2.7)+ Act 模式(MiniMax M2.7),Plan 和 Act 用同一模型但物理隔离两个心智阶段(纪律性)
|
||||
- VS Code 终端打开测试:能跑 `python --version` / `node --version` / `git --version` / `psql --version`
|
||||
|
||||
**Phase 0 收尾**:
|
||||
@@ -163,7 +162,7 @@ git push origin phase0-env-ready
|
||||
|
||||
**Day 3-4:后端骨架**
|
||||
|
||||
- **路径 B 推荐**:从家用机迁移 backend 整目录,Sonnet 审一遍,跑通验证
|
||||
- **路径 B 推荐**:从家用机迁移 backend 整目录,MiniMax M2.7 审一遍,跑通验证
|
||||
- **路径 A 备选**:从零开始重写
|
||||
- 验收:`uvicorn app.main:app --reload` 能起,`/docs` 页面能开,`/api/auth/login` + `/api/auth/me` + `/api/auth/logout` 三接口验证通过
|
||||
|
||||
@@ -202,7 +201,7 @@ git push origin phase0-env-ready
|
||||
- 编辑 episodes / yearly_targets / users 的 SQLModel 模型
|
||||
|
||||
**前端**:
|
||||
- 仪表盘页面(近 4 期收视 + 未来 6 期排期)
|
||||
- 仪表盘页面(近 9 期收视 + 未来 6 期排期)
|
||||
- 责编录入界面
|
||||
- 管理员用户管理界面
|
||||
|
||||
@@ -213,7 +212,7 @@ git push origin phase0-env-ready
|
||||
- 后端用 pandas 解析,批量 INSERT
|
||||
- **死规矩**:导入时检测 yearly_targets 是否对应年份齐全,缺则报错让责编先填(防止颜色判定时找不到目标)
|
||||
|
||||
**Phase 2 起 Plan 模式切换为 MiniMax M2.7**:Sonnet 余额耗尽前最后一个由 Sonnet 出方案的 Phase,Phase 2 末做切换的复盘评估。
|
||||
**Phase 2 已进入**:Plan = Act = MiniMax M2.7,DeepSeek v4 pro 保留为备选。
|
||||
|
||||
**Phase 2 里程碑**:
|
||||
- 责编可以开始录入(或导入)历史收视数据
|
||||
@@ -250,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 +489,9 @@ 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 收尾仪式** |
|
||||
| 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 正式日常使用;更新版本演进记录 |
|
||||
| **v5** | **2026-05-27** | **Phase 4a 升级为"对话式 TPS"(对齐 project_plan v5):模块 A/C/G 合成左对话右报告形态;补硬门槛(数据灌入 + Phase 3 朴素检索先行);加 AI 成本机制落地说明** |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
# 项目协作主控文件 (doco/CLAUDE.md)
|
||||
|
||||
<!--
|
||||
Claude Code 每次开 session 自动读取。两个角色:
|
||||
|
||||
1. 给 AI 的项目说明书(项目是什么、用什么、怎么跑)
|
||||
|
||||
2. 双账号/多工具之间的"活快照"交接区(现在到哪了、下一步、刚做的决定)
|
||||
维护原则:增量更新,不要整篇重写。改动写进对应小节即可。
|
||||
|
||||
3. 每次结束session或通哥明确说结束今天的工作,应由Claude修改更新本文件,交接备注和待确认等部分内容应为最新。
|
||||
|
||||
-->
|
||||
|
||||
---
|
||||
|
||||
## 🔖 状态栏 (STATUS — 每次结束 session 前必须更新这三行)
|
||||
|
||||
- **最后更新**:Claude Code | 2026-06-26
|
||||
- **当前状态一句话**:**doco 子项目开发收尾。** 22 期融合A稿全部产出(含说话人区分重跑),已归拢至 `doco/deliverables/`。第01期(武器装备里的形状规律)缺A稿暂跳过。下一步:带 22 期成品回 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. 技术栈与运行方式
|
||||
|
||||
- **语言**:Python(openai 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. 当前进度(核心交接区,以最新快照为准)
|
||||
|
||||
- **已完成至**:**doco 子项目开发收尾。** 22 期融合A稿全部产出(ep001-022,含说话人区分 C4 重跑),已归拢至 `doco/deliverables/`(按播出顺序命名,22 份)。第01期(武器装备里的形状规律)缺A稿暂跳过。排播对照表 `doco/data/2026episode_list.xlsx` 已就位。
|
||||
- **正在做**:无。doco 开发完成,等待回归 TPS 主项目。
|
||||
- **卡点 / 待解**:下一步把 22 期融合A稿批量导入 TPS 主项目知识库(走 Phase 3 上传/embedding 链路)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 已完成(只追加,最新在最上)
|
||||
|
||||
- [2026-06-26 |Claude Code] **doco 子项目收尾:ep021/022 补稿 + 说话人重跑 + 成品归拢。** 本 session 完成:① **ep021(海战颠覆者)、ep022(硬核脑洞)两期新补 A 稿全流程 P1→C4 跑通**——ep021 骨架 LLM 自动生成失败(`extract_a_paragraphs()` 过滤空段后重编号,LLM 用了 raw docx 下标),手写骨架 JSON(32 段=26 normal+6 ignore 粗体章节标题)通过校验;ep022 骨架 LLM 自动通过(38 段全 normal,inline header);ep021 C3 review 13 条(均 OCR 修正,专名零替换)、C4 punct_ok 25/26;ep022 C3 review 5 条、C4 punct_ok 全过;② **cli.py 路径 bug 修复**:`episode_dir = Path("programs")/episode_id` 相对路径在子进程中二次解析导致路径翻倍(`programs/ep021/programs/ep021/...`),加 `.resolve()` 修复——此 bug 从未触发因 ep004 用 `--skip-p1` 且 `_batch_run.py` 用绝对路径;③ **ep007、ep010 C4 说话人区分重跑**(06-24 批量重跑时这两期被遗漏,融合A稿时间戳仍为 06-23),重跑完成、产物已更新;④ **排播对照表** `doco/data/2026episode_list.xlsx`(25 期播出日期/期次/节目名/编导/收视)导入,建立 ep→播出顺序完整映射;⑤ **成品归拢** `doco/deliverables/` 文件夹:22 份融合A稿按播出顺序统一命名(`第XX期_YYYYMMDD_节目名_编导_融合A稿.docx`),缺第01期(武器装备里的形状规律,无A稿);⑥ 通哥人工核验 ep021/022 质量高,仅两处小毛刺(ep021 一段话归错段落、ep022 一个标签应为三维动画),确认技术路径有效。
|
||||
- [2026-06-23 |Claude Code] **批量化基础设施完工 + ep004 全流程通过 + 16 期批量跑启动**。本 session 完成:① ep004(枪王对决,最难·小剧场密·58段)全流程 P1→C4 一次通过,C4 punct_ok 58/58 全过、confidence 全 ≥0.80(batch_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 个空段(seg15–18:主持人7/解说8/主持人8/解说9)确认是播出剪掉的 VSS 微声狙击枪 + NTW-20 南非大炮整段**——融合B稿里 VSS/NTW/南非/消音/反器材**零出现**,seg14 边界抽查(593–681 全 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/28;seg8(专家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 |本轮之前] 本地 OCR(DeepSeek-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 调优**:已降至 25,ep004 实测 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] **批量跑 22 期(完成)**:ep001-020 共 20 期 + ep021/022 新补 2 期,全部出稿。说话人区分 C4 重跑覆盖全部 22 期。成品归拢至 `doco/deliverables/`(22 份,按播出顺序命名)。
|
||||
- [ ] **doco 收工后回 TPS 主项目**:22 期融合A稿批量导入知识库(走 Phase 3 已有的上传/embedding 链路),为 Phase 4a 语义搜索提供冷启动数据。成品位于 `doco/deliverables/`。
|
||||
- [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.80,punct_ok 58/58),相比 ep002 的 batch_size=40(47% 批次回退)大幅改善。`_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 且给足 token(C4 对齐 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 个空段(seg15–18 整段 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-26 重写)**
|
||||
|
||||
- **大局**:**doco 子项目开发已完成。** 22 期融合A稿全部产出(含说话人区分重跑),成品归拢至 `doco/deliverables/`(22 份,按播出顺序 `第XX期_YYYYMMDD_节目名_编导_融合A稿.docx` 命名)。缺第01期(武器装备里的形状规律,无A稿)。下一步是把成品带回 TPS 主项目知识库。
|
||||
- **第一句话该干的**:切回 TPS 主项目,把 `doco/deliverables/` 里 22 份融合A稿批量导入知识库(走 Phase 3 上传/embedding 链路),为 Phase 4a 语义搜索提供冷启动数据。
|
||||
- **排播对照表**:`doco/data/2026episode_list.xlsx`(25 期完整播出日期/期次/节目名/编导/收视),内部 ep ID → 播出顺序的映射已建立。
|
||||
- **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,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 分任务用:骨架/对齐开,标点/抽取/融合关。
|
||||
7. **骨架注意**:`extract_a_paragraphs()` 会过滤空段并重编号,LLM 生成骨架时必须基于过滤后的下标(ep021 踩过此坑,需手写骨架)。
|
||||
8. **cli.py 路径**:`episode_dir` 已改为 `.resolve()` 绝对路径,修复了子进程路径翻倍 bug。
|
||||
- **出稿命名**:episode 目录内为 `{原始A稿stem}_融合A稿.docx`;deliverables 文件夹统一为 `第XX期_YYYYMMDD_节目名_编导_融合A稿.docx`。
|
||||
- **字体坑**:大标题方正小标宋_GBK(商业字体),出稿前确认已装。
|
||||
- **工具脚本清单**(一次性,跑完可删):`_setup_episodes.py`(建目录+拷文件)、`_batch_skeleton.py`(批量生成骨架)、`_batch_run.py`(批量全流程)。
|
||||
- **doco 收工后的路**:22 期成品 → TPS 主项目知识库批量导入(走 Phase 3 上传/embedding 链路)→ Phase 4a 语义搜索提供冷启动数据。
|
||||
|
||||
---
|
||||
|
||||
## 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
@@ -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 Pro(OpenAI兼容端点,`openai` SDK)
|
||||
- **OCR**:本地 Ollama + DeepSeek-OCR 模型
|
||||
- **ASR**:讯飞开放平台 录音文件转写(标准版)
|
||||
- **视频处理**:ffmpeg(subprocess调用)
|
||||
- **文档生成**:python-docx
|
||||
- **CLI框架**:Click
|
||||
@@ -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
|
||||
@@ -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()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user