import { cinemaCharacterSheetHash } from './cinema-evidence.js'; import type { CinemaEvidenceLedgerArtifact } from './cinema-evidence-types.js'; import { validateCinemaContinuity, validateCinemaTimedAudio } from './cinema-continuity.js'; import { compileCinemaContinuityLedger, compileCinemaTimedAudioContract } from './cinema-production-contract-compiler.js'; import { writeCinemaProductionContracts, type CinemaProductionContractPaths } from './cinema-production-contract-store.js'; import type { CinemaContinuityLedgerArtifact, CinemaGeographyMapArtifact, CinemaTimedAudioContractArtifact } from './cinema-continuity-types.js'; import type { CinemaPlanningPackage } from './cinema-project-planner.js'; import { createCinemaPreflightReport, type CinemaPreflightReportArtifact } from './cinema-preflight.js'; import { compileCinemaShotManifest, stableCinemaJson } from './cinema-shot-compiler.js'; import { readCinemaReferencePack, readCinemaShotManifest, writeCinemaReferencePack, writeCinemaShotManifest } from './cinema-store.js'; import type { CinemaCharacterSheetArtifact, CinemaReferencePackArtifact, CinemaShotManifestArtifact } from './cinema-types.js'; import { validateCinemaReferencePack, validateCinemaShotManifest } from './cinema-validation.js'; export interface CinemaCompiledProject { preflight: CinemaPreflightReportArtifact; geographyMap: CinemaGeographyMapArtifact; continuityLedger: CinemaContinuityLedgerArtifact; timedAudioContract: CinemaTimedAudioContractArtifact; referencePacks: CinemaReferencePackArtifact[]; shotManifests: CinemaShotManifestArtifact[]; providerCalls: 0; spendAuthorized: false; executable: false; } function visualPrefix(planning: CinemaPlanningPackage): string { const { visual, sound } = planning.visualSoundBible; return [ `Visual law ${planning.visualSoundBible.visualLawVersion}.`, `Palette: ${visual.palette.join(', ')}. Contrast: ${visual.contrast}`, `Lighting: ${visual.lighting.sources.join(', ')}; ${visual.lighting.direction}; ${visual.lighting.quality}`, `Optics: ${visual.opticsFamilies.join(', ')}. Camera grammar: ${visual.cameraGrammar}`, `Motion: ${visual.motionCharacter}. Texture: ${visual.texture}. Aspect: ${visual.aspectRatio}; cadence ${visual.cadenceFps}fps.`, `Sound law: ${sound.ambience}; dialogue ${sound.dialoguePerspective}; music ${sound.musicStrategy}.`, `Forbidden drift: ${sound.forbiddenDrift.join('; ')}.`, ].join(' '); } function assertValid(label: string, validation: { ok: boolean; issues: Array<{ path: string; message: string }> }): void { if (validation.ok) return; throw new Error(`${label} failed validation: ${validation.issues.map((issue) => `${issue.path}: ${issue.message}`).join('; ')}`); } export function compileCinemaProject(input: { planning: CinemaPlanningPackage; evidenceLedger: CinemaEvidenceLedgerArtifact; characterSheets: CinemaCharacterSheetArtifact[]; }): CinemaCompiledProject { const generatedAt = input.evidenceLedger.generatedAt; const preflight = createCinemaPreflightReport({ ...input, generatedAt }); if (!preflight.compileReady) throw new Error(`Cinema compilation is evidence-blocked: ${preflight.blockers.join('; ')}`); const hero = input.planning.story.storyBible.characters[0]; const sheet = input.characterSheets.find((item) => item.characterVersionId === input.planning.assetEvidence.assets[0].versionId); if (!sheet) throw new Error(`Cinema compilation cannot resolve the approved sheet for ${hero.characterId}`); const assignmentsByBeat = new Map(); for (const assignment of input.planning.coverage.shotAssignments) { const requirement = input.planning.coverage.requirements.find((item) => assignment.requirementIds.includes(item.requirementId)); if (requirement) assignmentsByBeat.set(requirement.beatId, (assignmentsByBeat.get(requirement.beatId) ?? 0) + 1); } const cameras: Array = [ { shotSize: 'wide', position: 'Primary camera side, geography readable from entry through consequence point', movement: 'Controlled establishing drift along the declared left-to-right axis', stability: 'controlled' }, { shotSize: 'close-up', position: 'Primary side detail angle preserving the scene axis', movement: 'Measured push toward the concrete setup detail', stability: 'controlled' }, { shotSize: 'medium', position: 'Primary side at the decision point', movement: 'Track the irreversible action without crossing the axis', stability: 'controlled' }, { shotSize: 'close-up', position: 'Primary side reaction angle with matching light direction', movement: 'Settle into stillness as the cost becomes legible', stability: 'locked' }, { shotSize: 'wide', position: 'Primary side consequence view toward the declared end landmark', movement: 'Slow release that preserves left-to-right trajectory', stability: 'controlled' }, ]; const prefix = visualPrefix(input.planning); const referencePacks: CinemaReferencePackArtifact[] = []; const shotManifests: CinemaShotManifestArtifact[] = []; input.planning.coverage.shotAssignments.forEach((assignment, index) => { const requirement = input.planning.coverage.requirements.find((item) => assignment.requirementIds.includes(item.requirementId)); if (!requirement) throw new Error(`Shot ${assignment.shotId} has no resolvable coverage requirement`); const beat = input.planning.story.storyBible.beats.find((item) => item.beatId === requirement.beatId); const sceneIndex = input.planning.story.storyBible.scenes.findIndex((item) => item.beatIds.includes(requirement.beatId)); const scene = input.planning.story.storyBible.scenes[sceneIndex]; if (!beat || !scene) throw new Error(`Shot ${assignment.shotId} cannot resolve beat/scene ${requirement.beatId}`); const rear = index === input.planning.coverage.shotAssignments.length - 1; const body = sheet.bodyPlates.find((plate) => plate.view === (rear ? 'back' : 'front')); if (!body) throw new Error(`Character sheet ${sheet.characterVersionId} lacks ${rear ? 'back' : 'front'} plate`); const pack: CinemaReferencePackArtifact = { schemaVersion: 1, projectSlug: input.planning.receipt.projectSlug, packId: `${assignment.shotId}-references-v1`, shotId: assignment.shotId, generatedAt, references: [ { assetId: sheet.identityAnchor.assetId, path: sheet.identityAnchor.path, contentHash: sheet.identityAnchor.contentHash, role: 'identity_face', visibilityIntent: rear ? 'offscreen_context' : 'visible_subject', subjectId: hero.characterId, characterVersionId: sheet.characterVersionId, identityBearingFaceCount: 1, identityVariant: 'canonical' }, { assetId: body.assetId, path: body.path, contentHash: body.contentHash, role: rear ? 'body_back_headless' : 'body_front_headless', visibilityIntent: 'visible_subject', subjectId: hero.characterId, characterVersionId: sheet.characterVersionId, identityBearingFaceCount: 0 }, ], transitions: [], referenceBudget: { maximum: 8, actual: 2, withinLimit: true }, }; assertValid(`Cinema reference pack ${pack.packId}`, validateCinemaReferencePack(pack)); const durationSeconds = Math.round((scene.durationSeconds / (assignmentsByBeat.get(requirement.beatId) ?? 1)) * 1000) / 1000; const dialogue = input.planning.story.creativeContract.dialoguePosture === 'none' ? 'forbidden' : input.planning.story.creativeContract.dialoguePosture === 'dialogue-led' ? 'required' : 'optional'; const manifest = compileCinemaShotManifest({ schemaVersion: 1, projectSlug: input.planning.receipt.projectSlug, shotId: assignment.shotId, sceneIndex, ordinal: index + 1, durationSeconds, referencePackId: pack.packId, generatedAt, story: { act: beat.act, beatId: beat.beatId, scenePurpose: `${scene.purpose} ${assignment.editorialPurpose}`, characterObjective: hero.objective, expectedChange: scene.expectedChange }, visualLaw: { prefix, overrides: [] }, camera: cameras[index] ?? cameras[cameras.length - 1], actionBeats: [{ order: 1, startSeconds: 0, endSeconds: durationSeconds, description: `${requirement.purpose} ${beat.change}`, timingMode: 'realtime', transition: index === 0 ? 'none' : 'hard-cut' }], occupancy: { characterIds: [hero.characterId], vehicleIds: [], propIds: [] }, audio: { music: 'optional', dialogue, sfx: 'optional', ambience: 'required', intentionalSilence: false, notes: input.planning.visualSoundBible.sound.ambience }, providerParameters: { values: { providerRoute: null, submissionAuthorized: false, planningReceiptHash: input.planning.receipt.contentHash, evidenceLedgerHash: input.evidenceLedger.contentHash, characterSheetHash: cinemaCharacterSheetHash(sheet), activeReferenceHashes: pack.references.map((reference) => reference.contentHash), visualSoundBibleHash: input.planning.visualSoundBible.contentHash, geographyMapHash: input.planning.geographyMap.contentHash, coverageHash: input.planning.coverage.contentHash, } }, }, pack); assertValid(`Cinema shot manifest ${manifest.shotId}`, validateCinemaShotManifest(manifest)); referencePacks.push(pack); shotManifests.push(manifest); }); const continuityLedger = compileCinemaContinuityLedger(input.planning, shotManifests, generatedAt); const timedAudioContract = compileCinemaTimedAudioContract(input.planning, shotManifests, generatedAt); const boundManifests = shotManifests.map((manifest, index) => { const { compiledPrompt: _compiledPrompt, providerParameters, visualLaw, ...draft } = manifest; const bound = compileCinemaShotManifest({ ...draft, visualLaw: { prefix: visualLaw.prefix, overrides: visualLaw.overrides }, providerParameters: { values: { ...providerParameters.values, continuityLedgerHash: continuityLedger.contentHash, timedAudioContractHash: timedAudioContract.contentHash, } }, }, referencePacks[index]); assertValid(`Cinema bound shot manifest ${bound.shotId}`, validateCinemaShotManifest(bound)); return bound; }); assertValid('Cinema bound continuity', validateCinemaContinuity(continuityLedger, input.planning.geographyMap, boundManifests)); assertValid('Cinema bound timed audio', validateCinemaTimedAudio(timedAudioContract, boundManifests)); return { preflight, geographyMap: input.planning.geographyMap, continuityLedger, timedAudioContract, referencePacks, shotManifests: boundManifests, providerCalls: 0, spendAuthorized: false, executable: false }; } export async function writeCompiledCinemaProject(root: string, compiled: CinemaCompiledProject): Promise<{ referencePackPaths: string[]; shotManifestPaths: string[] } & CinemaProductionContractPaths> { for (const pack of compiled.referencePacks) { const existing = await readCinemaReferencePack(root, pack.projectSlug, pack.packId); if (existing && stableCinemaJson(existing) !== stableCinemaJson(pack)) throw new Error(`Cinema reference pack ${pack.packId} conflicts with the immutable compiled bundle`); } for (const manifest of compiled.shotManifests) { const existing = await readCinemaShotManifest(root, manifest.projectSlug, manifest.shotId); if (existing && stableCinemaJson(existing) !== stableCinemaJson(manifest)) throw new Error(`Cinema shot manifest ${manifest.shotId} conflicts with the immutable compiled bundle`); } const contractPaths = await writeCinemaProductionContracts(root, compiled.continuityLedger, compiled.timedAudioContract, compiled.geographyMap, compiled.shotManifests); const referencePackPaths = await Promise.all(compiled.referencePacks.map((pack) => writeCinemaReferencePack(root, pack))); const shotManifestPaths = await Promise.all(compiled.shotManifests.map((manifest) => writeCinemaShotManifest(root, manifest))); return { ...contractPaths, referencePackPaths, shotManifestPaths }; }