Files
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

260 lines
8.8 KiB
TypeScript
Executable File

import { beforeEach, describe, expect, it, vi } from "vitest";
import { readFileSync } from "node:fs";
import { join } from "node:path";
vi.mock("../../src/bridge/file-bridge.js", () => ({
sendCommand: vi.fn().mockResolvedValue({ success: true, data: {} }),
}));
import { sendCommand } from "../../src/bridge/file-bridge.js";
import {
getAudioTools,
invertToSegments,
mapSilenceIntervalsToTimeline,
parseDurationSeconds,
parseSilenceDetectOutput,
} from "../../src/tools/audio.js";
const mockedSendCommand = vi.mocked(sendCommand);
const tools = getAudioTools({ tempDir: "/tmp/test", timeoutMs: 1000 });
beforeEach(() => vi.clearAllMocks());
describe("parseSilenceDetectOutput", () => {
it("pairs each silence_start with its silence_end", () => {
const stderr = [
"[silencedetect @ 0x1] silence_start: 2.5",
"[silencedetect @ 0x1] silence_end: 6.25 | silence_duration: 3.75",
"[silencedetect @ 0x1] silence_start: 10",
"[silencedetect @ 0x1] silence_end: 12.5 | silence_duration: 2.5",
].join("\n");
expect(parseSilenceDetectOutput(stderr, 20)).toEqual([
{ start: 2.5, end: 6.25, duration: 3.75 },
{ start: 10, end: 12.5, duration: 2.5 },
]);
});
it("closes a silence that runs to the end of the file at the media duration", () => {
// ffmpeg emits no silence_end when the file ends mid-silence.
const stderr = [
"[silencedetect @ 0x1] silence_start: 2",
"[silencedetect @ 0x1] silence_end: 5 | silence_duration: 3",
"[silencedetect @ 0x1] silence_start: 17.5",
].join("\n");
expect(parseSilenceDetectOutput(stderr, 20)).toEqual([
{ start: 2, end: 5, duration: 3 },
{ start: 17.5, end: 20, duration: 2.5 },
]);
});
it("drops an unterminated silence when the duration is unknown", () => {
const stderr = "[silencedetect @ 0x1] silence_start: 17.5";
expect(parseSilenceDetectOutput(stderr, null)).toEqual([]);
});
it("clamps a negative silence_start to zero", () => {
const stderr = [
"[silencedetect @ 0x1] silence_start: -0.001",
"[silencedetect @ 0x1] silence_end: 4 | silence_duration: 4",
].join("\n");
expect(parseSilenceDetectOutput(stderr, 20)[0].start).toBe(0);
});
it("returns nothing for output containing no silence markers", () => {
expect(parseSilenceDetectOutput("frame= 480 fps=0.0 q=-0.0", 20)).toEqual([]);
});
});
describe("parseDurationSeconds", () => {
it("reads the duration banner", () => {
expect(parseDurationSeconds(" Duration: 00:00:20.00, start: 0.000000")).toBe(20);
});
it("handles hours and fractional seconds", () => {
expect(parseDurationSeconds(" Duration: 01:02:03.50, bitrate: 1000 kb/s")).toBeCloseTo(3723.5, 3);
});
it("returns null when no duration is present", () => {
expect(parseDurationSeconds("no banner here")).toBeNull();
});
});
describe("invertToSegments", () => {
it("returns the ranges between silences", () => {
const silences = [
{ start: 2, end: 5, duration: 3 },
{ start: 10, end: 12, duration: 2 },
];
expect(invertToSegments(silences, 20)).toEqual([
{ start: 0, end: 2, duration: 2 },
{ start: 5, end: 10, duration: 5 },
{ start: 12, end: 20, duration: 8 },
]);
});
it("omits a leading segment when the media opens on silence", () => {
const silences = [{ start: 0, end: 4, duration: 4 }];
expect(invertToSegments(silences, 20)).toEqual([
{ start: 4, end: 20, duration: 16 },
]);
});
it("omits a trailing segment when the media ends on silence", () => {
const silences = [{ start: 16, end: 20, duration: 4 }];
expect(invertToSegments(silences, 20)).toEqual([
{ start: 0, end: 16, duration: 16 },
]);
});
it("returns the whole media when there is no silence", () => {
expect(invertToSegments([], 20)).toEqual([
{ start: 0, end: 20, duration: 20 },
]);
});
it("returns nothing usable when the duration is unknown", () => {
expect(invertToSegments([], null)).toEqual([]);
});
it("collapses fully silent media to no segments", () => {
const silences = [{ start: 0, end: 20, duration: 20 }];
expect(invertToSegments(silences, 20)).toEqual([]);
});
});
describe("mapSilenceIntervalsToTimeline", () => {
it("clips source ranges and maps the retained intervals onto a 1x placement", () => {
expect(mapSilenceIntervalsToTimeline([
{ start: 0, end: 4, duration: 4 },
{ start: 7, end: 12, duration: 5 },
], {
sourceInSeconds: 2,
sourceOutSeconds: 10,
timelineStartSeconds: 100,
maxCandidates: 50,
})).toEqual({
totalCandidateCount: 2,
truncated: false,
candidates: [
{
sourceStartSeconds: 2,
sourceEndSeconds: 4,
durationSeconds: 2,
timelineStartSeconds: 100,
timelineEndSeconds: 102,
},
{
sourceStartSeconds: 7,
sourceEndSeconds: 10,
durationSeconds: 3,
timelineStartSeconds: 105,
timelineEndSeconds: 108,
},
],
});
});
it("keeps counting bounded candidates after its response limit", () => {
const result = mapSilenceIntervalsToTimeline([
{ start: 1, end: 2, duration: 1 },
{ start: 3, end: 4, duration: 1 },
], {
sourceInSeconds: 0,
sourceOutSeconds: 10,
timelineStartSeconds: 0,
maxCandidates: 1,
});
expect(result.totalCandidateCount).toBe(2);
expect(result.truncated).toBe(true);
expect(result.candidates).toHaveLength(1);
});
});
describe("detect_silence argument handling", () => {
it("requires either a media path or a project item", async () => {
const result = await tools.detect_silence.handler({});
expect(result.success).toBe(false);
expect(result.error).toContain("media_path or project_item_id");
expect(mockedSendCommand).not.toHaveBeenCalled();
});
it("rejects a positive noise threshold", async () => {
const result = await tools.detect_silence.handler({
media_path: "/tmp/whatever.mp4",
noise_threshold_db: 12,
});
expect(result.success).toBe(false);
expect(result.error).toContain("dBFS");
});
it("rejects a non-positive minimum duration", async () => {
const result = await tools.detect_silence.handler({
media_path: "/tmp/whatever.mp4",
min_duration_seconds: 0,
});
expect(result.success).toBe(false);
expect(result.error).toContain("greater than 0");
});
it("reports a missing file before shelling out to ffmpeg", async () => {
const result = await tools.detect_silence.handler({
media_path: "/tmp/definitely-not-here-9f3a2b.mp4",
});
expect(result.success).toBe(false);
expect(result.error).toContain("not found on disk");
});
it("resolves a project item's media path through the bridge", async () => {
mockedSendCommand.mockResolvedValueOnce({
success: false,
error: "Project item not found: missing-clip",
});
const result = await tools.detect_silence.handler({
project_item_id: "missing-clip",
});
expect(mockedSendCommand).toHaveBeenCalledTimes(1);
expect(mockedSendCommand.mock.calls[0][0]).toContain("getMediaPath");
expect(result.success).toBe(false);
expect(result.error).toContain("missing-clip");
});
});
describe("plan_silence_review_markers argument handling", () => {
const plan = tools.plan_silence_review_markers;
it("rejects invalid placement or response bounds before decoding media", async () => {
await expect(plan.handler({
media_path: "/tmp/does-not-matter.mov",
timeline_start_seconds: -1,
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("timeline_start_seconds") });
await expect(plan.handler({
media_path: "/tmp/does-not-matter.mov",
timeline_start_seconds: 0,
source_in_seconds: 5,
source_out_seconds: 5,
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("source_out_seconds") });
await expect(plan.handler({
media_path: "/tmp/does-not-matter.mov",
timeline_start_seconds: 0,
max_candidates: 201,
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("max_candidates") });
expect(mockedSendCommand).not.toHaveBeenCalled();
});
});
describe("detect_silence distribution prerequisites", () => {
it("installs ffmpeg in the production container", () => {
const dockerfile = readFileSync(join(process.cwd(), "Dockerfile"), "utf8");
expect(dockerfile).toMatch(/apk add --no-cache ffmpeg/);
});
it("documents the local ffmpeg prerequisite", () => {
const readme = readFileSync(join(process.cwd(), "README.md"), "utf8");
expect(readme).toContain("`detect_silence`");
expect(readme).toContain("winget install Gyan.FFmpeg");
expect(readme).toContain("brew install ffmpeg");
});
});