nixamp 0.66.3 → 0.67.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/ai-label.d.ts +10 -5
  2. package/dist/ai-label.js +102 -37
  3. package/dist/station-cli.js +1 -0
  4. package/dist/station-mcp.d.ts +15 -0
  5. package/dist/station-mcp.js +2 -1
  6. package/package.json +1 -1
  7. package/src/ai-label.ts +106 -36
  8. package/src/station-cli.ts +1 -0
  9. package/src/station-mcp.ts +2 -1
  10. package/web/dist/assets/{ar-BQfRAPxT.js → ar-DPe7Pp8y.js} +1 -1
  11. package/web/dist/assets/{cs-CIlyIUyM.js → cs-CJcjEsDV.js} +1 -1
  12. package/web/dist/assets/{da-B73sEH_l.js → da-Bd1GXRyz.js} +1 -1
  13. package/web/dist/assets/{de-CR_qS1kD.js → de-CLZfazCs.js} +1 -1
  14. package/web/dist/assets/{es-CWcxhMQ1.js → es-BLqEfqLZ.js} +1 -1
  15. package/web/dist/assets/{fi-sw-rgeRG.js → fi-anv-MFsC.js} +1 -1
  16. package/web/dist/assets/{fr-DWwCpFG-.js → fr-BzD2HDGv.js} +1 -1
  17. package/web/dist/assets/{hi-BDa2YD-x.js → hi-D71Abrr0.js} +1 -1
  18. package/web/dist/assets/{hu-CsYXXp0t.js → hu-BmLNPlTb.js} +1 -1
  19. package/web/dist/assets/{id-CzrnSbAL.js → id-D804zCDF.js} +1 -1
  20. package/web/dist/assets/{it-DT6eQX_8.js → it-BAuY8mMB.js} +1 -1
  21. package/web/dist/assets/{main-B-WZ8VbE.js → main-DbQrVbPR.js} +2 -2
  22. package/web/dist/assets/{nl-C57z9Sux.js → nl-D8x7GCIF.js} +1 -1
  23. package/web/dist/assets/{ru-CW9If0BU.js → ru-Ci9fQdcv.js} +1 -1
  24. package/web/dist/assets/{sv-CiW_iXWQ.js → sv-CZtgl4E6.js} +1 -1
  25. package/web/dist/assets/{uk-D_agLCru.js → uk-VhX0Jvpy.js} +1 -1
  26. package/web/dist/assets/{vi-C4wrS-qx.js → vi-TdUCiab8.js} +1 -1
  27. package/web/dist/assets/{zh-TOIwUGfV.js → zh-BMJXXIBE.js} +1 -1
  28. package/web/dist/index.html +1 -1
  29. package/web/dist/sw.js +19 -19
