自动裁剪 - 基于测量的静音与冗余内容修剪
原则
- 在构建场景/字幕之前裁剪。 之后裁剪会打乱所有时间戳(字幕、缩放、音效)。此步骤输出:
assets/<source>.cut.mp4+assets/transcript.cut.json- 后续每一步都使用裁剪后的版本。 - 当指令启用 autoCut 时,裁剪是强制性的 - 这不是一个建议。如果没有任何内容可裁剪,您必须在报告中说明具体原因(例如视频已经足够紧凑)。
- 阈值存在于服务器中,而非您的脑海中。 不要自己运行
silencedetect,也不要凭感觉选择 dB 阈值。两个经过测量的事实淘汰了这种工作流:- 正确的阈值是文件的属性,而不是常数。在一个真实文件上,-40dB 发现 0 个静音,-30dB 发现 13 个,-25dB 发现 21 个。硬编码的数字要么无用,要么会吞噬语音。
- 仅靠响度无法区分“停顿”和“轻声说话”。在测量的源文件中,
silencedetect在 -30dB 时报告了 48.6 秒的“静音”,但其中只有 18.2 秒位于词语之间的真正间隙中;其他 30.4 秒在词语内部(音节边界、不发音的越南语尾音 c/t/p/ch)。仅根据音量进行裁剪会吞掉语音。 服务器负责测量(autoTrim.ts)和候选片段生成(deadWeight.ts)。您负责审查。 这种分工正是其核心所在。
- 两种类型的冗余内容会被裁剪:静音(机器测量,由字幕守卫)和内容死重(填充词、口吃、重述片段 - 由服务器提出,由您批准;加上重复的观点,只有您能发现 - 参见第 3 步)。
第 0 步 - 先决条件:包含单词时间戳的字幕
整个守卫功能依赖于它。服务器会查找 assets/transcript.raw.json,然后是 assets/transcript.json。如果没有字幕,分析仍然会运行,但返回的结果是 guarded: false,死重列表为空,并且数字在两个方向上都是猜测(它可能通过一个坏的裁剪,也可能拒绝一个好的裁剪)。请先转录 - 切勿在没有守卫的情况下修剪越南语说话人头视频。
第 1 步 - 分析(免费、可重复、不编码)
POST http://localhost:6869/api/projects/<id>/auto-trim/analyze
body: {} # 或 { "source": "assets/face.mp4", "level": "tight" }
level 是 natural | default | tight,默认值为 brief.autoCutLevel。响应:
silence-durationSec,选择的thresholdDb+thresholdNote(为什么该阈值胜出),测量的noiseFloorDb,将要裁剪的silences,keepRanges,removedSec,完整的sweep表,以及wordGuard(字幕否决了多少秒)。deadWeight-candidates[],包含kind(filler | stutter | repeat-take | hesitation),start/end,text,confidence,reason,context;以及totalSec和byKind。guarded- 仅当找到字幕时为true。如果为false,请先修复此问题再裁剪。
不会进行编码,因此您可以随意调用(一个 217 秒的源文件大约在一秒内完成分析)。
第 2 步 - 审查每个候选片段(您的工作,而非机器的)
列表是确定性的:相同的字幕,相同的候选片段,相同的置信度。机器无法做到的是理解含义。请逐一检查它们,只保留您愿意为之辩护的那些:
- 低置信度意味着“先阅读上下文”,而不是“可能没问题”。 越南语填充词几乎总是与真实词语冲突。
đó,ấy,thế,mà,là既是填充词,也是真实的指示词/连词。 - 连接短语(
hoặc là,tức là,bởi vì là,với lại là)是陷阱。 在一个真实字幕上测量:“…có thể là ok ứng dụng nó / Hoặc là / Tham khảo để tìm cách…” 是一个真正的替代方案 - 裁剪它会破坏句子的一支。但“Còn trường hợp mà mọi người có thể nghe / Hoặc là / Còn trường hợp mà người AI không ứng dụng được…” 是一个被放弃的句子,应该被裁剪。表面形式相同;只有含义将它们区分开。正是因为这个原因,这些候选片段会以低基础置信度返回,并附上写着“阅读上下文”的理由。 repeat-take候选片段:在批准之前,检查后来的片段是否真的是更完整的版本。hesitation候选片段是词语之间的纯静音 - 通常安全,但请检查您是否没有移除一个笑话前的故意停顿。
自由地拒绝。保留一个填充词的成本是 0.4 秒;裁掉一个真实单词会使句子变得无意义,观众会立即察觉到。
第 3 步 - 重复观点(只有您能发现这些 - 强制性)
没有检测器能捕捉到这一点,而这是用户抱怨最多的冗余内容。将字幕作为一段口头语言阅读,并从语义上进行分析:
- 将表达相同观点的句子分组 - 词语不需要匹配,只需要内容相同。说话者经常将同一观点重述 2-3 次:先简短/结巴,后更完整。也要考虑非相邻句子(提出观点 A,离题,然后更详细地回到观点 A)。
- 每组只保留一个版本 - 最完整的那个:
- 早期简短版本,后期更完整的重述 -> 保留后期版本,裁剪早期版本。
- 后期句子只是一个简短的回声(“đúng vậy,như tôi nói…”)-> 保留早期版本,裁剪回声。
- 两个等效版本 -> 保留更流畅的表达(更少的结巴,无填充词)。
- 检查裁剪后的流畅性:从头到尾重读保留的文本。如果一个保留的句子指回(“như vừa nói”)一个被裁剪的句子,要么两者都保留,要么选择没有反向指代的版本。
- 将每个决定转化为一个
{start, end}范围,并在第 4 步中与批准的候选片段一起发送。 - 在裁剪前构建一个表格(将其放入报告中):
时间戳 | 被裁剪的句子 | 原因 | 保留的句子- 以便用户可以审查每一个决定。
第 4 步 - 应用(通过渲染队列)
POST http://localhost:6869/api/projects/<id>/auto-trim/apply
body: { "cutCandidates": [{ "start": 51.81, "end": 52.25 }, …] } # 仅限您批准的内容
-> 202 { job }
如果您没有批准任何内容,则发送空列表 - 测量的静音仍然会被裁剪。然后轮询 GET /api/jobs/<jobId> 直到它完成。该任务:
- 使用字幕守卫重新分析,将静音范围与您批准的范围合并(重叠部分合并,因此不会重复计算)。
- 丢弃任何会吞掉单词中点的已批准范围 - 这是对您审查的最后一道安全网,并记录它拒绝的每个范围。日志中被拒绝的范围意味着您批准了位于真实语音上的内容;请返回并重新阅读该位置。
- 在一次 ffmpeg 过程中裁剪(
trim/atrim + setpts/asetpts + concat),写入assets/<stem>.cut.mp4。 - 将每个时间戳重新映射到裁剪后的时间线 ->
assets/transcript.cut.json。 - 使用重新映射的单词验证输出,并写入
assets/auto-trim-report.json。
如果没有任何值得裁剪的内容,该任务故意不会生成 .cut.mp4(重新编码的相同副本只会损失画质)- 报告中的 output 为 null,您继续使用源文件。
第 5 步 - 阅读报告(直到您阅读报告,任务才算完成)
assets/auto-trim-report.json 包含:裁剪前后的时长,removed.silenceSec 与 removed.approvedSec,选择的阈值及其原因,被拒绝的候选片段,以及 verification。
verdict: "pass"- 结果符合该级别的配置文件。verdict: "fail"- 任务仍然完成,文件可用,但不符合配置文件。日志会明确说明。批准更多的死重(第 2/3 步)并再次运行 apply,或者在最终报告中说明您接受此结果的原因。切勿在忽视失败的情况下报告裁剪已完成。
通过标准不是旧的“无静音 > 0.8s”规则 - 测量表明该规则过于宽松。一个真实文件通过了该规则,同时携带了 13 个持续 0.45-0.67 秒的残留静音,总共 6.7 秒(运行时长的 4.2%),听起来明显拖沓。现在的配置文件同时限制了最长单个静音和总比例(default:0.5 秒 / 3%)。
第 6 步 - 在所有地方使用裁剪后的版本
从此以后,字幕、关键布局、音效时间和缩放都读取 assets/transcript.cut.json 和 .cut.mp4。QC 检查 dead-air 会重新测量组装后的视频与字幕的对比,并且当 brief.autoCut 开启且死空气仍然超过配置文件时失败 - 这是后盾,而不是计划。
最终报告必须包含:第 3 步的裁剪表格,按静音与批准的死重分割的已移除秒数,以及验证结果。数字取自 auto-trim-report.json。
已知问题
- 裁剪后使用旧字幕 -> 字幕在结尾处同步偏差越来越大。始终切换到
transcript.cut.json。 - 仅裁剪静音而忽略重复观点 - 用户所说的“冗长内容”大部分是第 3 步。
- 裁剪渲染后的草稿而不是源文件 -> 质量因第二次编码而下降。始终从源文件裁剪(服务器拒绝覆盖源文件,并在选择默认值时跳过已命名为
*.cut.*的文件)。 - 分析
.cut.mp4而唯一的字幕是原始字幕 -> 守卫与错误的时间线对齐。确保源文件与描述它的字幕匹配。 - 带有自身前导静音的音效/旁白文件 -> 使用
data-media-start在 HyperFrames 中进行修剪(参见 noti-tiktok-vn 技能中的“音效响度和前导静音”),不要仅仅为了 0.3 秒的前导静音而重新编码音频文件。 - 连接裁剪段时没有淡入淡出 -> 在每个连接处产生咔嗒声。已在
autoTrim.ts中修复;规则如下,适用于任何裁剪或连接音频的新代码。
音频:在每个裁剪边缘淡入淡出 30 毫秒(硬性规则)
一个裁剪落在波形中间。一个片段的最后一个样本和下一个片段的第一个样本没有关联,因此连接处是一个垂直台阶,而一个台阶就是一个咔嗒声。原因不在于编码器,也不在于源文件 - 而在于连接本身,因此它会在下游任何重新编码中幸存。
它的重要性取决于裁剪落在何处,并且差异很大。 在一个真实的 278 秒说话人头视频上以无损方式测量,比较相同裁剪有无淡入淡出的效果(连接处最大的样本间台阶;任何高于约 0.02 都是可听的):
| 连接类型 | 之前 | 之后 |
|---|---|---|
静音修剪,40 个连接(balanced) | 中位数 0.0040,最差 0.0113 - 无一听得见 | 中位数 0.0000 |
| 语音中间,4 个连接(死重 / 重复观点) | 0.1195, 0.0744, 0.0232, 0.0064 - 4 个中有 3 个听得见 | 所有 ≤ 0.0008 |
作为参考,在这些文件中的正常语音中,任何地方最大的台阶是 0.06-0.13。因此,没有淡入淡出的语音中间连接可能比内容本身产生的任何跳变都大,而静音连接则远不可听。
不要因为第一行数据就跳过淡入淡出。仅静音修剪是它碰巧无关紧要的情况;第 3 步(重复观点)和片段提取直接穿过语音进行裁剪,而这些正是会产生咔嗒声的连接。
修复:在每个片段的两边淡入淡出 30 毫秒。足够长以消除台阶,又太短以至于无法被听为淡入淡出。使用 apps/server/src/util.ts 中的 audioCutFade();不要手动编写滤镜。
[0:a]atrim=start=S:end=E,asetpts=PTS-STARTPTS,afade=t=in:st=0:d=0.030,afade=t=out:st=D-0.030:d=0.030[aN]
三件事会让它不起作用:
- 顺序。
afade必须在asetpts=PTS-STARTPTS之后。afade从流自身的时钟读取st;在原始时间戳上,st=0已经过去,淡入永远不会触发。 - 长度。
D是片段持续时间,因此淡出的开始是D - 0.030。将淡出限制为D/2,否则两个淡入淡出会在短片段上重叠并吞没它。 - 提取一个片段,而不仅仅是连接。 从一个较长视频中裁剪出的片段具有相同的两个原始边缘,即使没有东西被连接 -
reframe.ts通过-af对它们进行淡入淡出。
关于自行测量的一个说明:不要比较两个 AAC 编码。重新编码会在各处扰动样本,这会将 40 个真实连接淹没在 294 个虚假差异区域中,并在两个文件之间产生 54 毫秒的偏移。相反,将两个变体直接渲染为 pcm_s16le - 然后这两个文件样本对齐,连接正好位于累积的片段长度处。