import { existsSync } from 'node:fs'; import { resolveWorkspaceRootFromEnv } from './workspace-root.js'; import { readFile } from 'node:fs/promises'; import { artifactPathFor, writeArtifact } from './artifact-store.js'; import { appendProjectEvent } from './events.js'; import { resolveProjectWorkspace } from './workspace.js'; import type { ProviderRouteId, VideoOperationKind } from './provider-platform/types.js'; import type { VideoExecutionPlan, VideoProductionMode } from './types.js'; import type { BriefArtifact } from './artifacts.js'; type ExecutionProfile = VideoExecutionPlan['executionProfile']; function normalizeAspectRatio(value: unknown): ExecutionProfile['aspectRatio'] | null { return value === '16:9' || value === '9:16' || value === '1:1' ? value : null; } function normalizeQuality(value: unknown): ExecutionProfile['quality'] | null { return value === 'fast' || value === 'quality' ? value : null; } function normalizeResolution(value: unknown): ExecutionProfile['resolution'] | null { return value === '720p' || value === '1080p' ? value : null; } function normalizeOutputCount(value: unknown): number | null { return Number.isInteger(value) && Number(value) >= 1 && Number(value) <= 4 ? Number(value) : null; } function normalizeGenerateAudio(value: unknown): boolean | null { return typeof value === 'boolean' ? value : null; } function normalizeVeoModel(value: unknown): NonNullable | null { // fast/quality/omni-flash + the cheaper Veo tiers: `lite` (veo-3.1-lite) and // `free` (veo-3.1-lite-low-priority — the relaxed/explore lane, Ultra-gated at // the provider). flow.ts mapModelToUseApi resolves these short names. return value === 'fast' || value === 'quality' || value === 'lite' || value === 'free' || value === 'omni-flash' ? value : null; } async function readExecutionProfileOverrides( projectSlug: string, root: string, ): Promise> { const workspace = resolveProjectWorkspace(projectSlug, root); const briefPath = artifactPathFor(workspace, 'brief'); if (!existsSync(briefPath)) return {}; const brief = JSON.parse(await readFile(briefPath, 'utf-8')) as { metadata?: { platform?: string; executionProfile?: Record; }; }; const executionProfile = brief.metadata?.executionProfile ?? {}; const platform = String(brief.metadata?.platform ?? '').toLowerCase(); return { ...(normalizeAspectRatio(executionProfile.aspectRatio) ? { aspectRatio: normalizeAspectRatio(executionProfile.aspectRatio)! } : {}), ...(normalizeQuality(executionProfile.quality) ? { quality: normalizeQuality(executionProfile.quality)! } : {}), ...(normalizeResolution(executionProfile.resolution) ? { resolution: normalizeResolution(executionProfile.resolution)! } : {}), ...(typeof executionProfile.generateAudio === 'boolean' ? { generateAudio: executionProfile.generateAudio } : {}), ...(normalizeOutputCount(executionProfile.outputCount) ? { outputCount: normalizeOutputCount(executionProfile.outputCount)! } : {}), ...(normalizeVeoModel(executionProfile.veoModel) ? { veoModel: normalizeVeoModel(executionProfile.veoModel)! } : {}), ...(!normalizeAspectRatio(executionProfile.aspectRatio) && ['tiktok', 'reels', 'shorts'].includes(platform) ? { aspectRatio: '9:16' as const } : {}), }; } export async function setExecutionProfileOverrides( projectSlug: string, input: Partial, root = resolveWorkspaceRootFromEnv(), ): Promise<{ artifactPath: string; brief: BriefArtifact; }> { const workspace = resolveProjectWorkspace(projectSlug, root); const briefPath = artifactPathFor(workspace, 'brief'); if (!existsSync(briefPath)) { throw new Error(`Execution profile cannot be updated for "${projectSlug}" because the brief artifact is missing.`); } const brief = JSON.parse(await readFile(briefPath, 'utf-8')) as BriefArtifact; const existingProfile = ((brief.metadata ?? {}).executionProfile ?? {}) as Record; const nextProfile = { ...existingProfile, ...(input.aspectRatio ? { aspectRatio: input.aspectRatio } : {}), ...(input.quality ? { quality: input.quality } : {}), ...(input.resolution ? { resolution: input.resolution } : {}), ...(typeof input.generateAudio === 'boolean' ? { generateAudio: input.generateAudio } : {}), ...(typeof input.outputCount === 'number' ? { outputCount: input.outputCount } : {}), ...(input.veoModel ? { veoModel: input.veoModel } : {}), }; const nextBrief: BriefArtifact = { ...brief, metadata: { ...(brief.metadata ?? {}), executionProfile: nextProfile, }, }; const artifactPath = await writeArtifact(workspace, 'brief', nextBrief); await appendProjectEvent(workspace, { type: 'artifact.brief.execution-profile.updated', payload: { artifactPath, executionProfile: nextProfile, }, }); return { artifactPath, brief: nextBrief }; } export function parseExecutionProfileInput(input: { aspectRatio?: unknown; quality?: unknown; resolution?: unknown; generateAudio?: unknown; outputCount?: unknown; veoModel?: unknown; }): Partial { const profile: Partial = {}; const aspectRatio = normalizeAspectRatio(input.aspectRatio); const quality = normalizeQuality(input.quality); const resolution = normalizeResolution(input.resolution); const generateAudio = normalizeGenerateAudio(input.generateAudio); const outputCount = normalizeOutputCount(input.outputCount); const veoModel = normalizeVeoModel(input.veoModel); if (aspectRatio) profile.aspectRatio = aspectRatio; if (quality) profile.quality = quality; if (resolution) profile.resolution = resolution; if (generateAudio !== null) profile.generateAudio = generateAudio; if (outputCount !== null) profile.outputCount = outputCount; if (veoModel) profile.veoModel = veoModel; return profile; } export async function buildExecutionProfile(input: { projectSlug: string; root?: string; productionMode: VideoProductionMode; routeId: ProviderRouteId | null; operationKind: VideoOperationKind; }): Promise { const root = input.root ?? resolveWorkspaceRootFromEnv(); const overrides = await readExecutionProfileOverrides(input.projectSlug, root); const defaults: ExecutionProfile = { aspectRatio: '16:9', quality: input.productionMode === 'director' ? 'quality' : 'fast', resolution: '720p', generateAudio: input.routeId === 'seedance-direct', outputCount: 1, }; return { ...defaults, ...overrides, }; }