nixamp 0.59.4 → 0.60.0
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.
- package/dist/ai-label.d.ts +37 -0
- package/dist/ai-label.js +127 -10
- package/dist/music-hub.d.ts +6 -0
- package/dist/music-hub.js +15 -1
- package/dist/server.d.ts +5 -0
- package/dist/server.js +64 -2
- package/dist/station-cli.js +4 -1
- package/dist/station-mcp.d.ts +30 -0
- package/dist/station-mcp.js +3 -1
- package/dist/station-usage.d.ts +1 -1
- package/dist/station-usage.js +1 -0
- package/dist/veo-studio.d.ts +39 -0
- package/dist/veo-studio.js +110 -0
- package/package.json +1 -1
- package/src/ai-label.ts +128 -12
- package/src/music-hub.ts +12 -1
- package/src/server.ts +50 -3
- package/src/station-cli.ts +4 -1
- package/src/station-mcp.ts +3 -1
- package/src/station-usage.ts +1 -0
- package/src/veo-studio.ts +123 -0
- package/web/dist/assets/{ar-BO4rWQ9P.js → ar-jc2nfbwU.js} +1 -1
- package/web/dist/assets/{cs-BbzHmyap.js → cs-CsQm5s-8.js} +1 -1
- package/web/dist/assets/{da-BycUJKjY.js → da-DHCYXrSL.js} +1 -1
- package/web/dist/assets/{de-CsO18_9e.js → de-IO9rajjn.js} +1 -1
- package/web/dist/assets/{es-DU675bF4.js → es-CIkJTUJE.js} +1 -1
- package/web/dist/assets/{fi-CblqzoKx.js → fi-JklvgzxQ.js} +1 -1
- package/web/dist/assets/{fr-YYXvYy_4.js → fr-CUk3yUOe.js} +1 -1
- package/web/dist/assets/{hi-CkCMt25E.js → hi-CEdvI1Y5.js} +1 -1
- package/web/dist/assets/{hu-BHSVpBm0.js → hu-B_oRzY-F.js} +1 -1
- package/web/dist/assets/{id-BjFgJnnl.js → id-D5Wx0nsW.js} +1 -1
- package/web/dist/assets/{it-HsdMsXQn.js → it-DP4ncIUW.js} +1 -1
- package/web/dist/assets/{main-DYyAppfI.js → main-D1zLWorp.js} +2 -2
- package/web/dist/assets/{nl-D_p5aMmJ.js → nl-BSvol-ht.js} +1 -1
- package/web/dist/assets/{ru-BgvTz-66.js → ru-35Ln-kD-.js} +1 -1
- package/web/dist/assets/{sv-BwkC3xqh.js → sv-D1DNLvXv.js} +1 -1
- package/web/dist/assets/{uk-Bhfl4oKh.js → uk-B0fTytVz.js} +1 -1
- package/web/dist/assets/{vi-CCYFXsEm.js → vi-6o60v8Th.js} +1 -1
- package/web/dist/assets/{zh-D8lP7hhM.js → zh-CbbDoNoL.js} +1 -1
- package/web/dist/index.html +1 -1
- package/web/dist/sw.js +19 -19
package/dist/ai-label.d.ts
CHANGED
|
@@ -88,6 +88,10 @@ export interface LabelSettings {
|
|
|
88
88
|
songSeconds: number;
|
|
89
89
|
/** A music video for every song (the cover over itself, blurred, and the song's waveform), and a TV station of them. */
|
|
90
90
|
videos: boolean;
|
|
91
|
+
/** Each album's single (song 1) gets filmed Veo shots of the band, while the month's Veo budget lasts. */
|
|
92
|
+
veo: boolean;
|
|
93
|
+
/** How many 8 s Veo shots a single gets. */
|
|
94
|
+
veoShots: number;
|
|
91
95
|
/** Public, listed and owned, so the genre stations are on the guide. */
|
|
92
96
|
publish: boolean;
|
|
93
97
|
/** Songs made at the same time (ElevenLabs takes several requests at once on a paid plan). */
|
|
@@ -160,6 +164,15 @@ export declare function syncLines(lyrics: string, timed: TimedWord[]): SyncedLin
|
|
|
160
164
|
export declare function writeStoryboard(artist: Pick<LabelArtist, "name" | "genre" | "sound"> & {
|
|
161
165
|
cast?: string;
|
|
162
166
|
}, album: Pick<LabelAlbum, "title" | "concept">, song: Pick<LabelSong, "title" | "style" | "lyrics">, scenes: number, ask?: typeof askJson): Promise<string[]>;
|
|
167
|
+
/** Thrown by a Veo dependency when this month's budget is spent: no more shots until next month. */
|
|
168
|
+
export declare class VeoUnavailable extends Error {
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The prompts for a single's filmed shots. The people are the reference
|
|
172
|
+
* images, described by role and look only: Veo refuses names with faces
|
|
173
|
+
* ("we can't create videos with real people's names or likenesses").
|
|
174
|
+
*/
|
|
175
|
+
export declare function veoPrompts(artist: Pick<LabelArtist, "genre" | "sound">, song: Pick<LabelSong, "style">, members: Array<Pick<Member, "role" | "look">>, count: number): string[];
|
|
163
176
|
/**
|
|
164
177
|
* A song's music video from its storyboard: each scene's picture moves (a
|
|
165
178
|
* slow zoom in, a pan, a zoom out, a pan back, in turn), scenes cross-fade,
|
|
@@ -173,6 +186,10 @@ export declare function renderStoryVideo(ffmpeg: string[], input: {
|
|
|
173
186
|
artist: string;
|
|
174
187
|
title: string;
|
|
175
188
|
out: string;
|
|
189
|
+
clips?: Array<{
|
|
190
|
+
path: string;
|
|
191
|
+
seconds: number;
|
|
192
|
+
}>;
|
|
176
193
|
}): Promise<void>;
|
|
177
194
|
/**
|
|
178
195
|
* A song's music video without a storyboard: its album cover over a blurred
|
|
@@ -210,6 +227,12 @@ export interface AiLabelDeps {
|
|
|
210
227
|
video?: (audio: string, cover: string, out: string) => Promise<void>;
|
|
211
228
|
/** Times each sung word in a recording; ElevenLabs forced alignment by default, null without a key. */
|
|
212
229
|
align?: ((audio: string, text: string) => Promise<TimedWord[]>) | null;
|
|
230
|
+
/** A filmed shot (Veo, through nixamp.com): the MP4, or a VeoUnavailable when the month's budget is spent. */
|
|
231
|
+
veo?: ((input: {
|
|
232
|
+
prompt: string;
|
|
233
|
+
seconds: number;
|
|
234
|
+
references: Buffer[];
|
|
235
|
+
}) => Promise<Buffer>) | null;
|
|
213
236
|
/** Renders a storyboard music video; ffmpeg by default (renderStoryVideo). */
|
|
214
237
|
storyVideo?: (input: {
|
|
215
238
|
audio: string;
|
|
@@ -218,6 +241,10 @@ export interface AiLabelDeps {
|
|
|
218
241
|
artist: string;
|
|
219
242
|
title: string;
|
|
220
243
|
out: string;
|
|
244
|
+
clips?: Array<{
|
|
245
|
+
path: string;
|
|
246
|
+
seconds: number;
|
|
247
|
+
}>;
|
|
221
248
|
}) => Promise<void>;
|
|
222
249
|
/** A scene's picture for this prompt, or null when nothing can draw one; Workers AI by default. */
|
|
223
250
|
image?: (prompt: string) => Promise<Buffer | null>;
|
|
@@ -250,6 +277,8 @@ export declare class AiLabel {
|
|
|
250
277
|
private videoFailed;
|
|
251
278
|
/** Songs whose words were already sent to the aligner since this server started. */
|
|
252
279
|
private alignTried;
|
|
280
|
+
/** The month ("YYYY-MM") Veo said its budget was spent; no more shots until the next. */
|
|
281
|
+
private veoSpentIn;
|
|
253
282
|
/** Old videos already tried as storyboards since this server started. */
|
|
254
283
|
private upgradeTried;
|
|
255
284
|
/** The day's album is tried at most every 30 minutes, so a failing model is not hammered. */
|
|
@@ -308,6 +337,14 @@ export declare class AiLabel {
|
|
|
308
337
|
* image model can draw it (version 2), else the cover-and-waveform kind (1).
|
|
309
338
|
*/
|
|
310
339
|
private filmSong;
|
|
340
|
+
/** Whether this song gets filmed Veo shots: an album's single, Veo on, and the month's budget not spent. */
|
|
341
|
+
private wantsVeo;
|
|
342
|
+
/**
|
|
343
|
+
* A single's filmed shots: the band, from their portraits (up to three as
|
|
344
|
+
* Veo's reference images), on stage, in the song's mood. Stops for the
|
|
345
|
+
* month when the budget is spent; a shot Veo will not make is skipped.
|
|
346
|
+
*/
|
|
347
|
+
private filmShots;
|
|
311
348
|
/** The label's music videos as one TV station: free to watch, with its breaks. */
|
|
312
349
|
private placeVideos;
|
|
313
350
|
stopSchedule(): void;
|
package/dist/ai-label.js
CHANGED
|
@@ -272,6 +272,8 @@ export const DEFAULTS = {
|
|
|
272
272
|
songSeconds: 150,
|
|
273
273
|
publish: true,
|
|
274
274
|
videos: true,
|
|
275
|
+
veo: false,
|
|
276
|
+
veoShots: 4,
|
|
275
277
|
parallel: 1,
|
|
276
278
|
genreOrder: ["Punk", "Blues", "Country", "Jazz", "Classic Rock", "Metal", "Hip-Hop", "Pop", "R&B"],
|
|
277
279
|
};
|
|
@@ -486,6 +488,33 @@ export async function writeStoryboard(artist, album, song, scenes, ask = askJson
|
|
|
486
488
|
throw new Error("the model wrote no storyboard");
|
|
487
489
|
return prompts;
|
|
488
490
|
}
|
|
491
|
+
/** Thrown by a Veo dependency when this month's budget is spent: no more shots until next month. */
|
|
492
|
+
export class VeoUnavailable extends Error {
|
|
493
|
+
}
|
|
494
|
+
const SHOTS = [
|
|
495
|
+
"Wide shot of the whole band performing on a stage, the crowd in the foreground",
|
|
496
|
+
"Close-up of the lead singer singing the chorus into the microphone",
|
|
497
|
+
"The camera moves across the stage between the band members as they play hard",
|
|
498
|
+
"Low-angle shot of the band at the song's climax, lights flaring behind them",
|
|
499
|
+
"Over-the-shoulder shot from behind the drums, looking out at the band and the crowd",
|
|
500
|
+
"Slow push-in on the band in a quieter moment, moody light",
|
|
501
|
+
"Handheld shot from the crowd, fans with hands up, the band lit on stage",
|
|
502
|
+
"The band walking out on stage before the show, backlit silhouettes turning into faces",
|
|
503
|
+
];
|
|
504
|
+
/**
|
|
505
|
+
* The prompts for a single's filmed shots. The people are the reference
|
|
506
|
+
* images, described by role and look only: Veo refuses names with faces
|
|
507
|
+
* ("we can't create videos with real people's names or likenesses").
|
|
508
|
+
*/
|
|
509
|
+
export function veoPrompts(artist, song, members, count) {
|
|
510
|
+
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("; ");
|
|
511
|
+
return SHOTS.slice(0, count).map((shot) => [
|
|
512
|
+
`Music video for a fictional ${artist.genre} band. ${shot}.`,
|
|
513
|
+
cast ? `The performers are exactly the people in the reference images: ${cast}.` : "",
|
|
514
|
+
`Mood and sound: ${song.style} ${artist.sound}.`.slice(0, 400),
|
|
515
|
+
"Cinematic, 35mm film look, dynamic camera. No text, no logos.",
|
|
516
|
+
].filter(Boolean).join(" "));
|
|
517
|
+
}
|
|
489
518
|
/** The DejaVu bold face most Linux boxes have, for the title card; "" when there is none. */
|
|
490
519
|
function titleFont() {
|
|
491
520
|
for (const file of ["/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", "/usr/share/fonts/dejavu/DejaVuSans-Bold.ttf", "/System/Library/Fonts/Supplemental/Arial Bold.ttf"]) {
|
|
@@ -518,8 +547,22 @@ function runLow(ffmpeg, args, timeout, what) {
|
|
|
518
547
|
*/
|
|
519
548
|
export function renderStoryVideo(ffmpeg, input) {
|
|
520
549
|
const fade = 1;
|
|
521
|
-
const
|
|
522
|
-
|
|
550
|
+
const clips = input.clips ?? [];
|
|
551
|
+
// Stills fill what the filmed shots do not; each gets at least 3 s, so on a short song some are left out.
|
|
552
|
+
let images = [...input.images];
|
|
553
|
+
const clipTime = clips.reduce((sum, one) => sum + one.seconds, 0);
|
|
554
|
+
const stillFor = (count) => (input.seconds + (count + clips.length - 1) * fade - clipTime) / Math.max(1, count);
|
|
555
|
+
while (images.length > 0 && stillFor(images.length) < 3)
|
|
556
|
+
images = images.slice(0, -1);
|
|
557
|
+
const each = images.length ? stillFor(images.length) : 0;
|
|
558
|
+
// A still, then a filmed shot, in turn; whatever is left of either at the end.
|
|
559
|
+
const order = [];
|
|
560
|
+
for (let i = 0; i < Math.max(images.length, clips.length); i++) {
|
|
561
|
+
if (images[i])
|
|
562
|
+
order.push({ kind: "image", path: images[i] });
|
|
563
|
+
if (clips[i])
|
|
564
|
+
order.push({ kind: "clip", ...clips[i] });
|
|
565
|
+
}
|
|
523
566
|
const frames = Math.max(25, Math.round(each * 25));
|
|
524
567
|
const moves = [
|
|
525
568
|
`z='1+0.15*on/${frames}':x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)'`,
|
|
@@ -528,12 +571,26 @@ export function renderStoryVideo(ffmpeg, input) {
|
|
|
528
571
|
`z='1.15':x='(iw-iw/zoom)*(1-on/${frames})':y='(ih-ih/zoom)*on/${frames}'`,
|
|
529
572
|
];
|
|
530
573
|
const parts = [];
|
|
531
|
-
|
|
532
|
-
|
|
574
|
+
const lengths = [];
|
|
575
|
+
let still = 0;
|
|
576
|
+
order.forEach((item, i) => {
|
|
577
|
+
if (item.kind === "image") {
|
|
578
|
+
parts.push(`[${i}:v]scale=1600:900:force_original_aspect_ratio=increase,crop=1600:900,zoompan=${moves[still++ % moves.length]}:d=1:s=1280x720:fps=25,setsar=1[v${i}]`);
|
|
579
|
+
lengths.push(each);
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
// A filmed shot plays as it was filmed; the last one holds its final frame if the song runs on.
|
|
583
|
+
const hold = i === order.length - 1 ? `,tpad=stop_mode=clone:stop_duration=${Math.max(0, input.seconds).toFixed(2)}` : "";
|
|
584
|
+
parts.push(`[${i}:v]scale=1280:720:force_original_aspect_ratio=increase,crop=1280:720,fps=25,setsar=1,trim=duration=${item.seconds},setpts=PTS-STARTPTS${hold}[v${i}]`);
|
|
585
|
+
lengths.push(item.seconds);
|
|
586
|
+
}
|
|
533
587
|
});
|
|
588
|
+
const count = order.length;
|
|
534
589
|
let last = "v0";
|
|
590
|
+
let at = 0;
|
|
535
591
|
for (let i = 1; i < count; i++) {
|
|
536
|
-
|
|
592
|
+
at += lengths[i - 1] - fade;
|
|
593
|
+
parts.push(`[${last}][v${i}]xfade=transition=fade:duration=${fade}:offset=${at.toFixed(2)}[x${i}]`);
|
|
537
594
|
last = `x${i}`;
|
|
538
595
|
}
|
|
539
596
|
const font = titleFont();
|
|
@@ -542,7 +599,7 @@ export function renderStoryVideo(ffmpeg, input) {
|
|
|
542
599
|
? `[${last}]drawtext=fontfile=${font}:text='${card}':fontcolor=white:fontsize=44:x=60:y=h-110:enable='between(t,0.5,6)':alpha='if(lt(t,1.5),t-0.5,if(gt(t,5),6-t,1))':shadowcolor=black@0.7:shadowx=2:shadowy=2,format=yuv420p[v]`
|
|
543
600
|
: `[${last}]format=yuv420p[v]`);
|
|
544
601
|
const args = ["-hide_banner", "-loglevel", "error", "-y", "-threads", "2",
|
|
545
|
-
...
|
|
602
|
+
...order.flatMap((item) => (item.kind === "image" ? ["-loop", "1", "-t", each.toFixed(2), "-i", item.path] : ["-i", item.path])),
|
|
546
603
|
"-i", input.audio, "-filter_complex", parts.join(";"),
|
|
547
604
|
"-map", "[v]", "-map", `${count}:a`, "-map_metadata", String(count), "-c:v", "libx264", "-preset", "veryfast", "-crf", "24", "-r", "25", "-c:a", "copy", "-shortest",
|
|
548
605
|
"-movflags", "+faststart+use_metadata_tags", input.out];
|
|
@@ -632,6 +689,8 @@ export class AiLabel {
|
|
|
632
689
|
videoFailed = new Set();
|
|
633
690
|
/** Songs whose words were already sent to the aligner since this server started. */
|
|
634
691
|
alignTried = new Set();
|
|
692
|
+
/** The month ("YYYY-MM") Veo said its budget was spent; no more shots until the next. */
|
|
693
|
+
veoSpentIn = "";
|
|
635
694
|
/** Old videos already tried as storyboards since this server started. */
|
|
636
695
|
upgradeTried = new Set();
|
|
637
696
|
/** The day's album is tried at most every 30 minutes, so a failing model is not hammered. */
|
|
@@ -782,6 +841,11 @@ export class AiLabel {
|
|
|
782
841
|
this.settings.publish = input["publish"] !== false;
|
|
783
842
|
if (input["videos"] !== undefined)
|
|
784
843
|
this.settings.videos = input["videos"] !== false;
|
|
844
|
+
if (input["veo"] !== undefined)
|
|
845
|
+
this.settings.veo = input["veo"] === true;
|
|
846
|
+
const shots = whole("veoShots", 1, 8);
|
|
847
|
+
if (shots !== undefined)
|
|
848
|
+
this.settings.veoShots = shots;
|
|
785
849
|
const perDay = whole("albumsPerDay", 0, 24);
|
|
786
850
|
if (perDay !== undefined)
|
|
787
851
|
this.settings.albumsPerDay = perDay;
|
|
@@ -973,7 +1037,7 @@ export class AiLabel {
|
|
|
973
1037
|
.flatMap((album) => album.songs.filter((song) => song.file && !this.videoFailed.has(song.file)).map((song) => ({ artist, album, song }))));
|
|
974
1038
|
const missing = songs.filter(({ song }) => !song.video);
|
|
975
1039
|
const untagged = songs.filter(({ song }) => song.video && !song.videoTagged);
|
|
976
|
-
const old = songs.filter(({ song }) => song.video && song.videoTagged && (song.videoVersion ?? 1) < 2 && !this.upgradeTried.has(song.file));
|
|
1040
|
+
const old = songs.filter(({ song }) => song.video && song.videoTagged && ((song.videoVersion ?? 1) < 2 || (this.wantsVeo(song) && (song.videoVersion ?? 1) < 3)) && !this.upgradeTried.has(song.file));
|
|
977
1041
|
let made = 0;
|
|
978
1042
|
for (const { album, song } of untagged) {
|
|
979
1043
|
if (!this.settings.videos)
|
|
@@ -1062,15 +1126,68 @@ export class AiLabel {
|
|
|
1062
1126
|
this.log(`no storyboard for "${song.title}" (${error.message}); the cover video instead`);
|
|
1063
1127
|
}
|
|
1064
1128
|
}
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1129
|
+
// An album's single gets filmed shots of the band too, while the month's Veo budget lasts.
|
|
1130
|
+
const clips = this.wantsVeo(song) ? await this.filmShots(artist, song, scenes) : [];
|
|
1131
|
+
if (images.length >= 2 || clips.length > 0) {
|
|
1132
|
+
if (storyOnly && (song.videoVersion ?? 1) >= 2 && clips.length === 0)
|
|
1133
|
+
throw new Error("no filmed shots this time; the video it has stays");
|
|
1134
|
+
await (this.deps.storyVideo ?? ((input) => renderStoryVideo(this.deps.ffmpeg, input)))({ audio: song.file, images, seconds, artist: artist.name, title: song.title, out, ...(clips.length ? { clips } : {}) });
|
|
1135
|
+
return clips.length ? 3 : 2;
|
|
1068
1136
|
}
|
|
1069
1137
|
if (storyOnly)
|
|
1070
1138
|
throw new Error("no storyboard could be drawn; the video it has stays");
|
|
1071
1139
|
await (this.deps.video ?? ((audio, cover, to) => renderVideo(this.deps.ffmpeg, audio, cover, to)))(song.file, album.cover, out);
|
|
1072
1140
|
return 1;
|
|
1073
1141
|
}
|
|
1142
|
+
/** Whether this song gets filmed Veo shots: an album's single, Veo on, and the month's budget not spent. */
|
|
1143
|
+
wantsVeo(song) {
|
|
1144
|
+
return this.settings.veo && song.n === 1 && Boolean(this.deps.veo) && this.veoSpentIn !== new Date(this.now()).toISOString().slice(0, 7);
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* A single's filmed shots: the band, from their portraits (up to three as
|
|
1148
|
+
* Veo's reference images), on stage, in the song's mood. Stops for the
|
|
1149
|
+
* month when the budget is spent; a shot Veo will not make is skipped.
|
|
1150
|
+
*/
|
|
1151
|
+
async filmShots(artist, song, dir) {
|
|
1152
|
+
const veo = this.deps.veo;
|
|
1153
|
+
if (!veo)
|
|
1154
|
+
return [];
|
|
1155
|
+
mkdirSync(dir, { recursive: true });
|
|
1156
|
+
const members = this.cast(artist);
|
|
1157
|
+
const references = [];
|
|
1158
|
+
for (const member of members.slice(0, 3)) {
|
|
1159
|
+
const portrait = join(this.folderOf(artist), "members", `${member.slug}.jpg`);
|
|
1160
|
+
const small = join(dir, `ref-${member.slug}.jpg`);
|
|
1161
|
+
try {
|
|
1162
|
+
// 512 px is plenty for a face and keeps the request small.
|
|
1163
|
+
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");
|
|
1164
|
+
references.push(readFileSync(small));
|
|
1165
|
+
}
|
|
1166
|
+
catch {
|
|
1167
|
+
// A member without a portrait is described in words only.
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
const clips = [];
|
|
1171
|
+
for (const [i, prompt] of veoPrompts(artist, song, members, this.settings.veoShots).entries()) {
|
|
1172
|
+
try {
|
|
1173
|
+
const bytes = await veo({ prompt, seconds: 8, references });
|
|
1174
|
+
const file = join(dir, `shot-${i}.mp4`);
|
|
1175
|
+
writeFileSync(file, bytes);
|
|
1176
|
+
clips.push({ path: file, seconds: 8 });
|
|
1177
|
+
}
|
|
1178
|
+
catch (error) {
|
|
1179
|
+
if (error instanceof VeoUnavailable) {
|
|
1180
|
+
this.veoSpentIn = new Date(this.now()).toISOString().slice(0, 7);
|
|
1181
|
+
this.log(`Veo: ${error.message}; singles get the storyboard until next month`);
|
|
1182
|
+
break;
|
|
1183
|
+
}
|
|
1184
|
+
this.log(`Veo shot ${i + 1} for "${song.title}": ${error.message}`);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
if (clips.length)
|
|
1188
|
+
this.log(`Veo: ${clips.length} filmed shots for "${song.title}" (${artist.name})`);
|
|
1189
|
+
return clips;
|
|
1190
|
+
}
|
|
1074
1191
|
/** The label's music videos as one TV station: free to watch, with its breaks. */
|
|
1075
1192
|
placeVideos() {
|
|
1076
1193
|
const next = this.placing.then(async () => {
|
package/dist/music-hub.d.ts
CHANGED
|
@@ -92,6 +92,12 @@ export interface MusicRoutesContext {
|
|
|
92
92
|
status: number;
|
|
93
93
|
body: unknown;
|
|
94
94
|
}>;
|
|
95
|
+
/** Veo shots for a label server's videos: the month's spend (GET), a clip (POST); answers bytes or JSON. */
|
|
96
|
+
veo?: (request: IncomingMessage) => Promise<{
|
|
97
|
+
status: number;
|
|
98
|
+
body: unknown;
|
|
99
|
+
video?: Buffer;
|
|
100
|
+
}>;
|
|
95
101
|
}
|
|
96
102
|
export declare function handleMusicRoutes(request: IncomingMessage, response: ServerResponse, path: string, ctx: MusicRoutesContext): Promise<boolean>;
|
|
97
103
|
export {};
|
package/dist/music-hub.js
CHANGED
|
@@ -136,7 +136,7 @@ export async function musicPageFor(hub, path) {
|
|
|
136
136
|
return "";
|
|
137
137
|
}
|
|
138
138
|
export function isMusicPath(path) {
|
|
139
|
-
return path === "/music" || path.startsWith("/music/") || path === "/api/v1/music" || FILE.test(path) || path === "/api/v1/music/released";
|
|
139
|
+
return path === "/music" || path.startsWith("/music/") || path === "/api/v1/music" || FILE.test(path) || path === "/api/v1/music/released" || path === "/api/v1/music/veo";
|
|
140
140
|
}
|
|
141
141
|
export async function handleMusicRoutes(request, response, path, ctx) {
|
|
142
142
|
if (!isMusicPath(path))
|
|
@@ -159,6 +159,20 @@ export async function handleMusicRoutes(request, response, path, ctx) {
|
|
|
159
159
|
ctx.json(response, answer.status, answer.body);
|
|
160
160
|
return true;
|
|
161
161
|
}
|
|
162
|
+
if (path === "/api/v1/music/veo") {
|
|
163
|
+
if (!ctx.veo) {
|
|
164
|
+
ctx.json(response, 404, { error: "Veo is not set up here" });
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
const answer = await ctx.veo(request);
|
|
168
|
+
if (answer.video) {
|
|
169
|
+
response.writeHead(200, { "content-type": "video/mp4", "content-length": String(answer.video.length), "cache-control": "no-store" });
|
|
170
|
+
response.end(answer.video);
|
|
171
|
+
}
|
|
172
|
+
else
|
|
173
|
+
ctx.json(response, answer.status, answer.body);
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
162
176
|
if (method !== "GET" && method !== "HEAD") {
|
|
163
177
|
ctx.json(response, 405, { error: "not like that" });
|
|
164
178
|
return true;
|
package/dist/server.d.ts
CHANGED
|
@@ -841,6 +841,11 @@ export interface HandlerOptions {
|
|
|
841
841
|
status: number;
|
|
842
842
|
body: unknown;
|
|
843
843
|
}>;
|
|
844
|
+
veo?: (request: IncomingMessage) => Promise<{
|
|
845
|
+
status: number;
|
|
846
|
+
body: unknown;
|
|
847
|
+
video?: Buffer;
|
|
848
|
+
}>;
|
|
844
849
|
};
|
|
845
850
|
sharedTranslations?: SharedTranslations;
|
|
846
851
|
/** Translation: texts in another language, by a model here. The same optional library. */
|
package/dist/server.js
CHANGED
|
@@ -61,10 +61,11 @@ import { ListenerStore } from "./library-user.js";
|
|
|
61
61
|
import { Importer } from "./handoff.js";
|
|
62
62
|
import { handleHandoffs } from "./handoff-routes.js";
|
|
63
63
|
import { handleAiRoutes, isPublicSharePath } from "./ai-routes.js";
|
|
64
|
-
import { AiLabel } from "./ai-label.js";
|
|
64
|
+
import { AiLabel, VeoUnavailable } from "./ai-label.js";
|
|
65
65
|
import { buyLink, handleLabelRoutes, isPublicLabelPath } from "./label-routes.js";
|
|
66
66
|
import { GrantVerifier, MusicStore, PRICES } from "./music-store.js";
|
|
67
67
|
import { handleMusicRoutes, isMusicPath, MusicHub, musicPageFor, serverId } from "./music-hub.js";
|
|
68
|
+
import { VeoBudgetError, VeoStudio } from "./veo-studio.js";
|
|
68
69
|
import { handleStoreRoutes, isStorePath } from "./store-routes.js";
|
|
69
70
|
import { AiRadio } from "./ai-radio.js";
|
|
70
71
|
import { ListenerShares } from "./listener-shares.js";
|
|
@@ -2888,6 +2889,7 @@ export function createHandler(engine, options) {
|
|
|
2888
2889
|
return null;
|
|
2889
2890
|
} },
|
|
2890
2891
|
released: music.released,
|
|
2892
|
+
...(music.veo ? { veo: music.veo } : {}),
|
|
2891
2893
|
}))
|
|
2892
2894
|
return;
|
|
2893
2895
|
}
|
|
@@ -7194,6 +7196,24 @@ export async function serve(argv, version = "0.1.0") {
|
|
|
7194
7196
|
root: process.env["NIXAMP_LABEL_ROOT"] || (labelHome ? join(labelHome.root, "ai") : join(stateDir(), "ai-label")),
|
|
7195
7197
|
owner: options.owner || "",
|
|
7196
7198
|
mayStart: mayStartStation,
|
|
7199
|
+
// Filmed shots for singles come from nixamp.com's Veo studio, asked as this server's owner, under its monthly cap.
|
|
7200
|
+
veo: async (shot) => {
|
|
7201
|
+
const session = readSession();
|
|
7202
|
+
const here = stations.publicBase?.() ?? "";
|
|
7203
|
+
if (!session?.token || !/^https:\/\//.test(here))
|
|
7204
|
+
throw new VeoUnavailable("this server is not signed in to nixamp.com");
|
|
7205
|
+
const answer = await fetch(`${session.site.replace(/\/+$/, "")}/api/v1/music/veo`, {
|
|
7206
|
+
method: "POST",
|
|
7207
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${session.token}`, "x-nixamp-server": here },
|
|
7208
|
+
body: JSON.stringify({ prompt: shot.prompt, seconds: shot.seconds, references: shot.references.map((one) => one.toString("base64")) }),
|
|
7209
|
+
signal: AbortSignal.timeout(12 * 60_000),
|
|
7210
|
+
});
|
|
7211
|
+
if (answer.status === 402 || answer.status === 404)
|
|
7212
|
+
throw new VeoUnavailable((await answer.json().catch(() => ({}))).error ?? `HTTP ${answer.status}`);
|
|
7213
|
+
if (!answer.ok)
|
|
7214
|
+
throw new Error((await answer.json().catch(() => ({}))).error ?? `nixamp.com answered ${answer.status}`);
|
|
7215
|
+
return Buffer.from(await answer.arrayBuffer());
|
|
7216
|
+
},
|
|
7197
7217
|
// A release is told to nixamp.com as this server's owner; nixamp.com tells the artist's followers.
|
|
7198
7218
|
announce: (release) => {
|
|
7199
7219
|
const session = readSession();
|
|
@@ -7479,6 +7499,48 @@ export async function serve(argv, version = "0.1.0") {
|
|
|
7479
7499
|
* followers are told. Only the account that owns a listed server may say
|
|
7480
7500
|
* so, only for an artist that server's catalogue has, once per release.
|
|
7481
7501
|
*/
|
|
7502
|
+
// Veo shots for label music videos, under a monthly dollar cap (veo-studio.ts). nixamp.com only.
|
|
7503
|
+
const veoStudio = options.directory && process.env["NIXAMP_VEO_API_KEY"] ? new VeoStudio({
|
|
7504
|
+
key: process.env["NIXAMP_VEO_API_KEY"],
|
|
7505
|
+
file: VeoStudio.fileFor(stateDir(), options.port),
|
|
7506
|
+
monthlyUsd: Number(process.env["NIXAMP_VEO_MONTHLY_USD"] ?? 50),
|
|
7507
|
+
usdPerSecond: Number(process.env["NIXAMP_VEO_USD_PER_SECOND"] ?? 0.15),
|
|
7508
|
+
}) : undefined;
|
|
7509
|
+
/** The signed-in account owns a server listed in the directory at this base. */
|
|
7510
|
+
const ownsListed = async (request, base) => {
|
|
7511
|
+
const me = accounts ? await accounts.whoIs(tokenFrom(request.headers)).catch(() => null) : null;
|
|
7512
|
+
return Boolean(me && base && directory?.list().some((one) => one.ownerId === me.id && peerBase(one.url) === base));
|
|
7513
|
+
};
|
|
7514
|
+
const musicVeo = async (request) => {
|
|
7515
|
+
if (!veoStudio)
|
|
7516
|
+
return { status: 404, body: { error: "Veo is not set up here" } };
|
|
7517
|
+
const base = peerBase(String(request.headers["x-nixamp-server"] ?? ""));
|
|
7518
|
+
if (!(await ownsListed(request, base)))
|
|
7519
|
+
return { status: 403, body: { error: "only the owner of a listed server" } };
|
|
7520
|
+
if (request.method === "GET")
|
|
7521
|
+
return { status: 200, body: veoStudio.status() };
|
|
7522
|
+
if (request.method !== "POST")
|
|
7523
|
+
return { status: 405, body: { error: "GET or POST" } };
|
|
7524
|
+
let input;
|
|
7525
|
+
try {
|
|
7526
|
+
input = JSON.parse(await readBody(request, 2 * 1024 * 1024));
|
|
7527
|
+
}
|
|
7528
|
+
catch {
|
|
7529
|
+
return { status: 400, body: { error: "a JSON body under 2 MB" } };
|
|
7530
|
+
}
|
|
7531
|
+
const prompt = String(input.prompt ?? "").trim();
|
|
7532
|
+
if (prompt.length < 20)
|
|
7533
|
+
return { status: 400, body: { error: "describe the shot" } };
|
|
7534
|
+
const references = (Array.isArray(input.references) ? input.references : []).slice(0, 3).map((one) => Buffer.from(String(one), "base64")).filter((one) => one.length > 1000);
|
|
7535
|
+
try {
|
|
7536
|
+
return { status: 200, body: null, video: await veoStudio.clip({ prompt, seconds: Number(input.seconds ?? 8), references }) };
|
|
7537
|
+
}
|
|
7538
|
+
catch (error) {
|
|
7539
|
+
if (error instanceof VeoBudgetError)
|
|
7540
|
+
return { status: 402, body: { error: error.message, code: "budget", ...veoStudio.status() } };
|
|
7541
|
+
return { status: 422, body: { error: error.message, code: "refused" } };
|
|
7542
|
+
}
|
|
7543
|
+
};
|
|
7482
7544
|
const announced = new Set();
|
|
7483
7545
|
const musicReleased = async (request, body) => {
|
|
7484
7546
|
const me = accounts ? await accounts.whoIs(tokenFrom(request.headers)).catch(() => null) : null;
|
|
@@ -8062,7 +8124,7 @@ export async function serve(argv, version = "0.1.0") {
|
|
|
8062
8124
|
...(supportPayments ? { support: supportPayments } : {}),
|
|
8063
8125
|
...(supportMonthly ? { supportSubscriptions: supportMonthly } : {}),
|
|
8064
8126
|
...(musicStore ? { musicStore } : {}),
|
|
8065
|
-
...(musicHub ? { music: { hub: musicHub, released: musicReleased } } : {}),
|
|
8127
|
+
...(musicHub ? { music: { hub: musicHub, released: musicReleased, veo: musicVeo } } : {}),
|
|
8066
8128
|
...(process.env["COINPAY_WEBHOOK_SECRET"] ? { coinpayWebhookSecret: process.env["COINPAY_WEBHOOK_SECRET"] } : {}),
|
|
8067
8129
|
...(sharedTranslations ? { sharedTranslations } : {}),
|
|
8068
8130
|
...(translator ? { translator } : {}),
|
package/dist/station-cli.js
CHANGED
|
@@ -449,7 +449,8 @@ async function labelCommand(argv, call, show) {
|
|
|
449
449
|
return [
|
|
450
450
|
`${String(one["name"])} [${String(one["key"])}] ${String(one["genre"])}${one["enabled"] ? "" : " (off)"}`,
|
|
451
451
|
...albums.map((album) => ` ${String(album["year"])} ${String(album["title"])} ${String(album["composed"])}/${String(album["songs"])} songs${album["doneAt"] ? "" : " (recording)"}`),
|
|
452
|
-
|
|
452
|
+
// On the daily schedule an enabled artist has no date of its own: it is in the daily draw.
|
|
453
|
+
` next album ${Number(one["nextAlbumAt"] ?? -1) === -1 && one["enabled"] ? "in the daily draw (one album a day across the label)" : when(Number(one["nextAlbumAt"] ?? -1))}`,
|
|
453
454
|
...(one["lastError"] ? [` problem ${String(one["lastError"])}`] : []),
|
|
454
455
|
].join("\n");
|
|
455
456
|
};
|
|
@@ -537,6 +538,8 @@ async function labelCommand(argv, call, show) {
|
|
|
537
538
|
videos: has(argv, "--no-videos") ? false : has(argv, "--videos") ? true : undefined,
|
|
538
539
|
newArtistChance: number("--new-artists"),
|
|
539
540
|
resetDay: number("--reset-day"),
|
|
541
|
+
veo: has(argv, "--veo") ? true : has(argv, "--no-veo") ? false : undefined,
|
|
542
|
+
veoShots: number("--veo-shots"),
|
|
540
543
|
genreOrder: flag(argv, "--genres"),
|
|
541
544
|
enabled: has(argv, "--off") ? false : has(argv, "--on") ? true : undefined,
|
|
542
545
|
publish: has(argv, "--unlisted") ? false : has(argv, "--public") ? true : undefined,
|
package/dist/station-mcp.d.ts
CHANGED
|
@@ -50,6 +50,8 @@ export declare const STATION_TOOLS: ({
|
|
|
50
50
|
albumsPerDay?: undefined;
|
|
51
51
|
newArtistChance?: undefined;
|
|
52
52
|
resetDay?: undefined;
|
|
53
|
+
veo?: undefined;
|
|
54
|
+
veoShots?: undefined;
|
|
53
55
|
genreOrder?: undefined;
|
|
54
56
|
state?: undefined;
|
|
55
57
|
reason?: undefined;
|
|
@@ -126,6 +128,8 @@ export declare const STATION_TOOLS: ({
|
|
|
126
128
|
albumsPerDay?: undefined;
|
|
127
129
|
newArtistChance?: undefined;
|
|
128
130
|
resetDay?: undefined;
|
|
131
|
+
veo?: undefined;
|
|
132
|
+
veoShots?: undefined;
|
|
129
133
|
genreOrder?: undefined;
|
|
130
134
|
state?: undefined;
|
|
131
135
|
reason?: undefined;
|
|
@@ -225,6 +229,8 @@ export declare const STATION_TOOLS: ({
|
|
|
225
229
|
albumsPerDay?: undefined;
|
|
226
230
|
newArtistChance?: undefined;
|
|
227
231
|
resetDay?: undefined;
|
|
232
|
+
veo?: undefined;
|
|
233
|
+
veoShots?: undefined;
|
|
228
234
|
genreOrder?: undefined;
|
|
229
235
|
state?: undefined;
|
|
230
236
|
reason?: undefined;
|
|
@@ -321,6 +327,8 @@ export declare const STATION_TOOLS: ({
|
|
|
321
327
|
albumsPerDay?: undefined;
|
|
322
328
|
newArtistChance?: undefined;
|
|
323
329
|
resetDay?: undefined;
|
|
330
|
+
veo?: undefined;
|
|
331
|
+
veoShots?: undefined;
|
|
324
332
|
genreOrder?: undefined;
|
|
325
333
|
state?: undefined;
|
|
326
334
|
reason?: undefined;
|
|
@@ -399,6 +407,8 @@ export declare const STATION_TOOLS: ({
|
|
|
399
407
|
albumsPerDay?: undefined;
|
|
400
408
|
newArtistChance?: undefined;
|
|
401
409
|
resetDay?: undefined;
|
|
410
|
+
veo?: undefined;
|
|
411
|
+
veoShots?: undefined;
|
|
402
412
|
genreOrder?: undefined;
|
|
403
413
|
state?: undefined;
|
|
404
414
|
reason?: undefined;
|
|
@@ -484,6 +494,8 @@ export declare const STATION_TOOLS: ({
|
|
|
484
494
|
albumsPerDay?: undefined;
|
|
485
495
|
newArtistChance?: undefined;
|
|
486
496
|
resetDay?: undefined;
|
|
497
|
+
veo?: undefined;
|
|
498
|
+
veoShots?: undefined;
|
|
487
499
|
genreOrder?: undefined;
|
|
488
500
|
state?: undefined;
|
|
489
501
|
reason?: undefined;
|
|
@@ -569,6 +581,8 @@ export declare const STATION_TOOLS: ({
|
|
|
569
581
|
albumsPerDay?: undefined;
|
|
570
582
|
newArtistChance?: undefined;
|
|
571
583
|
resetDay?: undefined;
|
|
584
|
+
veo?: undefined;
|
|
585
|
+
veoShots?: undefined;
|
|
572
586
|
genreOrder?: undefined;
|
|
573
587
|
state?: undefined;
|
|
574
588
|
reason?: undefined;
|
|
@@ -671,6 +685,8 @@ export declare const STATION_TOOLS: ({
|
|
|
671
685
|
albumsPerDay?: undefined;
|
|
672
686
|
newArtistChance?: undefined;
|
|
673
687
|
resetDay?: undefined;
|
|
688
|
+
veo?: undefined;
|
|
689
|
+
veoShots?: undefined;
|
|
674
690
|
genreOrder?: undefined;
|
|
675
691
|
state?: undefined;
|
|
676
692
|
reason?: undefined;
|
|
@@ -793,6 +809,14 @@ export declare const STATION_TOOLS: ({
|
|
|
793
809
|
type: string;
|
|
794
810
|
description: string;
|
|
795
811
|
};
|
|
812
|
+
veo: {
|
|
813
|
+
type: string;
|
|
814
|
+
description: string;
|
|
815
|
+
};
|
|
816
|
+
veoShots: {
|
|
817
|
+
type: string;
|
|
818
|
+
description: string;
|
|
819
|
+
};
|
|
796
820
|
genreOrder: {
|
|
797
821
|
type: string;
|
|
798
822
|
items: {
|
|
@@ -864,6 +888,8 @@ export declare const STATION_TOOLS: ({
|
|
|
864
888
|
albumsPerDay?: undefined;
|
|
865
889
|
newArtistChance?: undefined;
|
|
866
890
|
resetDay?: undefined;
|
|
891
|
+
veo?: undefined;
|
|
892
|
+
veoShots?: undefined;
|
|
867
893
|
genreOrder?: undefined;
|
|
868
894
|
server: {
|
|
869
895
|
type: "string";
|
|
@@ -958,6 +984,8 @@ export declare const STATION_TOOLS: ({
|
|
|
958
984
|
albumsPerDay?: undefined;
|
|
959
985
|
newArtistChance?: undefined;
|
|
960
986
|
resetDay?: undefined;
|
|
987
|
+
veo?: undefined;
|
|
988
|
+
veoShots?: undefined;
|
|
961
989
|
genreOrder?: undefined;
|
|
962
990
|
state?: undefined;
|
|
963
991
|
reason?: undefined;
|
|
@@ -1054,6 +1082,8 @@ export declare const STATION_TOOLS: ({
|
|
|
1054
1082
|
albumsPerDay?: undefined;
|
|
1055
1083
|
newArtistChance?: undefined;
|
|
1056
1084
|
resetDay?: undefined;
|
|
1085
|
+
veo?: undefined;
|
|
1086
|
+
veoShots?: undefined;
|
|
1057
1087
|
genreOrder?: undefined;
|
|
1058
1088
|
state?: undefined;
|
|
1059
1089
|
reason?: undefined;
|
package/dist/station-mcp.js
CHANGED
|
@@ -122,6 +122,8 @@ export const STATION_TOOLS = [
|
|
|
122
122
|
albumsPerDay: { type: "integer", description: "After the first sweep: new albums a day, by a random artist or a newly invented one." },
|
|
123
123
|
newArtistChance: { type: "number", description: "0-1: how often the day's album is by a new artist." },
|
|
124
124
|
resetDay: { type: "integer", description: "Day of the month (1-28) the budget renews: the music provider's billing day." },
|
|
125
|
+
veo: { type: "boolean", description: "Each album's single gets filmed Veo shots of the band (nixamp.com's monthly Veo cap applies)." },
|
|
126
|
+
veoShots: { type: "integer", description: "8 s Veo shots per single (1-8)." },
|
|
125
127
|
genreOrder: { type: "array", items: STRING, description: "Genres whose artists get albums first, in order." },
|
|
126
128
|
},
|
|
127
129
|
},
|
|
@@ -241,7 +243,7 @@ export async function callStationTool(name, args, options = {}) {
|
|
|
241
243
|
const key = typeof args["key"] === "string" && /^[a-f0-9]{10}$/.test(args["key"]) ? args["key"] : "";
|
|
242
244
|
const answer = action === "status" ? await call("GET", "/api/v1/stations/label")
|
|
243
245
|
: action === "sign" ? await call("POST", "/api/v1/stations/label/artists", pick(["preset", "roster", "name", "genre", "sound", "voice", "bio", "themes", "enabled"]))
|
|
244
|
-
: action === "configure" ? await call("POST", "/api/v1/stations/label", pick(["holder", "monthlySeconds", "everyDays", "seedAlbums", "songsPerAlbum", "songSeconds", "parallel", "genreOrder", "albumsPerDay", "newArtistChance", "resetDay", "enabled", "publish"]))
|
|
246
|
+
: action === "configure" ? await call("POST", "/api/v1/stations/label", pick(["holder", "monthlySeconds", "everyDays", "seedAlbums", "songsPerAlbum", "songSeconds", "parallel", "genreOrder", "albumsPerDay", "newArtistChance", "resetDay", "veo", "veoShots", "enabled", "publish"]))
|
|
245
247
|
: action === "run" ? await call("POST", "/api/v1/stations/label/run", {})
|
|
246
248
|
: action === "resume" ? await call("POST", "/api/v1/stations/label", { resume: true })
|
|
247
249
|
: !key ? null
|
package/dist/station-usage.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** `nixamp station` usage, apart so `nixamp help station` does not load the client. */
|
|
2
|
-
export declare const USAGE = "nixamp station \u2014 make a collection into a station\n\n nixamp station list\n nixamp station create --name NAME (--folder PATH | --playlist ID | --items 0,1,2) [--shuffle] [--no-repeat] [--bitrate 128] [--breaks 600 (the default; 0 for none)]\n nixamp station show ID\n nixamp station status ID\n nixamp station start ID\n nixamp station stop ID\n nixamp station skip ID\n nixamp station breaks ID SECONDS a spot between entries every SECONDS (120..86400; --off for none)\n nixamp station retry ID try every left-out file again\n nixamp station publish ID --basis owned|permission|licensed|public-domain [--note TEXT] [--unlisted] [--no-list]\n nixamp station unpublish ID [--private]\n nixamp station link ID [--rotate]\n nixamp station delete ID\n nixamp station guide-host NAME [--remove]\n NAME's front page is the public station guide (e.g. a Moshpit name)\n nixamp station guide-ads [--site ID] [--slot ID] [--format banner_300x250|text_link] [--off]\n crawlproof analytics (project ID) and an ad unit (slot ID) on the guide\n nixamp station guide-peer URL [--remove]\n the guide also lists the public, listed stations of the server at URL\n nixamp station auto [--no-video] [--min 10] [--films 3] [--tv 2] [--breaks 600] [--no-start]\n a station per genre, kept in step with the library (--off stops it)\n\n AI radio (music composed for a station, a few tracks a day):\n nixamp station ai what is composing, this month's seconds, the stations\n nixamp station ai create (--preset lofi|synthwave|ambient|chiptune|punk|jazz | --name NAME --style TEXT)\n [--vocals] [--seconds 120] [--per-day 2] [--unlisted] public on the guide unless --unlisted\n nixamp station ai generate KEY compose one now\n nixamp station ai set KEY [--style TEXT] [--per-day N] [--seconds N] [--off|--on]\n nixamp station ai remove KEY [--files]\n nixamp station ai budget SECONDS seconds of music a month across AI stations (0: no cap)\n nixamp station ai resume after the provider ran out of credit\n nixamp station ai key NAME ELEVENLABS_API_KEY | GEMINI_API_KEY | OPENAI_API_KEY | ANTHROPIC_API_KEY,\n NIXAMP_OPENAI_URL | NIXAMP_OPENAI_MODEL (an OpenAI-compatible stand-in),\n NIXAMP_MUSIC_MODEL (music_v1|music_v2|music_v2_5), from stdin\n\n AI label (AI artists who release albums on their own; a station per genre; the catalogue at /label):\n nixamp station label artists, albums, this month's seconds, what it is waiting for\n nixamp station label sign --roster the roster: three artists in each of Rock, Hip-Hop, Synthwave, Country,\n Metal and Pop (nixamp station label lists their keys)\n nixamp station label sign (--preset KEY | --name NAME --genre GENRE --sound TEXT [--voice TEXT] [--bio TEXT] [--themes TEXT])\n nixamp station label album KEY plan the artist's next album now and record it\n nixamp station label set KEY [--sound TEXT] [--voice TEXT] [--bio TEXT] [--themes TEXT] [--off|--on]\n nixamp station label drop KEY [--files]\n nixamp station label config [--holder \"Profullstack, Inc.\"] [--budget SECONDS] [--every-days 30] [--seed-albums 1]\n [--songs 8] [--seconds 150] [--on|--off] [--public|--unlisted]\n [--parallel 4] [--genres \"Punk,Blues,Country\"] songs made at once; genres first\n [--per-day 1] [--new-artists 0.25] after the first sweep: albums a day, share by new artists\n [--videos|--no-videos] a music video per song and a TV station of them\n [--reset-day 21] the day the monthly budget renews (the provider's billing day)\n nixamp station label run record whatever is due now\n nixamp station label resume after the provider ran out of credit\n\n Listener shares (anybody shares an MP3 link or file from the guide; AI files and checks it):\n nixamp station shares the setting and what is waiting\n nixamp station shares on [--owner-moderation] [--unlisted] [--max-mb 60] [--per-hour 6]\n nixamp station shares off\n nixamp station shares list [--state held|approved|reported|rejected]\n nixamp station shares approve|reject|delete SHARE\n nixamp station shares trust ADDRESS [--remove] believe this proxy's X-Forwarded-For\n\n Lyrics (shown on the station page from each file's own tag, else nichedb.dev; ours published back):\n nixamp station lyrics the key, what was published, which stations may publish\n nixamp station lyrics publish ID publish a station's words now (public, listed, basis owned only)\n nixamp station lyrics key nichedb's key (NIXAMP_LYRICS_KEY), from stdin (--clear forgets it)\n\n Copyright holders (the now-playing card: who holds a track's copyright, and how to support them):\n nixamp station holder list\n nixamp station holder set NAME [--artist ARTIST]... [--station ID]... [--support https://...] [--label Subscribe] [--license TEXT]\n nixamp station holder remove ID\n\n --json machine-readable output\n --url, --key another server, as with `nixamp admin`\n\nA new station is private and stopped. Starting it does not publish it.";
|
|
2
|
+
export declare const USAGE = "nixamp station \u2014 make a collection into a station\n\n nixamp station list\n nixamp station create --name NAME (--folder PATH | --playlist ID | --items 0,1,2) [--shuffle] [--no-repeat] [--bitrate 128] [--breaks 600 (the default; 0 for none)]\n nixamp station show ID\n nixamp station status ID\n nixamp station start ID\n nixamp station stop ID\n nixamp station skip ID\n nixamp station breaks ID SECONDS a spot between entries every SECONDS (120..86400; --off for none)\n nixamp station retry ID try every left-out file again\n nixamp station publish ID --basis owned|permission|licensed|public-domain [--note TEXT] [--unlisted] [--no-list]\n nixamp station unpublish ID [--private]\n nixamp station link ID [--rotate]\n nixamp station delete ID\n nixamp station guide-host NAME [--remove]\n NAME's front page is the public station guide (e.g. a Moshpit name)\n nixamp station guide-ads [--site ID] [--slot ID] [--format banner_300x250|text_link] [--off]\n crawlproof analytics (project ID) and an ad unit (slot ID) on the guide\n nixamp station guide-peer URL [--remove]\n the guide also lists the public, listed stations of the server at URL\n nixamp station auto [--no-video] [--min 10] [--films 3] [--tv 2] [--breaks 600] [--no-start]\n a station per genre, kept in step with the library (--off stops it)\n\n AI radio (music composed for a station, a few tracks a day):\n nixamp station ai what is composing, this month's seconds, the stations\n nixamp station ai create (--preset lofi|synthwave|ambient|chiptune|punk|jazz | --name NAME --style TEXT)\n [--vocals] [--seconds 120] [--per-day 2] [--unlisted] public on the guide unless --unlisted\n nixamp station ai generate KEY compose one now\n nixamp station ai set KEY [--style TEXT] [--per-day N] [--seconds N] [--off|--on]\n nixamp station ai remove KEY [--files]\n nixamp station ai budget SECONDS seconds of music a month across AI stations (0: no cap)\n nixamp station ai resume after the provider ran out of credit\n nixamp station ai key NAME ELEVENLABS_API_KEY | GEMINI_API_KEY | OPENAI_API_KEY | ANTHROPIC_API_KEY,\n NIXAMP_OPENAI_URL | NIXAMP_OPENAI_MODEL (an OpenAI-compatible stand-in),\n NIXAMP_MUSIC_MODEL (music_v1|music_v2|music_v2_5), from stdin\n\n AI label (AI artists who release albums on their own; a station per genre; the catalogue at /label):\n nixamp station label artists, albums, this month's seconds, what it is waiting for\n nixamp station label sign --roster the roster: three artists in each of Rock, Hip-Hop, Synthwave, Country,\n Metal and Pop (nixamp station label lists their keys)\n nixamp station label sign (--preset KEY | --name NAME --genre GENRE --sound TEXT [--voice TEXT] [--bio TEXT] [--themes TEXT])\n nixamp station label album KEY plan the artist's next album now and record it\n nixamp station label set KEY [--sound TEXT] [--voice TEXT] [--bio TEXT] [--themes TEXT] [--off|--on]\n nixamp station label drop KEY [--files]\n nixamp station label config [--holder \"Profullstack, Inc.\"] [--budget SECONDS] [--every-days 30] [--seed-albums 1]\n [--songs 8] [--seconds 150] [--on|--off] [--public|--unlisted]\n [--parallel 4] [--genres \"Punk,Blues,Country\"] songs made at once; genres first\n [--per-day 1] [--new-artists 0.25] after the first sweep: albums a day, share by new artists\n [--videos|--no-videos] a music video per song and a TV station of them\n [--reset-day 21] the day the monthly budget renews (the provider's billing day)\n [--veo|--no-veo] [--veo-shots 4] filmed Veo shots of the band in each album's single (nixamp.com caps the month)\n nixamp station label run record whatever is due now\n nixamp station label resume after the provider ran out of credit\n\n Listener shares (anybody shares an MP3 link or file from the guide; AI files and checks it):\n nixamp station shares the setting and what is waiting\n nixamp station shares on [--owner-moderation] [--unlisted] [--max-mb 60] [--per-hour 6]\n nixamp station shares off\n nixamp station shares list [--state held|approved|reported|rejected]\n nixamp station shares approve|reject|delete SHARE\n nixamp station shares trust ADDRESS [--remove] believe this proxy's X-Forwarded-For\n\n Lyrics (shown on the station page from each file's own tag, else nichedb.dev; ours published back):\n nixamp station lyrics the key, what was published, which stations may publish\n nixamp station lyrics publish ID publish a station's words now (public, listed, basis owned only)\n nixamp station lyrics key nichedb's key (NIXAMP_LYRICS_KEY), from stdin (--clear forgets it)\n\n Copyright holders (the now-playing card: who holds a track's copyright, and how to support them):\n nixamp station holder list\n nixamp station holder set NAME [--artist ARTIST]... [--station ID]... [--support https://...] [--label Subscribe] [--license TEXT]\n nixamp station holder remove ID\n\n --json machine-readable output\n --url, --key another server, as with `nixamp admin`\n\nA new station is private and stopped. Starting it does not publish it.";
|
package/dist/station-usage.js
CHANGED
|
@@ -50,6 +50,7 @@ export const USAGE = `nixamp station — make a collection into a station
|
|
|
50
50
|
[--per-day 1] [--new-artists 0.25] after the first sweep: albums a day, share by new artists
|
|
51
51
|
[--videos|--no-videos] a music video per song and a TV station of them
|
|
52
52
|
[--reset-day 21] the day the monthly budget renews (the provider's billing day)
|
|
53
|
+
[--veo|--no-veo] [--veo-shots 4] filmed Veo shots of the band in each album's single (nixamp.com caps the month)
|
|
53
54
|
nixamp station label run record whatever is due now
|
|
54
55
|
nixamp station label resume after the provider ran out of credit
|
|
55
56
|
|