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
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
import { mkdtempSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { mockedExecFile, mockedExecFileAsync } = vi.hoisted(() => {
|
||||
const mockedExecFileAsync = vi.fn();
|
||||
const mockedExecFile = vi.fn();
|
||||
mockedExecFile[Symbol.for("nodejs.util.promisify.custom")] = mockedExecFileAsync;
|
||||
return { mockedExecFile, mockedExecFileAsync };
|
||||
});
|
||||
|
||||
vi.mock("../../src/bridge/file-bridge.js", () => ({
|
||||
sendCommand: vi.fn().mockResolvedValue({ success: true, data: {} }),
|
||||
}));
|
||||
|
||||
vi.mock("node:child_process", () => ({ execFile: mockedExecFile }));
|
||||
|
||||
import { sendCommand } from "../../src/bridge/file-bridge.js";
|
||||
import { getAudioTools, parseEbur128Summary } from "../../src/tools/audio.js";
|
||||
|
||||
const mockedSendCommand = vi.mocked(sendCommand);
|
||||
const tools = getAudioTools({ tempDir: "/tmp/test", timeoutMs: 1000 });
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
function enqueueFfmpegResult(stderr = "") {
|
||||
mockedExecFileAsync.mockResolvedValueOnce({ stdout: "", stderr });
|
||||
}
|
||||
|
||||
function enqueueFfmpegFailure(error: Error) {
|
||||
mockedExecFileAsync.mockRejectedValueOnce(error);
|
||||
}
|
||||
|
||||
function createMediaFixture() {
|
||||
const directory = mkdtempSync(join(tmpdir(), "premiere-loudness-"));
|
||||
const mediaPath = join(directory, "dialogue.wav");
|
||||
writeFileSync(mediaPath, "audio-fixture");
|
||||
return mediaPath;
|
||||
}
|
||||
|
||||
const measurableSummary = `
|
||||
Summary:
|
||||
Integrated loudness:
|
||||
I: -16.2 LUFS
|
||||
Loudness range:
|
||||
LRA: 4.1 LU
|
||||
True peak:
|
||||
Peak: -1.3 dBFS
|
||||
`;
|
||||
|
||||
describe("parseEbur128Summary", () => {
|
||||
it("extracts the final integrated loudness, range, and true peak", () => {
|
||||
const output = `
|
||||
Summary:
|
||||
|
||||
Integrated loudness:
|
||||
I: -16.2 LUFS
|
||||
Threshold: -26.2 LUFS
|
||||
|
||||
Loudness range:
|
||||
LRA: 4.1 LU
|
||||
|
||||
True peak:
|
||||
Peak: -1.3 dBFS
|
||||
`;
|
||||
expect(parseEbur128Summary(output)).toEqual({
|
||||
integratedLufs: -16.2,
|
||||
loudnessRangeLu: 4.1,
|
||||
truePeakDbfs: -1.3,
|
||||
});
|
||||
});
|
||||
|
||||
it("reports digital silence as unmeasurable instead of fabricating a number", () => {
|
||||
const output = `Integrated loudness:\n I: -inf LUFS\nLoudness range:\n LRA: 0.0 LU\nTrue peak:\n Peak: -inf dBFS`;
|
||||
expect(parseEbur128Summary(output)).toEqual({
|
||||
integratedLufs: null,
|
||||
loudnessRangeLu: 0,
|
||||
truePeakDbfs: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("analyze_loudness argument boundary", () => {
|
||||
it("rejects invalid targets before contacting Premiere or FFmpeg", async () => {
|
||||
await expect(tools.analyze_loudness.handler({ media_path: "missing.wav", target_lufs: 4 }))
|
||||
.resolves.toMatchObject({ success: false, error: expect.stringContaining("target_lufs") });
|
||||
await expect(tools.analyze_loudness.handler({ media_path: "missing.wav", tolerance_lu: -1 }))
|
||||
.resolves.toMatchObject({ success: false, error: expect.stringContaining("tolerance_lu") });
|
||||
await expect(tools.analyze_loudness.handler({ media_path: "missing.wav", max_true_peak_dbfs: 1 }))
|
||||
.resolves.toMatchObject({ success: false, error: expect.stringContaining("max_true_peak_dbfs") });
|
||||
await expect(tools.analyze_loudness.handler({}))
|
||||
.resolves.toMatchObject({ success: false, error: expect.stringContaining("media_path") });
|
||||
expect(mockedSendCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("resolves project-item paths through one read-only bridge call", async () => {
|
||||
mockedSendCommand.mockResolvedValueOnce({ success: false, error: "Project item not found" });
|
||||
const result = await tools.analyze_loudness.handler({ project_item_id: "missing" });
|
||||
expect(mockedSendCommand).toHaveBeenCalledTimes(1);
|
||||
expect(mockedSendCommand.mock.calls[0][0]).toContain("getMediaPath");
|
||||
expect(result).toMatchObject({ success: false, error: "Project item not found" });
|
||||
});
|
||||
|
||||
it("returns a local EBU R128 measurement and evaluates an optional delivery target", async () => {
|
||||
const mediaPath = createMediaFixture();
|
||||
enqueueFfmpegResult();
|
||||
enqueueFfmpegResult(measurableSummary);
|
||||
|
||||
await expect(tools.analyze_loudness.handler({
|
||||
media_path: mediaPath,
|
||||
target_lufs: -16,
|
||||
tolerance_lu: 0.5,
|
||||
max_true_peak_dbfs: -1,
|
||||
})).resolves.toMatchObject({
|
||||
success: true,
|
||||
data: {
|
||||
mediaPath,
|
||||
integratedLufs: -16.2,
|
||||
loudnessRangeLu: 4.1,
|
||||
truePeakDbfs: -1.3,
|
||||
target: {
|
||||
lufs: -16,
|
||||
toleranceLu: 0.5,
|
||||
deltaLu: -0.2,
|
||||
loudnessPass: true,
|
||||
truePeakPass: true,
|
||||
passes: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockedExecFileAsync).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("resolves a project item to a local file before measuring it", async () => {
|
||||
const mediaPath = createMediaFixture();
|
||||
mockedSendCommand.mockResolvedValueOnce({ success: true, data: { mediaPath, name: "Dialogue" } });
|
||||
enqueueFfmpegResult();
|
||||
enqueueFfmpegResult(measurableSummary);
|
||||
|
||||
await expect(tools.analyze_loudness.handler({ project_item_id: "dialogue-item" }))
|
||||
.resolves.toMatchObject({ success: true, data: { mediaPath, projectItemName: "Dialogue", target: null } });
|
||||
expect(mockedSendCommand).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("reports a failed target and true-peak check without changing the media", async () => {
|
||||
const mediaPath = createMediaFixture();
|
||||
enqueueFfmpegResult();
|
||||
enqueueFfmpegResult(measurableSummary);
|
||||
|
||||
await expect(tools.analyze_loudness.handler({
|
||||
media_path: mediaPath,
|
||||
target_lufs: -14,
|
||||
tolerance_lu: 0.1,
|
||||
max_true_peak_dbfs: -2,
|
||||
})).resolves.toMatchObject({
|
||||
success: true,
|
||||
data: {
|
||||
target: {
|
||||
loudnessPass: false,
|
||||
truePeakPass: false,
|
||||
passes: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("reports unavailable analysis prerequisites, timeouts, and digital silence without making up a metric", async () => {
|
||||
const mediaPath = createMediaFixture();
|
||||
enqueueFfmpegFailure(new Error("not found"));
|
||||
await expect(tools.analyze_loudness.handler({ media_path: mediaPath }))
|
||||
.resolves.toMatchObject({ success: false, error: expect.stringContaining("not found on PATH") });
|
||||
|
||||
enqueueFfmpegResult();
|
||||
enqueueFfmpegFailure(Object.assign(new Error("timed out"), { killed: true }));
|
||||
await expect(tools.analyze_loudness.handler({ media_path: mediaPath }))
|
||||
.resolves.toMatchObject({ success: false, error: expect.stringContaining("timed out") });
|
||||
|
||||
enqueueFfmpegResult();
|
||||
enqueueFfmpegResult("Integrated loudness:\n I: -inf LUFS");
|
||||
await expect(tools.analyze_loudness.handler({ media_path: mediaPath }))
|
||||
.resolves.toMatchObject({ success: false, error: expect.stringContaining("did not return measurable") });
|
||||
});
|
||||
|
||||
it("does not invoke FFmpeg for an unavailable file and preserves an analysis error detail", async () => {
|
||||
await expect(tools.analyze_loudness.handler({ media_path: "missing-dialogue.wav" }))
|
||||
.resolves.toMatchObject({ success: false, error: expect.stringContaining("Media file not found") });
|
||||
expect(mockedExecFileAsync).not.toHaveBeenCalled();
|
||||
|
||||
const mediaPath = createMediaFixture();
|
||||
enqueueFfmpegResult();
|
||||
enqueueFfmpegFailure(Object.assign(new Error("decode failed"), { stderr: "invalid stream" }));
|
||||
await expect(tools.analyze_loudness.handler({ media_path: mediaPath }))
|
||||
.resolves.toMatchObject({ success: false, error: expect.stringContaining("invalid stream") });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user