import { copyFile, mkdtemp, stat, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import type { MediaJobContext, MediaJobInputFile, MediaJobOutputFile } from "@rakazo/adapter-kit"; import type { MediaOutput, MediaOutputKind } from "@rakazo/contracts"; import { attachmentKindOf } from "@rakazo/contracts"; import { runFfmpeg } from "./ffmpeg.js"; export interface TestClips { dir: string; /** 320×180, 25 fps, 3 s, 440 Hz tone. */ landscape: string; /** 180×320, 30 fps, 2 s, no audio. */ silentPortrait: string; srt: string; vtt: string; } /** Tiny generated clips; nothing binary is committed. */ export async function createTestClips(): Promise { const dir = await mkdtemp(join(tmpdir(), "evoo-media-test-")); const landscape = join(dir, "landscape.mp4"); const silentPortrait = join(dir, "silent-portrait.mp4"); const encode = ["-c:v", "libx264", "-preset", "ultrafast", "-pix_fmt", "yuv420p"]; await runFfmpeg([ ...["-f", "lavfi", "-i", "testsrc=size=320x180:rate=25:duration=3"], ...["-f", "lavfi", "-i", "sine=frequency=440:sample_rate=44100:duration=3"], ...encode, ...["-c:a", "aac", "-shortest", `file:${landscape}`], ]); await runFfmpeg([ ...["-f", "lavfi", "-i", "testsrc2=size=180x320:rate=30:duration=2"], ...encode, `file:${silentPortrait}`, ]); const srt = join(dir, "captions.srt"); await writeFile( srt, "1\n00:00:00,100 --> 00:00:01,500\nHello from the subtitles\n\n2\n00:00:01,500 --> 00:00:02,900\nZweite Zeile äöü\n", ); const vtt = join(dir, "captions.vtt"); await writeFile(vtt, "WEBVTT\n\n00:00.100 --> 00:01.500\nHello from WebVTT\n"); return { dir, landscape, silentPortrait, srt, vtt }; } export interface TestArtifact { path: string; name: string; mimeType: string; kind?: MediaOutputKind; } /** A MediaJobContext over local files that records progress and outputs. */ export async function fakeMediaJobContext(input: unknown, artifacts: Record) { const tempDir = await mkdtemp(join(tmpdir(), "evoo-media-job-")); const controller = new AbortController(); const progress: number[] = []; const outputs: (MediaJobOutputFile & { size: number; posterSize?: number })[] = []; const context: MediaJobContext = { jobId: "job-1", spaceId: "space-1", userId: "user-1", botId: null, input, tempDir, signal: controller.signal, progress: async (fraction) => { progress.push(fraction); }, fetchArtifact: async (artifactId): Promise => { const artifact = artifacts[artifactId]; if (!artifact) throw new Error(`Artifact ${artifactId} not found.`); const path = join(tempDir, `input-${artifactId}`); await copyFile(artifact.path, path); return { artifactId, path, name: artifact.name, mimeType: artifact.mimeType, kind: artifact.kind ?? attachmentKindOf(artifact.mimeType), size: (await stat(path)).size, }; }, addOutput: async (file): Promise => { // Files must exist when handed over; the runner stores them before the job ends. const size = (await stat(file.path)).size; const posterSize = file.posterPath ? (await stat(file.posterPath)).size : undefined; outputs.push({ ...file, size, ...(posterSize !== undefined ? { posterSize } : {}) }); const index = outputs.length; return { artifactId: `output-${index}`, kind: attachmentKindOf(file.mimeType), mimeType: file.mimeType, name: file.name, size, ...(file.width ? { width: file.width } : {}), ...(file.height ? { height: file.height } : {}), ...(file.durationMs !== undefined ? { durationMs: file.durationMs } : {}), ...(file.posterPath ? { posterArtifactId: `poster-${index}` } : {}), ...(file.parentArtifactId ? { parentArtifactId: file.parentArtifactId } : {}), ...(file.preset ? { preset: file.preset } : {}), }; }, }; return { context, controller, progress, outputs, tempDir }; }