@audiofool/moondog 0.3.0-beta.2 → 0.3.0-beta.4
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/README.md +1 -1
- package/package.json +1 -1
- package/scripts/moondog.mjs +17 -3
- package/src/core/listening-now.mjs +75 -6
- package/src/core/moondog-application.mjs +66 -9
- package/src/i18n/messages/de.mjs +1 -1
- package/src/i18n/messages/en.mjs +1 -1
- package/src/i18n/messages/es.mjs +1 -1
- package/src/i18n/messages/fr.mjs +1 -1
- package/src/i18n/messages/it.mjs +1 -1
- package/src/i18n/messages/ja.mjs +1 -1
- package/src/i18n/messages/ko.mjs +1 -1
- package/src/i18n/messages/pt.mjs +1 -1
- package/src/i18n/messages/zh-Hant.mjs +1 -1
- package/src/i18n/messages/zh.mjs +1 -1
- package/src/i18n/screen/es.mjs +58 -0
- package/src/i18n/screen/ja.mjs +58 -0
- package/src/i18n/screen/pt.mjs +58 -0
- package/src/i18n/screen/zh.mjs +58 -0
- package/src/integrations/apple-music/terminal-preview-player.mjs +8 -0
- package/src/integrations/stable-audio/cover.mjs +324 -0
- package/src/integrations/stable-audio/local-generator.mjs +127 -37
- package/src/integrations/stable-audio/setup.mjs +653 -0
- package/src/integrations/stable-audio/sources.mjs +106 -0
- package/src/profile/taste-compiler.mjs +14 -5
- package/src/runtime/pi/agent-runtime.mjs +45 -11
- package/src/runtime/pi/record-name.mjs +80 -0
- package/src/surfaces/cli/command-completions.mjs +15 -1
- package/src/surfaces/cli/tui.mjs +229 -6
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Makes music on this Mac with a local Stable Audio 3 install (Stability AI's optimized MLX
|
|
2
2
|
// build). The listener turns it on once, in Settings or with /generation; until then nothing
|
|
3
3
|
// runs. Each track is a WAV and a receipt in Moondog's state directory, and nothing leaves the
|
|
4
|
-
// machine:
|
|
4
|
+
// machine: a run never downloads anything, and the receipt keeps no absolute path. Moondog's own
|
|
5
|
+
// install (setup.mjs) downloads only when the listener asks, from the pinned official sources.
|
|
5
6
|
import { spawn } from "node:child_process";
|
|
6
7
|
import { randomBytes, randomInt, randomUUID } from "node:crypto";
|
|
7
8
|
import { unwatchFile, watchFile } from "node:fs";
|
|
@@ -12,10 +13,12 @@ import path from "node:path";
|
|
|
12
13
|
import { resolveMoondogStateDirectory } from "../../core/state-directory.mjs";
|
|
13
14
|
import { streamVariation } from "../../profile/taste-compiler.mjs";
|
|
14
15
|
import { resolveMoondogAuthFile } from "../../runtime/pi/persistent-credential-store.mjs";
|
|
16
|
+
import { readLoudness } from "./cover.mjs";
|
|
17
|
+
import { DEFAULT_STABLE_AUDIO_MODEL, isStableAudioModel, SHARED_WEIGHTS, STABLE_AUDIO_MODELS } from "./sources.mjs";
|
|
15
18
|
|
|
16
|
-
// The weights
|
|
17
|
-
const
|
|
18
|
-
// Medium
|
|
19
|
+
// The weights a model needs for text to music: the text encoder, the model and its decoder.
|
|
20
|
+
export const stableAudioWeights = (model = DEFAULT_STABLE_AUDIO_MODEL) => [...SHARED_WEIGHTS, ...STABLE_AUDIO_MODELS[model].weights];
|
|
21
|
+
// Medium and Small Music were trained on up to 380 seconds; very short clips come out rough.
|
|
19
22
|
export const GENERATION_SECONDS = Object.freeze({ min: 15, max: 380, default: 120 });
|
|
20
23
|
// A cold start reads about 4.8 GB of weights before the first step; a 380-second track then takes about 35 seconds.
|
|
21
24
|
const generationTimeoutMs = 10 * 60_000;
|
|
@@ -29,6 +32,11 @@ export function defaultStableAudioRoot() {
|
|
|
29
32
|
return path.join(homedir(), "Developer", "stable-audio-3", "optimized", "mlx");
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
/** The install Moondog downloads and manages itself; the MLX build is in its mlx folder. */
|
|
36
|
+
export function resolveStableAudioManagedRoot(environment = process.env) {
|
|
37
|
+
return path.join(resolveMoondogStateDirectory(environment), "stable-audio-3");
|
|
38
|
+
}
|
|
39
|
+
|
|
32
40
|
// Kept next to the playback preference, apart from runtime settings.
|
|
33
41
|
export function resolveGenerationSettingsFile(environment = process.env) {
|
|
34
42
|
return path.join(path.dirname(resolveMoondogAuthFile(environment)), "generation.json");
|
|
@@ -47,41 +55,92 @@ function literalFolder(value) {
|
|
|
47
55
|
return path.isAbsolute(expanded) ? path.resolve(expanded) : null;
|
|
48
56
|
}
|
|
49
57
|
|
|
58
|
+
export async function readSavedSettings(environment) {
|
|
59
|
+
try {
|
|
60
|
+
const document = JSON.parse(await readFile(resolveGenerationSettingsFile(environment), "utf8"));
|
|
61
|
+
return document?.version === 1 ? document : {};
|
|
62
|
+
} catch {
|
|
63
|
+
return {}; // Not set up yet.
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
50
67
|
/**
|
|
51
|
-
* The listener's choice: { enabled, path, source }. Generation is off until they
|
|
52
|
-
* MOONDOG_SA3_DIR can point at another install but never turns it on.
|
|
68
|
+
* The listener's choice: { enabled, path, source, model, folder }. Generation is off until they
|
|
69
|
+
* turn it on; MOONDOG_SA3_DIR can point at another install but never turns it on. source is
|
|
70
|
+
* "managed" for Moondog's own install, "settings" or "default" for the listener's own folder
|
|
71
|
+
* (folder, which Moondog only reads), or "environment". model picks the weights in either.
|
|
53
72
|
* A missing or unreadable file reads as off, so an older or newer file never stops the room.
|
|
54
73
|
*/
|
|
55
|
-
export async function readGenerationSettings(environment = process.env) {
|
|
56
|
-
|
|
57
|
-
try {
|
|
58
|
-
document = JSON.parse(await readFile(resolveGenerationSettingsFile(environment), "utf8"));
|
|
59
|
-
} catch { /* Not set up yet. */ }
|
|
60
|
-
const saved = document?.version === 1 ? document : {};
|
|
74
|
+
export async function readGenerationSettings(environment = process.env, { platform = process.platform, arch = process.arch } = {}) {
|
|
75
|
+
const saved = await readSavedSettings(environment);
|
|
61
76
|
const fromEnvironment = literalFolder(environment.MOONDOG_SA3_DIR);
|
|
62
77
|
const fromFile = literalFolder(saved.path);
|
|
78
|
+
const managed = !fromEnvironment && saved.source === "managed";
|
|
79
|
+
const folder = fromFile ?? defaultStableAudioRoot();
|
|
80
|
+
let model = isStableAudioModel(saved.model) ? saved.model : DEFAULT_STABLE_AUDIO_MODEL;
|
|
81
|
+
// A folder Moondog doesn't manage (MOONDOG_SA3_DIR, a typed folder, ~/Developer) runs the saved model
|
|
82
|
+
// only if it has it, otherwise the first model it can run; a model chosen for Moondog's own install
|
|
83
|
+
// waits until that install is chosen again. A folder that runs nothing keeps the saved model.
|
|
84
|
+
if (!managed) {
|
|
85
|
+
for (const name of [...new Set([model, DEFAULT_STABLE_AUDIO_MODEL, ...Object.keys(STABLE_AUDIO_MODELS)])]) {
|
|
86
|
+
if ((await checkStableAudioInstall(fromEnvironment ?? folder, { model: name, platform, arch })).state === "ready") { model = name; break; }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
63
89
|
return {
|
|
64
90
|
enabled: saved.enabled === true,
|
|
65
|
-
path: fromEnvironment ??
|
|
66
|
-
source: fromEnvironment ? "environment" : fromFile ? "settings" : "default",
|
|
91
|
+
path: fromEnvironment ?? (managed ? path.join(resolveStableAudioManagedRoot(environment), "mlx") : folder),
|
|
92
|
+
source: fromEnvironment ? "environment" : managed ? "managed" : fromFile ? "settings" : "default",
|
|
93
|
+
model,
|
|
94
|
+
folder,
|
|
67
95
|
};
|
|
68
96
|
}
|
|
69
97
|
|
|
70
|
-
|
|
71
|
-
|
|
98
|
+
/**
|
|
99
|
+
* Saves what the listener changed: enabled; path, their own folder, which also switches to it;
|
|
100
|
+
* source "managed" or "settings" to switch between Moondog's install and their folder; model.
|
|
101
|
+
* A model chosen without a folder or a source switches to Moondog's install, which holds it.
|
|
102
|
+
* Older Moondogs read the same version 1 file and see only enabled and path.
|
|
103
|
+
*/
|
|
104
|
+
export async function writeGenerationSettings({ enabled, path: folder, source, model } = {}, environment = process.env, { platform = process.platform, arch = process.arch } = {}) {
|
|
105
|
+
// The file's own view: a folder MOONDOG_SA3_DIR pins in this process is never written into it.
|
|
106
|
+
const current = await readGenerationSettings({ ...environment, MOONDOG_SA3_DIR: "" }, { platform, arch });
|
|
107
|
+
const saved = await readSavedSettings(environment);
|
|
72
108
|
const typed = folder === undefined ? undefined : literalFolder(folder);
|
|
73
109
|
if (folder !== undefined && folder !== null && !typed) {
|
|
74
110
|
throw Object.assign(new Error("Give the Stable Audio 3 folder as a full path, or one starting with ~/."), { code: "generation_path_invalid" });
|
|
75
111
|
}
|
|
112
|
+
if (model !== undefined && !isStableAudioModel(model)) {
|
|
113
|
+
throw Object.assign(new Error("Choose medium or small-music."), { code: "generation_model_invalid" });
|
|
114
|
+
}
|
|
115
|
+
const savedFolder = typed ?? literalFolder(saved.path);
|
|
116
|
+
const managed = typed ? false
|
|
117
|
+
: source === "managed" ? true
|
|
118
|
+
: source === "settings" ? false
|
|
119
|
+
: model !== undefined ? true
|
|
120
|
+
: saved.source === "managed";
|
|
121
|
+
// Switching to the listener's own folder without a model keeps the model only if that folder has it;
|
|
122
|
+
// otherwise the first model it can run, so a working install is never checked for weights it lacks.
|
|
123
|
+
let chosen = model ?? current.model;
|
|
124
|
+
if (model === undefined && !managed && (typed || source === "settings")) {
|
|
125
|
+
const folderRoot = savedFolder ?? current.folder;
|
|
126
|
+
const candidates = [current.model, DEFAULT_STABLE_AUDIO_MODEL, ...Object.keys(STABLE_AUDIO_MODELS)];
|
|
127
|
+
// A folder that runs nothing yet gets Medium, the model the install steps Moondog shows download.
|
|
128
|
+
chosen = DEFAULT_STABLE_AUDIO_MODEL;
|
|
129
|
+
for (const name of [...new Set(candidates)]) {
|
|
130
|
+
if ((await checkStableAudioInstall(folderRoot, { model: name, platform, arch })).state === "ready") { chosen = name; break; }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
76
133
|
const document = {
|
|
77
134
|
version: 1,
|
|
78
135
|
enabled: enabled === undefined ? current.enabled : enabled === true,
|
|
79
|
-
...(
|
|
136
|
+
...(savedFolder ? { path: savedFolder } : {}),
|
|
137
|
+
...(managed ? { source: "managed" } : {}),
|
|
138
|
+
model: chosen,
|
|
80
139
|
};
|
|
81
140
|
const file = resolveGenerationSettingsFile(environment);
|
|
82
141
|
await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
83
142
|
await writeFile(file, `${JSON.stringify(document, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
84
|
-
return readGenerationSettings(environment);
|
|
143
|
+
return readGenerationSettings(environment, { platform, arch });
|
|
85
144
|
}
|
|
86
145
|
|
|
87
146
|
const isFile = async (file, { executable = false, minimumBytes = 0 } = {}) => {
|
|
@@ -97,16 +156,16 @@ const isFile = async (file, { executable = false, minimumBytes = 0 } = {}) => {
|
|
|
97
156
|
};
|
|
98
157
|
|
|
99
158
|
/**
|
|
100
|
-
* Whether an install can make music now, without running it: "ready", "unsupported"
|
|
101
|
-
* Apple silicon), "missing" (no install at the folder) or "incomplete" (no Python
|
|
102
|
-
* or
|
|
159
|
+
* Whether an install can make music now with a model, without running it: "ready", "unsupported"
|
|
160
|
+
* (MLX needs Apple silicon), "missing" (no install at the folder) or "incomplete" (no Python
|
|
161
|
+
* environment or not that model's weights, which Moondog's setup or Stability's ./install.sh fetches).
|
|
103
162
|
*/
|
|
104
|
-
export async function checkStableAudioInstall(root, { platform = process.platform, arch = process.arch } = {}) {
|
|
163
|
+
export async function checkStableAudioInstall(root, { model = DEFAULT_STABLE_AUDIO_MODEL, platform = process.platform, arch = process.arch } = {}) {
|
|
105
164
|
if (platform !== "darwin" || arch !== "arm64") return { state: "unsupported" };
|
|
106
165
|
if (!root || !path.isAbsolute(root)) return { state: "missing" };
|
|
107
166
|
if (!(await isFile(path.join(root, "scripts", "sa3_mlx.py")))) return { state: "missing" };
|
|
108
167
|
if (!(await isFile(path.join(root, ".venv", "bin", "python"), { executable: true }))) return { state: "incomplete", missing: "environment" };
|
|
109
|
-
for (const weight of
|
|
168
|
+
for (const weight of stableAudioWeights(isStableAudioModel(model) ? model : DEFAULT_STABLE_AUDIO_MODEL)) {
|
|
110
169
|
if (!(await isFile(path.join(root, "models", "mlx", weight), { minimumBytes: 1024 }))) return { state: "incomplete", missing: "weights" };
|
|
111
170
|
}
|
|
112
171
|
return { state: "ready" };
|
|
@@ -171,7 +230,7 @@ export function createLocalGenerator({
|
|
|
171
230
|
spawnImpl = spawn,
|
|
172
231
|
now = () => new Date(),
|
|
173
232
|
} = {}) {
|
|
174
|
-
let current = { state: "unknown", enabled: false, install: "unknown", path: null, source: null };
|
|
233
|
+
let current = { state: "unknown", enabled: false, install: "unknown", path: null, source: null, model: DEFAULT_STABLE_AUDIO_MODEL, managed: false };
|
|
175
234
|
let running = null;
|
|
176
235
|
// The tracks made here, newest last, with what happened each time they played: the loop back
|
|
177
236
|
// from listening reads them. Each track's outcomes.jsonl keeps them; the receipt never changes.
|
|
@@ -196,8 +255,8 @@ export function createLocalGenerator({
|
|
|
196
255
|
const check = () => {
|
|
197
256
|
checking ??= (async () => {
|
|
198
257
|
try {
|
|
199
|
-
const settings = await readGenerationSettings(environment);
|
|
200
|
-
const install = await checkStableAudioInstall(settings.path, { platform, arch });
|
|
258
|
+
const settings = await readGenerationSettings(environment, { platform, arch });
|
|
259
|
+
const install = await checkStableAudioInstall(settings.path, { model: settings.model, platform, arch });
|
|
201
260
|
return publish({
|
|
202
261
|
state: !settings.enabled ? "off" : install.state,
|
|
203
262
|
enabled: settings.enabled,
|
|
@@ -205,6 +264,8 @@ export function createLocalGenerator({
|
|
|
205
264
|
...(install.missing ? { missing: install.missing } : {}),
|
|
206
265
|
path: settings.path,
|
|
207
266
|
source: settings.source,
|
|
267
|
+
model: settings.model,
|
|
268
|
+
managed: settings.source === "managed",
|
|
208
269
|
});
|
|
209
270
|
} finally {
|
|
210
271
|
checking = null;
|
|
@@ -233,13 +294,13 @@ export function createLocalGenerator({
|
|
|
233
294
|
};
|
|
234
295
|
|
|
235
296
|
const stopped = () => Object.assign(new Error("Making the track was stopped."), { name: "AbortError", code: "generation_stopped" });
|
|
236
|
-
const run = ({ root, prompt, seconds, seed, out }, { signal, onProgress }) => new Promise((resolve, reject) => {
|
|
297
|
+
const run = ({ root, model, prompt, seconds, seed, out }, { signal, onProgress }) => new Promise((resolve, reject) => {
|
|
237
298
|
// A stop that came while the install was checked starts nothing.
|
|
238
299
|
if (signal?.aborted) return reject(stopped());
|
|
239
300
|
// The Python script itself, not Stability's ./sa3 wrapper: the wrapper wants uv on PATH and a
|
|
240
301
|
// terminal, and its exec would leave a shell between this process and the one to stop.
|
|
241
302
|
const child = spawnImpl(path.join(root, ".venv", "bin", "python"), [
|
|
242
|
-
"scripts/sa3_mlx.py", "--prompt", prompt, "--dit",
|
|
303
|
+
"scripts/sa3_mlx.py", "--prompt", prompt, "--dit", STABLE_AUDIO_MODELS[model].dit, "--decoder", STABLE_AUDIO_MODELS[model].decoder,
|
|
243
304
|
"--seconds", String(seconds), "--seed", String(seed), "--steps", "8", "--out", out,
|
|
244
305
|
], {
|
|
245
306
|
cwd: root,
|
|
@@ -414,7 +475,7 @@ export function createLocalGenerator({
|
|
|
414
475
|
busy: () => running !== null,
|
|
415
476
|
check,
|
|
416
477
|
async save(settings) {
|
|
417
|
-
await writeGenerationSettings(settings, environment);
|
|
478
|
+
await writeGenerationSettings(settings, environment, { platform, arch });
|
|
418
479
|
return check();
|
|
419
480
|
},
|
|
420
481
|
subscribe(listener) {
|
|
@@ -469,10 +530,13 @@ export function createLocalGenerator({
|
|
|
469
530
|
await mkdir(temporary, { mode: 0o700 });
|
|
470
531
|
const wav = path.join(temporary, "track.wav");
|
|
471
532
|
onProgress?.({ stage: "starting" });
|
|
472
|
-
const
|
|
533
|
+
const model = isStableAudioModel(status.model) ? status.model : DEFAULT_STABLE_AUDIO_MODEL;
|
|
534
|
+
const measured = await run({ root: status.path, model, prompt, seconds, seed, out: wav }, { signal, onProgress });
|
|
473
535
|
const audio = await wavInfo(wav).catch(() => null);
|
|
474
536
|
if (!audio || audio.duration_seconds <= 0) throw generationError("generation_failed", "Stable Audio 3 finished but wrote no playable track.");
|
|
475
537
|
await chmod(wav, 0o600);
|
|
538
|
+
// How loud it runs from start to end, which its cover draws (cover.mjs); kept after a stream drops the audio.
|
|
539
|
+
const loudness = await readLoudness(wav).catch(() => null);
|
|
476
540
|
const elapsed = Date.now() - started;
|
|
477
541
|
const document = {
|
|
478
542
|
receipt_version: "moondog-generated-track/1",
|
|
@@ -482,11 +546,11 @@ export function createLocalGenerator({
|
|
|
482
546
|
title,
|
|
483
547
|
...receipt,
|
|
484
548
|
generator: {
|
|
485
|
-
adapter: "stable-audio-3", adapter_version: 1, model:
|
|
549
|
+
adapter: "stable-audio-3", adapter_version: 1, model: STABLE_AUDIO_MODELS[model].receipt, decoder: STABLE_AUDIO_MODELS[model].decoder,
|
|
486
550
|
prompt, seconds, seed, steps: 8, cfg: 1, elapsed_ms: elapsed, ...(measured ? { measured } : {}),
|
|
487
551
|
license: "Stability AI Community License; outputs belong to the listener",
|
|
488
552
|
},
|
|
489
|
-
audio: { file: "track.wav", ...audio },
|
|
553
|
+
audio: { file: "track.wav", ...audio, ...(loudness ? { loudness } : {}) },
|
|
490
554
|
};
|
|
491
555
|
await writeFile(path.join(temporary, "receipt.json"), `${JSON.stringify(document, null, 2)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
492
556
|
await rename(temporary, path.join(folder, id));
|
|
@@ -509,6 +573,8 @@ export function createLocalGenerator({
|
|
|
509
573
|
// background. Stopping the player, other music, or the time limit ends it. One stream per room, on
|
|
510
574
|
// the room's one generator; only its newest tracks keep their audio, and every receipt stays.
|
|
511
575
|
const STREAM_SKIP_MS = 30_000;
|
|
576
|
+
// How long a later track waits for its own name before it takes a numbered one.
|
|
577
|
+
const NAMING_TIMEOUT_MS = 10_000;
|
|
512
578
|
export function createAtmosphereStream({ generator, player, now = Date.now, keepAudio = 3 }) {
|
|
513
579
|
let stream = null;
|
|
514
580
|
// What the surfaces see (view()) and hear (subscribe): the phase, and the end with its reason.
|
|
@@ -562,9 +628,30 @@ export function createAtmosphereStream({ generator, player, now = Date.now, keep
|
|
|
562
628
|
};
|
|
563
629
|
const trackFor = (ending, position) => {
|
|
564
630
|
const variation = streamVariation(ending.compiled, position, ending.leaveOut);
|
|
565
|
-
|
|
631
|
+
// The first track carries the stream's own name; a later one is named as it is made (nameFor), or numbered.
|
|
632
|
+
return { variation, prompt: stableAudioPrompt(variation), title: position === 0 ? ending.title : `${ending.title} · ${position + 1}`,
|
|
566
633
|
receipt: { ...ending.receipt, shaped_by: variation.descriptors, ...(variation.bpm ? { bpm: variation.bpm } : {}),
|
|
567
|
-
stream: { started_at: new Date(ending.startedAt).toISOString(), position: position + 1, featured: variation.featured } } };
|
|
634
|
+
stream: { name: ending.title, started_at: new Date(ending.startedAt).toISOString(), position: position + 1, featured: variation.featured } } };
|
|
635
|
+
};
|
|
636
|
+
// A name of its own for a later track, asked of the stream's namer within a few seconds. One that
|
|
637
|
+
// fails, is slow, or repeats a name this stream gave keeps the numbered name.
|
|
638
|
+
const nameFor = async (ending, variation, numbered) => {
|
|
639
|
+
if (typeof ending.nameTrack !== "function") return numbered;
|
|
640
|
+
const timeout = AbortSignal.timeout(NAMING_TIMEOUT_MS);
|
|
641
|
+
try {
|
|
642
|
+
const name = await Promise.race([
|
|
643
|
+
ending.nameTrack({
|
|
644
|
+
stream: ending.title, featured: variation.featured,
|
|
645
|
+
sound: variation.descriptors.map((descriptor) => descriptor.term).filter((term) => !variation.featured.includes(term)),
|
|
646
|
+
...(variation.bpm ? { bpm: variation.bpm } : {}), earlier: [...ending.names],
|
|
647
|
+
}, { signal: AbortSignal.any([timeout, ending.controller.signal]) }),
|
|
648
|
+
new Promise((resolve) => { timeout.addEventListener("abort", () => resolve(null), { once: true }); }),
|
|
649
|
+
]);
|
|
650
|
+
const text = typeof name === "string" ? name.trim() : "";
|
|
651
|
+
return text && !ending.names.some((earlier) => earlier.toLocaleLowerCase() === text.toLocaleLowerCase()) ? text : numbered;
|
|
652
|
+
} catch {
|
|
653
|
+
return numbered;
|
|
654
|
+
}
|
|
568
655
|
};
|
|
569
656
|
// The tracks that started playing keep their audio, the newest few; the rest keep their receipts.
|
|
570
657
|
const prune = (current) => {
|
|
@@ -608,7 +695,10 @@ export function createAtmosphereStream({ generator, player, now = Date.now, keep
|
|
|
608
695
|
current.making = true;
|
|
609
696
|
changed();
|
|
610
697
|
try {
|
|
611
|
-
const { prompt, title, receipt, variation } = trackFor(current, current.made.length);
|
|
698
|
+
const { prompt, title: numbered, receipt, variation } = trackFor(current, current.made.length);
|
|
699
|
+
const title = await nameFor(current, variation, numbered);
|
|
700
|
+
if (stream !== current) return;
|
|
701
|
+
current.names.push(title);
|
|
612
702
|
const made = await generator.generate({ prompt, seconds: current.seconds, title, receipt }, { signal: current.controller.signal });
|
|
613
703
|
if (stream !== current) return;
|
|
614
704
|
const file = await generator.trackFile(made.id);
|
|
@@ -697,10 +787,10 @@ export function createAtmosphereStream({ generator, player, now = Date.now, keep
|
|
|
697
787
|
* play. Returns the first track. A stream already running gives way to the new one, and its track
|
|
698
788
|
* plays on until the first new one takes over, so nothing goes silent while that is made.
|
|
699
789
|
*/
|
|
700
|
-
async start({ compiled, title, seconds, minutes, receipt = {} }, { signal, onProgress, onTrack, beforePlay } = {}) {
|
|
790
|
+
async start({ compiled, title, seconds, minutes, receipt = {} }, { signal, onProgress, onTrack, beforePlay, nameTrack } = {}) {
|
|
701
791
|
end("replaced");
|
|
702
792
|
const current = { compiled, title, seconds, receipt, onTrack, startedAt: now(), endsAt: now() + minutes * 60_000,
|
|
703
|
-
made: [], leaveOut: new Set(), current: null, next: null, waiting: false, making: true, controller: new AbortController() };
|
|
793
|
+
made: [], names: [title], nameTrack, leaveOut: new Set(), current: null, next: null, waiting: false, making: true, controller: new AbortController() };
|
|
704
794
|
stream = current;
|
|
705
795
|
changed();
|
|
706
796
|
const stopped = () => Object.assign(new Error("The stream was stopped before it began."), { code: "generation_stopped" });
|