nixamp 0.59.5 → 0.60.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 (47) hide show
  1. package/dist/ai-label.d.ts +37 -0
  2. package/dist/ai-label.js +127 -10
  3. package/dist/music-hub.d.ts +8 -0
  4. package/dist/music-hub.js +30 -6
  5. package/dist/music-seo.d.ts +38 -0
  6. package/dist/music-seo.js +160 -0
  7. package/dist/radio-pages.d.ts +4 -2
  8. package/dist/radio-pages.js +11 -4
  9. package/dist/server.d.ts +5 -0
  10. package/dist/server.js +81 -7
  11. package/dist/station-cli.js +2 -0
  12. package/dist/station-mcp.d.ts +30 -0
  13. package/dist/station-mcp.js +3 -1
  14. package/dist/station-usage.d.ts +1 -1
  15. package/dist/station-usage.js +1 -0
  16. package/dist/veo-studio.d.ts +39 -0
  17. package/dist/veo-studio.js +110 -0
  18. package/package.json +1 -1
  19. package/src/ai-label.ts +128 -12
  20. package/src/music-hub.ts +29 -6
  21. package/src/music-seo.ts +191 -0
  22. package/src/radio-pages.ts +12 -4
  23. package/src/server.ts +62 -8
  24. package/src/station-cli.ts +2 -0
  25. package/src/station-mcp.ts +3 -1
  26. package/src/station-usage.ts +1 -0
  27. package/src/veo-studio.ts +123 -0
  28. package/web/dist/assets/{ar-D7L1Lzg5.js → ar-5jTau3by.js} +1 -1
  29. package/web/dist/assets/{cs-DR-oumcX.js → cs-nlVt79qU.js} +1 -1
  30. package/web/dist/assets/{da-B7ofIVod.js → da-C_mRyuJW.js} +1 -1
  31. package/web/dist/assets/{de-CglXPk_n.js → de-Duqxb30z.js} +1 -1
  32. package/web/dist/assets/{es-CYVNiI20.js → es-CAHgck_F.js} +1 -1
  33. package/web/dist/assets/{fi-Bktga6d_.js → fi-BPXwol2s.js} +1 -1
  34. package/web/dist/assets/{fr-CNxMDHg5.js → fr-C9N4WLtT.js} +1 -1
  35. package/web/dist/assets/{hi-DEyWHgib.js → hi-PWlH7Sya.js} +1 -1
  36. package/web/dist/assets/{hu-mcHUOvhY.js → hu-DHmawwq3.js} +1 -1
  37. package/web/dist/assets/{id-T9wfahs5.js → id-DwMuYDOT.js} +1 -1
  38. package/web/dist/assets/{it-CASLaiyC.js → it-DuVsDcAz.js} +1 -1
  39. package/web/dist/assets/{main-D9-BuE2r.js → main-BV2N47tZ.js} +2 -2
  40. package/web/dist/assets/{nl-DQ-YPUBS.js → nl-BcdvPtpE.js} +1 -1
  41. package/web/dist/assets/{ru-CDSDPqFZ.js → ru-CfMl9LlQ.js} +1 -1
  42. package/web/dist/assets/{sv-liuu51-p.js → sv-CDtolH7I.js} +1 -1
  43. package/web/dist/assets/{uk-B3vreqbA.js → uk-DwjjSnVU.js} +1 -1
  44. package/web/dist/assets/{vi-B0NyoUEc.js → vi-CNYK9Tlb.js} +1 -1
  45. package/web/dist/assets/{zh-Bw5OQASg.js → zh-6nGSjON5.js} +1 -1
  46. package/web/dist/index.html +1 -1
  47. package/web/dist/sw.js +19 -19
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";
@@ -78,7 +79,7 @@ import { handleRoomProxy, handleRooms, isRoomPath, ROOM_PAGE, roomAddress, RoomP
78
79
  import { handleVods, VodProbe, vodEntries, VodStore, vodView } from "./guide-vods.js";
79
80
  import { withPitFooter } from "./pit-footer.js";
80
81
  import { handleFollowPage } from "./follow-page.js";
81
- import { asRadioPage, buildCatalogue, isRadioPath, NAV_STYLE, RADIO_ALBUM, RADIO_ARTIST, RADIO_CSP, RADIO_STATION, RADIO_SUB, RADIO_BUY, radioBuyLink, radioBuyQuery, radioNav, radioPeers, radioRewriter, radioStations, renderAlbum, renderArtist, renderDirectory, renderMissing, rewriteResponse, } from "./radio-pages.js";
82
+ import { asRadioPage, buildCatalogue, isRadioPath, NAV_STYLE, RADIO_ALBUM, RADIO_ARTIST, RADIO_CSP, RADIO_STATION, RADIO_SUB, RADIO_BUY, radioBuyLink, radioBuyQuery, radioNav, radioPeers, radioRewriter, radioStations, renderAlbum, stationImage, renderArtist, renderDirectory, renderMissing, rewriteResponse, } from "./radio-pages.js";
82
83
  import { adCsp, guideAds, guideCsp, withPageAd, guideHosts, guidePeers, peerBase, PeerGuides, renderGuide } from "./station-guide.js";
83
84
  import { StationRunner } from "./station-runner.js";
84
85
  import { StationError, StationStore, channelOf, stationOfChannel } from "./stations.js";
@@ -2290,6 +2291,15 @@ export function createHandler(engine, options) {
2290
2291
  const requestHost = String(request.headers["x-forwarded-host"] ?? request.headers.host ?? "").split(",")[0].trim().replace(/:\d+$/, "").toLowerCase();
2291
2292
  const isGuidePage = Boolean(options.stations) && (request.method === "GET" || request.method === "HEAD") &&
2292
2293
  (path === "/guide" || (path === "/" && guideHosts().has(requestHost)));
2294
+ // nixamp.com's own /radio is the radio app (web/radio.html), not live.streaming's
2295
+ // station list; its other /radio pages name nixamp.com, not the box behind it.
2296
+ const onDirectory = (() => { try {
2297
+ return new URL(storeSite).host === requestHost;
2298
+ }
2299
+ catch {
2300
+ return false;
2301
+ } })();
2302
+ const radioPath = isRadioPath(path) && !(onDirectory && (path === "/radio" || path === "/radio/"));
2293
2303
  // The built web bundle is the same public code nixamp.com hands anyone;
2294
2304
  // a station's page loads it, and a station link carries no server key.
2295
2305
  const isPublicAsset = (request.method === "GET" || request.method === "HEAD") &&
@@ -2329,7 +2339,7 @@ export function createHandler(engine, options) {
2329
2339
  !((path === "/vod" || /^\/vod\/[A-Za-z0-9_-]{1,32}$/.test(path) || path === "/api/v1/guide/vods" || path.startsWith("/api/v1/guide/vods/")) && Boolean(options.stations?.vods)) &&
2330
2340
  !STATION_PAGE.test(path) &&
2331
2341
  // Public, listed stations under readable names, and their artists and albums (radio-pages.ts).
2332
- !(isRadioPath(path) && Boolean(options.stations)) &&
2342
+ !(radioPath && Boolean(options.stations)) &&
2333
2343
  // Following an artist from those pages, signed in at nixamp.com.
2334
2344
  !(path === "/follow" && Boolean(options.follows && options.accounts)) &&
2335
2345
  !isPublicAsset &&
@@ -2656,11 +2666,11 @@ export function createHandler(engine, options) {
2656
2666
  return;
2657
2667
  }
2658
2668
  }
2659
- if (options.stations && isRadioPath(path) && (request.method === "GET" || request.method === "HEAD")) {
2669
+ if (options.stations && radioPath && (request.method === "GET" || request.method === "HEAD")) {
2660
2670
  const stations = options.stations;
2661
2671
  const named = radioStations(stations.store.list());
2662
2672
  const publicBase = stations.publicBase?.() ?? "";
2663
- const base = radioBaseFor(request, publicBase);
2673
+ const base = onDirectory ? storeSite : radioBaseFor(request, publicBase);
2664
2674
  const nav = radioNav(guideHosts().has(requestHost) ? "/" : "/guide");
2665
2675
  const send = (status, page, csp = RADIO_CSP) => {
2666
2676
  const ads = guideAds();
@@ -2764,7 +2774,9 @@ export function createHandler(engine, options) {
2764
2774
  const built = web !== null && isFile(join(web, "station.html")) ? readFileSync(join(web, "station.html"), "utf8") : null;
2765
2775
  // The built page carries the one ad; the inline fallback stays as it is.
2766
2776
  const ads = built ? guideAds() : {};
2767
- const page = withPageAd(asRadioPage(built ?? STATION_PAGE_HTML, station, slug, base, nav), ads);
2777
+ // Its link preview carries the cover of an album it plays.
2778
+ const image = stationImage(radioCatalogue(), slug, base);
2779
+ const page = withPageAd(asRadioPage(built ?? STATION_PAGE_HTML, station, slug, base, nav, undefined, image), ads);
2768
2780
  response.writeHead(200, {
2769
2781
  "content-type": "text/html; charset=utf-8",
2770
2782
  "cache-control": "no-cache",
@@ -2887,7 +2899,9 @@ export function createHandler(engine, options) {
2887
2899
  catch {
2888
2900
  return null;
2889
2901
  } },
2902
+ site: storeSite,
2890
2903
  released: music.released,
2904
+ ...(music.veo ? { veo: music.veo } : {}),
2891
2905
  }))
2892
2906
  return;
2893
2907
  }
@@ -7194,6 +7208,24 @@ export async function serve(argv, version = "0.1.0") {
7194
7208
  root: process.env["NIXAMP_LABEL_ROOT"] || (labelHome ? join(labelHome.root, "ai") : join(stateDir(), "ai-label")),
7195
7209
  owner: options.owner || "",
7196
7210
  mayStart: mayStartStation,
7211
+ // Filmed shots for singles come from nixamp.com's Veo studio, asked as this server's owner, under its monthly cap.
7212
+ veo: async (shot) => {
7213
+ const session = readSession();
7214
+ const here = stations.publicBase?.() ?? "";
7215
+ if (!session?.token || !/^https:\/\//.test(here))
7216
+ throw new VeoUnavailable("this server is not signed in to nixamp.com");
7217
+ const answer = await fetch(`${session.site.replace(/\/+$/, "")}/api/v1/music/veo`, {
7218
+ method: "POST",
7219
+ headers: { "content-type": "application/json", authorization: `Bearer ${session.token}`, "x-nixamp-server": here },
7220
+ body: JSON.stringify({ prompt: shot.prompt, seconds: shot.seconds, references: shot.references.map((one) => one.toString("base64")) }),
7221
+ signal: AbortSignal.timeout(12 * 60_000),
7222
+ });
7223
+ if (answer.status === 402 || answer.status === 404)
7224
+ throw new VeoUnavailable((await answer.json().catch(() => ({}))).error ?? `HTTP ${answer.status}`);
7225
+ if (!answer.ok)
7226
+ throw new Error((await answer.json().catch(() => ({}))).error ?? `nixamp.com answered ${answer.status}`);
7227
+ return Buffer.from(await answer.arrayBuffer());
7228
+ },
7197
7229
  // A release is told to nixamp.com as this server's owner; nixamp.com tells the artist's followers.
7198
7230
  announce: (release) => {
7199
7231
  const session = readSession();
@@ -7479,6 +7511,48 @@ export async function serve(argv, version = "0.1.0") {
7479
7511
  * followers are told. Only the account that owns a listed server may say
7480
7512
  * so, only for an artist that server's catalogue has, once per release.
7481
7513
  */
7514
+ // Veo shots for label music videos, under a monthly dollar cap (veo-studio.ts). nixamp.com only.
7515
+ const veoStudio = options.directory && process.env["NIXAMP_VEO_API_KEY"] ? new VeoStudio({
7516
+ key: process.env["NIXAMP_VEO_API_KEY"],
7517
+ file: VeoStudio.fileFor(stateDir(), options.port),
7518
+ monthlyUsd: Number(process.env["NIXAMP_VEO_MONTHLY_USD"] ?? 50),
7519
+ usdPerSecond: Number(process.env["NIXAMP_VEO_USD_PER_SECOND"] ?? 0.15),
7520
+ }) : undefined;
7521
+ /** The signed-in account owns a server listed in the directory at this base. */
7522
+ const ownsListed = async (request, base) => {
7523
+ const me = accounts ? await accounts.whoIs(tokenFrom(request.headers)).catch(() => null) : null;
7524
+ return Boolean(me && base && directory?.list().some((one) => one.ownerId === me.id && peerBase(one.url) === base));
7525
+ };
7526
+ const musicVeo = async (request) => {
7527
+ if (!veoStudio)
7528
+ return { status: 404, body: { error: "Veo is not set up here" } };
7529
+ const base = peerBase(String(request.headers["x-nixamp-server"] ?? ""));
7530
+ if (!(await ownsListed(request, base)))
7531
+ return { status: 403, body: { error: "only the owner of a listed server" } };
7532
+ if (request.method === "GET")
7533
+ return { status: 200, body: veoStudio.status() };
7534
+ if (request.method !== "POST")
7535
+ return { status: 405, body: { error: "GET or POST" } };
7536
+ let input;
7537
+ try {
7538
+ input = JSON.parse(await readBody(request, 2 * 1024 * 1024));
7539
+ }
7540
+ catch {
7541
+ return { status: 400, body: { error: "a JSON body under 2 MB" } };
7542
+ }
7543
+ const prompt = String(input.prompt ?? "").trim();
7544
+ if (prompt.length < 20)
7545
+ return { status: 400, body: { error: "describe the shot" } };
7546
+ const references = (Array.isArray(input.references) ? input.references : []).slice(0, 3).map((one) => Buffer.from(String(one), "base64")).filter((one) => one.length > 1000);
7547
+ try {
7548
+ return { status: 200, body: null, video: await veoStudio.clip({ prompt, seconds: Number(input.seconds ?? 8), references }) };
7549
+ }
7550
+ catch (error) {
7551
+ if (error instanceof VeoBudgetError)
7552
+ return { status: 402, body: { error: error.message, code: "budget", ...veoStudio.status() } };
7553
+ return { status: 422, body: { error: error.message, code: "refused" } };
7554
+ }
7555
+ };
7482
7556
  const announced = new Set();
7483
7557
  const musicReleased = async (request, body) => {
7484
7558
  const me = accounts ? await accounts.whoIs(tokenFrom(request.headers)).catch(() => null) : null;
@@ -8062,7 +8136,7 @@ export async function serve(argv, version = "0.1.0") {
8062
8136
  ...(supportPayments ? { support: supportPayments } : {}),
8063
8137
  ...(supportMonthly ? { supportSubscriptions: supportMonthly } : {}),
8064
8138
  ...(musicStore ? { musicStore } : {}),
8065
- ...(musicHub ? { music: { hub: musicHub, released: musicReleased } } : {}),
8139
+ ...(musicHub ? { music: { hub: musicHub, released: musicReleased, veo: musicVeo } } : {}),
8066
8140
  ...(process.env["COINPAY_WEBHOOK_SECRET"] ? { coinpayWebhookSecret: process.env["COINPAY_WEBHOOK_SECRET"] } : {}),
8067
8141
  ...(sharedTranslations ? { sharedTranslations } : {}),
8068
8142
  ...(translator ? { translator } : {}),
@@ -538,6 +538,8 @@ async function labelCommand(argv, call, show) {
538
538
  videos: has(argv, "--no-videos") ? false : has(argv, "--videos") ? true : undefined,
539
539
  newArtistChance: number("--new-artists"),
540
540
  resetDay: number("--reset-day"),
541
+ veo: has(argv, "--veo") ? true : has(argv, "--no-veo") ? false : undefined,
542
+ veoShots: number("--veo-shots"),
541
543
  genreOrder: flag(argv, "--genres"),
542
544
  enabled: has(argv, "--off") ? false : has(argv, "--on") ? true : undefined,
543
545
  publish: has(argv, "--unlisted") ? false : has(argv, "--public") ? true : undefined,
@@ -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;
@@ -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
@@ -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.";
@@ -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
 
@@ -0,0 +1,39 @@
1
+ export declare class VeoBudgetError extends Error {
2
+ }
3
+ export declare class VeoRefused extends Error {
4
+ }
5
+ export interface VeoStudioOptions {
6
+ key: string;
7
+ file: string;
8
+ monthlyUsd?: number;
9
+ usdPerSecond?: number;
10
+ model?: string;
11
+ fetcher?: typeof fetch;
12
+ now?: () => number;
13
+ sleep?: (ms: number) => Promise<void>;
14
+ }
15
+ export declare class VeoStudio {
16
+ private readonly options;
17
+ private spent;
18
+ constructor(options: VeoStudioOptions);
19
+ static fileFor(stateDir: string, port: number): string;
20
+ private month;
21
+ private get cap();
22
+ private get rate();
23
+ private save;
24
+ status(): {
25
+ month: string;
26
+ spentUsd: number;
27
+ capUsd: number;
28
+ leftUsd: number;
29
+ usdPerSecond: number;
30
+ secondsLeft: number;
31
+ };
32
+ /** One shot: 4, 6 or 8 seconds, 16:9, with up to three reference images (JPEG). */
33
+ clip(input: {
34
+ prompt: string;
35
+ seconds: number;
36
+ references: Buffer[];
37
+ }): Promise<Buffer>;
38
+ private make;
39
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Veo clips for the AI label's music videos, made on nixamp.com.
3
+ *
4
+ * A label server asks for a shot (a prompt, its length, up to three member
5
+ * portraits as reference images) and gets back an MP4. Google's Veo 3.1 Fast
6
+ * makes it; nixamp.com keeps the month's spend under a cap in dollars
7
+ * (NIXAMP_VEO_MONTHLY_USD, $50 by default), counting every second at a
8
+ * deliberately high rate (NIXAMP_VEO_USD_PER_SECOND, $0.15) so the cap holds
9
+ * whatever Google's price is today. A shot is reserved before it is asked for
10
+ * and given back when Google makes nothing (a refusal, a filter, a timeout).
11
+ *
12
+ * The key is NIXAMP_VEO_API_KEY, its own name, so nothing else on nixamp.com
13
+ * starts spending Gemini credit because a Gemini key appeared.
14
+ */
15
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
16
+ import { dirname, join } from "node:path";
17
+ export class VeoBudgetError extends Error {
18
+ }
19
+ export class VeoRefused extends Error {
20
+ }
21
+ const BASE = "https://generativelanguage.googleapis.com/v1beta";
22
+ export class VeoStudio {
23
+ options;
24
+ spent = {};
25
+ constructor(options) {
26
+ this.options = options;
27
+ try {
28
+ this.spent = JSON.parse(readFileSync(options.file, "utf8")).spent ?? {};
29
+ }
30
+ catch {
31
+ this.spent = {};
32
+ }
33
+ }
34
+ static fileFor(stateDir, port) {
35
+ return join(stateDir, `veo-${port}.json`);
36
+ }
37
+ month() {
38
+ return new Date(this.options.now?.() ?? Date.now()).toISOString().slice(0, 7);
39
+ }
40
+ get cap() { return this.options.monthlyUsd ?? 50; }
41
+ get rate() { return this.options.usdPerSecond ?? 0.15; }
42
+ save() {
43
+ mkdirSync(dirname(this.options.file), { recursive: true });
44
+ const temporary = `${this.options.file}.${process.pid}.tmp`;
45
+ writeFileSync(temporary, `${JSON.stringify({ version: 1, spent: this.spent }, null, 2)}\n`, { mode: 0o600 });
46
+ renameSync(temporary, this.options.file);
47
+ }
48
+ status() {
49
+ const spent = this.spent[this.month()] ?? 0;
50
+ const left = Math.max(0, this.cap - spent);
51
+ return { month: this.month(), spentUsd: Math.round(spent * 100) / 100, capUsd: this.cap, leftUsd: Math.round(left * 100) / 100, usdPerSecond: this.rate, secondsLeft: Math.floor(left / this.rate) };
52
+ }
53
+ /** One shot: 4, 6 or 8 seconds, 16:9, with up to three reference images (JPEG). */
54
+ async clip(input) {
55
+ const seconds = [4, 6, 8].includes(input.seconds) ? input.seconds : 8;
56
+ const cost = seconds * this.rate;
57
+ const month = this.month();
58
+ if ((this.spent[month] ?? 0) + cost > this.cap + 1e-9)
59
+ throw new VeoBudgetError(`this month's $${this.cap} of Veo is spent`);
60
+ // Reserved first, so two at once cannot both squeeze under the cap.
61
+ this.spent[month] = (this.spent[month] ?? 0) + cost;
62
+ this.save();
63
+ try {
64
+ return await this.make(input.prompt, seconds, input.references.slice(0, 3));
65
+ }
66
+ catch (error) {
67
+ this.spent[month] = Math.max(0, (this.spent[month] ?? 0) - cost);
68
+ this.save();
69
+ throw error;
70
+ }
71
+ }
72
+ async make(prompt, seconds, references) {
73
+ const send = this.options.fetcher ?? fetch;
74
+ const headers = { "x-goog-api-key": this.options.key, "content-type": "application/json" };
75
+ const model = this.options.model ?? "veo-3.1-fast-generate-preview";
76
+ const started = await send(`${BASE}/models/${model}:predictLongRunning`, {
77
+ method: "POST",
78
+ headers,
79
+ signal: AbortSignal.timeout(60_000),
80
+ body: JSON.stringify({
81
+ instances: [{
82
+ prompt: prompt.slice(0, 1800),
83
+ ...(references.length ? { referenceImages: references.map((bytes) => ({ image: { bytesBase64Encoded: bytes.toString("base64"), mimeType: "image/jpeg" }, referenceType: "asset" })) } : {}),
84
+ }],
85
+ parameters: { aspectRatio: "16:9", durationSeconds: seconds },
86
+ }),
87
+ });
88
+ const operation = await started.json().catch(() => ({}));
89
+ if (!started.ok || !operation.name)
90
+ throw new VeoRefused(`Veo refused the shot: ${operation.error?.message?.slice(0, 200) ?? `HTTP ${started.status}`}`);
91
+ const sleep = this.options.sleep ?? ((ms) => new Promise((done) => setTimeout(done, ms)));
92
+ for (let tries = 0; tries < 60; tries++) {
93
+ await sleep(10_000);
94
+ const state = await (await send(`${BASE}/${operation.name}`, { headers, signal: AbortSignal.timeout(30_000) })).json().catch(() => ({}));
95
+ if (!state.done)
96
+ continue;
97
+ if (state.error)
98
+ throw new VeoRefused(`Veo failed: ${state.error.message?.slice(0, 200) ?? "no reason"}`);
99
+ const uri = state.response?.generateVideoResponse?.generatedSamples?.[0]?.video?.uri;
100
+ if (!uri)
101
+ throw new VeoRefused(`Veo made nothing: ${state.response?.generateVideoResponse?.raiMediaFilteredReasons?.[0]?.slice(0, 200) ?? "no sample"}`);
102
+ const video = await send(uri, { headers: { "x-goog-api-key": this.options.key }, signal: AbortSignal.timeout(120_000) });
103
+ const bytes = Buffer.from(await video.arrayBuffer());
104
+ if (!video.ok || bytes.length < 10_000)
105
+ throw new VeoRefused(`the Veo clip did not download (HTTP ${video.status})`);
106
+ return bytes;
107
+ }
108
+ throw new VeoRefused("Veo took too long (10 minutes)");
109
+ }
110
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nixamp",
3
- "version": "0.59.5",
3
+ "version": "0.60.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",