import type { HookPatternId } from './cinematography.js'; export type SubjectType = 'character' | 'product'; export type BeatTemplate = | 'three-act' | 'ad-hook-feature-cta' | 'turntable' | 'lookbook' | 'song-structure' | 'tension-release' | 'social-2s-hook' | 'panel-sequence'; export interface CategoryDescriptor { id: string; label: string; subjectType: SubjectType; beatTemplate: BeatTemplate; cameraVocab: string; genre: string; audioProfile: 'diegetic' | 'ad-mix'; hookSeconds: number; /** * The category's signature 2-second opening hook. When set, a caller that * has not been given an explicit `--hook` opens the prompt with this hook. * Left undefined on the existing categories (incl. the cinematic default) so * their output stays byte-stable; operators can still pass any `--hook`. */ defaultHookId?: HookPatternId; } const CATEGORY_REGISTRY: Record = { cinematic: { id: 'cinematic', label: 'Cinematic / Narrative', subjectType: 'character', beatTemplate: 'three-act', cameraVocab: 'cinematic', genre: 'live-action', audioProfile: 'diegetic', hookSeconds: 0, }, 'ecommerce-ad': { id: 'ecommerce-ad', label: 'E-commerce Ad', subjectType: 'product', beatTemplate: 'ad-hook-feature-cta', cameraVocab: 'cinematic', genre: 'live-action', audioProfile: 'ad-mix', hookSeconds: 2, }, 'brand-story': { id: 'brand-story', label: 'Brand Story', subjectType: 'character', beatTemplate: 'ad-hook-feature-cta', cameraVocab: 'cinematic', genre: 'live-action', audioProfile: 'ad-mix', hookSeconds: 2, }, 'product-360': { id: 'product-360', label: 'Product 360 / Turntable', subjectType: 'product', beatTemplate: 'turntable', cameraVocab: 'orbit', genre: 'live-action', audioProfile: 'ad-mix', hookSeconds: 0, }, 'fashion-lookbook': { id: 'fashion-lookbook', label: 'Fashion Lookbook', subjectType: 'product', beatTemplate: 'lookbook', cameraVocab: 'cinematic', genre: 'live-action', audioProfile: 'ad-mix', hookSeconds: 2, }, 'food-beverage': { id: 'food-beverage', label: 'Food & Beverage', subjectType: 'product', beatTemplate: 'ad-hook-feature-cta', cameraVocab: 'macro', genre: 'live-action', audioProfile: 'ad-mix', hookSeconds: 2, }, 'real-estate': { id: 'real-estate', label: 'Real Estate', subjectType: 'product', beatTemplate: 'ad-hook-feature-cta', cameraVocab: 'glide', genre: 'live-action', audioProfile: 'ad-mix', hookSeconds: 2, }, 'motion-design-ad': { id: 'motion-design-ad', label: 'Motion Design Ad', subjectType: 'product', beatTemplate: 'ad-hook-feature-cta', cameraVocab: 'stylized', genre: 'pixar', audioProfile: 'ad-mix', hookSeconds: 2, }, 'comic-to-video': { id: 'comic-to-video', label: 'Comic to Video', subjectType: 'character', beatTemplate: 'panel-sequence', cameraVocab: 'stylized', genre: 'anime', audioProfile: 'ad-mix', hookSeconds: 2, }, // Previously-unwired skill categories (Track A → Track B bridge). Genre/beat // values reuse the closest existing register so nothing throws; dedicated // genres (cgi-render, cartoon) and beat templates (song-structure, // tension-release, social-2s-hook) are a follow-up enrichment pass. '3d-cgi': { id: '3d-cgi', label: '3D / CGI', subjectType: 'character', beatTemplate: 'three-act', cameraVocab: 'stylized', genre: 'cgi', audioProfile: 'diegetic', hookSeconds: 2, defaultHookId: 'scale-reveal', }, cartoon: { id: 'cartoon', label: 'Cartoon', subjectType: 'character', beatTemplate: 'three-act', cameraVocab: 'stylized', genre: 'cartoon', audioProfile: 'ad-mix', hookSeconds: 2, defaultHookId: 'smash-zoom', }, 'fight-scenes': { id: 'fight-scenes', label: 'Fight Scenes', subjectType: 'character', beatTemplate: 'tension-release', cameraVocab: 'cinematic', genre: 'action', audioProfile: 'diegetic', hookSeconds: 2, defaultHookId: 'weapon-clash-spark', }, 'anime-action': { id: 'anime-action', label: 'Anime Action', subjectType: 'character', beatTemplate: 'tension-release', cameraVocab: 'stylized', genre: 'anime', audioProfile: 'diegetic', hookSeconds: 2, defaultHookId: 'speed-line-burst', }, 'music-video': { id: 'music-video', label: 'Music Video', subjectType: 'character', beatTemplate: 'song-structure', cameraVocab: 'cinematic', genre: 'music-video', audioProfile: 'diegetic', hookSeconds: 0, defaultHookId: 'hi-hat-flash-cuts', }, 'social-hook': { id: 'social-hook', label: 'Social Hook', subjectType: 'character', beatTemplate: 'social-2s-hook', cameraVocab: 'cinematic', genre: 'social', audioProfile: 'ad-mix', hookSeconds: 2, defaultHookId: 'impossible-scale', }, }; export type ReferenceBuildStep = 'base-ref' | 'sheet' | 'scene-plate'; /** * Canonical identity-reference build order (the banana-pro-director discipline). * * Always build identity references in this sequence, for both character and * product subjects: * 1. `base-ref` — a neutral white-seamless base reference establishing the * subject's canonical identity, free of scene context. * 2. `sheet` — a multi-angle reference sheet derived from the base ref. * 3. `scene-plate` — scene plates that place the established subject in context. * * Building out of order (e.g. a scene plate before a clean base ref) lets scene * lighting and framing contaminate the identity anchor, so the order is fixed. */ export function referenceBuildOrder(_subjectType: SubjectType): ReferenceBuildStep[] { return ['base-ref', 'sheet', 'scene-plate']; } export const CATEGORY_IDS: string[] = Object.keys(CATEGORY_REGISTRY); export function resolveCategory(id?: string): CategoryDescriptor { const key = id ?? 'cinematic'; const descriptor = CATEGORY_REGISTRY[key]; if (!descriptor) { throw new Error(`unknown category: ${id}`); } return descriptor; }