Files
jhonny-editor/code/src/tools/silence-removal.ts
T
João Henrique b541f502ba feat: initial commit - Jhonny Editor
- Adicionado estrutura completa do projeto
- Configurado MCP server para Premiere Pro
- Adicionado documentação e skills
- Configurado Gitignore para o projeto
2026-09-08 09:59:31 -04:00

72 lines
3.8 KiB
TypeScript
Executable File

export type SilenceRange = { startSeconds: number; endSeconds: number };
export type FrameRange = { startFrame: number; endFrame: number };
export type SilenceRemovalPlan = {
frameRate: number;
totalFrames: number;
removedFrames: number;
keptFrames: number;
removalRanges: FrameRange[];
keepRanges: FrameRange[];
};
function finite(value: number, label: string): number {
if (!Number.isFinite(value)) throw new Error(`${label} must be finite`);
return value;
}
export function planDerivedSilenceRemoval(input: {
durationSeconds: number;
frameRate: number;
silenceRanges: SilenceRange[];
keepHandleFrames?: number;
maximumRemovals?: number;
}): SilenceRemovalPlan {
const duration = finite(input.durationSeconds, "durationSeconds");
const frameRate = finite(input.frameRate, "frameRate");
if (duration <= 0 || frameRate <= 0) throw new Error("durationSeconds and frameRate must be positive");
if (!Array.isArray(input.silenceRanges)) throw new Error("silenceRanges must be an array");
const keepHandleFrames = input.keepHandleFrames ?? 0;
const maximumRemovals = input.maximumRemovals ?? 256;
if (!Number.isInteger(keepHandleFrames) || keepHandleFrames < 0 || keepHandleFrames > 2400) throw new Error("keepHandleFrames must be an integer between 0 and 2400");
if (!Number.isInteger(maximumRemovals) || maximumRemovals < 1 || maximumRemovals > 512) throw new Error("maximumRemovals must be an integer between 1 and 512");
const totalFrames = Math.floor(duration * frameRate + 1e-7);
if (totalFrames < 1) throw new Error("media duration is shorter than one frame");
const snappedSilenceRanges: FrameRange[] = [];
for (let index = 0; index < input.silenceRanges.length; index += 1) {
const range = input.silenceRanges[index];
const start = finite(range?.startSeconds, `silenceRanges[${index}].startSeconds`);
const end = finite(range?.endSeconds, `silenceRanges[${index}].endSeconds`);
if (start < 0 || end <= start || end > duration + 1e-7) throw new Error(`silenceRanges[${index}] is outside the media duration`);
const startFrame = Math.max(0, Math.ceil(start * frameRate - 1e-7));
const endFrame = Math.min(totalFrames, Math.floor(end * frameRate + 1e-7));
if (endFrame > startFrame) snappedSilenceRanges.push({ startFrame, endFrame });
}
snappedSilenceRanges.sort((left, right) => left.startFrame - right.startFrame || left.endFrame - right.endFrame);
const mergedSilenceRanges: FrameRange[] = [];
for (const candidate of snappedSilenceRanges) {
const previous = mergedSilenceRanges.at(-1);
if (previous && candidate.startFrame <= previous.endFrame) previous.endFrame = Math.max(previous.endFrame, candidate.endFrame);
else mergedSilenceRanges.push({ ...candidate });
}
const removalRanges: FrameRange[] = [];
for (const silence of mergedSilenceRanges) {
const startFrame = silence.startFrame + keepHandleFrames;
const endFrame = silence.endFrame - keepHandleFrames;
if (endFrame > startFrame) removalRanges.push({ startFrame, endFrame });
}
if (removalRanges.length > maximumRemovals) throw new Error(`silence plan exceeds maximumRemovals (${maximumRemovals})`);
const keepRanges: FrameRange[] = [];
let cursor = 0;
for (const removal of removalRanges) {
if (removal.startFrame > cursor) keepRanges.push({ startFrame: cursor, endFrame: removal.startFrame });
cursor = removal.endFrame;
}
if (cursor < totalFrames) keepRanges.push({ startFrame: cursor, endFrame: totalFrames });
if (!keepRanges.length) throw new Error("silence plan would remove the entire source");
const removedFrames = removalRanges.reduce((sum, range) => sum + range.endFrame - range.startFrame, 0);
return { frameRate, totalFrames, removedFrames, keptFrames: totalFrames - removedFrames, removalRanges, keepRanges };
}