- Adicionado estrutura completa do projeto - Configurado MCP server para Premiere Pro - Adicionado documentação e skills - Configurado Gitignore para o projeto
95 lines
4.4 KiB
TypeScript
Executable File
95 lines
4.4 KiB
TypeScript
Executable File
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() }));
|
|
vi.mock("node:child_process", () => ({ execFile: mockedExecFile }));
|
|
|
|
import { analyzeBeatPcm, getAudioTools } from "../../src/tools/audio.js";
|
|
|
|
const tools = getAudioTools({ tempDir: "/tmp/beat-tests" });
|
|
|
|
beforeEach(() => vi.clearAllMocks());
|
|
|
|
function pulsePcm(seconds = 12) {
|
|
const sampleRate = 200;
|
|
const samples = new Int16Array(sampleRate * seconds);
|
|
for (let at = 0; at < samples.length; at += sampleRate / 2) {
|
|
for (let width = 0; width < 4; width++) samples[at + width] = 30_000;
|
|
}
|
|
return { sampleRate, samples };
|
|
}
|
|
|
|
function sampleBuffer(samples: Int16Array) {
|
|
const buffer = Buffer.alloc(samples.length * 2);
|
|
for (let index = 0; index < samples.length; index++) buffer.writeInt16LE(samples[index], index * 2);
|
|
return buffer;
|
|
}
|
|
|
|
function createMediaFixture() {
|
|
const directory = mkdtempSync(join(tmpdir(), "premiere-beats-"));
|
|
const mediaPath = join(directory, "music.wav");
|
|
writeFileSync(mediaPath, "audio-fixture");
|
|
return mediaPath;
|
|
}
|
|
|
|
describe("detect_beats analysis", () => {
|
|
it("recovers a phase-aligned 120 BPM pulse train", () => {
|
|
const { sampleRate, samples } = pulsePcm();
|
|
const result = analyzeBeatPcm(samples, sampleRate);
|
|
expect(result.bpm).toBeCloseTo(120, 0);
|
|
expect(result.confidence).toBeGreaterThan(0.5);
|
|
expect(result.beatTimesSeconds.slice(0, 4)).toEqual([0, 0.5, 1, 1.5]);
|
|
});
|
|
|
|
it("rejects short or silent audio instead of fabricating a grid", () => {
|
|
expect(() => analyzeBeatPcm(new Int16Array(100), 200)).toThrow("at least two seconds");
|
|
expect(() => analyzeBeatPcm(new Int16Array(800), 200)).toThrow("No repeating beat evidence");
|
|
});
|
|
|
|
it("validates each input before invoking FFmpeg", async () => {
|
|
await expect(tools.detect_beats.handler({ media_path: "" }))
|
|
.resolves.toMatchObject({ success: false, error: "media_path is required." });
|
|
await expect(tools.detect_beats.handler({ media_path: "missing.wav", max_beats: 0 }))
|
|
.resolves.toMatchObject({ success: false, error: expect.stringContaining("max_beats") });
|
|
await expect(tools.detect_beats.handler({ media_path: "definitely-missing.wav" }))
|
|
.resolves.toMatchObject({ success: false, error: expect.stringContaining("not found") });
|
|
expect(mockedExecFileAsync).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("decodes a bounded stream and reports truncation without mutating Premiere", async () => {
|
|
const mediaPath = createMediaFixture();
|
|
mockedExecFileAsync.mockResolvedValueOnce({ stdout: sampleBuffer(pulsePcm().samples), stderr: Buffer.alloc(0) });
|
|
await expect(tools.detect_beats.handler({ media_path: mediaPath, max_beats: 3 })).resolves.toMatchObject({
|
|
success: true,
|
|
data: { bpm: 120, beatTimesSeconds: [0, 0.5, 1], beatTimesTruncated: true, reliable: true },
|
|
});
|
|
expect(mockedExecFileAsync).toHaveBeenCalledTimes(1);
|
|
const args = mockedExecFileAsync.mock.calls[0][1] as string[];
|
|
expect(args).toEqual(expect.arrayContaining(["-t", "1800", "-ar", "200", "pipe:1"]));
|
|
});
|
|
|
|
it("distinguishes missing FFmpeg, timeout, and decode failures", async () => {
|
|
const mediaPath = createMediaFixture();
|
|
mockedExecFileAsync.mockRejectedValueOnce(Object.assign(new Error("missing"), { code: "ENOENT" }));
|
|
await expect(tools.detect_beats.handler({ media_path: mediaPath }))
|
|
.resolves.toMatchObject({ success: false, error: expect.stringContaining("not found on PATH") });
|
|
|
|
mockedExecFileAsync.mockRejectedValueOnce(Object.assign(new Error("timeout"), { killed: true }));
|
|
await expect(tools.detect_beats.handler({ media_path: mediaPath }))
|
|
.resolves.toMatchObject({ success: false, error: expect.stringContaining("timed out") });
|
|
|
|
mockedExecFileAsync.mockRejectedValueOnce(Object.assign(new Error("decode"), { stderr: "invalid stream" }));
|
|
await expect(tools.detect_beats.handler({ media_path: mediaPath }))
|
|
.resolves.toMatchObject({ success: false, error: expect.stringContaining("invalid stream") });
|
|
});
|
|
});
|