import type { CinemaProviderAccountClass, CinemaProviderCapabilitySnapshotArtifact, CinemaProviderMediaKind, } from './cinema-provider-types.js'; import type { CinemaQueuePayload, CinemaQueueTaskKind } from './cinema-queue-types.js'; import type { CinemaReferenceEntry, CinemaReferenceMediaKind, CinemaSha256 } from './cinema-types.js'; import { validateCinemaCapabilitySnapshot } from './cinema-provider-contracts.js'; import { sha256Text, stableCinemaJson } from './cinema-shot-compiler.js'; export const CINEMA_PROVIDER_ROUTES = [ 'gobananas-images', 'openai-images', 'higgsfield-images', 'higgsfield-unlimited', 'higgsfield-cli', 'seedance-xskill', ] as const; export type CinemaProviderRouteId = (typeof CINEMA_PROVIDER_ROUTES)[number]; export interface CinemaProviderRouteDefinition { routeId: CinemaProviderRouteId; transportId: string; mediaKinds: CinemaProviderMediaKind[]; accountClass: CinemaProviderAccountClass; fallbackPolicy: 'forbidden'; description: string; } export const CINEMA_PROVIDER_ROUTE_REGISTRY: Record = { 'gobananas-images': { routeId: 'gobananas-images', transportId: 'gobananas-subscription-images', mediaKinds: ['image'], accountClass: 'subscription-unlimited', fallbackPolicy: 'forbidden', description: 'Preferred subscription image route for identity and controlled reference work.', }, 'openai-images': { routeId: 'openai-images', transportId: 'openai-image-api', mediaKinds: ['image'], accountClass: 'api-credits', fallbackPolicy: 'forbidden', description: 'Provider-neutral alternative image route; selection is explicit and never automatic after failure.', }, 'higgsfield-images': { routeId: 'higgsfield-images', transportId: 'official-higgsfield-cli-images', mediaKinds: ['image'], accountClass: 'provider-credits', fallbackPolicy: 'forbidden', description: 'Optional Higgsfield image route for provider-specific image workflows.', }, 'higgsfield-unlimited': { routeId: 'higgsfield-unlimited', transportId: 'higgs-cloak-unlimited', mediaKinds: ['video'], accountClass: 'subscription-unlimited', fallbackPolicy: 'forbidden', description: 'Authenticated unlimited-account transport with measured single-job lane capacity.', }, 'higgsfield-cli': { routeId: 'higgsfield-cli', transportId: 'official-higgsfield-cli', mediaKinds: ['video'], accountClass: 'provider-credits', fallbackPolicy: 'forbidden', description: 'Official CLI route for premium models and specialist cinematic workflows.', }, 'seedance-xskill': { routeId: 'seedance-xskill', transportId: 'xskill-ark', mediaKinds: ['video'], accountClass: 'api-credits', fallbackPolicy: 'forbidden', description: 'Explicit paid xskill/ARK route; legacy seedance-direct is a compatibility alias outside Cinema.', }, }; export interface CinemaProviderNeutralTask { taskId: string; kind: CinemaQueueTaskKind; mediaKind: CinemaProviderMediaKind; operation: string; queuePayload: CinemaQueuePayload; sourceHashes: CinemaSha256[]; /** * The media kind of each source hash, in order. Absent = every source is an * image (the shape every task had before video/audio references). The image * count must agree with `requirements.referenceImageCount`; video and audio * counts are gated against the route's own caps. */ sourceMediaKinds?: CinemaReferenceMediaKind[]; requirements: { model?: string; workflow?: string; identityConsistency: 'none' | 'preferred' | 'required'; referenceImageCount: number; targetResolution: string; aspectRatio: string; transparency: boolean; typography: boolean; }; normalizedParameters: Record; } export interface CinemaProviderProjection { taskId: string; taskKind: CinemaQueueTaskKind; /** Canonical Cinema route or an explicitly quoted legacy compatibility route. */ routeId: string; transportId: string; accountClass: CinemaProviderAccountClass; capabilitySnapshotId: string; capabilitySnapshotHash: CinemaSha256; payloadHash: CinemaSha256; sourceHashes: CinemaSha256[]; providerArguments: Record; providerArgumentsHash: CinemaSha256; fallbackPolicy: 'forbidden'; } function assertRouteSnapshot(routeId: CinemaProviderRouteId, snapshot: CinemaProviderCapabilitySnapshotArtifact, now: string): void { const validation = validateCinemaCapabilitySnapshot(snapshot, now); if (!validation.ok) throw new Error(`Cinema route ${routeId} capability snapshot is unusable: ${validation.issues.map((entry) => entry.code).join(', ')}`); const route = CINEMA_PROVIDER_ROUTE_REGISTRY[routeId]; if (snapshot.routeId !== routeId || snapshot.transportId !== route.transportId || snapshot.accountClass !== route.accountClass) { throw new Error(`Cinema route ${routeId} cannot use capability snapshot for ${snapshot.routeId}/${snapshot.transportId}`); } } /** The media kind a reference pack entry declares; absent means image. */ export function cinemaReferenceMediaKind(entry: Pick): CinemaReferenceMediaKind { return entry.mediaKind ?? 'image'; } /** The kinds of a task's sources, in order — all image when the task does not say. */ export function cinemaTaskSourceMediaKinds(task: Pick): CinemaReferenceMediaKind[] { return task.sourceMediaKinds ?? task.sourceHashes.map(() => 'image' as const); } function assertTaskSupported(task: CinemaProviderNeutralTask, snapshot: CinemaProviderCapabilitySnapshotArtifact): void { const caps = snapshot.capabilities; if (!caps.mediaKinds.includes(task.mediaKind)) throw new Error(`Cinema route ${snapshot.routeId} does not support ${task.mediaKind}`); if (!caps.operations.includes(task.operation)) throw new Error(`Cinema route ${snapshot.routeId} does not support operation ${task.operation}`); if (task.requirements.referenceImageCount > caps.maximumReferenceImages) { throw new Error(`Cinema route ${snapshot.routeId} accepts at most ${caps.maximumReferenceImages} image references`); } const kinds = cinemaTaskSourceMediaKinds(task); if (kinds.length !== task.sourceHashes.length) throw new Error('Cinema provider task sourceMediaKinds must name every source hash'); const videos = kinds.filter((kind) => kind === 'video').length; const audio = kinds.filter((kind) => kind === 'audio').length; if (videos > (caps.maximumReferenceVideos ?? 0)) { throw new Error(`Cinema route ${snapshot.routeId} accepts at most ${caps.maximumReferenceVideos ?? 0} video references`); } if (audio > (caps.maximumReferenceAudio ?? 0)) { throw new Error(`Cinema route ${snapshot.routeId} accepts at most ${caps.maximumReferenceAudio ?? 0} audio references`); } if (task.requirements.model && !caps.models.includes(task.requirements.model)) { throw new Error(`Cinema route ${snapshot.routeId} does not advertise model ${task.requirements.model}`); } if (task.requirements.workflow && !caps.workflows.includes(task.requirements.workflow)) { throw new Error(`Cinema route ${snapshot.routeId} does not advertise workflow ${task.requirements.workflow}`); } if (!Number.isInteger(task.requirements.referenceImageCount) || task.requirements.referenceImageCount < 0) { throw new Error('Cinema provider task referenceImageCount must be a non-negative integer'); } const images = kinds.filter((kind) => kind === 'image').length; if (images !== task.requirements.referenceImageCount) { throw new Error('Cinema provider task image source count must equal referenceImageCount'); } } export function projectCinemaProviderTask( routeId: CinemaProviderRouteId, task: CinemaProviderNeutralTask, snapshot: CinemaProviderCapabilitySnapshotArtifact, now: string, ): CinemaProviderProjection { assertRouteSnapshot(routeId, snapshot, now); assertTaskSupported(task, snapshot); const route = CINEMA_PROVIDER_ROUTE_REGISTRY[routeId]; const providerArguments = { operation: task.operation, model: task.requirements.model ?? null, workflow: task.requirements.workflow ?? null, targetResolution: task.requirements.targetResolution, aspectRatio: task.requirements.aspectRatio, references: task.sourceHashes.map((contentHash, index) => ({ index: index + 1, contentHash, mediaKind: cinemaTaskSourceMediaKinds(task)[index] })), parameters: structuredClone(task.normalizedParameters), }; return { taskId: task.taskId, taskKind: task.kind, routeId, transportId: route.transportId, accountClass: route.accountClass, capabilitySnapshotId: snapshot.snapshotId, capabilitySnapshotHash: snapshot.contentHash, payloadHash: sha256Text(stableCinemaJson(task.queuePayload)), sourceHashes: [...task.sourceHashes], providerArguments, providerArgumentsHash: sha256Text(stableCinemaJson(providerArguments)), fallbackPolicy: 'forbidden', }; } export function selectCinemaImageRoute( task: CinemaProviderNeutralTask, snapshots: Partial>, now: string, explicitRouteId?: CinemaProviderRouteId, ): CinemaProviderProjection { if (task.mediaKind !== 'image') throw new Error('Cinema image route selection accepts image tasks only'); if (explicitRouteId) { const snapshot = snapshots[explicitRouteId]; if (!snapshot) throw new Error(`Cinema explicit image route ${explicitRouteId} has no current capability snapshot`); return projectCinemaProviderTask(explicitRouteId, task, snapshot, now); } const plannedPreference: CinemaProviderRouteId[] = ['gobananas-images', 'openai-images', 'higgsfield-images']; const failures: string[] = []; for (const routeId of plannedPreference) { const snapshot = snapshots[routeId]; if (!snapshot) { failures.push(`${routeId}: no snapshot`); continue; } try { return projectCinemaProviderTask(routeId, task, snapshot, now); } catch (error) { failures.push(`${routeId}: ${error instanceof Error ? error.message : String(error)}`); } } throw new Error(`No Cinema image route satisfies the provider-neutral contract; explicit replan required. ${failures.join('; ')}`); } export function assertCinemaProjectionQuoteParity( projection: CinemaProviderProjection, quoted: { routeId: string; payloadHash: CinemaSha256; providerArgumentsHash: CinemaSha256; sourceHashes: CinemaSha256[] }, ): void { if (projection.routeId !== quoted.routeId || projection.payloadHash !== quoted.payloadHash || projection.providerArgumentsHash !== quoted.providerArgumentsHash || stableCinemaJson(projection.sourceHashes) !== stableCinemaJson(quoted.sourceHashes)) { throw new Error('Cinema provider create arguments differ from the exact quote; submission is forbidden'); } }