@audiofool/moondog 0.2.1 → 0.3.0-beta.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/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 +38 -2
- 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 +195 -2
- package/src/i18n/messages/de.mjs +2 -0
- package/src/i18n/messages/en.mjs +2 -0
- package/src/i18n/messages/es.mjs +2 -0
- package/src/i18n/messages/fr.mjs +2 -0
- package/src/i18n/messages/it.mjs +2 -0
- package/src/i18n/messages/ja.mjs +2 -0
- package/src/i18n/messages/ko.mjs +2 -0
- package/src/i18n/messages/pt.mjs +2 -0
- package/src/i18n/messages/zh-Hant.mjs +2 -0
- package/src/i18n/messages/zh.mjs +2 -0
- package/src/i18n/screen/es.mjs +26 -0
- package/src/i18n/screen/ja.mjs +26 -0
- package/src/i18n/screen/pt.mjs +26 -0
- package/src/i18n/screen/zh.mjs +26 -0
- package/src/integrations/apple-music/terminal-preview-player.mjs +38 -11
- package/src/integrations/spotify/authentication.mjs +23 -18
- package/src/integrations/stable-audio/local-generator.mjs +683 -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/runtime/pi/persistent-credential-store.mjs +6 -28
- package/src/surfaces/cli/command-completions.mjs +1 -0
- package/src/surfaces/cli/format-output.mjs +1 -0
- package/src/surfaces/cli/tui.mjs +70 -8
package/src/i18n/messages/es.mjs
CHANGED
|
@@ -130,6 +130,8 @@ export default {
|
|
|
130
130
|
"choice.like": ({ title, artist }) => { const name = title ? `"${title}" de ${artist}` : artist; return `\n\nGuardado: te gusta ${name}. Dímelo cuando quieras cambiarlo.`; },
|
|
131
131
|
"choice.avoid": ({ title, artist }) => { const name = title ? `"${title}" de ${artist}` : artist; return `\n\nGuardado: dejaré ${name} fuera de lo que pongo y te sugiero. Dímelo cuando quieras cambiarlo.`; },
|
|
132
132
|
"choice.retract": ({ title, artist }) => { const name = title ? `"${title}" de ${artist}` : artist; return `\n\nDeshecho: tu elección anterior sobre ${name}.`; },
|
|
133
|
+
"generate.made": ({ title, length, seed }) => `Hecho en este Mac con Stable Audio 3: «${title}» (${length}, semilla ${seed}).`,
|
|
134
|
+
"generate.stream": ({ minutes }) => `El flujo sigue en este Mac, con una pista nueva cada vez, hasta ${minutes} minutos. Puedes pararlo cuando quieras.`,
|
|
133
135
|
"turn.cancelled": "Cancelé el resto.",
|
|
134
136
|
"turn.interrupted": "La respuesta se interrumpió. Abajo están los resultados registrados en Spotify; no se repitió ninguna acción.",
|
|
135
137
|
};
|
package/src/i18n/messages/fr.mjs
CHANGED
|
@@ -130,6 +130,8 @@ export default {
|
|
|
130
130
|
"choice.like": ({ title, artist }) => { const name = title ? `« ${title} » de ${artist}` : artist; return `\n\nEnregistré : vous aimez ${name}. Dites-le-moi quand vous voulez pour le changer.`; },
|
|
131
131
|
"choice.avoid": ({ title, artist }) => { const name = title ? `« ${title} » de ${artist}` : artist; return `\n\nEnregistré : je laisserai ${name} de côté dans ce que je joue et propose. Dites-le-moi quand vous voulez pour le changer.`; },
|
|
132
132
|
"choice.retract": ({ title, artist }) => { const name = title ? `« ${title} » de ${artist}` : artist; return `\n\nAnnulé : votre choix précédent sur ${name}.`; },
|
|
133
|
+
"generate.made": ({ title, length, seed }) => `Fait sur ce Mac avec Stable Audio 3 : « ${title} » (${length}, graine ${seed}).`,
|
|
134
|
+
"generate.stream": ({ minutes }) => `Le flux continue sur ce Mac, avec un nouveau morceau à chaque fois, jusqu'à ${minutes} minutes. Vous pouvez l'arrêter à tout moment.`,
|
|
133
135
|
"turn.cancelled": "Suite annulée.",
|
|
134
136
|
"turn.interrupted": "La réponse a été interrompue. Les résultats Spotify enregistrés figurent ci-dessous ; aucune action n'a été relancée.",
|
|
135
137
|
};
|
package/src/i18n/messages/it.mjs
CHANGED
|
@@ -130,6 +130,8 @@ export default {
|
|
|
130
130
|
"choice.like": ({ title, artist }) => { const name = title ? `"${title}" di ${artist}` : artist; return `\n\nSalvato: ti piace ${name}. Dimmelo quando vuoi cambiarlo.`; },
|
|
131
131
|
"choice.avoid": ({ title, artist }) => { const name = title ? `"${title}" di ${artist}` : artist; return `\n\nSalvato: terrò ${name} fuori da ciò che suono e ti propongo. Dimmelo quando vuoi cambiarlo.`; },
|
|
132
132
|
"choice.retract": ({ title, artist }) => { const name = title ? `"${title}" di ${artist}` : artist; return `\n\nAnnullato: la tua scelta precedente su ${name}.`; },
|
|
133
|
+
"generate.made": ({ title, length, seed }) => `Fatto su questo Mac con Stable Audio 3: «${title}» (${length}, seed ${seed}).`,
|
|
134
|
+
"generate.stream": ({ minutes }) => `Il flusso continua su questo Mac, con un brano nuovo ogni volta, fino a ${minutes} minuti. Puoi fermarlo quando vuoi.`,
|
|
133
135
|
"turn.cancelled": "Ho annullato il resto.",
|
|
134
136
|
"turn.interrupted": "La risposta si è interrotta. Gli esiti di Spotify registrati sono qui sotto; nessuna azione è stata ripetuta.",
|
|
135
137
|
};
|
package/src/i18n/messages/ja.mjs
CHANGED
|
@@ -130,6 +130,8 @@ export default {
|
|
|
130
130
|
"choice.like": ({ title, artist }) => { const name = title ? `${artist}の「${title}」` : artist; return `\n\n保存しました: ${name}が好き。変えたいときはいつでも言ってください。`; },
|
|
131
131
|
"choice.avoid": ({ title, artist }) => { const name = title ? `${artist}の「${title}」` : artist; return `\n\n保存しました: ${name}は再生にもおすすめにも入れません。変えたいときはいつでも言ってください。`; },
|
|
132
132
|
"choice.retract": ({ title, artist }) => { const name = title ? `${artist}の「${title}」` : artist; return `\n\n取り消しました: ${name}についての前の選択。`; },
|
|
133
|
+
"generate.made": ({ title, length, seed }) => `この Mac の Stable Audio 3 で作りました:「${title}」(${length}、シード ${seed})。`,
|
|
134
|
+
"generate.stream": ({ minutes }) => `この Mac で一曲ずつ新しい曲を作り続けます(最長 ${minutes} 分)。いつでも止められます。`,
|
|
133
135
|
"turn.cancelled": "以降の処理をキャンセルしました。",
|
|
134
136
|
"turn.interrupted": "返事が中断されました。記録された Spotify の操作結果は下のとおりです。操作は繰り返していません。",
|
|
135
137
|
};
|
package/src/i18n/messages/ko.mjs
CHANGED
|
@@ -130,6 +130,8 @@ export default {
|
|
|
130
130
|
"choice.like": ({ title, artist }) => { const name = title ? `${artist}의 "${title}"` : artist; return `\n\n저장했습니다: ${name}을(를) 좋아하신다고요. 바꾸고 싶으면 언제든 말씀해 주세요.`; },
|
|
131
131
|
"choice.avoid": ({ title, artist }) => { const name = title ? `${artist}의 "${title}"` : artist; return `\n\n저장했습니다: ${name}은(는) 재생하거나 추천하지 않겠습니다. 바꾸고 싶으면 언제든 말씀해 주세요.`; },
|
|
132
132
|
"choice.retract": ({ title, artist }) => { const name = title ? `${artist}의 "${title}"` : artist; return `\n\n취소했습니다: ${name}에 대한 이전 선택.`; },
|
|
133
|
+
"generate.made": ({ title, length, seed }) => `이 Mac의 Stable Audio 3로 만들었습니다: "${title}" (${length}, 시드 ${seed}).`,
|
|
134
|
+
"generate.stream": ({ minutes }) => `이 Mac에서 매번 새 곡을 만들며 최대 ${minutes}분 동안 이어집니다. 언제든 멈출 수 있습니다.`,
|
|
133
135
|
"turn.cancelled": "남은 작업을 취소했습니다.",
|
|
134
136
|
"turn.interrupted": "답변이 중단되었습니다. 기록된 Spotify 결과는 아래와 같으며, 어떤 작업도 다시 실행하지 않았습니다.",
|
|
135
137
|
};
|
package/src/i18n/messages/pt.mjs
CHANGED
|
@@ -130,6 +130,8 @@ export default {
|
|
|
130
130
|
"choice.like": ({ title, artist }) => { const name = title ? `"${title}" de ${artist}` : artist; return `\n\nSalvo: você gosta de ${name}. É só me dizer quando quiser mudar.`; },
|
|
131
131
|
"choice.avoid": ({ title, artist }) => { const name = title ? `"${title}" de ${artist}` : artist; return `\n\nSalvo: vou deixar ${name} fora do que eu toco e sugiro. É só me dizer quando quiser mudar.`; },
|
|
132
132
|
"choice.retract": ({ title, artist }) => { const name = title ? `"${title}" de ${artist}` : artist; return `\n\nDesfeito: sua escolha anterior sobre ${name}.`; },
|
|
133
|
+
"generate.made": ({ title, length, seed }) => `Feito neste Mac com o Stable Audio 3: "${title}" (${length}, semente ${seed}).`,
|
|
134
|
+
"generate.stream": ({ minutes }) => `O fluxo continua neste Mac, com uma faixa nova a cada vez, por até ${minutes} minutos. Pare quando quiser.`,
|
|
133
135
|
"turn.cancelled": "Cancelei o resto.",
|
|
134
136
|
"turn.interrupted": "A resposta foi interrompida. Abaixo estão os resultados registrados no Spotify; nenhuma ação foi repetida.",
|
|
135
137
|
};
|
|
@@ -130,6 +130,8 @@ export default {
|
|
|
130
130
|
"choice.like": ({ title, artist }) => { const name = title ? `${artist}的《${title}》` : artist; return `\n\n已儲存:你喜歡${name}。想改的話隨時告訴我。`; },
|
|
131
131
|
"choice.avoid": ({ title, artist }) => { const name = title ? `${artist}的《${title}》` : artist; return `\n\n已儲存:我播放和推薦時都會避開${name}。想改的話隨時告訴我。`; },
|
|
132
132
|
"choice.retract": ({ title, artist }) => { const name = title ? `${artist}的《${title}》` : artist; return `\n\n已撤銷:你之前對${name}的選擇。`; },
|
|
133
|
+
"generate.made": ({ title, length, seed }) => `用這台 Mac 上的 Stable Audio 3 做的:「${title}」(${length},種子 ${seed})。`,
|
|
134
|
+
"generate.stream": ({ minutes }) => `音樂流會在這台 Mac 上一首接一首地做下去,最多 ${minutes} 分鐘,隨時可以停。`,
|
|
133
135
|
"turn.cancelled": "已取消後續操作。",
|
|
134
136
|
"turn.interrupted": "回覆中斷了。已記錄的 Spotify 結果如下;沒有重新執行任何操作。",
|
|
135
137
|
};
|
package/src/i18n/messages/zh.mjs
CHANGED
|
@@ -130,6 +130,8 @@ export default {
|
|
|
130
130
|
"choice.like": ({ title, artist }) => { const name = title ? `${artist}的《${title}》` : artist; return `\n\n已保存:你喜欢${name}。想改的话随时告诉我。`; },
|
|
131
131
|
"choice.avoid": ({ title, artist }) => { const name = title ? `${artist}的《${title}》` : artist; return `\n\n已保存:我播放和推荐时都会避开${name}。想改的话随时告诉我。`; },
|
|
132
132
|
"choice.retract": ({ title, artist }) => { const name = title ? `${artist}的《${title}》` : artist; return `\n\n已撤销:你之前对${name}的选择。`; },
|
|
133
|
+
"generate.made": ({ title, length, seed }) => `用这台 Mac 上的 Stable Audio 3 做的:「${title}」(${length},种子 ${seed})。`,
|
|
134
|
+
"generate.stream": ({ minutes }) => `音乐流会在这台 Mac 上一首接一首地做下去,最多 ${minutes} 分钟,随时可以停。`,
|
|
133
135
|
"turn.cancelled": "已取消后续操作。",
|
|
134
136
|
"turn.interrupted": "回复中断;已确认的 Spotify 操作结果如下。未重新执行任何操作。",
|
|
135
137
|
};
|
package/src/i18n/screen/es.mjs
CHANGED
|
@@ -1027,4 +1027,30 @@ export default {
|
|
|
1027
1027
|
"Skipped. /service and /model are here when you want them.": "Omitido. /service y /model siguen aquí cuando los quieras.",
|
|
1028
1028
|
"Saving your music service...": "Guardando tu servicio de música...",
|
|
1029
1029
|
"Choose where Moondog plays music": "Elige dónde pone Moondog la música",
|
|
1030
|
+
"Make music on this Mac with Stable Audio 3": "Hacer música en este Mac con Stable Audio 3",
|
|
1031
|
+
"Stop making music here": "Dejar de hacer música aquí",
|
|
1032
|
+
"Making music on this Mac": "Hacer música en este Mac",
|
|
1033
|
+
"Made by Moondog": "Hecho por Moondog",
|
|
1034
|
+
"Stable Audio 3 needs a Mac with Apple silicon, so Moondog can't make music here.": "Stable Audio 3 necesita un Mac con Apple silicon, así que Moondog no puede hacer música aquí.",
|
|
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 hace música en este Mac con Stable Audio 3, powered by Stability AI. Pide una pista. Stable Audio 3 funciona sin conexión y la pista se queda en este Mac.",
|
|
1036
|
+
"Making music on this Mac is off. Stable Audio 3 is ready at {folder}; turn it on with /generation on.": "Hacer música en este Mac está desactivado. Stable Audio 3 está listo en {folder}; actívalo con /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.": "Hacer música en este Mac está desactivado, y Stable Audio 3 no está en {folder}. Instala la versión MLX de Stability AI desde github.com/Stability-AI/stable-audio-3 y luego usa /generation on con su carpeta.",
|
|
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.": "Hacer música en este Mac está desactivado, y Stable Audio 3 en {folder} no está instalado del todo. Ejecuta ./install.sh --download medium ahí y luego usa /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.": "Stable Audio 3 en {folder} aún no tiene el modelo Medium. Ejecuta ./install.sh --download medium ahí y vuelve a usar /generation.",
|
|
1040
|
+
"Stable Audio 3 at {folder} isn't installed yet. Run ./install.sh --download medium there, then use /generation again.": "Stable Audio 3 en {folder} aún no está instalado. Ejecuta ./install.sh --download medium ahí y vuelve a usar /generation.",
|
|
1041
|
+
"Stable Audio 3 isn't at {folder}. Use /generation on with the folder that holds its sa3 script.": "Stable Audio 3 no está en {folder}. Usa /generation on con la carpeta que contiene su script sa3.",
|
|
1042
|
+
"Use /generation, /generation on [folder], or /generation off.": "Usa /generation, /generation on [carpeta] o /generation off.",
|
|
1043
|
+
"Music making on.": "Creación de música activada.",
|
|
1044
|
+
"Music making off.": "Creación de música desactivada.",
|
|
1045
|
+
"Music making isn't ready.": "La creación de música no está lista.",
|
|
1046
|
+
"warming up": "calentando",
|
|
1047
|
+
"reading your taste": "leyendo tus gustos",
|
|
1048
|
+
"composing": "componiendo",
|
|
1049
|
+
"pressing the record": "prensando el disco",
|
|
1050
|
+
"saving it": "guardándolo",
|
|
1051
|
+
"Let Moondog make music on this Mac": "Deja que Moondog haga música en este Mac",
|
|
1052
|
+
"Make a track": "Hacer una pista",
|
|
1053
|
+
"Play a track Moondog made": "Volver a poner una pista que hizo Moondog",
|
|
1054
|
+
"Start a stream": "Empezar un flujo",
|
|
1055
|
+
"Stop the stream": "Parar el flujo",
|
|
1030
1056
|
};
|
package/src/i18n/screen/ja.mjs
CHANGED
|
@@ -1027,4 +1027,30 @@ 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 がありません。sa3 スクリプトのあるフォルダを付けて /generation on を使ってください。",
|
|
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": "この Mac で Moondog に音楽を作らせる",
|
|
1052
|
+
"Make a track": "曲を作る",
|
|
1053
|
+
"Play a track Moondog made": "Moondog が作った曲をもう一度かける",
|
|
1054
|
+
"Start a stream": "音楽の流れを始める",
|
|
1055
|
+
"Stop the stream": "音楽の流れを止める",
|
|
1030
1056
|
};
|
package/src/i18n/screen/pt.mjs
CHANGED
|
@@ -1027,4 +1027,30 @@ export default {
|
|
|
1027
1027
|
"Skipped. /service and /model are here when you want them.": "Pulado. /service e /model continuam aqui quando você quiser.",
|
|
1028
1028
|
"Saving your music service...": "Salvando seu serviço de música...",
|
|
1029
1029
|
"Choose where Moondog plays music": "Escolha onde o Moondog toca música",
|
|
1030
|
+
"Make music on this Mac with Stable Audio 3": "Fazer música neste Mac com o Stable Audio 3",
|
|
1031
|
+
"Stop making music here": "Parar de fazer música aqui",
|
|
1032
|
+
"Making music on this Mac": "Fazer música neste Mac",
|
|
1033
|
+
"Made by Moondog": "Feito pelo Moondog",
|
|
1034
|
+
"Stable Audio 3 needs a Mac with Apple silicon, so Moondog can't make music here.": "O Stable Audio 3 precisa de um Mac com Apple silicon, então o Moondog não pode fazer música aqui.",
|
|
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.": "O Moondog faz música neste Mac com o Stable Audio 3, powered by Stability AI. Peça uma faixa. O Stable Audio 3 funciona offline, e a faixa fica neste Mac.",
|
|
1036
|
+
"Making music on this Mac is off. Stable Audio 3 is ready at {folder}; turn it on with /generation on.": "Fazer música neste Mac está desativado. O Stable Audio 3 está pronto em {folder}; ative com /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.": "Fazer música neste Mac está desativado, e o Stable Audio 3 não está em {folder}. Instale a versão MLX da Stability AI a partir de github.com/Stability-AI/stable-audio-3 e depois use /generation on com a pasta dela.",
|
|
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.": "Fazer música neste Mac está desativado, e o Stable Audio 3 em {folder} não está totalmente instalado. Execute ./install.sh --download medium lá e depois use /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.": "O Stable Audio 3 em {folder} ainda não tem o modelo Medium. Execute ./install.sh --download medium lá e use /generation de novo.",
|
|
1040
|
+
"Stable Audio 3 at {folder} isn't installed yet. Run ./install.sh --download medium there, then use /generation again.": "O Stable Audio 3 em {folder} ainda não está instalado. Execute ./install.sh --download medium lá e use /generation de novo.",
|
|
1041
|
+
"Stable Audio 3 isn't at {folder}. Use /generation on with the folder that holds its sa3 script.": "O Stable Audio 3 não está em {folder}. Use /generation on com a pasta que contém o script sa3.",
|
|
1042
|
+
"Use /generation, /generation on [folder], or /generation off.": "Use /generation, /generation on [pasta] ou /generation off.",
|
|
1043
|
+
"Music making on.": "Criação de música ativada.",
|
|
1044
|
+
"Music making off.": "Criação de música desativada.",
|
|
1045
|
+
"Music making isn't ready.": "A criação de música não está pronta.",
|
|
1046
|
+
"warming up": "aquecendo",
|
|
1047
|
+
"reading your taste": "lendo seu gosto",
|
|
1048
|
+
"composing": "compondo",
|
|
1049
|
+
"pressing the record": "prensando o disco",
|
|
1050
|
+
"saving it": "salvando",
|
|
1051
|
+
"Let Moondog make music on this Mac": "Deixe o Moondog fazer música neste Mac",
|
|
1052
|
+
"Make a track": "Fazer uma faixa",
|
|
1053
|
+
"Play a track Moondog made": "Tocar de novo uma faixa que o Moondog fez",
|
|
1054
|
+
"Start a stream": "Começar um fluxo",
|
|
1055
|
+
"Stop the stream": "Parar o fluxo",
|
|
1030
1056
|
};
|
package/src/i18n/screen/zh.mjs
CHANGED
|
@@ -1027,4 +1027,30 @@ 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": "停止音乐流",
|
|
1030
1056
|
};
|
|
@@ -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;
|
|
@@ -2,7 +2,6 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import {
|
|
3
3
|
createHash,
|
|
4
4
|
randomBytes,
|
|
5
|
-
randomUUID,
|
|
6
5
|
timingSafeEqual,
|
|
7
6
|
} from "node:crypto";
|
|
8
7
|
import { createServer } from "node:http";
|
|
@@ -245,6 +244,7 @@ function validateStoredCredential(value) {
|
|
|
245
244
|
value.tokenType !== "Bearer" ||
|
|
246
245
|
typeof value.scope !== "string" ||
|
|
247
246
|
value.scope.length === 0 ||
|
|
247
|
+
// Versions up to 0.2.1 left this marker after a failed refresh; it is read and ignored.
|
|
248
248
|
(value.refreshAttempt !== undefined && (
|
|
249
249
|
!value.refreshAttempt ||
|
|
250
250
|
!/^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/u.test(value.refreshAttempt.id) ||
|
|
@@ -285,6 +285,7 @@ async function requestToken(
|
|
|
285
285
|
fetchImpl,
|
|
286
286
|
signal,
|
|
287
287
|
failureCode,
|
|
288
|
+
rejectedGrant,
|
|
288
289
|
fallbackRefreshToken,
|
|
289
290
|
fallbackRefreshExpiresAt,
|
|
290
291
|
fallbackScopes,
|
|
@@ -307,6 +308,14 @@ async function requestToken(
|
|
|
307
308
|
throw safeError(failureCode, "Spotify token request failed.");
|
|
308
309
|
}
|
|
309
310
|
if (!response?.ok) {
|
|
311
|
+
// Only invalid_grant means the grant itself is gone; outages and rate limits are worth a retry.
|
|
312
|
+
if (rejectedGrant) {
|
|
313
|
+
let reason;
|
|
314
|
+
try {
|
|
315
|
+
reason = (await response.json())?.error;
|
|
316
|
+
} catch {}
|
|
317
|
+
if (reason === "invalid_grant") throw rejectedGrant();
|
|
318
|
+
}
|
|
310
319
|
throw safeError(failureCode, "Spotify token request failed.");
|
|
311
320
|
}
|
|
312
321
|
let payload;
|
|
@@ -478,6 +487,10 @@ export async function refreshSpotifyAccessToken({
|
|
|
478
487
|
fetchImpl,
|
|
479
488
|
signal,
|
|
480
489
|
failureCode: "spotify_token_refresh_failed",
|
|
490
|
+
rejectedGrant: () => safeError(
|
|
491
|
+
"spotify_refresh_rejected",
|
|
492
|
+
"Spotify no longer accepts this sign-in. Run /spotify login to connect again.",
|
|
493
|
+
),
|
|
481
494
|
fallbackRefreshToken: refreshToken,
|
|
482
495
|
fallbackRefreshExpiresAt: refreshExpiresAt,
|
|
483
496
|
fallbackScopes: requestedScopes,
|
|
@@ -731,7 +744,7 @@ export function createSpotifyAuthentication({
|
|
|
731
744
|
const credential = await readCredential({ signal });
|
|
732
745
|
if (!credential) throw safeError("spotify_auth_not_configured", "Spotify is not authenticated.");
|
|
733
746
|
const usable = (value) => value.accessExpiresAt > now() + ACCESS_TOKEN_REFRESH_WINDOW_MS;
|
|
734
|
-
if (
|
|
747
|
+
if (usable(credential) &&
|
|
735
748
|
((rejectedAccessToken && rejectedAccessToken !== credential.accessToken) || proactive)) {
|
|
736
749
|
return credential.accessToken;
|
|
737
750
|
}
|
|
@@ -743,32 +756,24 @@ export function createSpotifyAuthentication({
|
|
|
743
756
|
const task = { controller: new AbortController(), waiters: 0, generation, started: false, credential, rejectedAccessToken };
|
|
744
757
|
const sharedSignal = task.controller.signal;
|
|
745
758
|
const timer = setTimeout(() => task.controller.abort(safeError(
|
|
746
|
-
"spotify_refresh_timeout", "Spotify refresh timed out
|
|
759
|
+
"spotify_refresh_timeout", "Spotify refresh timed out; the next request tries again.",
|
|
747
760
|
)), refreshTimeoutMs);
|
|
748
761
|
task.promise = mutateCredential(async () => {
|
|
749
762
|
try {
|
|
750
|
-
const stored = await credentialStore.modify(async (value
|
|
763
|
+
const stored = await credentialStore.modify(async (value) => {
|
|
751
764
|
assertNotAborted(sharedSignal);
|
|
752
765
|
if (!value) throw safeError("spotify_auth_not_configured", "Spotify is not authenticated.");
|
|
753
766
|
const current = validateStoredCredential(value);
|
|
754
|
-
// This check precedes *all* token reuse. A durable marker left
|
|
755
|
-
// after crash/timeout fences every process until explicit login.
|
|
756
|
-
if (current.refreshAttempt) {
|
|
757
|
-
throw safeError("spotify_refresh_uncertain", "A previous Spotify refresh was interrupted. Run /spotify login to authorize again; no refresh was retried.");
|
|
758
|
-
}
|
|
759
767
|
const mayReuse = rejectedAccessToken
|
|
760
768
|
? current.accessToken !== rejectedAccessToken
|
|
761
769
|
: proactive || !credentialMatches(current, credential);
|
|
762
770
|
if (usable(current) && mayReuse) return undefined;
|
|
763
771
|
if (generation !== task.generation) throw safeError("spotify_auth_changed", "Spotify authentication changed during refresh.");
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
//
|
|
768
|
-
// because the last caller stops waiting: drain under the deadline.
|
|
772
|
+
// An exchange that failed, timed out or died with its process leaves the stored
|
|
773
|
+
// credential as it was, and the next request tries it again. If Spotify did rotate
|
|
774
|
+
// the refresh token, it answers invalid_grant, and only that asks for a new sign-in.
|
|
775
|
+
// Do not abandon rotation merely because the last caller stops waiting: drain under the deadline.
|
|
769
776
|
task.started = true;
|
|
770
|
-
await transaction.checkpoint({ ...current, refreshAttempt: { id: randomUUID(), startedAt: now() } });
|
|
771
|
-
assertNotAborted(sharedSignal);
|
|
772
777
|
const refreshed = await waitForExchange(refreshSpotifyAccessToken({
|
|
773
778
|
clientId, refreshToken: current.refreshToken,
|
|
774
779
|
refreshExpiresAt: current.refreshExpiresAt, scopes: current.scope,
|
|
@@ -782,7 +787,7 @@ export function createSpotifyAuthentication({
|
|
|
782
787
|
} catch (error) {
|
|
783
788
|
if (sharedSignal.aborted) throw sharedSignal.reason ?? abortError();
|
|
784
789
|
if (error instanceof SpotifyAuthenticationError) throw error;
|
|
785
|
-
throw safeError("spotify_credential_store_failed", "Spotify credential refresh could not be
|
|
790
|
+
throw safeError("spotify_credential_store_failed", "Spotify credential refresh could not be saved.");
|
|
786
791
|
}
|
|
787
792
|
}).finally(() => { clearTimeout(timer); if (refreshTask === task) refreshTask = null; });
|
|
788
793
|
refreshTask = task;
|
|
@@ -896,7 +901,7 @@ export function createSpotifyAuthentication({
|
|
|
896
901
|
"Spotify is not authenticated.",
|
|
897
902
|
);
|
|
898
903
|
}
|
|
899
|
-
if (
|
|
904
|
+
if (credential.accessExpiresAt > now() + ACCESS_TOKEN_REFRESH_WINDOW_MS) {
|
|
900
905
|
return credential.accessToken;
|
|
901
906
|
}
|
|
902
907
|
return forceRefreshAccessToken({ signal, proactive: true });
|