@@ -90,8 +90,10 @@ export interface LabelSettings {
90
90
  videos: boolean;
91
91
  /** Each album's single (song 1) is filmed with Veo, while the month's Veo budget lasts. */
92
92
  veo: boolean;
93
- /** How many 8 s Veo shots of the band a single is cut from. */
93
+ /** How many 8 s Veo shots of the band playing (never the singer) a single is cut from. */
94
94
  veoShots: number;
95
+ /** How many 8 s Veo story scenes (a short film from the song's words, nobody singing) a single gets. */
96
+ storyShots: number;
95
97
  /** How many close-ups of the singer a single gets, lip-synced to the song (0: none, the band only). */
96
98
  lipsyncShots: number;
97
99
  /** Public, listed and owned, so the genre stations are on the guide. */
@@ -166,6 +168,8 @@ export declare function syncLines(lyrics: string, timed: TimedWord[]): SyncedLin
166
168
  export declare function writeStoryboard(artist: Pick<LabelArtist, "name" | "genre" | "sound"> & {
167
169
  cast?: string;
168
170
  }, album: Pick<LabelAlbum, "title" | "concept">, song: Pick<LabelSong, "title" | "style" | "lyrics">, scenes: number, ask?: typeof askJson): Promise<string[]>;
171
+ /** A single's story scenes for Veo: a short film from its words, nobody singing, people by look not name. */
172
+ export declare function writeVeoStory(artist: Pick<LabelArtist, "genre" | "sound">, members: Array<Pick<Member, "role" | "look">>, album: Pick<LabelAlbum, "title" | "concept">, song: Pick<LabelSong, "title" | "style" | "lyrics">, scenes: number, ask?: typeof askJson): Promise<string[]>;
169
173
  /** Thrown by a Veo dependency when this month's budget is spent: no more shots until next month. */
170
174
  export declare class VeoUnavailable extends Error {
171
175
  }
@@ -203,11 +207,12 @@ export declare function closeUpTimes(seconds: number, lines: Array<{
203
207
  start: number;
204
208
  }>, count: number, length?: number): number[];
205
209
  /**
206
- * The version of a video cut from Veo footage: the band with nobody singing, and the
207
- * singer only in close-ups lip-synced to the song. Anything older (a slideshow, or 4:
208
- * footage whose singers were out of sync) is not shown.
210
+ * The version of a video cut from Veo footage: story scenes and the band playing with
211
+ * nobody singing, the singer only in close-ups lip-synced to the song. Anything older
212
+ * is not shown: a slideshow, 4 (singers out of sync) or 5 (all stage, the singer at
213
+ * the mic in the wide shots).
209
214
  */
210
- export declare const FILMED = 5;
215
+ export declare const FILMED = 6;
211
216
  /**
212
217
  * A single's music video, cut from its filmed shots alone: 4 s cuts, each
213
218
  * shot's first half then its second, round and round the shots until the song
package/dist/ai-label.js CHANGED
@@ -28,7 +28,7 @@
28
28
  */
29
29
  import { execFile } from "node:child_process";
30
30
  import { randomBytes } from "node:crypto";
31
- import { mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
31
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
32
32
  import { dirname, join, resolve, sep } from "node:path";
33
33
  import { aiKey } from "./ai-keys.js";
34
34
  import { composerFromEnv, OutOfCredit } from "./ai-radio.js";
@@ -273,7 +273,8 @@ export const DEFAULTS = {
273
273
  publish: true,
274
274
  videos: true,
275
275
  veo: false,
276
- veoShots: 6,
276
+ veoShots: 2,
277
+ storyShots: 4,
277
278
  lipsyncShots: 2,
278
279
  parallel: 1,
279
280
  genreOrder: ["Punk", "Blues", "Country", "Jazz", "Classic Rock", "Metal", "Hip-Hop", "Pop", "R&B"],
@@ -489,6 +490,34 @@ export async function writeStoryboard(artist, album, song, scenes, ask = askJson
489
490
  throw new Error("the model wrote no storyboard");
490
491
  return prompts;
491
492
  }
493
+ const VEO_STORY_SYSTEM = [
494
+ "You write the story scenes of a music video for an original song, as prompts for a video model that films 8-second moving shots.",
495
+ "Tell the song's story in cinematic scenes away from any stage: characters, places, action, weather, small details. Keep the same main character(s), setting and color palette across scenes so it reads as one short film; vary the shots (wide, tracking, close detail, aerial) and say how the camera moves.",
496
+ "Describe people by how they look, never by name. Nobody sings, speaks or lip-syncs, and nobody holds a microphone. Each prompt at most 350 characters. No text, letters, logos, real people or real brands.",
497
+ "Answer as JSON: {\"scenes\": [\"prompt\", ...]}.",
498
+ ].join(" ");
499
+ /** A single's story scenes for Veo: a short film from its words, nobody singing, people by look not name. */
500
+ export async function writeVeoStory(artist, members, album, song, scenes, ask = askJson) {
501
+ const cast = members.slice(0, 4).map((one) => `${one.role}: ${one.look.replace(/\b[A-Z][a-z]+ [A-Z][a-z]+\b/g, "")}`).join("; ");
502
+ const answer = await ask({
503
+ system: VEO_STORY_SYSTEM,
504
+ prompt: [
505
+ `Song: "${song.title}" (${artist.genre}; ${artist.sound}), from "${album.title}"${album.concept ? `: ${album.concept}` : ""}`,
506
+ cast ? `The band may appear as characters, described by look: ${cast}` : "",
507
+ `Mood and arrangement: ${song.style}`,
508
+ song.lyrics ? `Lyrics:\n${song.lyrics.slice(0, 1800)}` : "",
509
+ `Exactly ${scenes} scenes.`,
510
+ ].filter(Boolean).join("\n"),
511
+ maxTokens: 2500,
512
+ timeoutMs: 90_000,
513
+ });
514
+ const list = Array.isArray(answer.value["scenes"]) ? answer.value["scenes"] : [];
515
+ const prompts = list.map((one) => clean(one, 400)).filter((one) => one.length > 10).slice(0, scenes);
516
+ if (prompts.length < 1)
517
+ throw new Error("the model wrote no story");
518
+ return prompts.map((scene) => `Music video for a fictional ${artist.genre} song. ${scene} Mood: ${song.style}`.slice(0, 900)
519
+ + " Nobody sings, speaks or lip-syncs on camera. Cinematic, 35mm film look. No text, no logos.");
520
+ }
492
521
  /** Thrown by a Veo dependency when this month's budget is spent: no more shots until next month. */
493
522
  export class VeoUnavailable extends Error {
494
523
  }
@@ -498,14 +527,12 @@ const TRANSIENT = /\b50[234]\b|fetch failed|ECONN|timed? ?out/i;
498
527
  // so a mouth in shot is out of sync. The band plays; the vocalist is far off, from
499
528
  // behind, in silhouette or between lines, never a close-up of singing.
500
529
  const SHOTS = [
501
- "Wide shot of the whole band performing on a big stage at night, seen from the back of the crowd, the players small against the lights",
502
530
  "Close-up of the guitarist's hands on the fretboard and the strings, shallow focus, stage lights flaring",
503
- "The camera glides behind the band members as they play hard, seen from behind, looking out over the crowd",
504
531
  "Low-angle shot of the drummer hitting the cymbals at the song's climax, sticks blurred, sparks of light",
505
- "The lead vocalist from behind, arms raised to the crowd between lines, backlit silhouette, no face to camera",
506
- "Slow aerial push over the crowd toward the stage, fans with hands up, the band tiny in the distance",
507
- "Close-up of the bass player's fingers and the amplifier, then a tilt up to the band in silhouette",
508
- "The band walking out on stage before the show, backlit silhouettes, nobody speaking or singing",
532
+ "Close-up of the bass player's fingers and the amplifier, then a tilt up to the bassist in silhouette",
533
+ "The guitarist and the bassist seen from behind, playing hard toward a huge crowd, backlit",
534
+ "Slow push-in on the drummer in a quiet moment, moody light, sweat and smoke",
535
+ "Slow aerial push over the crowd toward the stage, fans with hands up, the stage tiny in the distance",
509
536
  ];
510
537
  // The singer's close-ups, which are lip-synced to the song afterwards (fal.ai). The mouth
511
538
  // must be clear: whatever covers it (a microphone, a hand) is smeared into the face when
@@ -525,7 +552,7 @@ export function veoPrompts(artist, song, members, count, kind = "band") {
525
552
  const cast = members.slice(0, 3).map((one, i) => `the ${one.role} from reference image ${i + 1} (${one.look.replace(/\b[A-Z][a-z]+ [A-Z][a-z]+\b/g, "")})`).join("; ");
526
553
  const rule = kind === "singer"
527
554
  ? "The vocalist sings clearly, the whole mouth always visible, never covered by a microphone or a hand. Cinematic, 35mm film look. No text, no logos."
528
- : "Nobody sings, speaks or lip-syncs on camera: no mouths in close-up. Cinematic, 35mm film look, dynamic camera. No text, no logos.";
555
+ : "The lead vocalist is not in this shot, and nobody sings, speaks or lip-syncs on camera. Cinematic, 35mm film look, dynamic camera. No text, no logos.";
529
556
  return (kind === "singer" ? CLOSE_UPS : SHOTS).slice(0, count).map((shot) => [
530
557
  `Music video for a fictional ${artist.genre} band. ${shot}.`,
531
558
  cast ? `The performers are exactly the people in the reference images: ${cast}.` : "",
@@ -645,11 +672,12 @@ export function closeUpTimes(seconds, lines, count, length = 8) {
645
672
  return chosen.sort((a, b) => a - b);
646
673
  }
647
674
  /**
648
- * The version of a video cut from Veo footage: the band with nobody singing, and the
649
- * singer only in close-ups lip-synced to the song. Anything older (a slideshow, or 4:
650
- * footage whose singers were out of sync) is not shown.
675
+ * The version of a video cut from Veo footage: story scenes and the band playing with
676
+ * nobody singing, the singer only in close-ups lip-synced to the song. Anything older
677
+ * is not shown: a slideshow, 4 (singers out of sync) or 5 (all stage, the singer at
678
+ * the mic in the wide shots).
651
679
  */
652
- export const FILMED = 5;
680
+ export const FILMED = 6;
653
681
  /**
654
682
  * A single's music video, cut from its filmed shots alone: 4 s cuts, each
655
683
  * shot's first half then its second, round and round the shots until the song
@@ -940,9 +968,12 @@ export class AiLabel {
940
968
  this.settings.videos = input["videos"] !== false;
941
969
  if (input["veo"] !== undefined)
942
970
  this.settings.veo = input["veo"] === true;
943
- const shots = whole("veoShots", 1, 8);
971
+ const shots = whole("veoShots", 0, 8);
944
972
  if (shots !== undefined)
945
973
  this.settings.veoShots = shots;
974
+ const story = whole("storyShots", 0, 8);
975
+ if (story !== undefined)
976
+ this.settings.storyShots = story;
946
977
  const close = whole("lipsyncShots", 0, 4);
947
978
  if (close !== undefined)
948
979
  this.settings.lipsyncShots = close;
@@ -1208,9 +1239,10 @@ export class AiLabel {
1208
1239
  catch { /* the asked-for length */ }
1209
1240
  // Paid-for shots live beside the album until the video is cut, so nothing filmed is filmed twice.
1210
1241
  const shots = join(_album.folder, `.veo-${song.n}`);
1211
- const { band: clips, singer } = await this.filmShots(artist, song, shots, scenes);
1212
- if (clips.length < Math.max(2, Math.ceil(this.settings.veoShots / 2)))
1213
- throw new Error(`only ${clips.length} of ${this.settings.veoShots} shots filmed so far`);
1242
+ const { band: clips, singer } = await this.filmShots(artist, _album, song, shots, scenes);
1243
+ const wanted = this.settings.veoShots + this.settings.storyShots;
1244
+ if (clips.length < Math.max(2, Math.ceil(wanted / 2)))
1245
+ throw new Error(`only ${clips.length} of ${wanted} shots filmed so far`);
1214
1246
  const pinned = await this.syncCloseUps(song, singer, seconds, shots);
1215
1247
  await (this.deps.filmedVideo ?? ((input) => renderFilmedVideo(this.deps.ffmpeg, input)))({ audio: song.file, clips, seconds, artist: artist.name, title: song.title, out, ...(pinned.length ? { pinned } : {}) });
1216
1248
  rmSync(shots, { recursive: true, force: true });
@@ -1288,34 +1320,57 @@ export class AiLabel {
1288
1320
  * Veo's reference images), on stage, in the song's mood. Stops for the
1289
1321
  * month when the budget is spent; a shot Veo will not make is skipped.
1290
1322
  */
1291
- async filmShots(artist, song, keep, dir) {
1323
+ async filmShots(artist, album, song, keep, dir) {
1292
1324
  const veo = this.deps.veo;
1293
1325
  if (!veo)
1294
1326
  return { band: [], singer: [] };
1295
1327
  mkdirSync(dir, { recursive: true });
1296
1328
  mkdirSync(keep, { recursive: true });
1297
1329
  const members = this.cast(artist);
1298
- const references = [];
1299
- for (const member of members.slice(0, 3)) {
1300
- const portrait = join(this.folderOf(artist), "members", `${member.slug}.jpg`);
1301
- const small = join(dir, `ref-${member.slug}.jpg`);
1330
+ // The players for the band shots, the voice for the close-ups: a singer's portrait in a band
1331
+ // shot puts the singer at a microphone, singing out of sync.
1332
+ const voice = (member) => /vocal|sing/i.test(member.role);
1333
+ const players = members.filter((member) => !voice(member));
1334
+ const singers = members.filter(voice).length ? members.filter(voice) : members.slice(0, 1);
1335
+ const portraits = async (list) => {
1336
+ const out = [];
1337
+ for (const member of list.slice(0, 3)) {
1338
+ const portrait = join(this.folderOf(artist), "members", `${member.slug}.jpg`);
1339
+ const small = join(dir, `ref-${member.slug}.jpg`);
1340
+ try {
1341
+ // 512 px is plenty for a face and keeps the request small.
1342
+ await runLow(this.deps.ffmpeg, [...this.deps.ffmpeg.slice(1), "-hide_banner", "-loglevel", "error", "-y", "-i", portrait, "-vf", "scale=512:-2", "-q:v", "4", small], 60_000, "the portrait did not shrink");
1343
+ out.push(readFileSync(small));
1344
+ }
1345
+ catch {
1346
+ // A member without a portrait is described in words only.
1347
+ }
1348
+ }
1349
+ return out;
1350
+ };
1351
+ const [playerRefs, singerRefs] = [await portraits(players), await portraits(singers)];
1352
+ const band = [];
1353
+ const story = [];
1354
+ const singer = [];
1355
+ // The story is written only while some of its scenes are still to film.
1356
+ const storyFiles = Array.from({ length: this.settings.storyShots }, (_, i) => join(keep, `story-${i}.mp4`));
1357
+ let storyPrompts = [];
1358
+ if (storyFiles.some((file) => !existsSync(file)) && this.canAsk()) {
1302
1359
  try {
1303
- // 512 px is plenty for a face and keeps the request small.
1304
- await runLow(this.deps.ffmpeg, [...this.deps.ffmpeg.slice(1), "-hide_banner", "-loglevel", "error", "-y", "-i", portrait, "-vf", "scale=512:-2", "-q:v", "4", small], 60_000, "the portrait did not shrink");
1305
- references.push(readFileSync(small));
1360
+ storyPrompts = await writeVeoStory(artist, members, album, song, storyFiles.length, this.deps.ask ?? askJson);
1306
1361
  }
1307
- catch {
1308
- // A member without a portrait is described in words only.
1362
+ catch (error) {
1363
+ this.log(`no story for "${song.title}" yet: ${error.message}`);
1309
1364
  }
1310
1365
  }
1311
- const band = [];
1312
- const singer = [];
1313
- // The singer's close-ups only where they can be lip-synced afterwards.
1366
+ // The singer's close-ups only where they can be lip-synced afterwards. Story scenes go without
1367
+ // the portraits: they would pull the whole band into every scene.
1314
1368
  const plan = [
1315
- ...veoPrompts(artist, song, members, this.settings.veoShots).map((prompt, i) => ({ prompt, file: join(keep, `shot-${i}.mp4`), into: band })),
1316
- ...(this.deps.lipsync ? veoPrompts(artist, song, members, this.settings.lipsyncShots, "singer").map((prompt, i) => ({ prompt, file: join(keep, `singer-${i}.mp4`), into: singer })) : []),
1369
+ ...storyFiles.map((file, i) => ({ prompt: storyPrompts[i] ?? "", file, into: story, refs: [] })),
1370
+ ...veoPrompts(artist, song, players, this.settings.veoShots).map((prompt, i) => ({ prompt, file: join(keep, `shot-${i}.mp4`), into: band, refs: playerRefs })),
1371
+ ...(this.deps.lipsync ? veoPrompts(artist, song, singers, this.settings.lipsyncShots, "singer").map((prompt, i) => ({ prompt, file: join(keep, `singer-${i}.mp4`), into: singer, refs: singerRefs })) : []),
1317
1372
  ];
1318
- for (const [i, { prompt, file, into }] of plan.entries()) {
1373
+ for (const [i, { prompt, file, into, refs }] of plan.entries()) {
1319
1374
  try {
1320
1375
  if (statSync(file).size > 10_000) {
1321
1376
  into.push({ path: file, seconds: 8 });
@@ -1323,12 +1378,14 @@ export class AiLabel {
1323
1378
  }
1324
1379
  }
1325
1380
  catch { /* not filmed yet */ }
1381
+ if (!prompt)
1382
+ continue;
1326
1383
  try {
1327
1384
  // nixamp.com briefly away (a deploy: 502/503) or the network: twice more, a minute apart.
1328
1385
  let bytes = null;
1329
1386
  for (let attempt = 0;; attempt++) {
1330
1387
  try {
1331
- bytes = await veo({ prompt, seconds: 8, references });
1388
+ bytes = await veo({ prompt, seconds: 8, references: refs });
1332
1389
  break;
1333
1390
  }
1334
1391
  catch (error) {
@@ -1351,9 +1408,17 @@ export class AiLabel {
1351
1408
  this.log(`Veo shot ${i + 1} for "${song.title}": ${error.message}`);
1352
1409
  }
1353
1410
  }
1354
- if (band.length || singer.length)
1355
- this.log(`Veo: ${band.length + singer.length} filmed shots for "${song.title}" (${artist.name})`);
1356
- return { band, singer };
1411
+ if (band.length || story.length || singer.length)
1412
+ this.log(`Veo: ${band.length + story.length + singer.length} filmed shots for "${song.title}" (${artist.name})`);
1413
+ // The cut takes them in turn: a story scene, then the band playing, and so on.
1414
+ const mixed = [];
1415
+ for (let i = 0; i < Math.max(story.length, band.length); i++) {
1416
+ if (story[i])
1417
+ mixed.push(story[i]);
1418
+ if (band[i])
1419
+ mixed.push(band[i]);
1420
+ }
1421
+ return { band: mixed, singer };
1357
1422
  }
1358
1423
  /** The label's music videos as one TV station: free to watch, with its breaks. */
1359
1424
  placeVideos() {
@@ -541,6 +541,7 @@ async function labelCommand(argv, call, show) {
541
541
  veo: has(argv, "--veo") ? true : has(argv, "--no-veo") ? false : undefined,
542
542
  veoShots: number("--veo-shots"),
543
543
  lipsyncShots: number("--lipsync-shots"),
544
+ storyShots: number("--story-shots"),
544
545
  genreOrder: flag(argv, "--genres"),
545
546
  enabled: has(argv, "--off") ? false : has(argv, "--on") ? true : undefined,
546
547
  publish: has(argv, "--unlisted") ? false : has(argv, "--public") ? true : undefined,
@@ -53,6 +53,7 @@ export declare const STATION_TOOLS: ({
53
53
  veo?: undefined;
54
54
  veoShots?: undefined;
55
55
  lipsyncShots?: undefined;
56
+ storyShots?: undefined;
56
57
  genreOrder?: undefined;
57
58
  state?: undefined;
58
59
  reason?: undefined;
@@ -132,6 +133,7 @@ export declare const STATION_TOOLS: ({
132
133
  veo?: undefined;
133
134
  veoShots?: undefined;
134
135
  lipsyncShots?: undefined;
136
+ storyShots?: undefined;
135
137
  genreOrder?: undefined;
136
138
  state?: undefined;
137
139
  reason?: undefined;
@@ -234,6 +236,7 @@ export declare const STATION_TOOLS: ({
234
236
  veo?: undefined;
235
237
  veoShots?: undefined;
236
238
  lipsyncShots?: undefined;
239
+ storyShots?: undefined;
237
240
  genreOrder?: undefined;
238
241
  state?: undefined;
239
242
  reason?: undefined;
@@ -333,6 +336,7 @@ export declare const STATION_TOOLS: ({
333
336
  veo?: undefined;
334
337
  veoShots?: undefined;
335
338
  lipsyncShots?: undefined;
339
+ storyShots?: undefined;
336
340
  genreOrder?: undefined;
337
341
  state?: undefined;
338
342
  reason?: undefined;
@@ -414,6 +418,7 @@ export declare const STATION_TOOLS: ({
414
418
  veo?: undefined;
415
419
  veoShots?: undefined;
416
420
  lipsyncShots?: undefined;
421
+ storyShots?: undefined;
417
422
  genreOrder?: undefined;
418
423
  state?: undefined;
419
424
  reason?: undefined;
@@ -502,6 +507,7 @@ export declare const STATION_TOOLS: ({
502
507
  veo?: undefined;
503
508
  veoShots?: undefined;
504
509
  lipsyncShots?: undefined;
510
+ storyShots?: undefined;
505
511
  genreOrder?: undefined;
506
512
  state?: undefined;
507
513
  reason?: undefined;
@@ -590,6 +596,7 @@ export declare const STATION_TOOLS: ({
590
596
  veo?: undefined;
591
597
  veoShots?: undefined;
592
598
  lipsyncShots?: undefined;
599
+ storyShots?: undefined;
593
600
  genreOrder?: undefined;
594
601
  state?: undefined;
595
602
  reason?: undefined;
@@ -695,6 +702,7 @@ export declare const STATION_TOOLS: ({
695
702
  veo?: undefined;
696
703
  veoShots?: undefined;
697
704
  lipsyncShots?: undefined;
705
+ storyShots?: undefined;
698
706
  genreOrder?: undefined;
699
707
  state?: undefined;
700
708
  reason?: undefined;
@@ -829,6 +837,10 @@ export declare const STATION_TOOLS: ({
829
837
  type: string;
830
838
  description: string;
831
839
  };
840
+ storyShots: {
841
+ type: string;
842
+ description: string;
843
+ };
832
844
  genreOrder: {
833
845
  type: string;
834
846
  items: {
@@ -903,6 +915,7 @@ export declare const STATION_TOOLS: ({
903
915
  veo?: undefined;
904
916
  veoShots?: undefined;
905
917
  lipsyncShots?: undefined;
918
+ storyShots?: undefined;
906
919
  genreOrder?: undefined;
907
920
  server: {
908
921
  type: "string";
@@ -1000,6 +1013,7 @@ export declare const STATION_TOOLS: ({
1000
1013
  veo?: undefined;
1001
1014
  veoShots?: undefined;
1002
1015
  lipsyncShots?: undefined;
1016
+ storyShots?: undefined;
1003
1017
  genreOrder?: undefined;
1004
1018
  state?: undefined;
1005
1019
  reason?: undefined;
@@ -1099,6 +1113,7 @@ export declare const STATION_TOOLS: ({
1099
1113
  veo?: undefined;
1100
1114
  veoShots?: undefined;
1101
1115
  lipsyncShots?: undefined;
1116
+ storyShots?: undefined;
1102
1117
  genreOrder?: undefined;
1103
1118
  state?: undefined;
1104
1119
  reason?: undefined;
@@ -125,6 +125,7 @@ export const STATION_TOOLS = [
125
125
  veo: { type: "boolean", description: "Each album's single gets filmed Veo shots of the band (nixamp.com's monthly Veo cap applies)." },
126
126
  veoShots: { type: "integer", description: "8 s Veo shots of the band per single (1-8); nobody sings in them." },
127
127
  lipsyncShots: { type: "integer", description: "Close-ups of the singer per single, lip-synced to the song on nixamp.com (0-4; 0 = the band only)." },
128
+ storyShots: { type: "integer", description: "Cinematic story scenes per single, a short film from the song's words, nobody singing (0-8)." },
128
129
  genreOrder: { type: "array", items: STRING, description: "Genres whose artists get albums first, in order." },
129
130
  },
130
131
  },
@@ -244,7 +245,7 @@ export async function callStationTool(name, args, options = {}) {
244
245
  const key = typeof args["key"] === "string" && /^[a-f0-9]{10}$/.test(args["key"]) ? args["key"] : "";
245
246
  const answer = action === "status" ? await call("GET", "/api/v1/stations/label")
246
247
  : action === "sign" ? await call("POST", "/api/v1/stations/label/artists", pick(["preset", "roster", "name", "genre", "sound", "voice", "bio", "themes", "enabled"]))
247
- : action === "configure" ? await call("POST", "/api/v1/stations/label", pick(["holder", "monthlySeconds", "everyDays", "seedAlbums", "songsPerAlbum", "songSeconds", "parallel", "genreOrder", "albumsPerDay", "newArtistChance", "resetDay", "veo", "veoShots", "lipsyncShots", "enabled", "publish"]))
248
+ : action === "configure" ? await call("POST", "/api/v1/stations/label", pick(["holder", "monthlySeconds", "everyDays", "seedAlbums", "songsPerAlbum", "songSeconds", "parallel", "genreOrder", "albumsPerDay", "newArtistChance", "resetDay", "veo", "veoShots", "lipsyncShots", "storyShots", "enabled", "publish"]))
248
249
  : action === "run" ? await call("POST", "/api/v1/stations/label/run", {})
249
250
  : action === "resume" ? await call("POST", "/api/v1/stations/label", { resume: true })
250
251
  : !key ? null
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nixamp",
3
- "version": "0.66.3",
3
+ "version": "0.67.1",
4
4
  "description": "It really whips the terminal's ass. A Winamp-shaped audio player for your terminal.",
5
5
  "license": "MIT",
6
6
  "type": "module",