import { mkdir, mkdtemp } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import sharp from 'sharp'; import { writeArtifact } from './artifact-store.js'; import type { AssetManifestArtifact, BriefArtifact, StoryboardArtifact } from './artifacts.js'; import { writeStageCheckpoint } from './checkpoints.js'; import { buildProjectCostEstimate } from './cost-estimate.js'; import { writeTextFileAtomic } from './atomic-write.js'; import { getBuiltinPipelineManifest } from './pipeline-manifest.js'; import { generatePreviewPortalSurfaces } from './preview-portal/index.js'; import { buildProjectReadiness } from './readiness.js'; import { ensureProjectWorkspace, writeProjectManifest } from './workspace.js'; import { buildCreatorCapabilities, type CreatorCapabilities } from './creator-ui/capabilities.js'; import type { StudioAspectRatio, StudioCreatorPlatform } from './studio/types.js'; export interface CreatorDemoOptions { root?: string; project?: string; intent?: string; platform?: StudioCreatorPlatform; aspectRatio?: StudioAspectRatio; durationSeconds?: number; now?: Date; keep?: boolean; env?: NodeJS.ProcessEnv; probeExecutable?: (name: 'python3' | 'bun' | 'ffmpeg') => string | undefined; } export interface CreatorDemoResult { root: string; project: string; projectDir: string; artifacts: { brief: string; storyboard: string; assetManifest: string; readiness: string; costEstimate: string; }; portal: { outputs: Array<{ surface: string; path: string }>; warnings: string[]; }; readiness: Awaited>; costEstimate: Awaited>; capabilities: CreatorCapabilities; providerCalls: 0; networkCalls: 0; renderedFinal: false; publishReady: false; nextAction: string; } function iso(now: Date): string { return now.toISOString(); } async function writePlaceholderImage(path: string, width: number, height: number): Promise { await sharp({ create: { width, height, channels: 4, background: { r: 19, g: 21, b: 28, alpha: 1 }, }, }) .composite([ { input: Buffer.from( `CREATOR DEMOStoryboard placeholder`, ), }, ]) .png() .toFile(path); } export async function runCreatorDemo(options: CreatorDemoOptions = {}): Promise { const now = options.now ?? new Date(); const root = options.root ?? await mkdtemp(join(tmpdir(), 'vclaw-creator-demo-')); const project = options.project ?? 'creator-demo'; const intent = options.intent ?? 'A 20-second coffee ad for a local cafe'; const platform = options.platform ?? 'youtube-shorts'; const aspectRatio = options.aspectRatio ?? (platform === 'generic' ? '16:9' : '9:16'); const durationSeconds = options.durationSeconds ?? 20; if (!Number.isInteger(durationSeconds) || durationSeconds < 3) { throw new Error('creator demo durationSeconds must be an integer of at least 3 seconds'); } const firstSceneSeconds = Math.max(1, Math.floor(durationSeconds * 0.35)); const secondSceneSeconds = Math.max(1, Math.floor(durationSeconds * 0.35)); const finalSceneSeconds = durationSeconds - firstSceneSeconds - secondSceneSeconds; const [placeholderWidth, placeholderHeight] = aspectRatio === '16:9' ? [1920, 1080] : aspectRatio === '1:1' ? [1080, 1080] : [1080, 1920]; const workspace = await ensureProjectWorkspace(project, root); const generatedAt = iso(now); await writeProjectManifest(workspace, { slug: project, productionMode: 'storyboard', createdAt: generatedAt, updatedAt: generatedAt, pipeline: getBuiltinPipelineManifest('storyboard'), currentStage: 'review', lastCompletedStage: 'assets', lastCheckpointStatus: 'completed', tags: ['creator-demo', 'zero-key'], }); const brief: BriefArtifact = { title: 'Creator Demo', intent, productionMode: 'storyboard', createdAt: generatedAt, metadata: { platform, durationSeconds, demo: true, executionProfile: { aspectRatio, quality: 'fast', resolution: '720p', generateAudio: false, outputs: 1, }, }, }; const storyboard: StoryboardArtifact = { projectSlug: project, productionMode: 'storyboard', scenes: [ { sceneIndex: 0, description: 'Hook: close-up of a fresh coffee pour with bold launch text.', durationSeconds: firstSceneSeconds, silent: true, }, { sceneIndex: 1, description: 'Proof: show the product, audience, and simple benefit in one vertical frame.', durationSeconds: secondSceneSeconds, silent: true, }, { sceneIndex: 2, description: 'CTA: logo, offer, and upload-ready copy direction for a human editor.', durationSeconds: finalSceneSeconds, silent: true, }, ], }; const imageDir = join(workspace.projectDir, 'images'); await mkdir(imageDir, { recursive: true }); const imagePath = join(imageDir, 'creator-demo-placeholder.png'); await writePlaceholderImage(imagePath, placeholderWidth, placeholderHeight); const assetManifest: AssetManifestArtifact = { projectSlug: project, assets: [{ id: 'creator-demo-placeholder', kind: 'image', path: 'images/creator-demo-placeholder.png', sceneIndex: 0, backend: 'local-placeholder', }], }; const briefPath = await writeArtifact(workspace, 'brief', brief); await writeStageCheckpoint(workspace, { stage: 'brief', status: 'completed', generatedAt, artifacts: { brief: briefPath }, summary: 'Zero-key demo brief written.', issues: [], }); const storyboardPath = await writeArtifact(workspace, 'storyboard', storyboard); await writeStageCheckpoint(workspace, { stage: 'storyboard', status: 'completed', generatedAt, artifacts: { storyboard: storyboardPath }, summary: 'Zero-key demo storyboard written.', issues: [], }); const assetManifestPath = await writeArtifact(workspace, 'asset-manifest', assetManifest); await writeStageCheckpoint(workspace, { stage: 'assets', status: 'completed', generatedAt, artifacts: { 'asset-manifest': assetManifestPath }, summary: 'Local placeholder media registered.', issues: [], }); const readiness = await buildProjectReadiness(project, root, 'storyboard'); const readinessPath = join(workspace.artifactsDir, 'readiness.json'); await writeTextFileAtomic(readinessPath, `${JSON.stringify(readiness, null, 2)}\n`); const costEstimate = await buildProjectCostEstimate({ projectSlug: project, root, narrationEnabled: false, }); const costEstimatePath = join(workspace.artifactsDir, 'cost-estimate.json'); await writeTextFileAtomic(costEstimatePath, `${JSON.stringify({ projectSlug: project, estimate: costEstimate }, null, 2)}\n`); const portal = await generatePreviewPortalSurfaces({ root, projectSlug: project, surfaces: ['review', 'preview', 'run'], }); const capabilities = buildCreatorCapabilities({ root, env: options.env ?? {}, now, ...(options.probeExecutable ? { probeExecutable: options.probeExecutable } : {}), }); return { root, project, projectDir: workspace.projectDir, artifacts: { brief: briefPath, storyboard: storyboardPath, assetManifest: assetManifestPath, readiness: readinessPath, costEstimate: costEstimatePath, }, portal, readiness, costEstimate, capabilities, providerCalls: 0, networkCalls: 0, renderedFinal: false, publishReady: false, nextAction: capabilities.lowCostPreset.available ? 'Review the portal, then provide real local/stock media before assembly.' : `Review the portal. To proceed to a final MP4, configure: ${capabilities.lowCostPreset.missingCapabilities.join(', ') || 'review approval'}.`, }; }