/** * Camera-bible shot grammar — shot sizes, angles, movements, and the * sensory-words rule, each annotated with its emotional trigger. * * Pure, deterministic descriptive registries. No I/O, no network. This is the * cinematography "why" layer (what each choice MEANS and which words to give an * AI model) that complements the quantified emitters in {@link ./cinematography} * (Kelvin / key-angle / velocity). The {@link ./project-blueprint} normalizer and * the {@link ./filmmaking-prompts} composer draw on these to turn a director's * intent into provider-ready prompt fragments. * * Source: the operator-supplied camera-bible reference * (references/video/camera-bible.md). */ /** Canonical shot-size ids. Order is intentional (widest → tightest, then the * relational / special framings); callers may iterate {@link SHOT_SIZE_IDS}. */ export const SHOT_SIZE_IDS = [ 'ews', 'ws', 'mws', 'ms', 'mcu', 'cu', 'ecu', 'two-shot', 'ots', 'pov', 'dutch', 'birds-eye', 'worms-eye', ] as const; export type ShotSizeId = (typeof SHOT_SIZE_IDS)[number]; export interface ShotSize { /** Human label, e.g. `Extreme Wide Shot`. */ label: string; /** Short abbreviation as a director would write it, e.g. `EWS`. */ abbr: string; /** The one-line emotional function this framing serves. */ emotion: string; /** What the framing SAYS to the audience. */ says: string; /** Ready-to-paste AI prompt words for this framing. */ promptWords: string; } const SHOT_SIZES: Record = { ews: { label: 'Extreme Wide Shot', abbr: 'EWS', emotion: 'isolation, scale, overwhelm, loneliness', says: 'the character is tiny; the world is vast and they are small in it', promptWords: 'extreme wide shot, subject tiny in vast environment, isolation, scale', }, ws: { label: 'Wide Shot', abbr: 'WS', emotion: 'context, arrival, journey, placement', says: 'the character exists within a world larger than themselves', promptWords: 'wide shot, full environment visible, character placed in world', }, mws: { label: 'Medium Wide Shot', abbr: 'MWS', emotion: 'action, physical storytelling, posture reads', says: 'full body visible — how they carry themselves tells us who they are', promptWords: 'medium wide shot, full body visible, posture and movement readable', }, ms: { label: 'Medium Shot', abbr: 'MS', emotion: 'conversation, relationship, neutrality', says: 'normal human distance, no agenda, just listening', promptWords: 'medium shot, waist up, eye level, neutral, conversational', }, mcu: { label: 'Medium Close-Up', abbr: 'MCU', emotion: 'emotional engagement beginning, investment', says: 'we are starting to feel WITH this person, not just watch them', promptWords: 'medium close-up, chest to head, emotional engagement, investment', }, cu: { label: 'Close-Up', abbr: 'CU', emotion: 'deep emotion, truth, vulnerability, revelation', says: "this person's inner life IS the story right now", promptWords: 'close-up, face fills frame, emotion is the subject, shallow depth of field', }, ecu: { label: 'Extreme Close-Up', abbr: 'ECU', emotion: 'obsession, dread, the world narrows to one thing', says: 'this single detail IS everything; nothing else exists', promptWords: 'extreme close-up, one specific element fills entire frame, world narrows', }, 'two-shot': { label: 'Two-Shot', abbr: '2S', emotion: 'relationship, tension, contrast, dynamic between two people', says: 'the space between them tells us everything', promptWords: 'two-shot, both characters in frame, charged space between them', }, ots: { label: 'Over The Shoulder', abbr: 'OTS', emotion: 'perspective, shared view, we are in the conversation', says: 'we see what this character sees; we share their position', promptWords: 'over the shoulder shot, camera behind one character looking at the other', }, pov: { label: 'Point of View', abbr: 'POV', emotion: 'full immersion, we ARE the character, first person', says: 'this is not observation — this is experience', promptWords: 'point of view shot, first person perspective, we are the character', }, dutch: { label: 'Dutch Angle', abbr: 'DA', emotion: 'psychological unease, instability, something is wrong', says: "the world is tilted; normal rules don't apply here", promptWords: 'dutch angle, tilted frame 15-30 degrees, horizon line wrong, unease', }, 'birds-eye': { label: "Bird's Eye / Top Down", abbr: 'BA', emotion: 'detachment, god view, pattern, insignificance', says: 'we observe from above, removed from the character experience', promptWords: "bird's eye view, camera directly overhead, top-down, subject seen as pattern", }, 'worms-eye': { label: "Worm's Eye", abbr: 'WE', emotion: 'overwhelm, the subject is massive, we are small', says: 'looking up makes the subject dominant, threatening, or heroic', promptWords: "worm's eye view, camera at ground level looking up, subject towers against sky", }, }; /** * Resolve a shot size by id, falling back to `ms` (the neutral, honest default) * for unknown ids rather than throwing — mirrors {@link cinemaMode}. */ export function shotSize(id: string): ShotSize { return SHOT_SIZES[id as ShotSizeId] ?? SHOT_SIZES.ms; } /** Canonical camera-angle ids. */ export const CAMERA_ANGLE_IDS = ['low', 'eye-level', 'high', 'overhead', 'canted'] as const; export type CameraAngleId = (typeof CAMERA_ANGLE_IDS)[number]; const CAMERA_ANGLES: Record = { low: 'subject looks powerful, heroic, threatening', 'eye-level': 'neutral, equal, honest, natural', high: 'subject looks small, judged, vulnerable', overhead: 'abstract, pattern, emotionally removed', canted: 'psychological unease, instability', }; /** * Resolve a camera angle's emotional effect, falling back to `eye-level` for * unknown ids. */ export function cameraAngle(id: string): string { return CAMERA_ANGLES[id as CameraAngleId] ?? CAMERA_ANGLES['eye-level']; } /** Camera-bible movement ids — a DESCRIPTIVE registry (emotion + Seedance * syntax + prompt words). Intentionally separate from the typed * {@link CameraMovement} union in {@link ./cinematography}; this registry adds * crane-up / whip-pan / vertigo without touching that union's exhaustive * velocity / qualifier / prose Records. */ export const MOVEMENT_GRAMMAR_IDS = [ 'static-locked', 'dolly-in', 'dolly-out', 'pan', 'tilt-up', 'tilt-down', 'orbit', 'tracking', 'handheld', 'crane-up', 'whip-pan', 'vertigo', ] as const; export type MovementGrammarId = (typeof MOVEMENT_GRAMMAR_IDS)[number]; export interface MovementGrammar { emotion: string; /** Provider-friendly (Seedance) syntax token(s). */ seedanceSyntax: string; promptWords: string; /** Movements that lose all power if repeated; the composer should warn when * one appears more than once. */ useOnce?: boolean; } const MOVEMENT_GRAMMAR: Record = { 'static-locked': { emotion: 'authority, calm, indifference — the world accepts what is happening', seedanceSyntax: 'static camera, locked off', promptWords: 'static locked camera, zero movement, locked tripod, absolute stillness', }, 'dolly-in': { emotion: 'tension builds, revelation approaching, we enter someone’s interior', seedanceSyntax: 'slow dolly in', promptWords: 'slow dolly in, camera pushes toward subject, tension building, revelation', }, 'dolly-out': { emotion: 'isolation, scale revealed, character left alone, endings', seedanceSyntax: 'slow dolly out', promptWords: 'slow dolly out, camera retreats, subject grows smaller, isolation, release', }, pan: { emotion: 'reveal, following action, world opening in one direction', seedanceSyntax: 'pan left / pan right', promptWords: 'controlled pan, camera rotates horizontally, revealing space', }, 'tilt-up': { emotion: 'awe, aspiration, power — the subject is greater than we realized', seedanceSyntax: 'tilt up', promptWords: 'slow tilt up, camera rotates upward, subject revealed bottom to top, awe', }, 'tilt-down': { emotion: 'shame, defeat, weight, judgment from above', seedanceSyntax: 'tilt down', promptWords: 'slow tilt down, camera rotates downward, gravity, defeat, weight', }, orbit: { emotion: 'wonder, examination, god-like observation, nothing hidden', seedanceSyntax: 'slow orbit left / orbit right', promptWords: 'slow orbit, camera arcs around subject, wonder, examination', }, tracking: { emotion: 'journey, accompaniment, we walk with them', seedanceSyntax: 'tracking shot', promptWords: 'tracking shot, camera follows subject at same distance, journey, witness', }, handheld: { // CRITICAL for Seedance: keep "subtle" or the model shakes too violently. emotion: 'intimacy, reality, urgency, documentary truth', seedanceSyntax: 'handheld, subtle', promptWords: 'subtle handheld, organic human sway, documentary feel, intimate, NOT shaky', }, 'crane-up': { emotion: 'transcendence, release, story ending — we rise above and see the whole', seedanceSyntax: 'slow crane up', promptWords: 'slow crane up, camera rises above scene, transcendence, story releasing', }, 'whip-pan': { emotion: 'surprise, energy shift, time skip, momentum', seedanceSyntax: 'whip pan left / whip pan right', promptWords: 'whip pan, fast horizontal snap, motion blur smear, energy shift', }, vertigo: { emotion: 'existential dread, reality shifting, something deeply wrong', seedanceSyntax: 'dolly out while zooming in', promptWords: 'vertigo effect, dolly out while zooming in, background warps, subject stays, dread', useOnce: true, }, }; /** * Resolve a movement grammar by id, falling back to `static-locked` (the most * underused, most powerful move) for unknown ids. */ export function movementGrammar(id: string): MovementGrammar { return MOVEMENT_GRAMMAR[id as MovementGrammarId] ?? MOVEMENT_GRAMMAR['static-locked']; } /** * The atmosphere/sensory signature — the camera-bible "secret weapon" rule: * ALWAYS exactly five sensory words per scene (one smell, one texture, one * sound, two feelings). Sensory language activates a deeper, more instinctive * visual library in the model than purely visual description. */ export interface SensorySignature { smell: string; texture: string; sound: string; /** Exactly two feelings. */ feelings: [string, string]; } /** * Render a {@link SensorySignature} as the canonical five-word, slash-joined * clause, e.g. `incense / worn tatami / brush on paper / restraint / waiting`. * Empty fields are dropped so a partial signature still produces a clean line. */ export function sensoryClause(sig: SensorySignature): string { return [sig.smell, sig.texture, sig.sound, sig.feelings?.[0], sig.feelings?.[1]] .map((word) => (typeof word === 'string' ? word.trim() : '')) .filter(Boolean) .join(' / '); } /** * Prefix the sensory clause with an `Atmosphere:` label for inlining into a * scene prompt. Returns an empty string when the signature renders empty. */ export function sensoryLine(sig: SensorySignature): string { const clause = sensoryClause(sig); return clause ? `Atmosphere: ${clause}` : ''; } /** The three framing intents a character blueprint declares. */ export type FramingIntent = 'power' | 'vulnerability' | 'signature'; export interface FramingRecommendation { shotId: ShotSizeId; angleId: CameraAngleId; note: string; } /** * Default camera-framing recommendation for a framing intent. A blueprint's * per-character `cameraLanguage` strings take precedence; this is the * deterministic fallback when a character declares an intent but no explicit * framing. */ export function framingFor(intent: FramingIntent): FramingRecommendation { switch (intent) { case 'power': return { shotId: 'mcu', angleId: 'low', note: 'low angle renders the subject dominant — they fill and command the frame', }; case 'vulnerability': return { shotId: 'cu', angleId: 'high', note: 'high angle from above renders the subject small, exposed, and judged', }; case 'signature': default: return { shotId: 'ms', angleId: 'eye-level', note: 'the neutral defining frame — honest, equal, at human distance', }; } }