From 56043713659fadc531dce0efbf1941a05abb5444 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Wed, 21 Oct 2026 17:31:52 +0000 Subject: [PATCH] fix(label): a story asked for more scenes than the model wrote is filled with its own scenes from other angles (0.70.2) "Fuel the Fire" planned 21 story shots; the model wrote 5, so 16 shots had no prompt and the video could not be cut ("only 12 of 28 shots filmed so far"). The written scenes are now repeated from other angles (closer, wide, tracking, overhead, low) with the same cast, so every paid-for shot films. Co-Authored-By: Claude Opus 5.5 --- package.json | 2 +- src/ai-label.ts | 5 +++++ test/ai-label.test.ts | 11 ++++++++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 841b405..3305ae1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nixamp", - "version": "0.70.1", + "version": "0.70.2", "description": "It really whips the terminal's ass. A Winamp-shaped audio player for your terminal.", "license": "MIT", "type": "module", diff --git a/src/ai-label.ts b/src/ai-label.ts index e8ea7d8..93539df 100644 --- a/src/ai-label.ts +++ b/src/ai-label.ts @@ -698,6 +698,11 @@ export async function writeVeoStory( return { prompt: clean(item.prompt, 400), cast: [...new Set(who)].slice(0, 3) }; }).filter((one) => one.prompt.length > 10).slice(0, scenes); if (out.length < 1) throw new Error("the model wrote no story"); + // A model asked for many scenes often writes fewer: the rest are its own scenes again, from other angles, + // so every shot the plan paid for has something to film. + const angles = ["a different angle and closer", "a wide establishing angle", "a slow tracking shot", "an overhead angle", "a low angle"]; + const written = out.length; + for (let i = 0; out.length < scenes; i++) out.push({ ...out[i % written]!, prompt: `${out[i % written]!.prompt} Filmed as ${angles[i % angles.length]}.` }); return out.map((scene) => ({ cast: scene.cast, prompt: `Music video for a fictional ${artist.genre} song. ${scene.prompt} Mood: ${song.style}`.slice(0, 900) diff --git a/test/ai-label.test.ts b/test/ai-label.test.ts index 0a130cf..bee93d9 100644 --- a/test/ai-label.test.ts +++ b/test/ai-label.test.ts @@ -6,7 +6,7 @@ import { createServer as createHttpServer } from "node:http"; import { copyFileSync, existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { basename, dirname, join } from "node:path"; -import { AiLabel, cleanLyrics, closeUpTimes, fileName, planUsd, shotPlan, genreSlug, nextReset, periodOf, renderFilmedVideo, renderStoryVideo, renderVideo, retagVideo, ROSTER, songBase, syncLines, VeoUnavailable } from "../src/ai-label.ts"; +import { AiLabel, cleanLyrics, closeUpTimes, writeVeoStory, fileName, planUsd, shotPlan, genreSlug, nextReset, periodOf, renderFilmedVideo, renderStoryVideo, renderVideo, retagVideo, ROSTER, songBase, syncLines, VeoUnavailable } from "../src/ai-label.ts"; import { elevenLabsComposer, OutOfCredit, type Composer } from "../src/ai-radio.ts"; import type { askJson } from "../src/ai-text.ts"; import { Channels } from "../src/channels.ts"; @@ -375,6 +375,15 @@ test("a single is a short film: story scenes from its words, cut in turn with th } }); +test("a story asked for more scenes than the model wrote is filled with its own scenes from other angles", async () => { + const ask: typeof askJson = async () => ({ model: "m", value: { scenes: [{ prompt: "The singer walks out of a diner at dawn", cast: ["lead vocals"] }, { prompt: "Rain on a car windshield at night", cast: [] }] } }); + const scenes = await writeVeoStory({ genre: "Rock", sound: "loud" }, [{ role: "lead vocals", look: "woman, curly hair" }], { title: "Record", concept: "" }, { title: "Song", style: "fast", lyrics: "" }, 5, ask); + assert.equal(scenes.length, 5, "every paid-for shot has a scene"); + assert.match(scenes[2]!.prompt, /diner at dawn.*Filmed as a different angle/); + assert.deepEqual(scenes[2]!.cast, ["lead vocals"], "with the same cast"); + assert.match(scenes[3]!.prompt, /windshield.*Filmed as a wide establishing angle/); +}); + test("story scenes star the band as their profiles describe them, never strangers", async () => { const writer = fakeWriter(); const ask: typeof askJson = async (options) => {