export const cameraMoves = ['hold','dolly-in','dolly-out','pan-left','pan-right','tilt-up','tilt-down','zoom-in','zoom-out','orbit-left','orbit-right'] export const soundscapeOptions = ['quiet room','distant birds','fabric rustle','footsteps','soft cloth','city hush','rain on glass','room tone only','no extra SFX'] export function normalizeBeat(beat = {}) { let moves = [...new Set((beat.moves || []).filter(m => cameraMoves.includes(m)))] if (moves.includes('hold') || !moves.length) moves = ['hold'] const duration = Number(beat.durationSec) return { moves, amplitude: ['subtle','moderate','large'].includes(beat.amplitude) ? beat.amplitude : 'subtle', speed: ['slow','medium','fast'].includes(beat.speed) ? beat.speed : 'slow', durationSec: beat.durationSec == null ? null : Number.isFinite(duration) ? Math.max(.5, Math.round(duration * 2) / 2) : null } } export function toggleMove(beat, move) { if (!cameraMoves.includes(move)) return normalizeBeat(beat) if (move === 'hold') return normalizeBeat({...beat,moves:['hold']}) const moves = beat.moves.filter(m => m !== 'hold') return normalizeBeat({...beat,moves:moves.includes(move) ? moves.filter(m => m !== move) : [...moves,move]}) } export function sectionsFrom(value = {}) { if (typeof value === 'string') value = {action:value} value ||= {} const s = Object.fromEntries(['action','camera','soundscape','music','locks'].map(k => [k,typeof value[k] === 'string' ? value[k] : ''])) const legacy = value.cameraChip s.cameraBeats = Array.isArray(value.cameraBeats) ? value.cameraBeats.map(normalizeBeat) : legacy ? [normalizeBeat({...legacy,moves:[legacy.family === 'static' ? 'hold' : legacy.family.replaceAll(' ','-')]})] : [] s.soundscapeChips = [...new Set((value.soundscapeChips || []).filter(x => soundscapeOptions.includes(x)))] s.soundscapeLevel = value.soundscapeLevel === 'crush' ? 'crush' : 'normal' const d = typeof value.dialogue === 'object' && value.dialogue ? value.dialogue : {mode:value.dialogue ? 'spoken to camera' : 'none',line:value.dialogue || ''} s.dialogue = {mode:['none','spoken to camera','spoken off-screen','whisper'].includes(d.mode) ? d.mode : 'none',language:String(d.language || 'English'),speaker:String(d.speaker || 'S1'),line:String(d.line || '')} s.musicChip = ['N/A','none','low underscore'].includes(value.musicChip) ? value.musicChip : 'N/A' return s } export function compileSections(value) { const s = sectionsFrom(value) const camera = s.cameraBeats.length ? ['One continuous take.', ...s.cameraBeats.map((b,i) => { const hold = b.moves[0] === 'hold' || b.moves.includes('hold') if (hold) { return `${i ? 'Then' : 'First'}, hold the camera locked on a tripod.` } const move = b.moves.map(m=>m.replaceAll('-',' ')).join(' and ') + (b.moves.length > 1 ? ' simultaneously' : '') const timing = b.durationSec == null ? 'through the end of the remaining clip' : i === 0 ? `for the first ${b.durationSec} seconds` : `for ${b.durationSec} seconds` return `${i ? 'Then' : 'First'}, ${move}, ${{subtle:'small',moderate:'medium',large:'large'}[b.amplitude]} amplitude, ${b.speed} speed, ${timing}.` }), 'No cuts. No new setup.'].join(' ') : s.camera const sound = [s.soundscapeChips.length ? `${s.soundscapeChips.join(', ')}.${s.soundscapeLevel === 'crush' ? ' Crushed soundscape dynamics.' : ''}` : '',s.soundscape].filter(Boolean).join(' ') const d=s.dialogue, line=d.line.trim() const dialogue = d.mode !== 'none' && line ? `${d.mode === 'spoken off-screen' ? 'Off-screen voice; on-screen lips remain closed. ' : ''} (${d.speaker}) ${d.mode === 'whisper' ? 'whispers' : 'says'} [${d.language}]${line}` : '' const music = s.music || (s.musicChip === 'N/A' ? 'non_diegetic_music: N/A' : s.musicChip === 'none' ? 'No non-diegetic music.' : 'A low non-diegetic musical underscore.') return [s.action,camera && `Camera: ${camera}`,sound && `Soundscape: ${sound}`,dialogue,music,s.locks && `Locks: ${s.locks}`].filter(Boolean).join('\n\n') }