@audiofool/moondog 0.3.0-beta.6 → 0.3.0-beta.8
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 +2 -1
- package/package.json +1 -1
- package/scripts/moondog.mjs +15 -5
- package/src/core/capability-catalog.mjs +38 -0
- package/src/core/library-store.mjs +500 -0
- package/src/core/listening-now.mjs +7 -2
- package/src/core/moondog-application.mjs +379 -10
- package/src/i18n/index.mjs +6 -3
- package/src/i18n/preferences.mjs +24 -0
- package/src/i18n/screen/es.mjs +18 -0
- package/src/i18n/screen/ja.mjs +18 -0
- package/src/i18n/screen/pt.mjs +18 -0
- package/src/i18n/screen/zh.mjs +18 -0
- package/src/integrations/apple-music/music-app.mjs +9 -3
- package/src/integrations/apple-music/terminal-preview-player.mjs +22 -2
- package/src/integrations/spotify/service.mjs +2 -1
- package/src/integrations/stable-audio/local-generator.mjs +8 -1
- package/src/runtime/pi/agent-runtime.mjs +250 -20
- package/src/runtime/pi/model-switch.mjs +26 -0
- package/src/runtime/pi/model-transport.mjs +34 -4
- package/src/runtime/pi/reply-language.mjs +45 -0
- package/src/runtime/pi/tool-arguments.mjs +89 -0
- package/src/surfaces/cli/tui.mjs +86 -1
package/src/i18n/screen/es.mjs
CHANGED
|
@@ -874,6 +874,7 @@ export default {
|
|
|
874
874
|
"See what you're listening to": "Ver lo que escuchas",
|
|
875
875
|
"Control the Music app": "Controlar la app Música",
|
|
876
876
|
"Choose a music service": "Elegir un servicio de música",
|
|
877
|
+
"Read or change Moondog's settings": "Ver o cambiar los ajustes de Moondog",
|
|
877
878
|
"Search outside your library": "Buscar fuera de tu biblioteca",
|
|
878
879
|
"Find music nearby": "Buscar música cercana",
|
|
879
880
|
"Remember what you've told me": "Recordar lo que me has dicho",
|
|
@@ -1118,4 +1119,21 @@ export default {
|
|
|
1118
1119
|
"downloading the model": "descargando el modelo",
|
|
1119
1120
|
"checking the files": "comprobando los archivos",
|
|
1120
1121
|
"Stable Audio 3's Python environment couldn't be set up. Check your connection, then use /generation download to try again.": "No se pudo preparar el entorno de Python de Stable Audio 3. Revisa tu conexión y usa /generation download para intentarlo de nuevo.",
|
|
1122
|
+
"The library isn't available here.": "La biblioteca no está disponible aquí.",
|
|
1123
|
+
"Saved": "Guardadas",
|
|
1124
|
+
"Play one with `/library play <name>`, or keep the song playing now with `/library save`.": "Reproduce una con `/library play <nombre>` o guarda lo que suena con `/library save`.",
|
|
1125
|
+
"No playlist is called “{name}”.": "No hay ninguna lista llamada “{name}”.",
|
|
1126
|
+
"{name} has nothing in it yet.": "{name} aún no tiene nada.",
|
|
1127
|
+
"None of its songs could be found.": "No se encontró ninguna de sus canciones.",
|
|
1128
|
+
"Playing {name}.": "Reproduciendo {name}.",
|
|
1129
|
+
"Moondog's tracks were left out. They play with Apple Music chosen.": "Las pistas de Moondog se quedaron fuera. Suenan con Apple Music elegido.",
|
|
1130
|
+
"Some songs couldn't be found.": "No se encontraron algunas canciones.",
|
|
1131
|
+
"Already in {name}.": "Ya está en {name}.",
|
|
1132
|
+
"Saved.": "Guardada.",
|
|
1133
|
+
"Saved to {name}.": "Guardada en {name}.",
|
|
1134
|
+
"Use /library, /library play <name>, or /library save [playlist].": "Usa /library, /library play <nombre> o /library save [lista].",
|
|
1135
|
+
"Your saved songs and playlists": "Tus canciones guardadas y tus listas",
|
|
1136
|
+
"Look in the library": "Mirar en la biblioteca",
|
|
1137
|
+
"Save to the library": "Guardar en la biblioteca",
|
|
1138
|
+
"Play from the library": "Reproducir de la biblioteca",
|
|
1121
1139
|
};
|
package/src/i18n/screen/ja.mjs
CHANGED
|
@@ -874,6 +874,7 @@ export default {
|
|
|
874
874
|
"See what you're listening to": "聴いている曲を確認",
|
|
875
875
|
"Control the Music app": "ミュージックAppを操作",
|
|
876
876
|
"Choose a music service": "音楽サービスを選ぶ",
|
|
877
|
+
"Read or change Moondog's settings": "Moondog の設定を確認・変更",
|
|
877
878
|
"Search outside your library": "ライブラリの外を探す",
|
|
878
879
|
"Find music nearby": "近い音楽を探す",
|
|
879
880
|
"Remember what you've told me": "教えてもらったことを覚える",
|
|
@@ -1118,4 +1119,21 @@ export default {
|
|
|
1118
1119
|
"downloading the model": "モデルをダウンロード中",
|
|
1119
1120
|
"checking the files": "ファイルを確認中",
|
|
1120
1121
|
"Stable Audio 3's Python environment couldn't be set up. Check your connection, then use /generation download to try again.": "Stable Audio 3 の Python 環境を準備できませんでした。接続を確かめてから、/generation download でやり直してください。",
|
|
1122
|
+
"The library isn't available here.": "ここではライブラリを使えません。",
|
|
1123
|
+
"Saved": "保存済み",
|
|
1124
|
+
"Play one with `/library play <name>`, or keep the song playing now with `/library save`.": "`/library play <名前>` で再生するか、`/library save` で再生中の曲を保存してください。",
|
|
1125
|
+
"No playlist is called “{name}”.": "「{name}」というプレイリストはありません。",
|
|
1126
|
+
"{name} has nothing in it yet.": "{name} にはまだ何もありません。",
|
|
1127
|
+
"None of its songs could be found.": "曲がひとつも見つかりませんでした。",
|
|
1128
|
+
"Playing {name}.": "{name} を再生しています。",
|
|
1129
|
+
"Moondog's tracks were left out. They play with Apple Music chosen.": "Moondog の曲は再生されませんでした。Apple Music を選ぶと再生されます。",
|
|
1130
|
+
"Some songs couldn't be found.": "見つからない曲がありました。",
|
|
1131
|
+
"Already in {name}.": "すでに「{name}」にあります。",
|
|
1132
|
+
"Saved.": "保存しました。",
|
|
1133
|
+
"Saved to {name}.": "「{name}」に保存しました。",
|
|
1134
|
+
"Use /library, /library play <name>, or /library save [playlist].": "/library、/library play <名前>、または /library save [プレイリスト] を使ってください。",
|
|
1135
|
+
"Your saved songs and playlists": "保存した曲とプレイリスト",
|
|
1136
|
+
"Look in the library": "ライブラリを見る",
|
|
1137
|
+
"Save to the library": "ライブラリに保存",
|
|
1138
|
+
"Play from the library": "ライブラリから再生",
|
|
1121
1139
|
};
|
package/src/i18n/screen/pt.mjs
CHANGED
|
@@ -874,6 +874,7 @@ export default {
|
|
|
874
874
|
"See what you're listening to": "Ver o que você está ouvindo",
|
|
875
875
|
"Control the Music app": "Controlar o app Música",
|
|
876
876
|
"Choose a music service": "Escolher um serviço de música",
|
|
877
|
+
"Read or change Moondog's settings": "Ver ou mudar as configurações do Moondog",
|
|
877
878
|
"Search outside your library": "Buscar fora da sua biblioteca",
|
|
878
879
|
"Find music nearby": "Encontrar música parecida",
|
|
879
880
|
"Remember what you've told me": "Lembrar o que você me disse",
|
|
@@ -1118,4 +1119,21 @@ export default {
|
|
|
1118
1119
|
"downloading the model": "baixando o modelo",
|
|
1119
1120
|
"checking the files": "conferindo os arquivos",
|
|
1120
1121
|
"Stable Audio 3's Python environment couldn't be set up. Check your connection, then use /generation download to try again.": "Não foi possível preparar o ambiente Python do Stable Audio 3. Verifique sua conexão e use /generation download para tentar de novo.",
|
|
1122
|
+
"The library isn't available here.": "A biblioteca não está disponível aqui.",
|
|
1123
|
+
"Saved": "Salvas",
|
|
1124
|
+
"Play one with `/library play <name>`, or keep the song playing now with `/library save`.": "Toque uma com `/library play <nome>` ou salve o que está tocando com `/library save`.",
|
|
1125
|
+
"No playlist is called “{name}”.": "Nenhuma playlist se chama “{name}”.",
|
|
1126
|
+
"{name} has nothing in it yet.": "{name} ainda não tem nada.",
|
|
1127
|
+
"None of its songs could be found.": "Nenhuma das músicas foi encontrada.",
|
|
1128
|
+
"Playing {name}.": "Tocando {name}.",
|
|
1129
|
+
"Moondog's tracks were left out. They play with Apple Music chosen.": "As faixas do Moondog ficaram de fora. Elas tocam com o Apple Music escolhido.",
|
|
1130
|
+
"Some songs couldn't be found.": "Algumas músicas não foram encontradas.",
|
|
1131
|
+
"Already in {name}.": "Já está em {name}.",
|
|
1132
|
+
"Saved.": "Salva.",
|
|
1133
|
+
"Saved to {name}.": "Salva em {name}.",
|
|
1134
|
+
"Use /library, /library play <name>, or /library save [playlist].": "Use /library, /library play <nome> ou /library save [playlist].",
|
|
1135
|
+
"Your saved songs and playlists": "Suas músicas salvas e playlists",
|
|
1136
|
+
"Look in the library": "Olhar na biblioteca",
|
|
1137
|
+
"Save to the library": "Salvar na biblioteca",
|
|
1138
|
+
"Play from the library": "Tocar da biblioteca",
|
|
1121
1139
|
};
|
package/src/i18n/screen/zh.mjs
CHANGED
|
@@ -874,6 +874,7 @@ export default {
|
|
|
874
874
|
"See what you're listening to": "查看你在听什么",
|
|
875
875
|
"Control the Music app": "控制“音乐”App",
|
|
876
876
|
"Choose a music service": "选择音乐服务",
|
|
877
|
+
"Read or change Moondog's settings": "读取或更改 Moondog 的设置",
|
|
877
878
|
"Search outside your library": "在你的曲库之外搜索",
|
|
878
879
|
"Find music nearby": "找附近的音乐",
|
|
879
880
|
"Remember what you've told me": "记住你告诉我的",
|
|
@@ -1118,4 +1119,21 @@ export default {
|
|
|
1118
1119
|
"downloading the model": "正在下载模型",
|
|
1119
1120
|
"checking the files": "正在校验文件",
|
|
1120
1121
|
"Stable Audio 3's Python environment couldn't be set up. Check your connection, then use /generation download to try again.": "没能准备好 Stable Audio 3 的 Python 环境。检查网络连接,再用 /generation download 重试。",
|
|
1122
|
+
"The library isn't available here.": "这里用不了资料库。",
|
|
1123
|
+
"Saved": "收藏",
|
|
1124
|
+
"Play one with `/library play <name>`, or keep the song playing now with `/library save`.": "用 `/library play <名字>` 播放一个,或者用 `/library save` 收藏正在播放的歌。",
|
|
1125
|
+
"No playlist is called “{name}”.": "没有叫“{name}”的播放列表。",
|
|
1126
|
+
"{name} has nothing in it yet.": "{name} 里还是空的。",
|
|
1127
|
+
"None of its songs could be found.": "里面的歌都没有找到。",
|
|
1128
|
+
"Playing {name}.": "正在播放 {name}。",
|
|
1129
|
+
"Moondog's tracks were left out. They play with Apple Music chosen.": "Moondog 做的曲子没有放。选择 Apple Music 时才会播放它们。",
|
|
1130
|
+
"Some songs couldn't be found.": "有些歌没有找到。",
|
|
1131
|
+
"Already in {name}.": "已经在“{name}”里了。",
|
|
1132
|
+
"Saved.": "已收藏。",
|
|
1133
|
+
"Saved to {name}.": "已收藏到“{name}”。",
|
|
1134
|
+
"Use /library, /library play <name>, or /library save [playlist].": "用 /library、/library play <名字> 或 /library save [播放列表]。",
|
|
1135
|
+
"Your saved songs and playlists": "你收藏的歌和播放列表",
|
|
1136
|
+
"Look in the library": "翻翻资料库",
|
|
1137
|
+
"Save to the library": "收进资料库",
|
|
1138
|
+
"Play from the library": "播放资料库里的歌",
|
|
1121
1139
|
};
|
|
@@ -12,6 +12,8 @@ const READ_TIMEOUT_MS = 15_000;
|
|
|
12
12
|
// A playlist longer than this is read as a window around the playing song.
|
|
13
13
|
const PLAYLIST_WINDOW = 50;
|
|
14
14
|
const PLAYLIST_BEFORE = 5;
|
|
15
|
+
// Keeping the rest of a playlist reads up to as many songs as a library playlist holds.
|
|
16
|
+
const REST_MAX = 2_000;
|
|
15
17
|
const COLUMN_READ_MAX = 2_000;
|
|
16
18
|
// Covers already drawn, by album, or by song for one without an album.
|
|
17
19
|
const KNOWN_COVERS = 40;
|
|
@@ -47,13 +49,15 @@ function run(argv) {
|
|
|
47
49
|
const playlist = music.currentPlaylist;
|
|
48
50
|
answer.playlist = { id: playlist.persistentID(), name: playlist.name() };
|
|
49
51
|
try { answer.playlist.special = playlist.specialKind(); } catch (error) {}
|
|
50
|
-
if (action === "tracks") {
|
|
52
|
+
if (action === "tracks" || action === "rest") {
|
|
51
53
|
const tracks = playlist.tracks;
|
|
52
54
|
const ids = tracks.persistentID();
|
|
53
55
|
answer.playlist.count = ids.length;
|
|
54
56
|
const at = answer.track ? ids.indexOf(answer.track.id) : -1;
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
+
// "rest" reads from the playing song to the playlist's end, for keeping it in the library.
|
|
58
|
+
const start = action === "rest" ? Math.max(0, at)
|
|
59
|
+
: ids.length <= ${PLAYLIST_WINDOW} || at < 0 ? 0 : Math.max(0, Math.min(at - ${PLAYLIST_BEFORE}, ids.length - ${PLAYLIST_WINDOW}));
|
|
60
|
+
const end = Math.min(ids.length, start + (action === "rest" ? ${REST_MAX} : ${PLAYLIST_WINDOW}));
|
|
57
61
|
answer.playlist.index = at;
|
|
58
62
|
answer.playlist.start = start;
|
|
59
63
|
answer.playlist.tracks = [];
|
|
@@ -197,6 +201,8 @@ export function createMusicAppPlayer({ platform = process.platform, run = osascr
|
|
|
197
201
|
return {
|
|
198
202
|
available,
|
|
199
203
|
nowPlaying: ({ tracks = false } = {}) => read(tracks ? "tracks" : "read"),
|
|
204
|
+
// The playing song and the rest of its playlist, past the window nowPlaying reads, for keeping in the library.
|
|
205
|
+
remainingPlaylist: () => read("rest"),
|
|
200
206
|
pause: () => read("pause"),
|
|
201
207
|
resume: () => read("resume"),
|
|
202
208
|
next: () => read("next"),
|
|
@@ -19,6 +19,8 @@ const systemPlayers = {
|
|
|
19
19
|
],
|
|
20
20
|
};
|
|
21
21
|
const longestQueue = 30;
|
|
22
|
+
// A library playlist plays as many entries as the application sends it, as the window does.
|
|
23
|
+
const longestPlaylist = 50;
|
|
22
24
|
const largestPreview = 10 * 1024 * 1024;
|
|
23
25
|
// Some routes to Apple's audio host take over 20 seconds for one preview.
|
|
24
26
|
const downloadTimeoutMs = 60_000;
|
|
@@ -213,13 +215,29 @@ export async function createTerminalPreviewPlayer({
|
|
|
213
215
|
});
|
|
214
216
|
start(accepted);
|
|
215
217
|
},
|
|
218
|
+
// A playlist from the listener's library: Apple Music previews and tracks Moondog made, in one queue.
|
|
219
|
+
// A song is { title, artist_credit, catalog_url, preview_url }; a made track { generated: true, id, title, file }.
|
|
220
|
+
playList(tracks) {
|
|
221
|
+
if (closed) return;
|
|
222
|
+
const accepted = (Array.isArray(tracks) ? tracks : []).map((track) => (track?.generated === true
|
|
223
|
+
? { title: String(track.title ?? "").trim(), artist_credit: "", generated: true, id: String(track.id ?? ""),
|
|
224
|
+
file: typeof track.file === "string" && path.isAbsolute(track.file) ? track.file : null }
|
|
225
|
+
: { title: String(track?.title ?? "").trim(), artist_credit: String(track?.artist_credit ?? "").trim(),
|
|
226
|
+
catalog_url: https(track?.catalog_url, linkHost), preview: https(track?.preview_url, previewHost) }))
|
|
227
|
+
.filter((track) => track.title && (track.generated ? track.file : track.catalog_url && track.preview)).slice(0, longestPlaylist);
|
|
228
|
+
if (!accepted.length) return;
|
|
229
|
+
stop();
|
|
230
|
+
emit({ type: "queue", tracks: accepted.map(({ title, artist_credit, catalog_url, generated, id }) => (generated
|
|
231
|
+
? { title, artist_credit: "", generated: true, id } : { title, artist_credit, catalog_url })), album: null });
|
|
232
|
+
start(accepted);
|
|
233
|
+
},
|
|
216
234
|
// A track Moondog made on this Mac: { id, title, file }, played whole and kept after it ends.
|
|
217
235
|
playGenerated({ id, title, file } = {}) {
|
|
218
236
|
const name = String(title ?? "").trim();
|
|
219
237
|
if (closed || !name || typeof file !== "string" || !path.isAbsolute(file)) return;
|
|
220
238
|
stop();
|
|
221
239
|
const track = { title: name, artist_credit: "", generated: true, id: String(id ?? "") };
|
|
222
|
-
emit({ type: "queue", tracks: [{ title: track.title, artist_credit: "", generated: true }], album: null });
|
|
240
|
+
emit({ type: "queue", tracks: [{ title: track.title, artist_credit: "", generated: true, id: track.id }], album: null });
|
|
223
241
|
start([{ ...track, file }]);
|
|
224
242
|
},
|
|
225
243
|
// A made track queued behind the one playing, as Moondog's stream does; with nothing playing, it plays now.
|
|
@@ -228,7 +246,7 @@ export async function createTerminalPreviewPlayer({
|
|
|
228
246
|
if (closed || !name || typeof file !== "string" || !path.isAbsolute(file)) return;
|
|
229
247
|
if (!queue || !queue.tracks.every((track) => track.generated)) return this.playGenerated({ id, title, file });
|
|
230
248
|
queue.tracks.push({ title: name, artist_credit: "", generated: true, id: String(id ?? ""), file });
|
|
231
|
-
now.appended({ title: name, generated: true });
|
|
249
|
+
now.appended({ title: name, generated: true, id: String(id ?? "") });
|
|
232
250
|
},
|
|
233
251
|
// Ends the song now playing, or still loading; the next one starts after it.
|
|
234
252
|
skip() {
|
|
@@ -240,6 +258,8 @@ export async function createTerminalPreviewPlayer({
|
|
|
240
258
|
stop,
|
|
241
259
|
// What plays now, as createPlayerNow reads it: null, or { state, song, position, count, next }.
|
|
242
260
|
nowPlaying: () => (closed ? null : now.read()),
|
|
261
|
+
// The song playing and every one after it, for keeping the whole queue in the library.
|
|
262
|
+
remaining: () => (closed ? [] : now.remaining()),
|
|
243
263
|
// Leaving Apple Music stops its previews here, never the tracks Moondog made.
|
|
244
264
|
holdsCatalogSongs: () => Boolean(queue?.tracks.some((track) => !track.generated)),
|
|
245
265
|
subscribe(listener) {
|
|
@@ -587,7 +587,8 @@ function refusedMove(error, target, player) {
|
|
|
587
587
|
const refused = new SpotifyServiceError("spotify_device_refused",
|
|
588
588
|
`Spotify would not move playback to ${clipText(target.name, 80)}, either as a transfer or as a song started there. ` +
|
|
589
589
|
`${stayed ? `Playback stays on ${stayed}${player.is_playing === false ? ", paused" : ""}. ` : ""}` +
|
|
590
|
-
"The Spotify app on that device usually needs to be open and played once before it accepts remote control."
|
|
590
|
+
"The Spotify app on that device usually needs to be open and played once before it accepts remote control. " +
|
|
591
|
+
"Both ways were already tried, so do not try another way in this turn; tell the listener.");
|
|
591
592
|
refused.status = error?.status;
|
|
592
593
|
refused.playbackStayedOn = stayed;
|
|
593
594
|
return refused;
|
|
@@ -229,6 +229,8 @@ export function createLocalGenerator({
|
|
|
229
229
|
arch = process.arch,
|
|
230
230
|
spawnImpl = spawn,
|
|
231
231
|
now = () => new Date(),
|
|
232
|
+
// Whether the listener's library holds a made track: a stream never drops the audio of one they kept.
|
|
233
|
+
keeps = () => false,
|
|
232
234
|
} = {}) {
|
|
233
235
|
let current = { state: "unknown", enabled: false, install: "unknown", path: null, source: null, model: DEFAULT_STABLE_AUDIO_MODEL, managed: false };
|
|
234
236
|
let running = null;
|
|
@@ -437,6 +439,9 @@ export function createLocalGenerator({
|
|
|
437
439
|
close(false);
|
|
438
440
|
playing = { id: track.id, heardMs: 0, since: Date.now() };
|
|
439
441
|
} else if (playing.since === null) playing.since = Date.now();
|
|
442
|
+
} else if (event?.type === "playing" && playing && track && track.generated !== true) {
|
|
443
|
+
// A song from the catalog after a made track in a library playlist: the made track's play ended there.
|
|
444
|
+
close(false);
|
|
440
445
|
} else if (event?.type === "paused" && playing && playing.since !== null) {
|
|
441
446
|
playing.heardMs += Date.now() - playing.since;
|
|
442
447
|
playing.since = null;
|
|
@@ -452,9 +457,11 @@ export function createLocalGenerator({
|
|
|
452
457
|
/** Waits for the outcomes still being written, as a room does before it closes. */
|
|
453
458
|
flush: () => Promise.allSettled([...writing]),
|
|
454
459
|
// A replay of the track playing now starts it over; that is not a skip.
|
|
455
|
-
// A stream keeps only its newest tracks' audio; the receipt and outcomes of the rest stay.
|
|
460
|
+
// A stream keeps only its newest tracks' audio; the receipt and outcomes of the rest stay. A track in the
|
|
461
|
+
// listener's library keeps its audio, and a library that can't be read keeps it too.
|
|
456
462
|
async removeAudio(id) {
|
|
457
463
|
if (!isGeneratedTrackId(id)) return;
|
|
464
|
+
try { if (keeps(id)) return; } catch { return; }
|
|
458
465
|
await rm(path.join(tracksDirectory(), id, "track.wav"), { force: true }).catch(() => {});
|
|
459
466
|
const entry = made.get(id);
|
|
460
467
|
if (entry) entry.audio_kept = false;
|