import { execFileSync } from 'node:child_process'; import { backendMissingEnvVars, backendRequiredEnvVars, isMusicBackendAvailable, isTtsBackendAvailable, MUSIC_BACKENDS, TTS_BACKENDS, } from '../audio-platform/registry.js'; import { buildProviderStatusReport } from '../provider-status.js'; type ProbeExecutable = (name: 'python3' | 'bun' | 'ffmpeg') => string | undefined; export interface CreatorCapabilityItem { id: string; label: string; available: boolean; summary: string; requiredEnvVars: string[]; missingEnvVars: string[]; requiredExecutables: string[]; missingExecutables: string[]; } export interface CreatorCapabilities { generatedAt: string; zeroKeyPlanning: CreatorCapabilityItem; localMedia: CreatorCapabilityItem; stockProviders: CreatorCapabilityItem[]; ttsBackends: CreatorCapabilityItem[]; musicBackends: CreatorCapabilityItem[]; generativeRoutes: CreatorCapabilityItem[]; lowCostPreset: { id: 'local-stock-review'; available: boolean; summary: string; missingCapabilities: string[]; }; } export interface CreatorCapabilityOptions { root: string; env?: NodeJS.ProcessEnv; now?: Date; probeExecutable?: ProbeExecutable; } function defaultProbeExecutable(name: 'python3' | 'bun' | 'ffmpeg'): string | undefined { try { return execFileSync('which', [name], { encoding: 'utf-8' }).trim() || undefined; } catch { return undefined; } } function missingEnv(env: NodeJS.ProcessEnv, names: string[]): string[] { return names.filter((name) => !env[name]?.trim()); } function item(input: { id: string; label: string; available: boolean; summary: string; requiredEnvVars?: string[]; missingEnvVars?: string[]; requiredExecutables?: string[]; missingExecutables?: string[]; }): CreatorCapabilityItem { return { id: input.id, label: input.label, available: input.available, summary: input.summary, requiredEnvVars: input.requiredEnvVars ?? [], missingEnvVars: input.missingEnvVars ?? [], requiredExecutables: input.requiredExecutables ?? [], missingExecutables: input.missingExecutables ?? [], }; } export function buildCreatorCapabilities(options: CreatorCapabilityOptions): CreatorCapabilities { const env = options.env ?? process.env; const probe = options.probeExecutable ?? defaultProbeExecutable; const providers = buildProviderStatusReport({ workspaceRoot: options.root, env, now: options.now, probeExecutable: probe, }); const ffmpeg = providers.runtimeDependencies.find((dependency) => dependency.name === 'ffmpeg'); const ffmpegMissing = ffmpeg?.available ? [] : ['ffmpeg']; const stockMissing = missingEnv(env, ['PEXELS_API_KEY']); const ttsBackends = TTS_BACKENDS.map((backend) => item({ id: backend.id, label: backend.displayName, available: isTtsBackendAvailable(backend, env), summary: backend.summary, requiredEnvVars: backendRequiredEnvVars(backend), missingEnvVars: backendMissingEnvVars(backend, env), })); const musicBackends = MUSIC_BACKENDS.map((backend) => item({ id: backend.id, label: backend.displayName, available: isMusicBackendAvailable(backend, env), summary: backend.summary, requiredEnvVars: backendRequiredEnvVars(backend), missingEnvVars: backendMissingEnvVars(backend, env), })); const localMedia = item({ id: 'local-media-ffmpeg', label: 'Local media and ffmpeg assembly', available: ffmpegMissing.length === 0, summary: ffmpegMissing.length === 0 ? 'Local media can be assembled after storyboard/review evidence exists.' : 'Local planning works; final MP4 assembly needs ffmpeg.', requiredExecutables: ['ffmpeg'], missingExecutables: ffmpegMissing, }); const pexels = item({ id: 'pexels', label: 'Pexels stock video', available: stockMissing.length === 0, summary: stockMissing.length === 0 ? 'Pexels stock search/import is configured.' : 'Set PEXELS_API_KEY to enable stock search/import.', requiredEnvVars: ['PEXELS_API_KEY'], missingEnvVars: stockMissing, }); const missingCapabilities = [ ...(!localMedia.available ? ['ffmpeg'] : []), ...(!pexels.available ? ['PEXELS_API_KEY'] : []), ]; return { generatedAt: (options.now ?? new Date()).toISOString(), zeroKeyPlanning: item({ id: 'zero-key-planning', label: 'Zero-key planning demo', available: true, summary: 'Idea, project, brief, storyboard, readiness, cost estimate, and preview portal require no provider credentials.', }), localMedia, stockProviders: [pexels], ttsBackends, musicBackends, generativeRoutes: providers.routes.map((route) => item({ id: route.routeId, label: route.displayName, available: route.availability === 'available', summary: route.summary, requiredEnvVars: route.requiredEnvVars, missingEnvVars: route.availability === 'available' ? [] : route.missingEnvVars, requiredExecutables: route.requiredDependencies, missingExecutables: route.missingDependencies, })), lowCostPreset: { id: 'local-stock-review', available: missingCapabilities.length === 0, summary: 'Uses local/licensed stock media, optional configured TTS, local ffmpeg assembly, and standard review/QC gates.', missingCapabilities, }, }; }