137 lines
4.9 KiB
TypeScript
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),
|
|
};
|
|
}
|