Repository Public
1import { z } from "zod";23/** Typed contracts shared by the studio, the pipeline and the LLM adapters. */45export const LANGS = ["en", "fr", "bilingual"] as const;6export const MODES = ["manual", "assisted", "autonomous"] as const;7export const KINDS = ["fiction", "factual", "experimental"] as const;8export const VOICES = ["altair", "ara", "atlas", "aurora", "carina", "celeste", "eve", "helios", "iris", "kepler", "leo", "luna", "orion", "sal", "ursa", "zenith"] as const;910export const CreativeBrief = z.object({11 theme: z.string().trim().min(3).max(600),12 objective: z.string().trim().max(400).default(""),13 audience: z.string().trim().max(200).default("curious general audience"),14 mood: z.string().trim().max(120).default(""),15 genre: z.string().trim().max(120).default(""),16 constraints: z.string().trim().max(600).default(""),17 language: z.enum(LANGS).default("en"),18 seriesSlug: z.string().trim().max(60).default(""),19 shots: z.coerce.number().int().min(2).max(6).default(5),20 mode: z.enum(MODES).default("assisted"),21 quality: z.enum(["draft", "master"]).default("master"),22});23export type CreativeBrief = z.infer<typeof CreativeBrief>;2425const score = z.number().describe("integer 0-10");2627export const Pitch = z.object({28 title: z.string(),29 logline: z.string().describe("one sentence, under 30 words"),30 thesis: z.string().describe("the single idea, takeaway or narrative payoff the viewer leaves with"),31 hook: z.string().describe("what the first two seconds show and why a viewer stays"),32 setup: z.string(), tension: z.string(), payoff: z.string(),33 visualWorld: z.string().describe("palette, texture, setting, recurring object or character"),34 artifactRisks: z.string().describe("what generative video is likely to get wrong here and how the plan avoids it"),35});36export type Pitch = z.infer<typeof Pitch>;37export const PitchSet = z.object({ pitches: z.array(Pitch).describe("exactly 4 clearly different pitches") });3839export const PitchScores = z.object({40 scores: z.array(z.object({41 index: z.number().describe("0-based index of the pitch"),42 originality: score, feasibility: score, visualPotential: score, narrativeClarity: score,43 artifactRisk: score.describe("0 = safe, 10 = very likely to show artifacts"),44 note: z.string().describe("one sentence justification"),45 })),46});47export type PitchScore = z.infer<typeof PitchScores>["scores"][number];48export const pitchTotal = (s: PitchScore) =>49 Math.round((0.3 * s.originality + 0.2 * s.feasibility + 0.2 * s.visualPotential + 0.2 * s.narrativeClarity + 0.1 * (10 - s.artifactRisk)) * 10) / 10;5051export const Beat = z.object({52 purpose: z.enum(["hook", "setup", "tension", "turn", "payoff"]),53 lang: z.enum(["en", "fr"]).describe("language this line is spoken in"),54 narration: z.string().describe("spoken line for this shot, in the spoken language; 8 to 22 words"),55 narrationAlt: z.string().describe("faithful adaptation of the line in the other language (French if spoken English, English otherwise)"),56 visual: z.string().describe("what the viewer sees during the line"),57});58export const Script = z.object({59 titleEn: z.string(), titleFr: z.string(),60 synopsisEn: z.string().describe("2 sentences"), synopsisFr: z.string().describe("2 sentences"),61 takeawayEn: z.string().describe("one sentence: what the viewer leaves with"), takeawayFr: z.string(),62 voiceId: z.enum(VOICES).describe("narrator voice"),63 voiceDirection: z.string().describe("tone and pace of the narration"),64 beats: z.array(Beat),65 claims: z.array(z.string()).describe("checkable factual statements made by the narration; empty for pure fiction"),66 tags: z.object({67 genre: z.array(z.string()), mood: z.array(z.string()), topic: z.array(z.string()),68 technique: z.array(z.string()), style: z.array(z.string()),69 }).describe("slugs chosen only from the vocabulary given in the prompt"),70});71export type Script = z.infer<typeof Script>;7273export const Research = z.object({74 claims: z.array(z.object({75 claim: z.string(),76 factual: z.boolean().describe("false when the sentence only describes the image or is plainly figurative, so there is nothing to verify"),77 verdict: z.enum(["supported", "inaccurate", "uncertain"]),78 correction: z.string().describe("corrected wording when inaccurate, otherwise empty"),79 sources: z.array(z.object({ title: z.string(), url: z.string() })),80 })),81});82export type Research = z.infer<typeof Research>;8384export const Bible = z.object({85 palette: z.string(), lighting: z.string(), lens: z.string().describe("lens, film stock, grain, depth of field"),86 world: z.string().describe("era, place, materials"),87 characters: z.array(z.object({ name: z.string(), description: z.string().describe("fixed visual description reused verbatim in every prompt") })),88 anchors: z.array(z.string()).describe("recurring objects or motifs that hold continuity"),89 avoid: z.string().describe("things that must not appear: text, logos, real people, known artifact traps"),90});91export type Bible = z.infer<typeof Bible>;9293export const ShotSpec = z.object({94 title: z.string(),95 camera: z.string().describe("framing and one motivated camera move"),96 action: z.string().describe("what the protagonist does in this shot, as two or three visible beats"),97 continuity: z.string().describe("what must match the previous shot"),98 transition: z.enum(["cut", "fade"]),99 imagePrompt: z.string().describe("shot 1: complete still-image prompt (subject first, then details, art direction, light, colour, composition). Later shots: an edit instruction beginning 'A new scene with the same …, seen from a different angle:'"),100 videoPrompt: z.string().describe("action beats in order, then one motivated camera move, then three or four diegetic sounds; ends with 'No speech.'"),101});102export const ShotPlan = z.object({ bible: Bible, shots: z.array(ShotSpec) });103export type ShotPlan = z.infer<typeof ShotPlan>;104105export const Critique = z.object({106 narrativeCoherence: score, visualFidelity: score, continuity: score, audioIntelligibility: score,107 factualIntegrity: score, accessibility: score, safety: score.describe("10 = entirely safe"),108 issues: z.array(z.object({109 shot: z.number().describe("1-based shot number, 0 for the whole film"),110 severity: z.enum(["minor", "major", "blocking"]),111 note: z.string(),112 fix: z.string().describe("concrete regeneration or edit instruction"),113 })),114 summary: z.string(),115});116export type Critique = z.infer<typeof Critique>;117export const critiqueScore = (c: Critique) =>118 Math.round(((c.narrativeCoherence + c.visualFidelity + c.continuity + c.audioIntelligibility + c.factualIntegrity + c.accessibility + c.safety) / 7) * 10) / 10;119120export const TechnicalCheck = z.object({121 id: z.string(), label: z.string(), ok: z.boolean(), severity: z.enum(["info", "warn", "fail"]), detail: z.string(),122});123export type TechnicalCheck = z.infer<typeof TechnicalCheck>;124125/** Public, prompt-free description of a finished film. */126export const FilmManifest = z.object({127 schema: z.literal("clip.film/1"),128 title: z.object({ en: z.string(), fr: z.string() }),129 language: z.enum(LANGS),130 kind: z.enum(KINDS),131 duration: z.number(),132 frame: z.object({ width: z.number(), height: z.number(), fps: z.number() }),133 shots: z.array(z.object({ index: z.number(), start: z.number(), duration: z.number(), transition: z.string() })),134 audio: z.object({ narration: z.string(), bed: z.string(), music: z.string(), loudnessTarget: z.string() }),135 captions: z.array(z.object({ language: z.string(), kind: z.string() })),136 provenance: z.object({137 aiGenerated: z.literal(true),138 models: z.array(z.object({ role: z.string(), provider: z.string(), model: z.string() })),139 pipeline: z.array(z.string()),140 generatedAt: z.string(),141 humanApproval: z.string(),142 }),143});144export type FilmManifest = z.infer<typeof FilmManifest>;145146const DROP = new Set(["minLength", "maxLength", "minimum", "maximum", "minItems", "maxItems", "pattern", "format", "default", "$schema", "exclusiveMinimum", "exclusiveMaximum"]);147/** JSON Schema accepted by strict structured-output endpoints (no numeric/length keywords). */148export function strictJsonSchema(schema: z.ZodType): Record<string, unknown> {149 const walk = (n: unknown): unknown => {150 if (Array.isArray(n)) return n.map(walk);151 if (n && typeof n === "object") {152 const o: Record<string, unknown> = {};153 for (const [k, v] of Object.entries(n)) if (!DROP.has(k)) o[k] = walk(v);154 if (o.type === "object" && o.properties) { o.required = Object.keys(o.properties as object); o.additionalProperties = false; }155 return o;156 }157 return n;158 };159 return walk(z.toJSONSchema(schema)) as Record<string, unknown>;160}161