nixamp 0.36.4 → 0.37.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-keys.d.ts +9 -0
- package/dist/ai-keys.js +46 -0
- package/dist/ai-radio.d.ts +148 -0
- package/dist/ai-radio.js +477 -0
- package/dist/ai-routes.d.ts +31 -0
- package/dist/ai-routes.js +191 -0
- package/dist/ai-text.d.ts +23 -0
- package/dist/ai-text.js +102 -0
- package/dist/library.d.ts +2 -0
- package/dist/library.js +1 -0
- package/dist/listener-shares.d.ts +136 -0
- package/dist/listener-shares.js +486 -0
- package/dist/playlist-input.d.ts +1 -1
- package/dist/playlist-input.js +2 -1
- package/dist/server.d.ts +8 -0
- package/dist/server.js +62 -1
- package/dist/station-cli.js +179 -0
- package/dist/station-guide.d.ts +4 -1
- package/dist/station-guide.js +118 -21
- package/dist/station-mcp.d.ts +291 -20
- package/dist/station-mcp.js +66 -0
- package/dist/station-routes.d.ts +4 -0
- package/dist/station-routes.js +8 -1
- package/dist/station-runner.js +4 -2
- package/dist/station-usage.d.ts +1 -1
- package/dist/station-usage.js +21 -1
- package/package.json +1 -1
- package/src/ai-keys.ts +49 -0
- package/src/ai-radio.ts +536 -0
- package/src/ai-routes.ts +220 -0
- package/src/ai-text.ts +117 -0
- package/src/library.ts +3 -0
- package/src/listener-shares.ts +528 -0
- package/src/playlist-input.ts +3 -2
- package/src/server.ts +69 -1
- package/src/station-cli.ts +165 -0
- package/src/station-guide.ts +122 -21
- package/src/station-mcp.ts +62 -0
- package/src/station-routes.ts +11 -2
- package/src/station-runner.ts +4 -2
- package/src/station-usage.ts +21 -1
- package/web/dist/assets/{ar-B9jc6q-w.js → ar-CO5BKvO0.js} +1 -1
- package/web/dist/assets/{cs-lU25OfSM.js → cs-BUpUmRvP.js} +1 -1
- package/web/dist/assets/{da-CAgdgQoL.js → da-zX4iMHja.js} +1 -1
- package/web/dist/assets/{de-CiusGizF.js → de-Tn7jOSGa.js} +1 -1
- package/web/dist/assets/{es-DVxl-KYi.js → es-7u7Y8_4k.js} +1 -1
- package/web/dist/assets/{fi-C1-78Aib.js → fi-V6JD2KmM.js} +1 -1
- package/web/dist/assets/{fr-BcAXoFCf.js → fr-Cywrbe0W.js} +1 -1
- package/web/dist/assets/{hi-DXdG5DSQ.js → hi-Bi1gVC9W.js} +1 -1
- package/web/dist/assets/{hls-3VKVEQE3-C3dxJvEy.js → hls-3VKVEQE3-DiRfULQE.js} +1 -1
- package/web/dist/assets/{hu-qrAMY3zu.js → hu-R_kphMiO.js} +1 -1
- package/web/dist/assets/{id-Cgtr-WML.js → id-DsjVfoDE.js} +1 -1
- package/web/dist/assets/{it-kuAXXts8.js → it-D_Jw0vo4.js} +1 -1
- package/web/dist/assets/{main-C5IsPzDk.js → main-26sEJB31.js} +2 -2
- package/web/dist/assets/{mpegts-BWzEpcbk.js → mpegts-DCmyLJq-.js} +1 -1
- package/web/dist/assets/{mpegts-LO6RVLD6-CYjdm0vd.js → mpegts-LO6RVLD6-BCbjcAdN.js} +1 -1
- package/web/dist/assets/{nl-CEXpOYtY.js → nl-D_AiIeeo.js} +1 -1
- package/web/dist/assets/{ru-v7b_XixQ.js → ru-BalU1IsS.js} +1 -1
- package/web/dist/assets/{sv-CJK5wtih.js → sv-CRhToKKj.js} +1 -1
- package/web/dist/assets/{uk-qDSjg5zB.js → uk-BzTMYhrk.js} +1 -1
- package/web/dist/assets/{vi-kckrzXyJ.js → vi-DkoVH56o.js} +1 -1
- package/web/dist/assets/{zh-Cbq-3ikH.js → zh-DHjjJx8Z.js} +1 -1
- package/web/dist/index.html +1 -1
- package/web/dist/sw.js +22 -22
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const AI_KEY_NAMES: readonly ["ELEVENLABS_API_KEY", "GEMINI_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY"];
|
|
2
|
+
export type AiKeyName = (typeof AI_KEY_NAMES)[number];
|
|
3
|
+
export declare function aiKeysFile(): string;
|
|
4
|
+
/** The key, from the environment or ai.json; "" when neither has it. */
|
|
5
|
+
export declare function aiKey(name: AiKeyName, env?: Record<string, string | undefined>, file?: string): string;
|
|
6
|
+
/** Which keys are set, never their values. */
|
|
7
|
+
export declare function aiKeysSet(env?: Record<string, string | undefined>, file?: string): Record<AiKeyName, boolean>;
|
|
8
|
+
/** Keep (or with "" forget) a key in ai.json. */
|
|
9
|
+
export declare function saveAiKey(name: AiKeyName, value: string, file?: string): void;
|
package/dist/ai-keys.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The keys AI radio uses, and where they come from.
|
|
3
|
+
*
|
|
4
|
+
* The environment first, as everywhere else; then `ai.json` beside the
|
|
5
|
+
* server's own keys (0600), which `nixamp station ai key NAME` fills from
|
|
6
|
+
* stdin so a daemon restarted by `nixamp update` still has them. A key is
|
|
7
|
+
* never shown back: the owner view says which ones are set, not what they are.
|
|
8
|
+
*/
|
|
9
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { stateDir } from "./daemon.js";
|
|
12
|
+
export const AI_KEY_NAMES = ["ELEVENLABS_API_KEY", "GEMINI_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY"];
|
|
13
|
+
export function aiKeysFile() {
|
|
14
|
+
return join(stateDir(), "ai.json");
|
|
15
|
+
}
|
|
16
|
+
function saved(file = aiKeysFile()) {
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
19
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
20
|
+
return {};
|
|
21
|
+
return Object.fromEntries(Object.entries(parsed).filter((pair) => typeof pair[1] === "string"));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** The key, from the environment or ai.json; "" when neither has it. */
|
|
28
|
+
export function aiKey(name, env = process.env, file = aiKeysFile()) {
|
|
29
|
+
return (env[name] ?? "").trim() || (saved(file)[name] ?? "").trim();
|
|
30
|
+
}
|
|
31
|
+
/** Which keys are set, never their values. */
|
|
32
|
+
export function aiKeysSet(env = process.env, file = aiKeysFile()) {
|
|
33
|
+
return Object.fromEntries(AI_KEY_NAMES.map((name) => [name, aiKey(name, env, file) !== ""]));
|
|
34
|
+
}
|
|
35
|
+
/** Keep (or with "" forget) a key in ai.json. */
|
|
36
|
+
export function saveAiKey(name, value, file = aiKeysFile()) {
|
|
37
|
+
const next = saved(file);
|
|
38
|
+
if (value.trim())
|
|
39
|
+
next[name] = value.trim();
|
|
40
|
+
else
|
|
41
|
+
delete next[name];
|
|
42
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
43
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
44
|
+
writeFileSync(temporary, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
|
|
45
|
+
renameSync(temporary, file);
|
|
46
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { askJson } from "./ai-text.ts";
|
|
2
|
+
import type { LibraryIndex, Probed } from "./library-index.ts";
|
|
3
|
+
import type { StationRunner } from "./station-runner.ts";
|
|
4
|
+
import { type StationStore } from "./stations.ts";
|
|
5
|
+
export interface AiTrack {
|
|
6
|
+
file: string;
|
|
7
|
+
title: string;
|
|
8
|
+
/** The brief the music model was given. */
|
|
9
|
+
prompt: string;
|
|
10
|
+
seconds: number;
|
|
11
|
+
/** Who wrote the brief and who composed it, e.g. "gemini-flash-latest + elevenlabs/music_v1". */
|
|
12
|
+
made: string;
|
|
13
|
+
createdAt: number;
|
|
14
|
+
}
|
|
15
|
+
export interface AiStation {
|
|
16
|
+
key: string;
|
|
17
|
+
name: string;
|
|
18
|
+
/** The owner's words for the music. Every brief starts from these. */
|
|
19
|
+
style: string;
|
|
20
|
+
description: string;
|
|
21
|
+
vocals: boolean;
|
|
22
|
+
/** Length of each track, 30-300 s. */
|
|
23
|
+
trackSeconds: number;
|
|
24
|
+
/** New tracks a day once the first few exist. */
|
|
25
|
+
perDay: number;
|
|
26
|
+
enabled: boolean;
|
|
27
|
+
/** Public, listed, basis owned, as soon as it has a track. */
|
|
28
|
+
publish: boolean;
|
|
29
|
+
/** The station it plays on; "" until the first track exists. */
|
|
30
|
+
stationId: string;
|
|
31
|
+
tracks: AiTrack[];
|
|
32
|
+
lastAttemptAt: number;
|
|
33
|
+
lastError: string;
|
|
34
|
+
/** Failures in a row; backs off after each. */
|
|
35
|
+
failures: number;
|
|
36
|
+
createdAt: number;
|
|
37
|
+
}
|
|
38
|
+
export interface Preset {
|
|
39
|
+
name: string;
|
|
40
|
+
style: string;
|
|
41
|
+
description: string;
|
|
42
|
+
vocals: boolean;
|
|
43
|
+
}
|
|
44
|
+
/** Styles to start from: `nixamp station ai create --preset lofi`. */
|
|
45
|
+
export declare const PRESETS: Record<string, Preset>;
|
|
46
|
+
export declare const DEFAULTS: {
|
|
47
|
+
trackSeconds: number;
|
|
48
|
+
perDay: number;
|
|
49
|
+
monthlySeconds: number;
|
|
50
|
+
seedTracks: number;
|
|
51
|
+
};
|
|
52
|
+
export interface Composer {
|
|
53
|
+
/** e.g. "elevenlabs/music_v1". */
|
|
54
|
+
name: string;
|
|
55
|
+
/** Writes an MP3 to `out`. Throws OutOfCredit when the account cannot pay. */
|
|
56
|
+
compose(input: {
|
|
57
|
+
prompt: string;
|
|
58
|
+
seconds: number;
|
|
59
|
+
vocals: boolean;
|
|
60
|
+
out: string;
|
|
61
|
+
}): Promise<void>;
|
|
62
|
+
}
|
|
63
|
+
export declare class OutOfCredit extends Error {
|
|
64
|
+
}
|
|
65
|
+
/** ElevenLabs Music: a paid plan's music may be broadcast. */
|
|
66
|
+
export declare function elevenLabsComposer(key: string, fetcher?: typeof fetch): Composer;
|
|
67
|
+
/**
|
|
68
|
+
* A model of the owner's own (MusicGen, ACE-Step...): NIXAMP_AI_MUSIC_COMMAND
|
|
69
|
+
* is run with {prompt}, {seconds}, {vocals} and {out} replaced, each as one
|
|
70
|
+
* argument, and must leave an MP3 at {out}.
|
|
71
|
+
*/
|
|
72
|
+
export declare function commandComposer(template: string): Composer;
|
|
73
|
+
/** The composer this server can use, or null with the reason. */
|
|
74
|
+
export declare function composerFromEnv(env?: Record<string, string | undefined>): Composer | null;
|
|
75
|
+
/** A brief without a language model: the style, with a mood and a name drawn for it. */
|
|
76
|
+
export declare function plainBrief(station: Pick<AiStation, "style" | "tracks">, random?: () => number): {
|
|
77
|
+
title: string;
|
|
78
|
+
prompt: string;
|
|
79
|
+
};
|
|
80
|
+
export declare function writeBrief(station: Pick<AiStation, "name" | "style" | "vocals" | "tracks">, ask?: typeof askJson): Promise<{
|
|
81
|
+
title: string;
|
|
82
|
+
prompt: string;
|
|
83
|
+
model: string;
|
|
84
|
+
}>;
|
|
85
|
+
export interface AiRadioDeps {
|
|
86
|
+
index: LibraryIndex;
|
|
87
|
+
store: StationStore;
|
|
88
|
+
runner: StationRunner;
|
|
89
|
+
probe: (path: string) => Promise<Probed>;
|
|
90
|
+
/** ffmpeg argv prefix, for the tags. */
|
|
91
|
+
ffmpeg: string[];
|
|
92
|
+
/** The folder the music goes in: a managed library source. */
|
|
93
|
+
root: string;
|
|
94
|
+
owner: string;
|
|
95
|
+
/** Whether another station may be started now (the server's running limit). */
|
|
96
|
+
mayStart: () => boolean;
|
|
97
|
+
composer?: () => Composer | null;
|
|
98
|
+
/** Writes `from` with these tags to `to`; ffmpeg's stream copy by default. */
|
|
99
|
+
tag?: (from: string, to: string, tags: Record<string, string>) => Promise<void>;
|
|
100
|
+
ask?: typeof askJson;
|
|
101
|
+
canAsk?: () => boolean;
|
|
102
|
+
now?: () => number;
|
|
103
|
+
log?: (line: string) => void;
|
|
104
|
+
}
|
|
105
|
+
export interface AiRadioSettings {
|
|
106
|
+
/** Seconds of music a month across every AI station; 0 for no cap. */
|
|
107
|
+
monthlySeconds: number;
|
|
108
|
+
/** Set by a provider that is out of credit; cleared by `resume`. */
|
|
109
|
+
pausedReason: string;
|
|
110
|
+
}
|
|
111
|
+
export declare class AiRadio {
|
|
112
|
+
private readonly file;
|
|
113
|
+
private readonly deps;
|
|
114
|
+
private stations;
|
|
115
|
+
private settings;
|
|
116
|
+
private usage;
|
|
117
|
+
private busy;
|
|
118
|
+
private sourceId;
|
|
119
|
+
private timer;
|
|
120
|
+
constructor(file: string, deps: AiRadioDeps);
|
|
121
|
+
static fileFor(stateDir: string, port: number): string;
|
|
122
|
+
private now;
|
|
123
|
+
private save;
|
|
124
|
+
private source;
|
|
125
|
+
private composer;
|
|
126
|
+
list(): AiStation[];
|
|
127
|
+
get(key: string): AiStation | undefined;
|
|
128
|
+
/** What the owner sees: the stations, this month's seconds, and why nothing is being made. */
|
|
129
|
+
status(): Record<string, unknown>;
|
|
130
|
+
/** Change the month's cap; `resume` clears a stop for lack of credit. */
|
|
131
|
+
configure(input: Record<string, unknown>): AiRadioSettings;
|
|
132
|
+
create(input: Record<string, unknown>): AiStation;
|
|
133
|
+
update(key: string, input: Record<string, unknown>): AiStation;
|
|
134
|
+
/** Forget an AI station. Its music and its station stay unless `files` is true. */
|
|
135
|
+
remove(key: string, files?: boolean): void;
|
|
136
|
+
/** When this station's next track is due; 0 when now, -1 when never. */
|
|
137
|
+
nextAt(station: AiStation): number;
|
|
138
|
+
/** Every 5 minutes: the station most overdue gets its next track. One at a time. */
|
|
139
|
+
startSchedule(everyMs?: number): void;
|
|
140
|
+
stopSchedule(): void;
|
|
141
|
+
private budgetLeft;
|
|
142
|
+
tick(): Promise<void>;
|
|
143
|
+
/** Compose one track for this station now (the owner's "generate"), within the month's cap. */
|
|
144
|
+
generate(key: string): Promise<AiTrack>;
|
|
145
|
+
/** The station gets its tracks: made with the first one, then kept in step. */
|
|
146
|
+
private place;
|
|
147
|
+
private require;
|
|
148
|
+
}
|