看板升级: GT v0.5.0(20期叙事结构全覆盖) + schema加字段方案 + git清理

- ground-truth v0.5.0: 补完10期narrative_structure标注(MiMo跑批+制片人审定)
- summarize.py: 修复无标注期次格式化崩溃
- 003_add_ai_labels.sql: episodes表加7列(6打标+1置信度)迁移文件
- episode.py: Model同步加7个Optional字段
- CLAUDE.md: 项目主控文件入库
- doco/deliverables: 20期终版文稿md入库
- doco/programs: 中间产物从git移除(本地保留), .gitignore统一管理
- note/寄存条: 两个子项目寄存条入库
- .gitignore: 补充doco中间产物/Excel锁文件/临时文件规则

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
simonkoson
2026-06-26 09:14:29 +08:00
parent 81b331d6d7
commit d91c20a126
91 changed files with 4148 additions and 20891 deletions
+586
View File
@@ -0,0 +1,586 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Doco 文稿整理流程图 — 《军事科技》</title>
<style>
:root {
--blue: #2563eb;
--blue-light: #dbeafe;
--green: #16a34a;
--green-light: #dcfce7;
--orange: #ea580c;
--orange-light: #ffedd5;
--purple: #9333ea;
--purple-light: #f3e8ff;
--red: #dc2626;
--red-light: #fee2e2;
--gray: #64748b;
--gray-light: #f1f5f9;
--bg: #fafbfc;
--card-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: "PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif;
background: var(--bg);
color: #1e293b;
line-height: 1.6;
padding: 40px 20px 80px;
}
.container { max-width: 960px; margin: 0 auto; }
h1 {
text-align: center;
font-size: 28px;
font-weight: 700;
margin-bottom: 8px;
color: #0f172a;
}
.subtitle {
text-align: center;
color: var(--gray);
font-size: 15px;
margin-bottom: 40px;
}
/* ---- Legend ---- */
.legend {
display: flex;
justify-content: center;
gap: 24px;
flex-wrap: wrap;
margin-bottom: 36px;
font-size: 13px;
}
.legend-item { display: flex; align-items: center; gap: 6px; }
.legend-dot {
width: 14px; height: 14px; border-radius: 4px;
}
/* ---- Pipeline column ---- */
.pipeline {
display: flex;
flex-direction: column;
align-items: center;
gap: 0;
}
/* ---- Arrow between stages ---- */
.arrow-down {
width: 3px; height: 36px;
background: #cbd5e1;
position: relative;
}
.arrow-down::after {
content: '';
position: absolute;
bottom: -6px; left: 50%; transform: translateX(-50%);
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-top: 8px solid #cbd5e1;
}
.arrow-label {
font-size: 11px;
color: var(--gray);
margin-top: 4px;
text-align: center;
}
/* ---- Stage card ---- */
.stage {
width: 100%;
max-width: 820px;
border-radius: 14px;
padding: 24px 28px;
box-shadow: var(--card-shadow);
position: relative;
border-left: 5px solid;
}
.stage-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.stage-badge {
font-size: 13px;
font-weight: 700;
padding: 3px 10px;
border-radius: 6px;
color: #fff;
white-space: nowrap;
}
.stage-title {
font-size: 18px;
font-weight: 600;
}
.stage-body { font-size: 14px; color: #475569; }
.stage-body p { margin-bottom: 8px; }
.stage-body p:last-child { margin-bottom: 0; }
.io-row {
display: flex;
gap: 16px;
margin-top: 14px;
flex-wrap: wrap;
}
.io-box {
flex: 1;
min-width: 200px;
border-radius: 8px;
padding: 10px 14px;
font-size: 13px;
}
.io-box strong { display: block; margin-bottom: 4px; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
.io-in { background: #f0f9ff; border: 1px solid #bae6fd; }
.io-in strong { color: #0369a1; }
.io-out { background: #f0fdf4; border: 1px solid #bbf7d0; }
.io-out strong { color: #15803d; }
.io-tool { background: #fdf4ff; border: 1px solid #e9d5ff; }
.io-tool strong { color: #7e22ce; }
/* Color themes per stage type */
.stage-prep { border-color: var(--blue); background: var(--blue-light); }
.stage-prep .stage-badge { background: var(--blue); }
.stage-core { border-color: var(--green); background: var(--green-light); }
.stage-core .stage-badge { background: var(--green); }
.stage-fuse { border-color: var(--orange); background: var(--orange-light); }
.stage-fuse .stage-badge { background: var(--orange); }
.stage-output { border-color: var(--purple); background: var(--purple-light); }
.stage-output .stage-badge { background: var(--purple); }
.stage-check { border-color: var(--red); background: var(--red-light); }
.stage-check .stage-badge { background: var(--red); }
/* ---- Parallel fork ---- */
.parallel-wrapper {
width: 100%;
max-width: 820px;
position: relative;
}
.parallel-label {
text-align: center;
font-size: 12px;
color: var(--gray);
margin-bottom: 10px;
font-weight: 600;
letter-spacing: 1px;
}
.parallel-row {
display: flex;
gap: 20px;
}
.parallel-row > .parallel-branch {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 0;
}
.parallel-row .stage {
width: 100%;
}
/* ---- Merge connector ---- */
.merge-connector {
width: 100%;
max-width: 820px;
height: 40px;
position: relative;
}
.merge-connector::before {
content: '';
position: absolute;
top: 0; left: 25%; right: 25%;
height: 20px;
border-left: 3px solid #cbd5e1;
border-right: 3px solid #cbd5e1;
border-bottom: 3px solid #cbd5e1;
border-radius: 0 0 12px 12px;
}
.merge-connector::after {
content: '';
position: absolute;
bottom: 2px; left: 50%; transform: translateX(-50%);
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-top: 8px solid #cbd5e1;
}
/* Fork connector */
.fork-connector {
width: 100%;
max-width: 820px;
height: 36px;
position: relative;
}
.fork-connector::before {
content: '';
position: absolute;
bottom: 0; left: 25%; right: 25%;
height: 20px;
border-left: 3px solid #cbd5e1;
border-right: 3px solid #cbd5e1;
border-top: 3px solid #cbd5e1;
border-radius: 12px 12px 0 0;
}
.fork-connector-center {
position: absolute;
top: 0; left: 50%; transform: translateX(-50%);
width: 3px; height: 16px;
background: #cbd5e1;
}
/* ---- Key principle banner ---- */
.principle {
width: 100%;
max-width: 820px;
background: linear-gradient(135deg, #fef3c7, #fde68a);
border: 2px solid #f59e0b;
border-radius: 10px;
padding: 16px 20px;
font-size: 14px;
color: #92400e;
text-align: center;
font-weight: 500;
}
.principle strong { color: #b45309; }
/* ---- Input materials row ---- */
.inputs-row {
display: flex;
gap: 20px;
width: 100%;
max-width: 820px;
margin-bottom: 0;
}
.input-card {
flex: 1;
border-radius: 12px;
padding: 18px 20px;
box-shadow: var(--card-shadow);
text-align: center;
border: 2px dashed;
}
.input-card.card-a {
background: #eff6ff;
border-color: #93c5fd;
}
.input-card.card-v {
background: #fef3c7;
border-color: #fcd34d;
}
.input-icon { font-size: 32px; margin-bottom: 6px; }
.input-label { font-size: 16px; font-weight: 600; }
.input-desc { font-size: 12px; color: var(--gray); margin-top: 4px; }
/* ---- Final output ---- */
.final-output {
width: 100%;
max-width: 820px;
border-radius: 14px;
padding: 24px;
background: linear-gradient(135deg, #7c3aed, #4f46e5);
color: #fff;
text-align: center;
box-shadow: 0 4px 20px rgba(79,70,229,0.3);
}
.final-output h3 { font-size: 20px; margin-bottom: 8px; }
.final-output p { font-size: 14px; opacity: 0.9; }
.output-cards {
display: flex;
gap: 16px;
margin-top: 16px;
justify-content: center;
}
.output-card {
background: rgba(255,255,255,0.15);
border: 1px solid rgba(255,255,255,0.3);
border-radius: 10px;
padding: 14px 20px;
min-width: 200px;
}
.output-card h4 { font-size: 15px; margin-bottom: 4px; }
.output-card p { font-size: 12px; opacity: 0.8; }
/* ---- Footer ---- */
.footer {
text-align: center;
margin-top: 48px;
font-size: 12px;
color: #94a3b8;
}
@media (max-width: 700px) {
.parallel-row { flex-direction: column; }
.inputs-row { flex-direction: column; }
.output-cards { flex-direction: column; align-items: center; }
.merge-connector::before { left: 10%; right: 10%; }
.fork-connector::before { left: 10%; right: 10%; }
}
</style>
</head>
<body>
<div class="container">
<h1>Doco 文稿整理流程</h1>
<p class="subtitle">《军事科技》栏目 &middot; 从原始素材到终版文稿的六步流水线</p>
<!-- Legend -->
<div class="legend">
<div class="legend-item"><div class="legend-dot" style="background:var(--blue)"></div> 准备阶段</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--green)"></div> 核心处理</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--orange)"></div> 交叉融合</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--purple)"></div> 最终出稿</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--red)"></div> 质量校验</div>
</div>
<!-- Principle -->
<div class="pipeline">
<div class="principle">
<strong>核心原则:</strong>AI 辅助而不替代 &mdash; 正文来自原始素材,<strong>一个汉字都不改</strong>;AI 只做结构判断和标点优化,最终由制片人审核。
</div>
<div class="arrow-down"></div>
<!-- ======== INPUTS ======== -->
<div class="inputs-row">
<div class="input-card card-a">
<div class="input-icon">&#128196;</div>
<div class="input-label">A稿(编导定稿)</div>
<div class="input-desc">Word 文档 &middot; 含【导视】【主持人】【解说】【专家】等分段标记</div>
</div>
<div class="input-card card-v">
<div class="input-icon">&#127909;</div>
<div class="input-label">播出视频文件</div>
<div class="input-desc">MP4 成片 &middot; 含屏幕字幕 + 完整音轨</div>
</div>
</div>
<!-- Fork -->
<div class="fork-connector">
<div class="fork-connector-center"></div>
</div>
<!-- ======== PARALLEL: P1+P2 / C1 ======== -->
<div class="parallel-wrapper">
<div class="parallel-label">&#9660; 以下三条路可以同时进行 &#9660;</div>
<div class="parallel-row">
<!-- Left branch: P1 + P2 -->
<div class="parallel-branch">
<div class="stage stage-prep">
<div class="stage-header">
<span class="stage-badge">P1 抽帧</span>
<span class="stage-title">视频截图 + 音频分离</span>
</div>
<div class="stage-body">
<p>把视频<strong>每秒截一张图</strong>(约 1600 张),同时把<strong>音轨单独分离</strong>出来(16kHz 高清音频文件)。</p>
<div class="io-row">
<div class="io-box io-in"><strong>输入</strong>播出视频 MP4</div>
<div class="io-box io-out"><strong>产出</strong>截图文件夹 + 音频 WAV</div>
<div class="io-box io-tool"><strong>工具</strong>ffmpeg(本地)</div>
</div>
</div>
</div>
<div class="arrow-down"></div>
<div class="stage stage-prep">
<div class="stage-header">
<span class="stage-badge">P2 读字幕</span>
<span class="stage-title">OCR 文字识别 → B稿</span>
</div>
<div class="stage-body">
<p>用本地 AI 模型<strong>逐帧识别屏幕上的字幕文字</strong>,再<strong>自动去除重复行</strong>(相邻帧字幕相同),得到约 700 行带时间戳的 B稿。</p>
<p>B稿 = 播出时屏幕上真实打出的字,<strong>是最贴近播出版的文本</strong></p>
<div class="io-row">
<div class="io-box io-in"><strong>输入</strong>~1600 张截图</div>
<div class="io-box io-out"><strong>产出</strong>B稿v2~700行 带时间戳)</div>
<div class="io-box io-tool"><strong>工具</strong>Ollama 本地 AI4路并发)</div>
</div>
</div>
</div>
</div>
<!-- Middle branch: C1 -->
<div class="parallel-branch">
<div class="stage stage-core">
<div class="stage-header">
<span class="stage-badge">C1 取词</span>
<span class="stage-title">从 A稿 提取专业术语</span>
</div>
<div class="stage-body">
<p>从编导定稿中<strong>自动提取武器型号、人名、机构名</strong>等专业术语。先用规则扫(数字+字母组合等),再让 AI 补充。</p>
<p>产出的<strong>热词表</strong>会喂给下一步语音识别,帮它认准专业词汇(比如"萨德"而不是"撒的")。</p>
<p>术语会<strong>累积到词典</strong>,做过的期数越多,识别越准。</p>
<div class="io-row">
<div class="io-box io-in"><strong>输入</strong>A稿 Word 文档</div>
<div class="io-box io-out"><strong>产出</strong>本期热词表(60-70 条)<br>+ 累积词典(已 235 条)</div>
</div>
</div>
</div>
<div style="height: 36px;"></div>
<div class="stage stage-core">
<div class="stage-header">
<span class="stage-badge">C2 听声</span>
<span class="stage-title">语音识别 → ASR 文本</span>
</div>
<div class="stage-body">
<p>把分离出的音频上传<strong>讯飞语音识别</strong>,加载热词表,把主持人/解说/专家说的话<strong>转成带时间戳的文字</strong></p>
<p>ASR 是<strong>口语实况</strong>,与 B稿(屏幕字幕)内容重叠但形态不同——字幕是精简书面语,语音是完整口语。</p>
<div class="io-row">
<div class="io-box io-in"><strong>输入</strong>音频 WAV + 热词表</div>
<div class="io-box io-out"><strong>产出</strong>ASR 文本(~300-400 句 带时间戳)</div>
<div class="io-box io-tool"><strong>工具</strong>讯飞录音转写</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Merge -->
<div class="merge-connector"></div>
<!-- ======== C3 FUSE ======== -->
<div class="stage stage-fuse">
<div class="stage-header">
<span class="stage-badge">C3 交叉融合</span>
<span class="stage-title">B稿 + ASR 逐行比对验证 → 融合B稿</span>
</div>
<div class="stage-body">
<p>把 B稿(字幕文字)和 ASR(语音文字)<strong>逐行交叉比对</strong></p>
<p>&bull; <strong>B稿为主</strong>(屏幕实打字幕,与播出最一致)<br>
&bull; ASR 作为参照——如果字幕 OCR 有漏字/错字,ASR 能帮忙发现<br>
&bull; <strong>专业术语以 B稿/A稿为准</strong>,语音识别的同音异写不采纳(如"斯泰尔"不能被改成"斯太尔"</p>
<p>产出<strong>融合B稿</strong>:碎句+密集时间戳格式,行数与原 B稿完全一致。同时产出 CSV 留痕记录,标注哪些行做了修改。</p>
<div class="io-row">
<div class="io-box io-in"><strong>输入</strong>B稿v2 + ASR 文本</div>
<div class="io-box io-out"><strong>产出</strong>融合B稿(碎句版,给爱德华)<br>+ fusion_review.csv(留痕)</div>
</div>
</div>
</div>
<div class="arrow-down"></div>
<!-- ======== SKELETON (between C3 and C4) ======== -->
<div class="stage stage-core" style="border-color: #0891b2; background: #ecfeff;">
<div class="stage-header">
<span class="stage-badge" style="background: #0891b2;">骨架分析</span>
<span class="stage-title">A稿段落结构识别</span>
</div>
<div class="stage-body">
<p>让 AI 读 A稿的 Word 格式,<strong>识别出每一段是什么角色</strong>(导视 / 主持人1 / 解说2 / 专家3 / 隔断 等),但<strong>不碰正文内容</strong>,只输出结构骨架。</p>
<p>骨架生成后由<strong>制片人肉眼核验</strong>(确认段落归属无误、没有真人姓名泄露),核验通过才用于下一步。</p>
<div class="io-row">
<div class="io-box io-in"><strong>输入</strong>A稿 Word 文档</div>
<div class="io-box io-out"><strong>产出</strong>骨架 JSON(段落编号 + 角色标签)<br>+ 人类预览表</div>
</div>
</div>
</div>
<div class="arrow-down"></div>
<!-- ======== C4 COMPOSE ======== -->
<div class="stage stage-output">
<div class="stage-header">
<span class="stage-badge">C4 出稿</span>
<span class="stage-title">段落归拢 + 加标点 → 终版文稿</span>
</div>
<div class="stage-body">
<p><strong>第一步:段落归拢</strong> — 把融合B稿的 700 行碎句,按照 A稿的骨架结构,<strong>归到对应段落</strong>里(哪些行属于【解说1】、哪些属于【主持人2】)。AI 做语义对齐判断,但文字 100% 来自融合B稿。</p>
<p><strong>第二步:加标点</strong> — 碎句拼起来后可能全是逗号。让 AI <strong>按语义断句</strong>,在合适的位置加句号、问号等,让读起来更通顺。</p>
<p><strong>第三步:套格式</strong> — 按【导视】【主持人N】【解说N】【专家N】【隔断】格式,生成公文样式的 Word 文档。</p>
<div class="io-row">
<div class="io-box io-in"><strong>输入</strong>融合B稿 + A稿骨架</div>
<div class="io-box io-out"><strong>产出</strong>融合A稿.docx(公文格式 Word<br>+ 对齐表 CSV</div>
</div>
</div>
</div>
<div class="arrow-down"></div>
<!-- ======== Quality check ======== -->
<div class="stage stage-check">
<div class="stage-header">
<span class="stage-badge">质检红线</span>
<span class="stage-title">机器 + 人工双重校验</span>
</div>
<div class="stage-body">
<p><strong>机器守门员:</strong>每一段产出后,自动检查"去掉标点后的文字"是否和输入完全一致。不一致就<strong>自动重试</strong>,3 次都不过就回退到原始文本,标记"需人工"。<strong>绝不允许 AI 偷偷改字。</strong></p>
<p><strong>人工核验:</strong>制片人审核融合A稿 Word,重点看:段落归属对不对、有无空段(播出可能删了某段戏,属正常)、专业术语有无被同音字替换。</p>
</div>
</div>
<div class="arrow-down"></div>
<!-- ======== FINAL OUTPUT ======== -->
<div class="final-output">
<h3>最终交付</h3>
<p>经制片人审核通过后,一期节目产出两份文稿:</p>
<div class="output-cards">
<div class="output-card">
<h4>&#128209; 融合A稿 (.docx)</h4>
<p>给编导 &mdash; 公文格式 Word<br>按【导视/主持人/解说/专家/隔断】分段<br>段落结构与编导定稿一致</p>
</div>
<div class="output-card">
<h4>&#128221; 融合B稿 (.txt)</h4>
<p>给爱德华 &mdash; 碎句+时间戳<br>格式:[XmYs] 文本<br>约 700 行,与字幕一一对应</p>
</div>
</div>
</div>
</div><!-- /pipeline -->
<!-- ======== Summary section ======== -->
<div style="max-width:820px; margin: 40px auto 0;">
<h2 style="font-size:20px; margin-bottom:16px; text-align:center;">一句话概括每一步</h2>
<table style="width:100%; border-collapse:collapse; font-size:14px;">
<tr style="background:#f8fafc;">
<td style="padding:10px 14px; border-bottom:1px solid #e2e8f0; font-weight:600; width:100px;">P1 抽帧</td>
<td style="padding:10px 14px; border-bottom:1px solid #e2e8f0;">把视频拆成截图 + 音频</td>
</tr>
<tr>
<td style="padding:10px 14px; border-bottom:1px solid #e2e8f0; font-weight:600;">P2 读字幕</td>
<td style="padding:10px 14px; border-bottom:1px solid #e2e8f0;">让 AI 认截图里的字 → B稿(屏幕字幕版)</td>
</tr>
<tr style="background:#f8fafc;">
<td style="padding:10px 14px; border-bottom:1px solid #e2e8f0; font-weight:600;">C1 取词</td>
<td style="padding:10px 14px; border-bottom:1px solid #e2e8f0;">从 A稿抓专业术语,给语音识别"提词"</td>
</tr>
<tr>
<td style="padding:10px 14px; border-bottom:1px solid #e2e8f0; font-weight:600;">C2 听声</td>
<td style="padding:10px 14px; border-bottom:1px solid #e2e8f0;">讯飞把音频变成文字 → ASR(口语版)</td>
</tr>
<tr style="background:#f8fafc;">
<td style="padding:10px 14px; border-bottom:1px solid #e2e8f0; font-weight:600;">C3 融合</td>
<td style="padding:10px 14px; border-bottom:1px solid #e2e8f0;">字幕 + 语音交叉验证,互相纠错 → 融合B稿</td>
</tr>
<tr>
<td style="padding:10px 14px; border-bottom:1px solid #e2e8f0; font-weight:600;">骨架</td>
<td style="padding:10px 14px; border-bottom:1px solid #e2e8f0;">AI 读 A稿格式,标出每段是谁的词(不碰内容)</td>
</tr>
<tr style="background:#f8fafc;">
<td style="padding:10px 14px; border-bottom:1px solid #e2e8f0; font-weight:600;">C4 出稿</td>
<td style="padding:10px 14px; border-bottom:1px solid #e2e8f0;">碎句归段 + 加标点 + 套公文格式 → 终版 Word</td>
</tr>
<tr>
<td style="padding:10px 14px; font-weight:600;">质检</td>
<td style="padding:10px 14px;">机器逐字比对 + 制片人肉眼审核</td>
</tr>
</table>
</div>
<div class="footer">
Doco 文稿整理模块 &middot; TPS 工作台 &middot; 《军事科技》栏目 &middot; 2026
</div>
</div>
</body>
</html>