Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
5 changes: 5 additions & 0 deletions src/ai-label.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 10 additions & 1 deletion test/ai-label.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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) => {
Expand Down
Loading