@audiofool/moondog 0.3.0-beta.1 → 0.3.0-beta.10
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 +138 -10
- package/src/core/capability-catalog.mjs +49 -0
- package/src/core/library-store.mjs +500 -0
- package/src/core/listening-now.mjs +80 -6
- package/src/core/moondog-application.mjs +570 -32
- package/src/i18n/index.mjs +6 -3
- package/src/i18n/messages/de.mjs +1 -0
- package/src/i18n/messages/en.mjs +2 -0
- package/src/i18n/messages/es.mjs +1 -0
- package/src/i18n/messages/fr.mjs +1 -0
- package/src/i18n/messages/it.mjs +1 -0
- package/src/i18n/messages/ja.mjs +1 -0
- package/src/i18n/messages/ko.mjs +1 -0
- package/src/i18n/messages/pt.mjs +1 -0
- package/src/i18n/messages/zh-Hant.mjs +1 -0
- package/src/i18n/messages/zh.mjs +1 -0
- package/src/i18n/preferences.mjs +24 -0
- package/src/i18n/screen/es.mjs +92 -0
- package/src/i18n/screen/ja.mjs +92 -0
- package/src/i18n/screen/pt.mjs +92 -0
- package/src/i18n/screen/zh.mjs +92 -0
- package/src/integrations/apple-music/music-app.mjs +9 -3
- package/src/integrations/apple-music/terminal-preview-player.mjs +31 -1
- package/src/integrations/netease/ncm-cli-player.mjs +12 -6
- package/src/integrations/playlist-links.mjs +96 -0
- package/src/integrations/public-playlists.mjs +10 -4
- package/src/integrations/spotify/service.mjs +22 -1
- package/src/integrations/stable-audio/cover.mjs +324 -0
- package/src/integrations/stable-audio/local-generator.mjs +259 -87
- package/src/integrations/stable-audio/setup.mjs +653 -0
- package/src/integrations/stable-audio/sources.mjs +106 -0
- package/src/profile/listening-history-store.mjs +1 -1
- package/src/profile/listening-profile-projection.mjs +1 -1
- package/src/profile/music-import-bundle.mjs +3 -1
- package/src/profile/music-providers.mjs +3 -0
- package/src/profile/taste-compiler.mjs +14 -5
- package/src/runtime/pi/agent-runtime.mjs +324 -31
- package/src/runtime/pi/model-switch.mjs +26 -0
- package/src/runtime/pi/model-transport.mjs +34 -4
- package/src/runtime/pi/record-name.mjs +80 -0
- package/src/runtime/pi/reply-language.mjs +45 -0
- package/src/runtime/pi/tool-arguments.mjs +89 -0
- package/src/surfaces/cli/command-completions.mjs +15 -1
- package/src/surfaces/cli/tui.mjs +404 -18
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// The official sources Moondog's one-click setup downloads Stable Audio 3 from, each pinned to
|
|
2
|
+
// the release, commit or snapshot tested with Moondog on Apple silicon, and the files every
|
|
3
|
+
// download is verified against. Nothing here is fetched until the listener asks for a model.
|
|
4
|
+
|
|
5
|
+
// The models a listener can choose, with the flags sa3_mlx.py takes and the weights text to music
|
|
6
|
+
// loads. The codec encoder is only for --init-audio, so it is never downloaded.
|
|
7
|
+
export const STABLE_AUDIO_MODELS = Object.freeze({
|
|
8
|
+
medium: Object.freeze({ dit: "medium", decoder: "same-l", receipt: "sa3-medium-ARC", weights: Object.freeze(["dit_medium_f16.npz", "same_l_decoder_f32.npz"]) }),
|
|
9
|
+
"small-music": Object.freeze({ dit: "sm-music", decoder: "same-s", receipt: "sa3-sm-music-ARC", weights: Object.freeze(["dit_sm-music_f16.npz", "same_s_decoder_f32.npz"]) }),
|
|
10
|
+
});
|
|
11
|
+
export const DEFAULT_STABLE_AUDIO_MODEL = "medium";
|
|
12
|
+
// The text encoder every model needs.
|
|
13
|
+
export const SHARED_WEIGHTS = Object.freeze(["t5gemma_f16.npz"]);
|
|
14
|
+
export const isStableAudioModel = (value) => typeof value === "string" && Object.hasOwn(STABLE_AUDIO_MODELS, value);
|
|
15
|
+
|
|
16
|
+
const HF_SNAPSHOT = "08b17423649b5714b22eab20f96bf00751f2358a";
|
|
17
|
+
const CODE_COMMIT = "77e0398c26f7d22d801ec1bb036307e8b231f741";
|
|
18
|
+
|
|
19
|
+
export const STABLE_AUDIO_SOURCES = Object.freeze({
|
|
20
|
+
// uv makes the Python environment, so the listener installs nothing.
|
|
21
|
+
uv: Object.freeze({
|
|
22
|
+
version: "0.9.7",
|
|
23
|
+
url: "https://github.com/astral-sh/uv/releases/download/0.9.7/uv-aarch64-apple-darwin.tar.gz",
|
|
24
|
+
size: 18_372_336,
|
|
25
|
+
sha256: "35572b9619fc14d67fc1cd72582c3cfc5c9c66d97f310192e04f26fb3fe96005",
|
|
26
|
+
}),
|
|
27
|
+
// Stability AI's code (MIT). A GitHub tarball's bytes may change between downloads, so each file
|
|
28
|
+
// extracted is checked against this manifest instead: [path in optimized/mlx, bytes, sha256].
|
|
29
|
+
code: Object.freeze({
|
|
30
|
+
commit: CODE_COMMIT,
|
|
31
|
+
url: `https://codeload.github.com/Stability-AI/stable-audio-3/tar.gz/${CODE_COMMIT}`,
|
|
32
|
+
folder: `stable-audio-3-${CODE_COMMIT}/optimized/mlx`,
|
|
33
|
+
files: Object.freeze([
|
|
34
|
+
[".gitignore", 420, "0dbc69aa372ac835f6ec00d4d18941443993b9d357722e6775857544eb0b4fa3"],
|
|
35
|
+
["README.md", 20260, "5fce0300becfed583577b0e0afacf6f68696196cd6597f136a455b17269e5c31"],
|
|
36
|
+
["TRAINING_CONVENTIONS.md", 14933, "3e915aefada47d3c56dc53f0fafd8ba274df0213b3cd6fb0d986e10420c1c727"],
|
|
37
|
+
["ableton/AudioInserter/AudioInserter.py", 5573, "9f318c73c8f4b81f15adb9d853334c9e6f8411e907044fb8c933e5c1fa98ef20"],
|
|
38
|
+
["ableton/AudioInserter/__init__.py", 112, "8530d242ecf41046d7e88b3e3aed707e2faa039db33630769dcfcdc297aa046c"],
|
|
39
|
+
["ableton/README.md", 2095, "2372e71f8e5fc05d8c4c3d609388e8a3a7390901d1af27eb897a018d8c29720c"],
|
|
40
|
+
["ableton/insert_audio.py", 7718, "439ec109d31c35c4da488e43b8593c315f2a0925d712538e0e813a5e565fd68d"],
|
|
41
|
+
["benchmark.sh", 1515, "1ca7b070f6272b3814214025d3d33849d3f0c12f2cf42ee830c2a3ea82db1aa5"],
|
|
42
|
+
["bootstrap.sh", 8812, "d05fa7f5849de1f2f8a31462e4bba174d3fdddcbd7ecd4bbbff3c0bc12199b44"],
|
|
43
|
+
["install.sh", 6509, "ca6b0af0325a9f6f43b5f2e6aec19fcba82e035b4dc55c74600fc43c38c027ec"],
|
|
44
|
+
["models/__init__.py", 0, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"],
|
|
45
|
+
["models/defs/__init__.py", 0, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"],
|
|
46
|
+
["models/defs/audio_encoding.py", 9380, "f31aa0026d5292adfa6eab288d076747fd04200d829de2defdc751a974d0d784"],
|
|
47
|
+
["models/defs/demo_mlx.py", 12751, "dfa7ec9ec4820538e95913a51409da060b7202ffee30eb2ac993acc335d305df"],
|
|
48
|
+
["models/defs/dit_mlx.py", 13470, "7a57f7140c1c2ecde477c1029770f05adf1b77f8dd876a1231c5255da509e75a"],
|
|
49
|
+
["models/defs/dit_mlx_medium.py", 17685, "12f4e0743f0c00f010f7e6b962e1de921378d1f4b3a3b77e6f106ece15e3ee4a"],
|
|
50
|
+
["models/defs/latent_dataset.py", 14908, "8be23c873273f42167069321fd69751e33187bd779669d2c2e75a2b1a17f7177"],
|
|
51
|
+
["models/defs/lora.py", 52582, "85464d21291d8ba8d9b1d281260ededc926d7fe91271945c7611cdef46fb30a0"],
|
|
52
|
+
["models/defs/lora_merge.py", 40136, "5a1738c078b088b367c24d7d7c12ab3a99b09e5c703ac9a5c343b5de4c121758"],
|
|
53
|
+
["models/defs/sa3_pipeline.py", 10775, "e8aca5225eeda40e1d3f53c04968bbce9df6290021f4bdc5c1a67d6d68013de8"],
|
|
54
|
+
["models/defs/same_l_decoder.py", 12795, "c9c4922169423407ee6acbf6e7c88a46761d75cacecdac4de24d4a484e762622"],
|
|
55
|
+
["models/defs/same_l_encoder.py", 5720, "71338711802fb5c7aaf15fea3852110c0a828cdbe94c643c553d4332944c5722"],
|
|
56
|
+
["models/defs/same_s_decoder.py", 11002, "2733dc254a67c133489108616652f353aec21566e900169b15d8d871ec57c88c"],
|
|
57
|
+
["models/defs/same_s_encoder.py", 7168, "d6785bb4ed549158d7dd2a3a13b85c2ec9cccf7f6ff05e9af58120ebbfd3e1df"],
|
|
58
|
+
["models/defs/t5gemma_mlx.py", 13913, "ec8f9c088c72174c75aabf9e54ce410d53ff306244ccd397657ac961683a11e3"],
|
|
59
|
+
["models/defs/training.py", 7674, "9bf99e2019fab90d75dd8caac8988e4ae723e3bc47fca137cab56025d705faa8"],
|
|
60
|
+
["requirements-gradio.txt", 170, "f1f4a2b95c58fc80b7881e7ee9bee98f9e565f2be58ccee63d53d5e4a8ccb896"],
|
|
61
|
+
["requirements.txt", 547, "c173edcf798d31b2b4c9dc919e439de3dc7c915d82a3f4255b62a4dae516b8ad"],
|
|
62
|
+
["sa3", 3108, "ff5411de89df7fdb986419ec441d57f0dd9856f89821aef48f8fd21fe1f1656b"],
|
|
63
|
+
["sa3-gradio", 1247, "86cf132971ee880892cf68687201a0f2a3d3e9ed8fbb4c5d6c61a7d433cc8aa7"],
|
|
64
|
+
["scripts/benchmark.py", 7201, "91c76adbfba3dadfb80fd32a3bd751be95a2b01f02a7939d3d849cbc70086715"],
|
|
65
|
+
["scripts/examples.py", 6672, "011f05d5a690b2f46a7b8a0b898a4d94aa130205a121d12dfbdd18c6c8506213"],
|
|
66
|
+
["scripts/export_base_npz.py", 4108, "0ef788a8ef9c679d2e8b8bb18ded0a2f775c7781df84d4dd73542971f46f62b1"],
|
|
67
|
+
["scripts/install.py", 6082, "7f629fccec01fd88ae159bae3495299f04be205733916851ea5b0138d851a1a8"],
|
|
68
|
+
["scripts/lora_train_mlx.py", 49794, "e5d87e10e122ff4a054ac7f0d261650830e5d77b7a9fa2a07a14452db4cfae60"],
|
|
69
|
+
["scripts/parity_forward_mlx.py", 4918, "a736a3a29ea0dcccf36543e8e9f63c38b1c1bef670a51c67915b7473c665e8f1"],
|
|
70
|
+
["scripts/parity_forward_torch.py", 5236, "54db7fca02a5ce618417ee52ec54d3d291d866ff67762e4fc2e95371f9af201b"],
|
|
71
|
+
["scripts/pre_encode_mlx.py", 15985, "f6e4a2c7bcec19b0e9fc4316ee31b2277663a7dc0e569eb76400b44457686729"],
|
|
72
|
+
["scripts/sa3_gradio.py", 69093, "72c6eafa087dd309151cc4524c93a9281c3f3bdde9580a5578458bce047a59fd"],
|
|
73
|
+
["scripts/sa3_mlx.py", 45778, "312c2833fac5ca270cbeebe7037b3fbeacd87b10c9b5d057587c5eaf824fb951"],
|
|
74
|
+
["scripts/spec.py", 6574, "3e85ee7850a4e3861733823c1c712a123550c48c1cbd47cbb0e0baea2f31b42d"],
|
|
75
|
+
["scripts/test_all_configs.py", 9969, "db210dfb7297c640feaede146a9538b1da6ea167436c04f8921c741f472d37de"],
|
|
76
|
+
["scripts/test_lora_merge.py", 20704, "efd88ade4205e53bf938135853e2c9cd53a61cccc844bc6f17dfb77409c8d36f"],
|
|
77
|
+
["scripts/weights.py", 7303, "9b367084a8cfa6dffa090bf39e84f850981f689e43aab78112787fdcb3d16bf0"],
|
|
78
|
+
]),
|
|
79
|
+
}),
|
|
80
|
+
// The Python and the packages that run on the Mac it was tested on; Pillow is only for training.
|
|
81
|
+
python: Object.freeze({
|
|
82
|
+
version: "3.11.14",
|
|
83
|
+
packages: Object.freeze([
|
|
84
|
+
"anyio==4.15.1", "cffi==2.1.1", "click==8.5.0", "filelock==4.2.4", "fsspec==2026.9.0", "h11==0.16.0",
|
|
85
|
+
"hf-xet==1.7.0", "httpcore2==2.16.0", "httpx2==2.16.0", "huggingface-hub==2.3.0", "idna==3.20",
|
|
86
|
+
"mlx==0.32.4", "mlx-metal==0.32.4", "numpy==2.4.6", "packaging==26.3", "pycparser==3.12", "pyyaml==6.0.3",
|
|
87
|
+
"sentencepiece==0.2.2", "soundfile==0.14.0", "tqdm==4.70.1", "truststore==0.10.5", "typing-extensions==4.16.0",
|
|
88
|
+
]),
|
|
89
|
+
}),
|
|
90
|
+
// Stability AI's weights (Stability AI Community License; T5Gemma under the Gemma Terms of Use).
|
|
91
|
+
weights: Object.freeze({
|
|
92
|
+
url: (name) => `https://huggingface.co/stabilityai/stable-audio-3-optimized/resolve/${HF_SNAPSHOT}/MLX/${name}`,
|
|
93
|
+
files: Object.freeze({
|
|
94
|
+
"t5gemma_f16.npz": Object.freeze({ size: 567_443_068, sha256: "8deb20489f36d9aec539f26c9c67321f99bc5fe300d470435ed6e76be4f16bbd" }),
|
|
95
|
+
"dit_medium_f16.npz": Object.freeze({ size: 2_907_300_946, sha256: "f9e5647ea3225818657d47d47ae4b34afa29c0568206ca89566c1a758944a38e" }),
|
|
96
|
+
"same_l_decoder_f32.npz": Object.freeze({ size: 1_704_311_976, sha256: "84924be2122d3a20fce443f40b782d9cd88e8e73707476326003ac47659a2287" }),
|
|
97
|
+
"dit_sm-music_f16.npz": Object.freeze({ size: 919_193_814, sha256: "8ed3f38e2597f361ee675051f1265d9aa2ae2fffce1c61acd2e9fe31e1db1cbc" }),
|
|
98
|
+
"same_s_decoder_f32.npz": Object.freeze({ size: 218_090_820, sha256: "909928a8e6937c1ebe6ac4b729f0462bd3773704a11ea18278e42671dc69bfe4" }),
|
|
99
|
+
}),
|
|
100
|
+
}),
|
|
101
|
+
// What the listener agrees to before the first download.
|
|
102
|
+
licenses: Object.freeze([
|
|
103
|
+
Object.freeze({ id: "stability-ai-community", name: "Stability AI Community License", url: "https://stability.ai/license" }),
|
|
104
|
+
Object.freeze({ id: "gemma-terms", name: "Gemma Terms of Use", url: "https://huggingface.co/stabilityai/stable-audio-3-optimized/blob/main/LICENSE_GEMMA.md" }),
|
|
105
|
+
]),
|
|
106
|
+
});
|
|
@@ -386,7 +386,7 @@ function normalizedProfileInput(value, batch) {
|
|
|
386
386
|
record.provenance.source_kind !== "import" ||
|
|
387
387
|
!(record.provenance.source_system === "spotify_account_data" ||
|
|
388
388
|
(record.provenance.source_system === "spotify_web_api" && profileImport.source_key === "spotify.web_api_library") ||
|
|
389
|
-
(["youtube_music", "qq_music", "netease", "lastfm"].includes(record.provenance.source_system) &&
|
|
389
|
+
(["youtube_music", "qq_music", "netease", "lastfm", "spotify_playlist", "apple_music_playlist"].includes(record.provenance.source_system) &&
|
|
390
390
|
profileImport.source_key === `${record.provenance.source_system}.music_import`)) ||
|
|
391
391
|
!profileImport.member_names.includes(record.provenance.source_member) ||
|
|
392
392
|
record.provenance.profile_import_id?.toLowerCase() !== profileImportId ||
|
|
@@ -1616,7 +1616,7 @@ function persistedExplanation(record) {
|
|
|
1616
1616
|
library_artist_followed: `The artist appears in the followed-artists snapshot captured at ${record.provenance.captured_at}.`,
|
|
1617
1617
|
library_track_banned: `The track appears in the explicitly banned-tracks snapshot captured at ${record.provenance.captured_at}.`,
|
|
1618
1618
|
library_artist_banned: `The artist appears in the explicitly banned-artists snapshot captured at ${record.provenance.captured_at}.`,
|
|
1619
|
-
playlist_track_added: `The track appears in the ${provider} playlist named ${JSON.stringify(attributes.playlist_name)} at position ${attributes.playlist_position}.` + (attributes.selected_public_playlist ? " You selected this public playlist; ownership and listening are not established." : ""),
|
|
1619
|
+
playlist_track_added: `The track appears in the ${provider} playlist named ${JSON.stringify(attributes.playlist_name)} at position ${attributes.playlist_position}.` + (attributes.listener_made ? " You made this playlist; listening is not established." : attributes.selected_public_playlist ? " You selected this public playlist; ownership and listening are not established." : ""),
|
|
1620
1620
|
search_result_interacted: `A Spotify search for ${JSON.stringify(label)} led to an interaction with a ${attributes.result_entity_type} result at ${record.observed_at}.`,
|
|
1621
1621
|
wrapped_track_ranked: `Spotify Wrapped ranked this track at position ${attributes.rank} for ${attributes.period}.`,
|
|
1622
1622
|
wrapped_artist_ranked: `Spotify Wrapped ranked this artist at position ${attributes.rank} for ${attributes.period}.`,
|
|
@@ -58,7 +58,9 @@ export function prepareMusicBundle({ provider, subjectId, capturedAt, rows, dige
|
|
|
58
58
|
label: title, ...(artist ? { artist_credit: artist } : {}), ...(album ? { release: album } : {}) },
|
|
59
59
|
observed_at: capturedAt,
|
|
60
60
|
attributes: row.playlistId ? { playlist_name: musicText(row.playlistName) || "Selected playlist",
|
|
61
|
-
playlist_position: row.position ?? 1, playlist_id: String(row.playlistId),
|
|
61
|
+
playlist_position: row.position ?? 1, playlist_id: String(row.playlistId),
|
|
62
|
+
// A playlist the service says the listener made is theirs; any other one is only one they chose.
|
|
63
|
+
...(row.listenerMade === true ? { listener_made: true } : { selected_public_playlist: true }) } : {},
|
|
62
64
|
provenance: { source_kind: "import", source_system: provider, source_member: row.member,
|
|
63
65
|
profile_import_id: profileId, captured_at: capturedAt },
|
|
64
66
|
});
|
|
@@ -5,6 +5,9 @@ export const MUSIC_PROVIDER_LABELS = Object.freeze({
|
|
|
5
5
|
lastfm: "Last.fm",
|
|
6
6
|
qq_music: "QQ Music",
|
|
7
7
|
netease: "NetEase Music",
|
|
8
|
+
// Playlists the listener imported into the library by their links.
|
|
9
|
+
spotify_playlist: "Spotify",
|
|
10
|
+
apple_music_playlist: "Apple Music",
|
|
8
11
|
});
|
|
9
12
|
|
|
10
13
|
export function musicProviderLabel(system) {
|
|
@@ -136,14 +136,23 @@ export function compileTaste(requested, context) {
|
|
|
136
136
|
const usable = kept.some((descriptor) => ["genre", "instrument", "mood", "texture"].includes(descriptor.facet));
|
|
137
137
|
const bpm = Number.isInteger(requested?.bpm) && requested.bpm >= BPM_RANGE.min && requested.bpm <= BPM_RANGE.max ? requested.bpm : null;
|
|
138
138
|
const bpmSource = bpm && ["request", "moment", "reading"].includes(requested?.bpm_source) ? requested.bpm_source : bpm ? "moondog" : null;
|
|
139
|
-
return { usable, descriptors: kept, dropped, bpm, ...(bpm ? { bpm_source: bpmSource } : {}), title: trackTitle(requested?.title, kept, names) };
|
|
139
|
+
return { usable, descriptors: kept, dropped, bpm, ...(bpm ? { bpm_source: bpmSource } : {}), title: trackTitle(requested?.title, kept, names), names };
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
142
|
+
/**
|
|
143
|
+
* A name Moondog may give its own record: short, plain, naming no one (names is what compileTaste
|
|
144
|
+
* returned) and asking for no singing. Returns the tidied name, or null.
|
|
145
|
+
*/
|
|
146
|
+
export function acceptableRecordTitle(proposed, names = {}) {
|
|
145
147
|
const title = clean(proposed);
|
|
146
|
-
|
|
148
|
+
return title && Array.from(title).length <= 60 && TITLE_SHAPE.test(title) && !namedBy(title, names) && !VOCAL_WORDS.test(title) ? title : null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Moondog names its own records. A proposed title stays when it is acceptable; otherwise the title
|
|
152
|
+
// is the sound in a few words.
|
|
153
|
+
function trackTitle(proposed, descriptors, names) {
|
|
154
|
+
const title = acceptableRecordTitle(proposed, names);
|
|
155
|
+
if (title) return title;
|
|
147
156
|
const pick = (facets) => descriptors.find((descriptor) => facets.includes(descriptor.facet))?.term;
|
|
148
157
|
const words = [pick(["mood"]) ?? pick(["texture"]), pick(["genre"]) ?? pick(["instrument"])].filter(Boolean);
|
|
149
158
|
const text = [...new Set(words)].join(" ");
|