#!/usr/bin/env node /** * Aplica um plano de cortes de silêncio (gerado pelo SilenceCutter em Python) * numa sequência do Premiere Pro, reaproveitando o bridge já usado pelo * servidor MCP (dist/tools/*.js). * * Fase A: divide (split) vídeo e áudio em todos os limites de corte, usando * as coordenadas originais do plano (splits não deslocam nada). * Fase B: remove os cortes em ordem DECRESCENTE de início (ripple=true por * track), para que cada remoção só desloque conteúdo já processado. * * Uso: * node scripts/apply-silence-cuts.mjs --plan --sequence "" * [--video-track 0] [--audio-track 0] [--dry-run] */ import { readFileSync } from "node:fs"; import { getTimelineTools } from "../dist/tools/timeline.js"; import { getDiscoveryTools } from "../dist/tools/discovery.js"; import { getProjectTools } from "../dist/tools/project.js"; // Progresso para a barra do painel CEP: linhas @@PROGRESS em stderr, que o painel // intercepta em vez de despejar no log. Fases A/B/C viram uma barra única. function reportProgress(stage, pct, detail) { const payload = { stage }; if (typeof pct === "number" && Number.isFinite(pct)) payload.pct = Math.max(0, Math.min(100, Math.round(pct * 10) / 10)); if (detail) payload.detail = detail; console.error("@@PROGRESS " + JSON.stringify(payload)); } function parseArgs(argv) { const args = { videoTrack: 0, audioTrack: 0, dryRun: false }; for (let i = 0; i < argv.length; i++) { const a = argv[i]; if (a === "--plan") args.plan = argv[++i]; else if (a === "--sequence") args.sequence = argv[++i]; else if (a === "--video-track") args.videoTrack = Number(argv[++i]); else if (a === "--audio-track") args.audioTrack = Number(argv[++i]); else if (a === "--dry-run") args.dryRun = true; } if (!args.plan || !args.sequence) { console.error("Uso: --plan --sequence [--video-track N] [--audio-track N] [--dry-run]"); process.exit(1); } return args; } function unwrap(result, label) { if (!result || result.success === false) { throw new Error(`${label} falhou: ${result?.error ?? JSON.stringify(result)}`); } return result.data ?? result; } async function main() { const args = parseArgs(process.argv.slice(2)); const plan = JSON.parse(readFileSync(args.plan, "utf-8")); const cuts = plan.cuts; if (!Array.isArray(cuts) || cuts.length === 0) { console.log(JSON.stringify({ ok: true, message: "Nenhum corte no plano." })); return; } const bridgeOptions = {}; reportProgress("Conectando ao Premiere", 0); const timeline = getTimelineTools(bridgeOptions); const discovery = getDiscoveryTools(bridgeOptions); const project = getProjectTools(bridgeOptions); console.error(`[apply-silence-cuts] sequência="${args.sequence}" cortes=${cuts.length} dryRun=${args.dryRun}`); if (args.dryRun) { console.log(JSON.stringify({ ok: true, dryRun: true, numCuts: cuts.length, plan: plan.summary }, null, 2)); return; } unwrap(await project.set_active_sequence.handler({ sequence_id: args.sequence }), "set_active_sequence"); // Fase A: splits em todos os limites, coordenadas originais (ordem não importa) console.error(`[apply-silence-cuts] Fase A: ${cuts.length * 4} splits...`); reportProgress("Dividindo a timeline", 0, `0 de ${cuts.length} cortes`); for (let i = 0; i < cuts.length; i++) { const { start, end } = cuts[i]; for (const time_seconds of [end, start]) { unwrap( await timeline.split_clip.handler({ time_seconds, track_index: args.videoTrack, track_type: "video" }), `split_clip video @${time_seconds}` ); unwrap( await timeline.split_clip.handler({ time_seconds, track_index: args.audioTrack, track_type: "audio" }), `split_clip audio @${time_seconds}` ); } reportProgress("Dividindo a timeline", ((i + 1) / cuts.length) * 50, `${i + 1} de ${cuts.length} cortes`); if ((i + 1) % 10 === 0 || i === cuts.length - 1) { console.error(`[apply-silence-cuts] Fase A: ${i + 1}/${cuts.length} cortes divididos`); } } // Fase B: remoções em ordem decrescente de start const ordered = [...cuts].sort((a, b) => b.start - a.start); console.error(`[apply-silence-cuts] Fase B: removendo ${ordered.length} cortes (ordem decrescente)...`); let removed = 0; for (const cut of ordered) { const mid = (cut.start + cut.end) / 2; const videoClip = unwrap( await discovery.get_clip_at_position.handler({ time_seconds: mid, track_index: args.videoTrack, track_type: "video" }), `get_clip_at_position video @${mid}` ); unwrap( await timeline.remove_from_timeline.handler({ node_id: videoClip.nodeId, ripple: true }), `remove_from_timeline video ${videoClip.nodeId}` ); const audioClip = unwrap( await discovery.get_clip_at_position.handler({ time_seconds: mid, track_index: args.audioTrack, track_type: "audio" }), `get_clip_at_position audio @${mid}` ); unwrap( await timeline.remove_from_timeline.handler({ node_id: audioClip.nodeId, ripple: true }), `remove_from_timeline audio ${audioClip.nodeId}` ); removed++; reportProgress("Removendo os silêncios", 50 + (removed / ordered.length) * 50, `${removed} de ${ordered.length} cortes`); if (removed % 10 === 0 || removed === ordered.length) { console.error(`[apply-silence-cuts] Fase B: ${removed}/${ordered.length} cortes removidos`); } } reportProgress("Conferindo o resultado", 100); const finalSeq = unwrap(await discovery.get_active_sequence.handler({}), "get_active_sequence"); console.log( JSON.stringify( { ok: true, cutsApplied: cuts.length, expectedDurationSeconds: plan.summary?.resulting_duration_seconds, finalSequenceEndSeconds: finalSeq.end, }, null, 2 ) ); } main().catch((err) => { console.error("[apply-silence-cuts] ERRO:", err.message); process.exit(1); });