1仕組みと動作原理
パイプライン全体はモデルによる推論ではなく決定論的アルゴリズムに基づいて動作します。先頭・末尾のデッドエアと --gap を超える単語間のポーズのみを処理し、発話内容は一言も変更しません。
Pure pause/silence trimmer. Takes any word-level transcript (ElevenLabs Scribe … shape, or { words: [{ text, start, end }] }) and removes silences only:
先頭および末尾のデッドエアは --head-pad / --tail-pad に従って直接カットされ、2つの独立した削除区間としてマージ処理に送られます。
// head + tail dead air const headEnd = words[0].start - opts.headPad; if (headEnd > 0) deleteRanges.push({ start: 0, end: headEnd, reason: "head trim before first word" });
単語間のポーズは完全にゼロにするのではなく、文脈に応じて「自然な呼吸」の間(長めのポーズ: 0.24秒、文末: 0.20秒、その他: 0.14秒)を保持します。
if (gap < opts.gap) continue; … const sentenceBreak = /[.!?]$/.test(prev.text ?? ""); const keep = gap >= 2 ? 0.24 : sentenceBreak ? 0.2 : 0.14;
残される呼吸の間は、意図的に後続文の冒頭側に寄せられます(前文の末尾側に 55%、後続文の先頭側に 45% を配分)。
// Bias the kept breath slightly toward the end of the previous phrase. const delStart = prev.end + keep * 0.55; const delEnd = next.start - keep * 0.45;
削除区間をマージした後に補集合を計算して保持区間(keep ranges)を取得し、40ms 未満の断片的な区間は破棄します。
if (cursor < duration) keepRanges.push({ start: cursor, end: duration }); const usableKeepRanges = keepRanges.filter((r) => r.end - r.start >= 0.04);
文字起こしは同一の削除リストに基づいて編集後のタイムラインに再マッピングされ、元動画との照合用に source_start / source_end が保持されます。
const editedWords = []; for (const word of words) { if (inDeleted(word)) continue; … start: Number((word.start - removedBefore(word.start)).toFixed(3)),
レンダリングには ffmpeg の trim/atrim + concat フィルターグラフを使用します。一度 *.silence-filter.txt に書き出してから -/filter_complex 経由で渡し、デフォルトではレンダリングを実行せず計算のみを行います。
lines.push(`[0:v]trim=start=${r.start.toFixed(3)}:end=${r.end.toFixed(3)},setpts=PTS-STARTPTS[v${i}];`); … const ffArgs = [ "-y", "-i", videoPath, "-/filter_complex", scriptPath, "-map", "[v]", "-map", "[a]", "-c:v", "libx264", "-preset", "medium", "-crf", "18", "-c:a", "aac", "-b:a", "192k", "-movflags", "+faststart", outputPath, ];
2コア機能
3外部依存関係
| Kind | Dependency / Endpoint |
|---|---|
| cli | Node.js(跑 cut-silences.mjs) |
| cli | ffmpeg(trim/atrim + concat 重编码,仅 --apply 时执行) |
| cli | ffprobe(无 audio_duration_secs 时探源片时长) |
| api | ElevenLabs Scribe(**前置步骤**:由仓库级 scripts/transcribe-elevenlabs.mjs 上传音频取词级时间戳;本次侦察未在本 skill 目录内发现任何调用代码) |
| cli | 本地 Whisper / whisper.cpp(文档列出的无 key 替代转录路线) |
4推奨統制措置と判定根拠
- scripts/: ローカルスクリプトまたはコマンドの実行: node .claude/skills/cut-silences/scripts/cut-silences.mjs <transcript.json>
- 网络端点: 外部ネットワークアクセスの宣言: 前提ステップの外部エンドポイント(リポジトリレベルのヘルパーによるもので、本ディレクトリ内ではありません): api.elevenlabs.io/v1/speech-to-text(音声を POST して単語レベルの文字起こしを取得)
- 本地工作目录: ファイル読み書きおよび成果物の出力: <stem>.silence-edl.json(保持/削除区間 + パラメータ + 再生時間)
5二重目の独立検証
- [ok] 目录文件清单与 Outputs 表对照 — 目录仅 SKILL.md + scripts/cut-silences.mjs;SKILL.md 列出的 5 类产物中 3 类无条件写、filter.txt 与 mp4 受 --video/--apply 门控,与源码 writeFileSync/spawnSync 分支一致。
- [ok] 网络面反例搜索 — 目录内 0 处 fetch/URL/网络模块导入;SKILL.md 出现的 `node scripts/transcribe-elevenlabs.mjs ...` 是仓库级前置命令,文件名不含 http,且未被本脚本引用。
- [ok] 凭证面反例搜索 — 0 处 process.env/API_KEY/TOKEN/.env;脚本对 process 的导入仅 argv/exit(`import { argv, exit } from "node:process";`)。
- [ok] 「只删沉默」是否被代码强制 — 删除区间只由 head/tail pad 与 gap>opts.gap 的停顿生成三处 push 产生,源码无重复词/相似度/语义判断,与文档声明一致,无夸大。
- [ok] 重定时映射正确性 — start = word.start - removedBefore(word.start),removedBefore 只累加严格位于其前的删除量,因此不会出现负值或跳变;edited_duration = duration - removedTotal 与被删总量自洽。
- [ok] 文档参数 vs 脚本 argv — --video/--out-dir/--output/-o/--gap/--head-pad/--tail-pad/--apply 全部在 argv 解析中定位到,未知 --flag 会 die,与 SKILL.md 表格一致。
6結論
484001b000655549…