evOo-local/packages/adapters/src/media-services.ts
2026-10-03 18:57:49 +00:00

137 lines
4.9 KiB
TypeScript

import type {
AgentRunModel,
AgentRuntime,
ArtifactStore,
JobPublisher,
MediaJobHandlers,
RealtimeFanout,
} from "@rakazo/adapter-kit";
import type { ModelTier } from "@rakazo/contracts";
import type { PrismaClient } from "@rakazo/db";
import { createMediaRepos } from "@rakazo/db";
import { getLogger } from "@rakazo/logging";
import type { ProfileSet } from "@rakazo/media";
import { mergeProfiles } from "@rakazo/media";
import { BUILTIN_PROFILES_DIR, loadProfilesFromDir } from "@rakazo/media/node";
import type { MediaServiceDeps, MediaServices } from "./media-context.js";
import { createMediaModelClient, mediaTopic } from "./media-context.js";
import {
createMediaInferenceProvider,
createMediaSourceProvider,
type MediaConfig,
mediaEmulatorEnabled,
} from "./media-factory.js";
import { FfmpegMediaRenderer } from "./media-ffmpeg-renderer.js";
import {
createMediaIngestHandlers,
createMediaIngestService,
resumeMediaIngest,
} from "./media-ingest.js";
import type { MediaJobLimits } from "./media-jobs.js";
import { createMediaMarketingJobHandlers, createMediaMarketingService } from "./media-marketing.js";
import { ResvgMotionGraphicsRenderer } from "./media-motion-renderer.js";
import { createMediaPrivacyService } from "./media-privacy.js";
import type { MediaUploadReminder } from "./media-publish.js";
import { BundleMediaPublisher } from "./media-publisher-bundle.js";
import { createMediaRenderHandler, createMediaRenderService } from "./media-render.js";
const PROFILE_CACHE_MS = 60_000;
/**
* Composition root for Media Studio, shared by the API and the worker. Returns
* null when no sidecar (or scripted emulator) is configured; media then stays
* hidden in every client.
*/
export function createMediaServices(input: {
config: MediaConfig;
prisma: PrismaClient;
jobs: JobPublisher;
artifacts: ArtifactStore;
runtime: AgentRuntime;
realtime: RealtimeFanout | null;
resolveModel: (scope: {
userId: string;
spaceId: string;
tier?: ModelTier;
}) => Promise<AgentRunModel>;
env?: NodeJS.ProcessEnv;
}): MediaServices | null {
const env = input.env ?? process.env;
if (!input.config.enabled) return null;
const inference = createMediaInferenceProvider(env);
if (!inference) return null;
const sources: MediaServiceDeps["sources"] = { folder: createMediaSourceProvider("folder", env) };
if (mediaEmulatorEnabled(env)) sources.emulator = createMediaSourceProvider("emulator", env);
let cached: { at: number; set: ProfileSet } | null = null;
const profiles = async (): Promise<ProfileSet> => {
if (cached && Date.now() - cached.at < PROFILE_CACHE_MS) return cached.set;
const [builtin, own] = await Promise.all([
loadProfilesFromDir(BUILTIN_PROFILES_DIR),
loadProfilesFromDir(input.config.profilesDir),
]);
const set = mergeProfiles(builtin.profiles, own.profiles);
set.errors = [...builtin.errors, ...own.errors];
for (const error of set.errors) getLogger().warn("media profile ignored", error);
cached = { at: Date.now(), set };
return set;
};
const deps: MediaServiceDeps = {
prisma: input.prisma,
repos: createMediaRepos(input.prisma),
jobs: input.jobs,
inference,
renderer: new FfmpegMediaRenderer({ root: input.config.mediaRoot }),
motion: new ResvgMotionGraphicsRenderer(),
publisher: new BundleMediaPublisher(),
shortener: null,
sources,
mediaRoot: input.config.mediaRoot,
profiles,
artifacts: input.artifacts,
model: createMediaModelClient({
prisma: input.prisma,
runtime: input.runtime,
resolveModel: input.resolveModel,
}),
publish: async (spaceId, event) => {
await input.realtime?.publish(mediaTopic(spaceId), JSON.stringify(event));
},
now: () => new Date(),
};
const privacy = createMediaPrivacyService(deps);
return {
deps,
ingest: createMediaIngestService(deps),
privacy,
render: createMediaRenderService(deps),
marketing: createMediaMarketingService(deps),
};
}
/**
* Every media job handler for one job host. Any process that runs a job host
* (worker, or the API in single-process mode) must register these, otherwise
* it would acknowledge media jobs without running them.
*/
export function createMediaJobHandlers(
services: MediaServices,
options: { signal?: AbortSignal; limits?: MediaJobLimits; remind?: MediaUploadReminder } = {},
): { handlers: MediaJobHandlers; resume: () => Promise<number> } {
const ingestOptions = {
...(options.signal ? { signal: options.signal } : {}),
...(options.limits ? { limits: options.limits } : {}),
};
return {
handlers: {
...createMediaIngestHandlers(services.deps, ingestOptions),
"media.render": createMediaRenderHandler(services.deps, services.privacy),
...createMediaMarketingJobHandlers(services.deps, services.marketing, {
...(options.remind ? { remind: options.remind } : {}),
}),
},
resume: () => resumeMediaIngest(services.deps, options.limits),
};
}