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.
- package/dist/ai-label.d.ts +37 -0
- package/dist/ai-label.js +127 -10
- package/dist/music-hub.d.ts +8 -0
- package/dist/music-hub.js +30 -6
- package/dist/music-seo.d.ts +38 -0
- package/dist/music-seo.js +160 -0
- package/dist/radio-pages.d.ts +4 -2
- package/dist/radio-pages.js +11 -4
- package/dist/server.d.ts +5 -0
- package/dist/server.js +81 -7
- package/dist/station-cli.js +2 -0
- 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 +29 -6
- package/src/music-seo.ts +191 -0
- package/src/radio-pages.ts +12 -4
- package/src/server.ts +62 -8
- package/src/station-cli.ts +2 -0
- 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-D7L1Lzg5.js → ar-5jTau3by.js} +1 -1
- package/web/dist/assets/{cs-DR-oumcX.js → cs-nlVt79qU.js} +1 -1
- package/web/dist/assets/{da-B7ofIVod.js → da-C_mRyuJW.js} +1 -1
- package/web/dist/assets/{de-CglXPk_n.js → de-Duqxb30z.js} +1 -1
- package/web/dist/assets/{es-CYVNiI20.js → es-CAHgck_F.js} +1 -1
- package/web/dist/assets/{fi-Bktga6d_.js → fi-BPXwol2s.js} +1 -1
- package/web/dist/assets/{fr-CNxMDHg5.js → fr-C9N4WLtT.js} +1 -1
- package/web/dist/assets/{hi-DEyWHgib.js → hi-PWlH7Sya.js} +1 -1
- package/web/dist/assets/{hu-mcHUOvhY.js → hu-DHmawwq3.js} +1 -1
- package/web/dist/assets/{id-T9wfahs5.js → id-DwMuYDOT.js} +1 -1
- package/web/dist/assets/{it-CASLaiyC.js → it-DuVsDcAz.js} +1 -1
- package/web/dist/assets/{main-D9-BuE2r.js → main-BV2N47tZ.js} +2 -2
- package/web/dist/assets/{nl-DQ-YPUBS.js → nl-BcdvPtpE.js} +1 -1
- package/web/dist/assets/{ru-CDSDPqFZ.js → ru-CfMl9LlQ.js} +1 -1
- package/web/dist/assets/{sv-liuu51-p.js → sv-CDtolH7I.js} +1 -1
- package/web/dist/assets/{uk-B3vreqbA.js → uk-DwjjSnVU.js} +1 -1
- package/web/dist/assets/{vi-B0NyoUEc.js → vi-CNYK9Tlb.js} +1 -1
- package/web/dist/assets/{zh-Bw5OQASg.js → zh-6nGSjON5.js} +1 -1
- package/web/dist/index.html +1 -1
- 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
|
-
!(
|
|
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 &&
|
|
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
|
-
|
|
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 } : {}),
|
package/dist/station-cli.js
CHANGED
|
@@ -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,
|
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
|
|
|
@@ -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
|
+
}
|