evOo-local/packages/adapters/src/media-factory.ts
Claude 270ea8068a
feat(media): inference emulator, sidecar client, folder source and factory
Add the Media Studio adapters behind the frozen provider contracts: a
deterministic offline inference emulator driven by file-name markers that
renders real redactions with ffmpeg, a synthetic emulator library, the
bearer-auth HTTP client for the local media sidecar with validated
responses, typed errors, busy retries and timeouts, a lazily paginated
folder source that never follows symlinks, and the configuration factory.
A shared conformance suite runs against the emulator and, in the Docker
integration job, against the sidecar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138YqDj3ttfayRwuMKKrqVj
2026-10-03 17:52:17 +00:00

91 lines
3.3 KiB
TypeScript

import { homedir } from "node:os";
import path from "node:path";
import type { MediaInferenceProvider, MediaSourceProvider } from "@rakazo/adapter-kit";
import { expandHome } from "@rakazo/media/node";
import { EmulatorMediaInferenceProvider } from "./media-emulator.js";
import { EmulatorMediaSourceProvider } from "./media-emulator-source.js";
import { FolderMediaSourceProvider } from "./media-folder-source.js";
import { SidecarMediaInferenceProvider } from "./media-sidecar.js";
export interface MediaConfig {
/** Media Studio runs: a sidecar is configured or the scripted runtime uses the emulator. */
enabled: boolean;
workerUrl: string | null;
workerToken: string | null;
profilesDir: string;
/** `${DATA_DIR}/media`, absolute. Every derived file and sidecar path lives here. */
mediaRoot: string;
}
export type MediaInferenceKind = "sidecar" | "emulator";
/** Emulators replace the sidecar whenever the scripted runtime is pinned (tests, e2e). */
export function mediaEmulatorEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
return env.AGENT_RUNTIME === "scripted";
}
export function mediaConfigFromEnv(env: NodeJS.ProcessEnv = process.env): MediaConfig {
const workerUrl = env.MEDIA_WORKER_URL?.trim() || null;
const workerToken = env.MEDIA_WORKER_TOKEN?.trim() || null;
return {
enabled: mediaEmulatorEnabled(env) || Boolean(workerUrl && workerToken),
workerUrl,
workerToken,
profilesDir: path.resolve(
expandHome(env.MEDIA_PROFILES_DIR?.trim() || "~/.evoo/media-profiles", homedir()),
),
mediaRoot: path.resolve(env.DATA_DIR ?? "./data", "media"),
};
}
export function resolveMediaInferenceKind(
env: NodeJS.ProcessEnv = process.env,
): MediaInferenceKind | null {
if (mediaEmulatorEnabled(env)) return "emulator";
const config = mediaConfigFromEnv(env);
return config.workerUrl && config.workerToken ? "sidecar" : null;
}
/** The configured inference provider, or null when Media Studio is not set up. */
export function createMediaInferenceProvider(
env: NodeJS.ProcessEnv = process.env,
): MediaInferenceProvider | null {
const config = mediaConfigFromEnv(env);
switch (resolveMediaInferenceKind(env)) {
case "emulator":
return new EmulatorMediaInferenceProvider({ root: config.mediaRoot });
case "sidecar":
return new SidecarMediaInferenceProvider({
baseUrl: config.workerUrl as string,
token: config.workerToken as string,
});
default:
return null;
}
}
/** Emulator clips are cached inside the media root so inference can read them. */
export function emulatorFixturesDir(env: NodeJS.ProcessEnv = process.env): string {
return path.join(mediaConfigFromEnv(env).mediaRoot, "emulator-library");
}
/**
* Server-side source adapter for a persisted MediaSource kind. Client-pushed
* kinds (apple_photos, ios_library) have no server adapter.
*/
export function createMediaSourceProvider(
kind: string,
env: NodeJS.ProcessEnv = process.env,
): MediaSourceProvider {
switch (kind) {
case "folder":
return new FolderMediaSourceProvider();
case "emulator":
if (!mediaEmulatorEnabled(env)) break;
return new EmulatorMediaSourceProvider({ fixturesDir: emulatorFixturesDir(env) });
default:
break;
}
throw new Error(`No server-side media source for "${kind}". Use folder.`);
}