test(media): transcribe real clips through ffmpeg
Generated clips (nothing binary committed) go through extractAudio: a video's audio reaches the server as 16 kHz mono WAV, a 12-second recording is uploaded in two ranges under a small limit, and a video without sound is refused before anything is uploaded. Skipped where ffmpeg is missing, which may include CI runners. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
This commit is contained in:
parent
a02cbb5374
commit
a61aa02a8c
@ -4,15 +4,19 @@ import { join } from "node:path";
|
||||
import type { MediaJobContext, MediaJobInputFile, MediaJobOutputFile } from "@rakazo/adapter-kit";
|
||||
import { MediaJobDeferredError } from "@rakazo/adapter-kit";
|
||||
import type { MediaOutput, MediaSettings } from "@rakazo/contracts";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
||||
import { createOpenAiCompatibleTranscriptionProvider } from "../openai-compatible-transcription.js";
|
||||
import {
|
||||
EmulatorTranscriptionProvider,
|
||||
OpenAiCompatibleTranscriptionServerEmulator,
|
||||
} from "../transcription-emulator.js";
|
||||
import { detectFfmpeg, runFfmpeg } from "./ffmpeg.js";
|
||||
import type { TestClips } from "./media-test-support.js";
|
||||
import { createTestClips, fakeMediaJobContext } from "./media-test-support.js";
|
||||
import type { TranscriptionAudioTools } from "./transcription.js";
|
||||
import {
|
||||
createTranscriptionOperations,
|
||||
ffmpegTranscriptionAudio,
|
||||
planAudioChunks,
|
||||
TRANSCRIPT_SUMMARY_MAX_CHARS,
|
||||
transcribeChunks,
|
||||
@ -326,3 +330,69 @@ describe("planAudioChunks", () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
const ffmpeg = await detectFfmpeg();
|
||||
|
||||
// CI runners without ffmpeg skip these; the unit tests above cover the same paths with fakes.
|
||||
describe.skipIf(!ffmpeg)("transcribe with ffmpeg", () => {
|
||||
let clips: TestClips;
|
||||
let longTone: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
clips = await createTestClips();
|
||||
longTone = join(clips.dir, "long-tone.m4a");
|
||||
await runFfmpeg([
|
||||
...["-f", "lavfi", "-i", "sine=frequency=330:sample_rate=44100:duration=12"],
|
||||
...["-c:a", "aac", `file:${longTone}`],
|
||||
]);
|
||||
});
|
||||
afterAll(async () => {
|
||||
if (clips) await rm(clips.dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function ffmpegOperation(maxBytes?: number) {
|
||||
return operation(TRANSCRIPTION, { audio: ffmpegTranscriptionAudio, maxBytes });
|
||||
}
|
||||
|
||||
it("transcribes a video's audio as 16 kHz mono WAV", async () => {
|
||||
const { registration, server } = ffmpegOperation();
|
||||
const job = await fakeMediaJobContext(
|
||||
{ artifactId: "video-1" },
|
||||
{ "video-1": { path: clips.landscape, name: "landscape.mp4", mimeType: "video/mp4" } },
|
||||
);
|
||||
dirs.push(job.tempDir);
|
||||
await registration.handler.run(job.context);
|
||||
// Three seconds at 32,000 bytes per second plus the WAV header.
|
||||
expect(server.requests).toEqual([
|
||||
expect.objectContaining({ fileName: "landscape.wav", fileType: "audio/wav" }),
|
||||
]);
|
||||
expect(Math.abs(server.requests[0]!.fileSize - 3 * 32_000)).toBeLessThan(2_000);
|
||||
expect(job.outputs.map((output) => output.name)).toEqual(["landscape.srt", "landscape.vtt"]);
|
||||
});
|
||||
|
||||
it("uploads a long recording in ranges below the server's limit", async () => {
|
||||
const { registration, server } = ffmpegOperation(1024);
|
||||
const job = await fakeMediaJobContext(
|
||||
{ artifactId: "audio-1" },
|
||||
{ "audio-1": { path: longTone, name: "long-tone.m4a", mimeType: "audio/mp4" } },
|
||||
);
|
||||
dirs.push(job.tempDir);
|
||||
await registration.handler.run(job.context);
|
||||
expect(server.requests).toHaveLength(2);
|
||||
expect(Math.abs(server.requests[0]!.fileSize - 10 * 32_000)).toBeLessThan(2_000);
|
||||
expect(Math.abs(server.requests[1]!.fileSize - 2 * 32_000)).toBeLessThan(2_000);
|
||||
});
|
||||
|
||||
it("refuses a video without sound", async () => {
|
||||
const { registration, server } = ffmpegOperation();
|
||||
const job = await fakeMediaJobContext(
|
||||
{ artifactId: "video-2" },
|
||||
{ "video-2": { path: clips.silentPortrait, name: "silent.mp4", mimeType: "video/mp4" } },
|
||||
);
|
||||
dirs.push(job.tempDir);
|
||||
await expect(registration.handler.run(job.context)).rejects.toThrow(
|
||||
"The file has no audio track.",
|
||||
);
|
||||
expect(server.requests).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user