claude-music 1.0.0 → 1.1.0
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 +15 -4
- package/dist/__tests__/download.test.js +20 -13
- package/dist/cli.js +4 -2
- package/dist/download.js +21 -15
- package/dist/install.js +64 -6
- package/dist/paths.js +39 -2
- package/dist/server.js +19 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,9 +15,20 @@ npm install -g claude-music # or: pnpm add -g claude-music
|
|
|
15
15
|
claude-music install
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
`claude-music install` downloads the prebuilt audio daemon and
|
|
19
|
-
the MCP server with Claude Code, and installs the `/music` command.
|
|
20
|
-
|
|
18
|
+
`claude-music install` downloads the prebuilt audio daemon and a model, registers
|
|
19
|
+
the MCP server with Claude Code, and installs the `/music` command. In a terminal it
|
|
20
|
+
prompts for which model to use; pass `--model small|base` to choose without a prompt:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
claude-music install --model small # ~0.5 GB, realtime on any Apple Silicon (default)
|
|
24
|
+
claude-music install --model base # ~2.8 GB, higher quality, realtime on M2 Pro+
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Small** is the default — smallest download and realtime on any Apple Silicon, so it's
|
|
28
|
+
the safe pick for older Macs. **Base** sounds better and runs in realtime on M2 Pro and
|
|
29
|
+
above (and usually on M1 too). The ~1.4 GB of shared resources download once and are
|
|
30
|
+
reused by both, so switching models only re-downloads the model itself. **Restart Claude
|
|
31
|
+
Code** after installing so it loads the MCP server, then:
|
|
21
32
|
|
|
22
33
|
```
|
|
23
34
|
/music # start — Claude picks an opening vibe
|
|
@@ -32,7 +43,7 @@ changes, and music stops when you end the session.
|
|
|
32
43
|
|
|
33
44
|
- **Apple-Silicon Mac** (M-series) — required by MLX/Metal.
|
|
34
45
|
- **Node ≥ 18** and the **`claude`** CLI (Claude Code).
|
|
35
|
-
- ~1.
|
|
46
|
+
- ~1.7 GB of disk for the model + resources, downloaded on first install.
|
|
36
47
|
|
|
37
48
|
No Xcode, CMake, or Python needed — the daemon ships as a prebuilt binary. (If a
|
|
38
49
|
prebuilt binary can't be fetched, `install` falls back to building from source,
|
|
@@ -1,25 +1,32 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
describe("
|
|
5
|
-
it("
|
|
6
|
-
expect(
|
|
7
|
-
|
|
2
|
+
import { RESOURCE_FILES, modelFiles } from "../download.js";
|
|
3
|
+
import { modelMlxfn, MAGENTA_HOME, RESOURCES_DIR } from "../paths.js";
|
|
4
|
+
describe("model + resource file lists", () => {
|
|
5
|
+
it("model files cover the compiled model and its state", () => {
|
|
6
|
+
expect(modelFiles("small")).toEqual([
|
|
7
|
+
"models/mrt2_small/mrt2_small.mlxfn",
|
|
8
|
+
"models/mrt2_small/mrt2_small_state.safetensors",
|
|
9
|
+
]);
|
|
10
|
+
expect(modelFiles("base")).toEqual([
|
|
11
|
+
"models/mrt2_base/mrt2_base.mlxfn",
|
|
12
|
+
"models/mrt2_base/mrt2_base_state.safetensors",
|
|
13
|
+
]);
|
|
8
14
|
});
|
|
9
15
|
it("includes all musiccoca and spectrostream resources", () => {
|
|
10
|
-
const musiccoca =
|
|
11
|
-
const spectrostream =
|
|
16
|
+
const musiccoca = RESOURCE_FILES.filter((f) => f.startsWith("resources/musiccoca/"));
|
|
17
|
+
const spectrostream = RESOURCE_FILES.filter((f) => f.startsWith("resources/spectrostream/"));
|
|
12
18
|
expect(musiccoca).toHaveLength(6);
|
|
13
19
|
expect(spectrostream).toHaveLength(4);
|
|
14
20
|
});
|
|
15
|
-
it("has no duplicate entries", () => {
|
|
16
|
-
expect(new Set(
|
|
21
|
+
it("has no duplicate resource entries", () => {
|
|
22
|
+
expect(new Set(RESOURCE_FILES).size).toBe(RESOURCE_FILES.length);
|
|
17
23
|
});
|
|
18
24
|
});
|
|
19
25
|
describe("paths", () => {
|
|
20
|
-
it("derives
|
|
21
|
-
expect(
|
|
26
|
+
it("derives model paths under the magenta home from the model key", () => {
|
|
27
|
+
expect(modelMlxfn("small").startsWith(MAGENTA_HOME)).toBe(true);
|
|
28
|
+
expect(modelMlxfn("small").endsWith("models/mrt2_small/mrt2_small.mlxfn")).toBe(true);
|
|
29
|
+
expect(modelMlxfn("base").endsWith("models/mrt2_base/mrt2_base.mlxfn")).toBe(true);
|
|
22
30
|
expect(RESOURCES_DIR.startsWith(MAGENTA_HOME)).toBe(true);
|
|
23
|
-
expect(MODEL_FILE.endsWith("models/mrt2_small/mrt2_small.mlxfn")).toBe(true);
|
|
24
31
|
});
|
|
25
32
|
});
|
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,9 @@ const USAGE = `claude-music — live AI background music for Claude Code
|
|
|
5
5
|
|
|
6
6
|
Usage:
|
|
7
7
|
claude-music install Download the daemon + model, register the MCP
|
|
8
|
-
server, and install the /music command.
|
|
8
|
+
server, and install the /music command. Prompts
|
|
9
|
+
for a model on a terminal; pass --model small|base
|
|
10
|
+
to choose non-interactively (default: small).
|
|
9
11
|
claude-music uninstall Remove the MCP registration and /music command.
|
|
10
12
|
(add --purge to also delete the cached binary + model)
|
|
11
13
|
claude-music doctor Check that everything is installed correctly.
|
|
@@ -22,7 +24,7 @@ async function main() {
|
|
|
22
24
|
await runServer();
|
|
23
25
|
return; // server keeps the process alive on the stdio transport
|
|
24
26
|
case "install":
|
|
25
|
-
await install();
|
|
27
|
+
await install(rest);
|
|
26
28
|
break;
|
|
27
29
|
case "uninstall":
|
|
28
30
|
await uninstall(rest.includes("--purge"));
|
package/dist/download.js
CHANGED
|
@@ -5,15 +5,12 @@ import * as path from "node:path";
|
|
|
5
5
|
import { pipeline } from "node:stream/promises";
|
|
6
6
|
import { Readable } from "node:stream";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { MAGENTA_HOME } from "./paths.js";
|
|
8
|
+
import { MAGENTA_HOME, MODELS } from "./paths.js";
|
|
9
9
|
const HF_REPO = "google/magenta-realtime-2";
|
|
10
10
|
const HF_BASE = `https://huggingface.co/${HF_REPO}/resolve/main`;
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
export const MODEL_FILES = [
|
|
15
|
-
"models/mrt2_small/mrt2_small.mlxfn",
|
|
16
|
-
"models/mrt2_small/mrt2_small_state.safetensors",
|
|
11
|
+
// Shared musiccoca / spectrostream resources, needed by every model. All are
|
|
12
|
+
// public on HuggingFace and download without auth.
|
|
13
|
+
export const RESOURCE_FILES = [
|
|
17
14
|
"resources/musiccoca/audio_preprocessor.tflite",
|
|
18
15
|
"resources/musiccoca/mapper.tflite",
|
|
19
16
|
"resources/musiccoca/music_encoder.tflite",
|
|
@@ -25,6 +22,14 @@ export const MODEL_FILES = [
|
|
|
25
22
|
"resources/spectrostream/quantizer.safetensors",
|
|
26
23
|
"resources/spectrostream/spectrostream_encoder.mlxfn",
|
|
27
24
|
];
|
|
25
|
+
/** The two files specific to a given model: its compiled .mlxfn + state. */
|
|
26
|
+
export function modelFiles(key) {
|
|
27
|
+
const { name } = MODELS[key];
|
|
28
|
+
return [
|
|
29
|
+
`models/${name}/${name}.mlxfn`,
|
|
30
|
+
`models/${name}/${name}_state.safetensors`,
|
|
31
|
+
];
|
|
32
|
+
}
|
|
28
33
|
function humanBytes(n) {
|
|
29
34
|
if (n >= 1 << 30)
|
|
30
35
|
return `${(n / (1 << 30)).toFixed(2)} GiB`;
|
|
@@ -77,18 +82,19 @@ async function downloadFile(url, dest, label) {
|
|
|
77
82
|
console.log(` ${label}: done (${humanBytes(received)})`);
|
|
78
83
|
}
|
|
79
84
|
/**
|
|
80
|
-
* Download
|
|
81
|
-
* already exists locally with the same size as the remote.
|
|
82
|
-
* files were actually fetched.
|
|
85
|
+
* Download the chosen model + the shared resources into MAGENTA_HOME, skipping
|
|
86
|
+
* any file that already exists locally with the same size as the remote.
|
|
87
|
+
* Returns how many files were actually fetched.
|
|
83
88
|
*/
|
|
84
|
-
export async function downloadModel() {
|
|
89
|
+
export async function downloadModel(key) {
|
|
90
|
+
const files = [...modelFiles(key), ...RESOURCE_FILES];
|
|
85
91
|
let fetched = 0;
|
|
86
|
-
console.log(`==> Downloading
|
|
87
|
-
for (let i = 0; i <
|
|
88
|
-
const rel =
|
|
92
|
+
console.log(`==> Downloading ${MODELS[key].name} + resources to ${MAGENTA_HOME}`);
|
|
93
|
+
for (let i = 0; i < files.length; i++) {
|
|
94
|
+
const rel = files[i];
|
|
89
95
|
const dest = path.join(MAGENTA_HOME, rel);
|
|
90
96
|
const url = `${HF_BASE}/${rel}`;
|
|
91
|
-
const tag = `[${i + 1}/${
|
|
97
|
+
const tag = `[${i + 1}/${files.length}] ${rel}`;
|
|
92
98
|
if (fs.existsSync(dest)) {
|
|
93
99
|
const local = fs.statSync(dest).size;
|
|
94
100
|
const remote = await remoteSize(url);
|
package/dist/install.js
CHANGED
|
@@ -2,9 +2,10 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
+
import * as readline from "node:readline";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
7
|
import { downloadBinary, downloadModel } from "./download.js";
|
|
7
|
-
import { BIN_DIR, CLAUDE_MUSIC_HOME, COMMANDS_DIR, DAEMON_BIN, MAGENTA_HOME,
|
|
8
|
+
import { BIN_DIR, CLAUDE_MUSIC_HOME, COMMANDS_DIR, DAEMON_BIN, DEFAULT_MODEL, MAGENTA_HOME, modelMlxfn, MODELS, MUSIC_COMMAND, readConfig, RESOURCES_DIR, RESOURCES_MB, writeConfig, } from "./paths.js";
|
|
8
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
10
|
const PKG_ROOT = path.resolve(__dirname, "..");
|
|
10
11
|
const CLI_JS = path.join(__dirname, "cli.js");
|
|
@@ -182,18 +183,73 @@ function installCommand() {
|
|
|
182
183
|
fs.copyFileSync(TEMPLATE_MUSIC, MUSIC_COMMAND);
|
|
183
184
|
}
|
|
184
185
|
// ---------------------------------------------------------------------------
|
|
186
|
+
// model selection
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
/** Parse a model choice from CLI args (`--model base|small`, `--base`, `--small`). */
|
|
189
|
+
function modelFromArgs(args) {
|
|
190
|
+
const i = args.indexOf("--model");
|
|
191
|
+
const val = i >= 0 ? args[i + 1] : undefined;
|
|
192
|
+
if (val === "base" || args.includes("--base"))
|
|
193
|
+
return "base";
|
|
194
|
+
if (val === "small" || args.includes("--small"))
|
|
195
|
+
return "small";
|
|
196
|
+
if (val !== undefined) {
|
|
197
|
+
throw new Error(`unknown --model '${val}' (expected 'small' or 'base')`);
|
|
198
|
+
}
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
function ask(question) {
|
|
202
|
+
const rl = readline.createInterface({
|
|
203
|
+
input: process.stdin,
|
|
204
|
+
output: process.stdout,
|
|
205
|
+
});
|
|
206
|
+
return new Promise((resolve) => rl.question(question, (a) => {
|
|
207
|
+
rl.close();
|
|
208
|
+
resolve(a.trim());
|
|
209
|
+
}));
|
|
210
|
+
}
|
|
211
|
+
/** Interactive model picker (only used on a TTY). */
|
|
212
|
+
async function promptModel() {
|
|
213
|
+
const totalSmall = ((MODELS.small.approxMB + RESOURCES_MB) / 1024).toFixed(1);
|
|
214
|
+
const totalBase = ((MODELS.base.approxMB + RESOURCES_MB) / 1024).toFixed(1);
|
|
215
|
+
console.log("\nWhich model would you like?");
|
|
216
|
+
console.log(` 1) ${MODELS.small.label} — ${MODELS.small.note}`);
|
|
217
|
+
console.log(` 2) ${MODELS.base.label} — ${MODELS.base.note}`);
|
|
218
|
+
console.log(` (total first-time download incl. shared resources: ` +
|
|
219
|
+
`~${totalSmall} GB small / ~${totalBase} GB base)`);
|
|
220
|
+
const answer = await ask("Choose [1]: ");
|
|
221
|
+
return answer === "2" || answer.toLowerCase() === "base" ? "base" : "small";
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Resolve which model to install: an explicit flag wins; otherwise prompt on an
|
|
225
|
+
* interactive terminal; otherwise fall back to the default (keeps the
|
|
226
|
+
* one-command, non-interactive flow working in scripts/CI).
|
|
227
|
+
*/
|
|
228
|
+
async function resolveModel(args) {
|
|
229
|
+
const flagged = modelFromArgs(args);
|
|
230
|
+
if (flagged)
|
|
231
|
+
return flagged;
|
|
232
|
+
if (process.stdin.isTTY && process.stdout.isTTY)
|
|
233
|
+
return promptModel();
|
|
234
|
+
return DEFAULT_MODEL;
|
|
235
|
+
}
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
185
237
|
// entry points
|
|
186
238
|
// ---------------------------------------------------------------------------
|
|
187
|
-
export async function install() {
|
|
239
|
+
export async function install(args = []) {
|
|
188
240
|
preflight();
|
|
241
|
+
const model = await resolveModel(args);
|
|
242
|
+
console.log(`==> Model: ${MODELS[model].name}`);
|
|
189
243
|
await ensureBinary();
|
|
190
|
-
await downloadModel();
|
|
244
|
+
await downloadModel(model);
|
|
245
|
+
writeConfig({ model });
|
|
191
246
|
await registerMcp();
|
|
192
247
|
installCommand();
|
|
193
248
|
console.log("");
|
|
194
|
-
console.log(
|
|
249
|
+
console.log(`claude-music installed (${MODELS[model].name}).`);
|
|
195
250
|
console.log(" Restart Claude Code, then type /music to start. It steers itself as you");
|
|
196
251
|
console.log(" work and stops when you end the session.");
|
|
252
|
+
console.log(" Switch models anytime: claude-music install --model small|base");
|
|
197
253
|
}
|
|
198
254
|
export async function uninstall(purge) {
|
|
199
255
|
console.log("==> Removing mrt2-mcp registration...");
|
|
@@ -212,7 +268,9 @@ export async function doctor() {
|
|
|
212
268
|
const platformOk = process.platform === "darwin" && process.arch === "arm64";
|
|
213
269
|
const claudeOk = have("claude");
|
|
214
270
|
const binOk = fs.existsSync(DAEMON_BIN);
|
|
215
|
-
const
|
|
271
|
+
const model = readConfig().model;
|
|
272
|
+
const modelPath = modelMlxfn(model);
|
|
273
|
+
const modelOk = fs.existsSync(modelPath);
|
|
216
274
|
const resourcesOk = fs.existsSync(RESOURCES_DIR);
|
|
217
275
|
let mcpOk = false;
|
|
218
276
|
const r = spawnSync("claude", ["mcp", "list"], { encoding: "utf8" });
|
|
@@ -223,7 +281,7 @@ export async function doctor() {
|
|
|
223
281
|
console.log(` [${ok(platformOk)}] platform: ${process.platform}/${process.arch}`);
|
|
224
282
|
console.log(` [${ok(claudeOk)}] claude CLI on PATH`);
|
|
225
283
|
console.log(` [${ok(binOk)}] daemon binary: ${DAEMON_BIN}`);
|
|
226
|
-
console.log(` [${ok(modelOk)}] model: ${
|
|
284
|
+
console.log(` [${ok(modelOk)}] model (${MODELS[model].name}): ${modelPath}`);
|
|
227
285
|
console.log(` [${ok(resourcesOk)}] resources: ${RESOURCES_DIR}`);
|
|
228
286
|
console.log(` [${ok(mcpOk)}] mrt2-mcp registered`);
|
|
229
287
|
console.log(` [${ok(cmdOk)}] /music command: ${MUSIC_COMMAND}`);
|
package/dist/paths.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
1
2
|
import * as os from "node:os";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
// Central path constants for claude-music. Everything the installer writes and
|
|
@@ -13,10 +14,46 @@ export const BIN_DIR = path.join(CLAUDE_MUSIC_HOME, "bin");
|
|
|
13
14
|
* fallback, which leaves the binary inside the magenta-realtime build tree).
|
|
14
15
|
*/
|
|
15
16
|
export const DAEMON_BIN = process.env.MRT2D_BIN || path.join(BIN_DIR, "mrt2d");
|
|
16
|
-
/** Where the model + shared resources are downloaded. The daemon
|
|
17
|
+
/** Where the model + shared resources are downloaded. The daemon defaults here. */
|
|
17
18
|
export const MAGENTA_HOME = path.join(HOME, "Documents", "Magenta", "magenta-rt-v2");
|
|
18
|
-
export const MODEL_FILE = path.join(MAGENTA_HOME, "models", "mrt2_small", "mrt2_small.mlxfn");
|
|
19
19
|
export const RESOURCES_DIR = path.join(MAGENTA_HOME, "resources");
|
|
20
|
+
/** Shared resources (musiccoca + spectrostream) add ~1.4 GB regardless of model. */
|
|
21
|
+
export const RESOURCES_MB = 1376;
|
|
22
|
+
export const MODELS = {
|
|
23
|
+
small: {
|
|
24
|
+
name: "mrt2_small",
|
|
25
|
+
label: "Small",
|
|
26
|
+
approxMB: 464,
|
|
27
|
+
note: "~0.5 GB · realtime on any Apple Silicon (default)",
|
|
28
|
+
},
|
|
29
|
+
base: {
|
|
30
|
+
name: "mrt2_base",
|
|
31
|
+
label: "Base",
|
|
32
|
+
approxMB: 2788,
|
|
33
|
+
note: "~2.8 GB · higher quality · realtime on M2 Pro and above",
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
export const DEFAULT_MODEL = "small";
|
|
37
|
+
/** Absolute path to a model's compiled .mlxfn for the given key. */
|
|
38
|
+
export function modelMlxfn(key) {
|
|
39
|
+
const { name } = MODELS[key];
|
|
40
|
+
return path.join(MAGENTA_HOME, "models", name, `${name}.mlxfn`);
|
|
41
|
+
}
|
|
42
|
+
export const CONFIG_FILE = path.join(CLAUDE_MUSIC_HOME, "config.json");
|
|
43
|
+
/** Read the persisted config, defaulting to the small model. */
|
|
44
|
+
export function readConfig() {
|
|
45
|
+
try {
|
|
46
|
+
const raw = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
|
|
47
|
+
return { model: raw.model === "base" ? "base" : "small" };
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return { model: DEFAULT_MODEL };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function writeConfig(cfg) {
|
|
54
|
+
fs.mkdirSync(CLAUDE_MUSIC_HOME, { recursive: true });
|
|
55
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n");
|
|
56
|
+
}
|
|
20
57
|
/** Runtime artefacts created by the daemon. */
|
|
21
58
|
export const SOCKET_PATH = path.join(HOME, ".mrt2d.sock");
|
|
22
59
|
export const PID_PATH = path.join(HOME, ".mrt2d.pid");
|
package/dist/server.js
CHANGED
|
@@ -5,7 +5,7 @@ import * as fs from "node:fs";
|
|
|
5
5
|
import * as net from "node:net";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { buildVibeMessage } from "./vibe.js";
|
|
8
|
-
import { DAEMON_BIN, LOG_PATH, PID_PATH, SOCKET_PATH } from "./paths.js";
|
|
8
|
+
import { DAEMON_BIN, LOG_PATH, modelMlxfn, PID_PATH, readConfig, RESOURCES_DIR, SOCKET_PATH, } from "./paths.js";
|
|
9
9
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
10
10
|
/** Can we actually connect to the daemon's socket right now? */
|
|
11
11
|
export function socketAlive() {
|
|
@@ -29,17 +29,34 @@ export async function ensureDaemon() {
|
|
|
29
29
|
if (!fs.existsSync(DAEMON_BIN)) {
|
|
30
30
|
throw new Error(`mrt2d not installed at ${DAEMON_BIN} — run: claude-music install`);
|
|
31
31
|
}
|
|
32
|
+
// Launch the model the user selected at install time (defaults to small).
|
|
33
|
+
const modelPath = modelMlxfn(readConfig().model);
|
|
34
|
+
// Fail fast with an actionable message if the model/resources are missing,
|
|
35
|
+
// rather than spawning a daemon that exits immediately and waiting it out.
|
|
36
|
+
if (!fs.existsSync(modelPath) || !fs.existsSync(RESOURCES_DIR)) {
|
|
37
|
+
throw new Error(`model not found at ${modelPath} — run: claude-music install`);
|
|
38
|
+
}
|
|
32
39
|
const out = fs.openSync(LOG_PATH, "a");
|
|
33
|
-
const child = spawn(DAEMON_BIN, [], {
|
|
40
|
+
const child = spawn(DAEMON_BIN, ["--model", modelPath, "--resources", RESOURCES_DIR], {
|
|
34
41
|
detached: true,
|
|
35
42
|
stdio: ["ignore", out, out],
|
|
36
43
|
});
|
|
44
|
+
// Detect an early crash (e.g. a corrupt/partial model) so we don't sit through
|
|
45
|
+
// the full timeout. The daemon stays alive past unref() on success.
|
|
46
|
+
let exitCode = null;
|
|
47
|
+
child.on("exit", (code) => {
|
|
48
|
+
exitCode = code ?? 1;
|
|
49
|
+
});
|
|
37
50
|
child.unref();
|
|
38
51
|
// Model load is fast for mrt2_small (~2s) but allow generous headroom.
|
|
39
52
|
for (let i = 0; i < 60; i++) {
|
|
40
53
|
await sleep(1000);
|
|
41
54
|
if (await socketAlive())
|
|
42
55
|
return;
|
|
56
|
+
if (exitCode !== null) {
|
|
57
|
+
throw new Error(`mrt2d exited during startup (code ${exitCode}) — check ${LOG_PATH}. ` +
|
|
58
|
+
`If the model is incomplete, re-run: claude-music install`);
|
|
59
|
+
}
|
|
43
60
|
}
|
|
44
61
|
throw new Error(`daemon did not come up within 60s — check ${LOG_PATH}`);
|
|
45
62
|
}
|
package/package.json
CHANGED