SPB Git

Repository Public

contracts.ts 8.5 KB 161 lines typescript

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
Files main
Menu

Theme