@audiofool/moondog 0.2.2 → 0.3.0-beta.2
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/NOTICE +12 -0
- package/README.md +3 -2
- package/licenses/stability-ai-community-license.md +58 -0
- package/package.json +3 -1
- package/scripts/moondog.mjs +118 -3
- package/src/core/apple-projection-domain-services.mjs +14 -0
- package/src/core/capability-catalog.mjs +55 -0
- package/src/core/listening-profile-domain-services.mjs +12 -0
- package/src/core/moondog-application.mjs +244 -11
- package/src/i18n/messages/de.mjs +3 -0
- package/src/i18n/messages/en.mjs +4 -0
- package/src/i18n/messages/es.mjs +3 -0
- package/src/i18n/messages/fr.mjs +3 -0
- package/src/i18n/messages/it.mjs +3 -0
- package/src/i18n/messages/ja.mjs +3 -0
- package/src/i18n/messages/ko.mjs +3 -0
- package/src/i18n/messages/pt.mjs +3 -0
- package/src/i18n/messages/zh-Hant.mjs +3 -0
- package/src/i18n/messages/zh.mjs +3 -0
- package/src/i18n/screen/es.mjs +33 -0
- package/src/i18n/screen/ja.mjs +33 -0
- package/src/i18n/screen/pt.mjs +33 -0
- package/src/i18n/screen/zh.mjs +33 -0
- package/src/integrations/apple-music/terminal-preview-player.mjs +40 -11
- package/src/integrations/netease/ncm-cli-player.mjs +12 -6
- package/src/integrations/stable-audio/local-generator.mjs +758 -0
- package/src/profile/moondog-suggestions.mjs +9 -0
- package/src/profile/taste-compiler.mjs +197 -0
- package/src/runtime/pi/agent-runtime.mjs +278 -9
- package/src/surfaces/cli/command-completions.mjs +1 -0
- package/src/surfaces/cli/format-output.mjs +1 -0
- package/src/surfaces/cli/tui.mjs +138 -17
package/src/i18n/screen/zh.mjs
CHANGED
|
@@ -1027,4 +1027,37 @@ export default {
|
|
|
1027
1027
|
"Skipped. /service and /model are here when you want them.": "已跳过。需要时随时可以用 /service 和 /model。",
|
|
1028
1028
|
"Saving your music service...": "正在保存音乐服务...",
|
|
1029
1029
|
"Choose where Moondog plays music": "选择 Moondog 在哪里放音乐",
|
|
1030
|
+
"Make music on this Mac with Stable Audio 3": "用 Stable Audio 3 在这台 Mac 上做音乐",
|
|
1031
|
+
"Stop making music here": "不再在这里做音乐",
|
|
1032
|
+
"Making music on this Mac": "在这台 Mac 上做音乐",
|
|
1033
|
+
"Made by Moondog": "Moondog 制作",
|
|
1034
|
+
"Stable Audio 3 needs a Mac with Apple silicon, so Moondog can't make music here.": "Stable Audio 3 需要搭载 Apple 芯片的 Mac,所以 Moondog 没法在这里做音乐。",
|
|
1035
|
+
"Moondog makes music on this Mac with Stable Audio 3, powered by Stability AI. Ask for a track. Stable Audio 3 runs offline, and the track stays on this Mac.": "Moondog 用这台 Mac 上的 Stable Audio 3 做音乐,Powered by Stability AI。想听就让它做一首。Stable Audio 3 离线运行,做出的曲子留在这台 Mac 上。",
|
|
1036
|
+
"Making music on this Mac is off. Stable Audio 3 is ready at {folder}; turn it on with /generation on.": "在这台 Mac 上做音乐已关闭。Stable Audio 3 已在 {folder} 准备好,用 /generation on 开启。",
|
|
1037
|
+
"Making music on this Mac is off, and Stable Audio 3 isn't at {folder}. Install Stability AI's MLX build from github.com/Stability-AI/stable-audio-3, then use /generation on with its folder.": "在这台 Mac 上做音乐已关闭,{folder} 里也没有 Stable Audio 3。先从 github.com/Stability-AI/stable-audio-3 安装 Stability AI 的 MLX 版本,再用 /generation on 加上它的文件夹。",
|
|
1038
|
+
"Making music on this Mac is off, and Stable Audio 3 at {folder} isn't fully installed. Run ./install.sh --download medium there, then use /generation on.": "在这台 Mac 上做音乐已关闭,{folder} 里的 Stable Audio 3 也还没装完整。在那里运行 ./install.sh --download medium,然后用 /generation on。",
|
|
1039
|
+
"Stable Audio 3 at {folder} doesn't have the Medium model yet. Run ./install.sh --download medium there, then use /generation again.": "{folder} 里的 Stable Audio 3 还没有 Medium 模型。在那里运行 ./install.sh --download medium,然后再用一次 /generation。",
|
|
1040
|
+
"Stable Audio 3 at {folder} isn't installed yet. Run ./install.sh --download medium there, then use /generation again.": "{folder} 里的 Stable Audio 3 还没装好。在那里运行 ./install.sh --download medium,然后再用一次 /generation。",
|
|
1041
|
+
"Stable Audio 3 isn't at {folder}. Use /generation on with the folder that holds its sa3 script.": "{folder} 里没有 Stable Audio 3。用 /generation on 加上包含 sa3 脚本的文件夹。",
|
|
1042
|
+
"Use /generation, /generation on [folder], or /generation off.": "用 /generation、/generation on [文件夹] 或 /generation off。",
|
|
1043
|
+
"Music making on.": "已开启音乐生成。",
|
|
1044
|
+
"Music making off.": "已关闭音乐生成。",
|
|
1045
|
+
"Music making isn't ready.": "音乐生成还没准备好。",
|
|
1046
|
+
"warming up": "热身中",
|
|
1047
|
+
"reading your taste": "读你的口味",
|
|
1048
|
+
"composing": "作曲中",
|
|
1049
|
+
"pressing the record": "压制唱片",
|
|
1050
|
+
"saving it": "保存中",
|
|
1051
|
+
"Let Moondog make music on this Mac": "让 Moondog 在这台 Mac 上做音乐",
|
|
1052
|
+
"Make a track": "做一首曲子",
|
|
1053
|
+
"Play a track Moondog made": "再放一首 Moondog 做的曲子",
|
|
1054
|
+
"Start a stream": "开始音乐流",
|
|
1055
|
+
"Stop the stream": "停止音乐流",
|
|
1056
|
+
"Go on air: Moondog keeps making music from your taste": "开播:Moondog 照着你的口味一直做音乐",
|
|
1057
|
+
"on air until {time}": "播出到 {time}",
|
|
1058
|
+
"Cutting the next record": "正在刻下一张",
|
|
1059
|
+
"Off air: the next record couldn't be made.": "停播了:下一张没刻成。",
|
|
1060
|
+
"The next record is still being cut.": "下一张还在刻。",
|
|
1061
|
+
"Off air.": "已停播。",
|
|
1062
|
+
"Turn on music making first: /generation on.": "先开启音乐生成:/generation on。",
|
|
1030
1063
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Plays the agent's Apple Music previews from the terminal. Each 30-second preview is
|
|
2
2
|
// downloaded to a temporary file and played by the system's own player, one after another.
|
|
3
|
+
// A track Moondog made on this Mac plays the same way, from its own folder.
|
|
3
4
|
import { spawn } from "node:child_process";
|
|
4
5
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
5
6
|
import { access, constants, rm, stat, writeFile } from "node:fs/promises";
|
|
@@ -102,22 +103,24 @@ export async function createTerminalPreviewPlayer({
|
|
|
102
103
|
return file;
|
|
103
104
|
};
|
|
104
105
|
|
|
106
|
+
// Resolves "ended" when the song played to its end, "stopped" when /skip or /stop ended it, and
|
|
107
|
+
// "failed" when the player could not play it (no audio device, an unreadable file).
|
|
105
108
|
const playFile = (file) => new Promise((resolve) => {
|
|
106
109
|
const playing = spawn(player.command, [...player.args, file], { stdio: "ignore" });
|
|
107
110
|
child = playing;
|
|
108
|
-
const finish = () => {
|
|
111
|
+
const finish = (how) => {
|
|
109
112
|
if (child === playing) child = null;
|
|
110
|
-
resolve();
|
|
113
|
+
resolve(how);
|
|
111
114
|
};
|
|
112
|
-
playing.once("error", finish);
|
|
113
|
-
playing.once("exit", finish);
|
|
115
|
+
playing.once("error", () => finish("failed"));
|
|
116
|
+
playing.once("exit", (code, signal) => finish(code === 0 ? "ended" : signal ? "stopped" : "failed"));
|
|
114
117
|
});
|
|
115
118
|
|
|
116
119
|
const start = (tracks) => {
|
|
117
120
|
const controller = new AbortController();
|
|
118
121
|
const downloads = new Map();
|
|
119
122
|
// skip() ends the song playing, or the download of the song about to play.
|
|
120
|
-
const current = { controller, skipDownload: null };
|
|
123
|
+
const current = { controller, skipDownload: null, tracks };
|
|
121
124
|
queue = current;
|
|
122
125
|
const fetchFile = (index) => {
|
|
123
126
|
if (!downloads.has(index)) {
|
|
@@ -133,24 +136,32 @@ export async function createTerminalPreviewPlayer({
|
|
|
133
136
|
void (async () => {
|
|
134
137
|
try {
|
|
135
138
|
for (let index = 0; index < tracks.length; index += 1) {
|
|
136
|
-
const { title, artist_credit, catalog_url } = tracks[index];
|
|
137
|
-
const song = { track: { title, artist_credit, catalog_url }, position: index + 1, count: tracks.length };
|
|
138
|
-
|
|
139
|
+
const { title, artist_credit, catalog_url, generated, id } = tracks[index];
|
|
140
|
+
const song = { track: generated ? { title, artist_credit, generated: true, id } : { title, artist_credit, catalog_url }, position: index + 1, count: tracks.length };
|
|
141
|
+
// A track Moondog made plays from its own folder, which outlives the queue.
|
|
142
|
+
const pending = tracks[index].file
|
|
143
|
+
? { ready: true, file: Promise.resolve(tracks[index].file), abort: () => {}, skipped: () => false }
|
|
144
|
+
: fetchFile(index);
|
|
139
145
|
if (!pending.ready) emit({ type: "loading", ...song });
|
|
140
146
|
current.skipDownload = pending.abort;
|
|
141
147
|
const file = await pending.file;
|
|
142
148
|
current.skipDownload = null;
|
|
143
149
|
if (controller.signal.aborted) break;
|
|
144
150
|
// The next preview downloads while this one plays.
|
|
145
|
-
if (index + 1 < tracks.length) void fetchFile(index + 1);
|
|
151
|
+
if (index + 1 < tracks.length && !tracks[index + 1].file) void fetchFile(index + 1);
|
|
146
152
|
if (!file) {
|
|
147
153
|
// An error, a timeout, or an oversized file; a listener's /skip is not a failure.
|
|
148
154
|
if (!pending.skipped()) emit({ type: "failed", ...song });
|
|
149
155
|
continue;
|
|
150
156
|
}
|
|
151
157
|
emit({ type: "playing", ...song });
|
|
152
|
-
|
|
153
|
-
|
|
158
|
+
// A listener may stop the queue as the song starts, as Moondog's stream does past its time limit.
|
|
159
|
+
if (controller.signal.aborted) break;
|
|
160
|
+
const how = await playFile(file);
|
|
161
|
+
// A made track that played to its end, that /skip ended, or that could not play, as the loop
|
|
162
|
+
// back from listening and Moondog's stream count it; /stop aborts the queue first.
|
|
163
|
+
if (generated && !controller.signal.aborted) emit({ type: { ended: "finished", stopped: "skipped", failed: "failed" }[how], ...song });
|
|
164
|
+
if (!tracks[index].file) await rm(file, { force: true });
|
|
154
165
|
if (controller.signal.aborted) break;
|
|
155
166
|
}
|
|
156
167
|
} finally {
|
|
@@ -197,6 +208,22 @@ export async function createTerminalPreviewPlayer({
|
|
|
197
208
|
});
|
|
198
209
|
start(accepted);
|
|
199
210
|
},
|
|
211
|
+
// A track Moondog made on this Mac: { id, title, file }, played whole and kept after it ends.
|
|
212
|
+
playGenerated({ id, title, file } = {}) {
|
|
213
|
+
const name = String(title ?? "").trim();
|
|
214
|
+
if (closed || !name || typeof file !== "string" || !path.isAbsolute(file)) return;
|
|
215
|
+
stop();
|
|
216
|
+
const track = { title: name, artist_credit: "", generated: true, id: String(id ?? "") };
|
|
217
|
+
emit({ type: "queue", tracks: [{ title: track.title, artist_credit: "", generated: true }], album: null });
|
|
218
|
+
start([{ ...track, file }]);
|
|
219
|
+
},
|
|
220
|
+
// A made track queued behind the one playing, as Moondog's stream does; with nothing playing, it plays now.
|
|
221
|
+
enqueueGenerated({ id, title, file } = {}) {
|
|
222
|
+
const name = String(title ?? "").trim();
|
|
223
|
+
if (closed || !name || typeof file !== "string" || !path.isAbsolute(file)) return;
|
|
224
|
+
if (!queue || !queue.tracks.every((track) => track.generated)) return this.playGenerated({ id, title, file });
|
|
225
|
+
queue.tracks.push({ title: name, artist_credit: "", generated: true, id: String(id ?? ""), file });
|
|
226
|
+
},
|
|
200
227
|
// Ends the song now playing, or still loading; the next one starts after it.
|
|
201
228
|
skip() {
|
|
202
229
|
if (!queue) return false;
|
|
@@ -205,6 +232,8 @@ export async function createTerminalPreviewPlayer({
|
|
|
205
232
|
return true;
|
|
206
233
|
},
|
|
207
234
|
stop,
|
|
235
|
+
// Leaving Apple Music stops its previews here, never the tracks Moondog made.
|
|
236
|
+
holdsCatalogSongs: () => Boolean(queue?.tracks.some((track) => !track.generated)),
|
|
208
237
|
subscribe(listener) {
|
|
209
238
|
listeners.add(listener);
|
|
210
239
|
return () => listeners.delete(listener);
|
|
@@ -245,9 +245,10 @@ export function createNeteasePlayer({
|
|
|
245
245
|
* up. mode "play" replaces the queue and starts the first; "queue" adds after what is
|
|
246
246
|
* playing. Songs NetEase marks as not playable are never sent to the player, nor songs
|
|
247
247
|
* allow(song) refuses, such as the listener's Avoids; those are only counted. Before a lookup,
|
|
248
|
-
* allow gets the title and artist asked for, as { title, artist_credit }.
|
|
248
|
+
* allow gets the title and artist asked for, as { title, artist_credit }. beforePlay runs just
|
|
249
|
+
* before a song starts, which a queue with nothing loaded does too.
|
|
249
250
|
*/
|
|
250
|
-
async play({ items = [], mode = "play", intent, allow = () => true } = {}) {
|
|
251
|
+
async play({ items = [], mode = "play", intent, allow = () => true, beforePlay } = {}) {
|
|
251
252
|
const found = [];
|
|
252
253
|
const unplayable = [];
|
|
253
254
|
const notFound = [];
|
|
@@ -278,6 +279,7 @@ export function createNeteasePlayer({
|
|
|
278
279
|
queue = [...queue, ...songs];
|
|
279
280
|
outcome = "queued";
|
|
280
281
|
} else {
|
|
282
|
+
await beforePlay?.();
|
|
281
283
|
queue = songs;
|
|
282
284
|
await playAt(0);
|
|
283
285
|
outcome = "playing";
|
|
@@ -297,15 +299,19 @@ export function createNeteasePlayer({
|
|
|
297
299
|
checked(await run(["resume"]), "resume");
|
|
298
300
|
stayConnected();
|
|
299
301
|
},
|
|
300
|
-
|
|
302
|
+
// beforePlay runs just before a song starts, so the other players go quiet only for one.
|
|
303
|
+
async next({ beforePlay } = {}) {
|
|
301
304
|
if (index + 1 >= queue.length) throw failure("netease_failed", "Nothing is queued after this song.");
|
|
305
|
+
await beforePlay?.();
|
|
302
306
|
await playAt(index + 1);
|
|
303
307
|
},
|
|
304
308
|
// Back to the start of the song, or to the one before when it has only just begun.
|
|
305
|
-
async previous() {
|
|
309
|
+
async previous({ beforePlay } = {}) {
|
|
306
310
|
const now = await state();
|
|
307
|
-
if (index > 0 && (Number(now.position) || 0) <= 3)
|
|
308
|
-
|
|
311
|
+
if (index > 0 && (Number(now.position) || 0) <= 3) {
|
|
312
|
+
await beforePlay?.();
|
|
313
|
+
await playAt(index - 1);
|
|
314
|
+
} else checked(await run(["seek", "0"]), "go back to the start");
|
|
309
315
|
},
|
|
310
316
|
async seek({ positionMs = 0 } = {}) {
|
|
311
317
|
checked(await run(["seek", String(Math.max(0, Math.round(positionMs / 1000)))]), "move in the song");
|