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; 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 => { 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 } { 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), }; }