Reference
Soundbox HTTP API
The local server's endpoints, and what they take and return.
Base URL: http://127.0.0.1:17494. All JSON. The Vite dev server proxies
/api, /mcp, /files and /artwork to this port, so the frontend uses
relative URLs.
Request rules
Two checks run before any route, so a web page open in your browser cannot drive the server:
Host header. Requests are served only when the
Hostheader nameslocalhost,127.0.0.1or[::1](any port), or the specific host passed tosoundbox --host. Anything else gets403, which blocks DNS-rebinding attacks. Binding a wildcard (--host 0.0.0.0) turns the check off.Mutating requests.
POST,PUT,PATCHandDELETEunder/api/must send either a non-"simple"Content-Type(application/json, or the raw audio typeaudio/wavorapplication/octet-streamon the upload routes) or anX-Soundboxheader (any value). A request with no body,text/plain,application/x-www-form-urlencodedormultipart/form-dataand noX-Soundboxheader gets403. Body-less calls such asPOST /api/sessionstherefore need the header:curl -X POST -H 'X-Soundbox: 1' http://127.0.0.1:17494/api/sessions
Endpoint index
Every route the server registers (server/src/routes/*.ts), grouped by file.
The sections after this one document the core routes in detail: health,
models, generation, jobs, the library, sessions, settings, profiles and the
prompt assistant. For the rest, the zod schema named in the route handler
(server/src/schemas.ts) is the authority on the body. → job means the
route answers 202 { "job_id": "..." } and the work is followed through
/api/jobs/{id}.
Health and models (routes/models.ts)
| Method | Path | What it does |
|---|---|---|
| GET | /api/health |
Version, backend device, loaded model, queue length |
| GET | /api/models |
Every enabled model with status and licence |
| GET | /api/models/audiocpp/catalog |
Every catalog entry, enabled or not |
| GET | /api/models/audiocpp/runtime |
Which audiocpp_cli resolved, how, and where weights live |
| POST | /api/models/audiocpp/engine/install |
Install audiocpp_cli. Body { version?, backend? } → job |
| GET | /api/models/{id} |
One model |
| POST | /api/models/{id}/download |
Download weights → job |
| POST | /api/models/{id}/load |
Load into memory, unloading any other |
| POST | /api/models/{id}/unload |
Unload |
| DELETE | /api/models/{id}/weights |
Delete downloaded weights |
Generation and jobs (routes/jobs.ts)
| Method | Path | What it does |
|---|---|---|
| POST | /api/generate |
Text to audio, or extend/cover/repaint an existing sound. Body { model, prompt, duration_s?, seed?, steps?, cfg?, negative_prompt?, lyrics?, variations?, tags?, source_sound_id?, edit?, edit_options?, session_id?, project_id? } → job |
| POST | /api/separate |
Split a sound into stems. Body { model, source_sound_id, stems?: { keep: string[], rest: "discard" | "merge" } } → job |
| POST | /api/repaint |
Regenerate one window of a sound. Body { source_sound_id, start_s, end_s, prompt?, lyrics?, model?, strength?, mode?, seed?, variations? } → job |
| POST | /api/speak |
Text to speech. Body { model, lines: [{ text, voice_id?, speaker? }], gap_s?, seed?, steps?, cfg?, name?, tags?, session_id?, project_id? } → job |
| POST | /api/convert |
Voice conversion. Body { model, source_sound_id, voice_id, seed? } → job |
| GET | /api/jobs |
Recent jobs, newest first (?limit) |
| GET | /api/jobs/{id} |
One job |
| POST | /api/jobs/{id}/cancel |
Cancel a queued or running job |
| GET | /api/jobs/{id}/events |
SSE stream of the job until it ends |
Library and files (routes/sounds.ts, routes/files.ts)
| Method | Path | What it does |
|---|---|---|
| GET | /api/sounds |
Search the library (?q, tag, favorite, model, models, session_id, project_id, parent_id, source, limit, offset) |
| GET | /api/sounds/{id} |
One sound |
| PATCH | /api/sounds/{id} |
Body { name?, favorite?, tags? } |
| DELETE | /api/sounds/{id} |
Delete the row and the file |
| POST | /api/sounds/upload |
Add audio from disk or the microphone. Body is the raw audio; ?name, source, project_id |
| POST | /api/sounds/{id}/edit |
Non-destructive edit: writes a new derived sound. Body { op: "trim" | "normalize" | "reverse" | "gain" | "highpass" | "lowpass" | "pitch" | "stretch", start_s?, end_s?, target_lufs?, gain_db?, cutoff_hz?, semitones?, rate? } → 201 Sound |
| POST | /api/sounds/{id}/reveal |
Show the file in the OS file manager |
| GET | /api/tags |
Tags with counts |
| GET | /files/{id}.wav |
The audio. ?format=mp3 transcodes when ffmpeg is installed |
| GET | /artwork/{file} |
Generated cover art and avatars |
Artwork (routes/image.ts, see ARTWORK.md)
| Method | Path | What it does |
|---|---|---|
| GET | /api/image/status |
What is configured and installed |
| POST | /api/image/engine/install |
Install sd-server → job |
| POST | /api/image/models/{id}/download |
Download an image model → job |
| DELETE | /api/image/models/{id} |
Delete its weights |
| POST / DELETE | /api/sounds/{id}/artwork |
Draw or remove a sound's cover art |
| POST / DELETE | /api/voices/{id}/artwork |
Draw or remove a voice's avatar |
Voices (routes/voices.ts)
| Method | Path | What it does |
|---|---|---|
| GET | /api/voices |
Saved voices |
| GET | /api/voices/{id} |
One voice |
| POST | /api/voices |
Body { name, kind: "clone" | "builtin", voice_id?, model_id?, notes?, source_sound_id? } |
| POST | /api/voices/upload |
Clone from a WAV. Body is the file; ?name, notes, model_id |
| PATCH | /api/voices/{id} |
Body { name?, notes? } |
| DELETE | /api/voices/{id} |
Delete the voice |
Stories: scripts and studio timelines (routes/stories.ts)
| Method | Path | What it does |
|---|---|---|
| GET | /api/stories |
Stories, most recently worked on first (?limit) |
| POST | /api/stories |
Body { title?, project_id?, builder?: "script" | "music" } |
| GET / PATCH / DELETE | /api/stories/{id} |
Read, update ({ title?, project_id?, request_options?, builder? }), delete |
| POST | /api/stories/from-sound |
Lay a sound out on a new timeline. Body { sound_id, separate?, model?, stems? } |
| GET | /api/stories/{id}/events |
SSE stream of changes to the story |
| GET | /api/stories/{id}/tracks |
Track settings |
| PATCH | /api/stories/{id}/tracks/{track_index} |
Body { name?, muted?, soloed?, gain_db? } |
| DELETE | /api/stories/{id}/tracks/{track_index} |
Delete the lane and everything on it |
| GET | /api/stories/{id}/segments |
Lines and clips |
| POST | /api/stories/{id}/segments |
Add one. Body { text?, kind?: "line" | "pause", speaker?, voice_id?, track_index?, start_s?, position?, sound_id?, duration_s?, trim_start_s?, fade_in_s?, fade_out_s?, gain_db?, muted? } |
| PUT | /api/stories/{id}/segments/reorder |
Body { segment_ids } |
| PATCH / DELETE | /api/stories/{id}/segments/{segment_id} |
Update (same fields as POST) or delete |
| POST | /api/stories/{id}/segments/{segment_id}/generate |
Voice one line. Body { model, seed?, steps?, cfg? } → job |
| POST | /api/stories/{id}/segments/{segment_id}/split |
Body { at_s } |
| POST | /api/stories/{id}/export |
Mix down to one sound → job |
Sessions and projects (routes/sessions.ts)
| Method | Path | What it does |
|---|---|---|
| GET / POST | /api/sessions |
List (?limit) or create ({ title }) |
| GET / PATCH / DELETE | /api/sessions/{id} |
Read, update ({ title?, project_id? }), delete with its sounds |
| GET / POST | /api/projects |
List or create ({ name }) |
| DELETE | /api/projects/{id} |
Delete a project |
Prompt assistant and studio chat (routes/llm.ts, routes/history.ts)
| Method | Path | What it does |
|---|---|---|
| POST | /api/enhance |
Rewrite a prompt. Body { prompt, n?, category?, lyrics?, model? } |
| POST | /api/enhance/lyrics |
Write lyrics. Body { theme, style? } |
| POST | /api/assistant/chat |
One turn of the assistant. Body { messages, studio?, draft? } |
| POST | /api/assistant/openers |
Suggested first messages. Body { studio? } |
| POST | /api/studio/command |
Turn a studio instruction into a plan of edits. Body { instruction, context: { tracks, clips, playhead_s?, selected_ids?, voices? } } |
| GET | /api/llm/status |
Engine and model install state, detected local endpoints |
| POST | /api/llm/engine/install |
Install llama-server → job |
| POST | /api/llm/models/{id}/download |
Download an assistant model → job |
| DELETE | /api/llm/models/{id} |
Delete its weights |
| GET / POST | /api/conversations |
List (?limit or ?story_id) or create ({ title?, story_id? }) the chat history |
| GET / PATCH / DELETE | /api/conversations/{id} |
Read, rename ({ title }), delete |
| GET / POST | /api/conversations/{id}/entries |
Read or append ({ entries: [{ role, content?, actions?, sound_ids?, job_id? }] }) |
Settings, logs, profiles (routes/settings.ts, routes/fs.ts, routes/profiles.ts)
| Method | Path | What it does |
|---|---|---|
| GET / PATCH | /api/settings |
Read or change settings |
| GET | /api/logs |
Tail of the server log (?lines) |
| GET | /api/fs/dirs |
List folders on the server's disk (?path) |
| GET / POST | /api/profiles |
List or create ({ name }) |
| PATCH / DELETE | /api/profiles/{id} |
Rename ({ name }) or remove (?delete_data=1 deletes its folder) |
| POST | /api/profiles/{id}/activate |
Switch library |
MCP (routes/mcp.ts, mcp/server.ts)
| Method | Path | What it does |
|---|---|---|
| POST / GET / DELETE | /mcp |
The streamable-HTTP MCP endpoint (MCP.md) |
| GET | /api/mcp/status |
Connection details, connected clients, totals, telemetry, skills |
| GET | /api/mcp/calls |
Recent tool calls (?limit, client, tool, status, since) |
| POST | /api/mcp/calls |
Record a call (the stdio bridge reports through this) |
| PATCH | /api/mcp/calls/{id} |
Finish a recorded call |
| GET | /api/mcp/calls/events |
SSE feed of calls as they start and settle |
| POST | /api/mcp/skills/install |
Copy the bundled skills. Body { target?, path?, skills? } |
GET / serves the built UI when it has been staged into server/web, and
otherwise a JSON stub saying it has not.
Health
GET /api/health
{ "status": "ok", "version": "0.1.0", "device": "cuda", "gpu_name": null,
"vram_total_mb": null, "vram_free_mb": null,
"loaded_model": "audiocpp-stable-audio-3-sfx" | null, "queue_length": 0 }device comes from the same backend detection audio.cpp uses. Inference runs
inside audio.cpp, so the server cannot query the GPU for its name or VRAM;
gpu_name and the vram_* fields are always null.
Models
GET /api/models → Model[]
{
"id": "audiocpp-stable-audio-3-sfx",
"name": "Stable Audio 3.0 SFX (audio.cpp)",
"vendor": "Stability AI",
"description": "…",
"license": { "name": "Stability AI Community License", "commercial": "conditional",
"url": "https://stability.ai/license", "note": "…" },
"hf_repo": "audio-cpp/audio.cpp-gguf",
"max_duration_s": 120,
"sample_rate": 44100,
"channels": 2,
"supports": { "negative_prompt": false, "seed": true, "steps": true, "cfg": true },
"defaults": { "duration_s": 10, "steps": 8, "cfg": 1 },
"vram_estimate_mb": 2500,
"provider": "local",
"status": "not_installed" | "needs_runtime" | "installed" | "downloading" | "loaded" | "error",
"installed_deps": true,
"error": null | "string",
"avg_duration_s": null | 187.4
}status says whether the model can be used now, so it covers both the weights
and the runtime. needs_runtime means the weights are downloaded but the
audio.cpp runtime is not installed. Fix it with soundbox model install audiocpp
(or POST /api/models/audiocpp/engine/install); there is nothing to download
again. installed_deps reports the runtime half on its own.
provider says where the model runs. It is always local: weights on this
machine, driven by audio.cpp, described by hf_repo and vram_estimate_mb.
avg_duration_s is a rolling average wall time (seconds) of this model's recent successful generate jobs,
null until at least one has completed. The app uses it to show a duration estimate before Generate is
clicked, and it feeds eta_s on queued jobs of the same model.
GET /api/models/{id} → Model, same shape as the list.
POST /api/models/{id}/download → 202 { "job_id": "..." }. Pulls the model's GGUF package from Hugging Face. Progress via GET /api/jobs/{job_id}/events (SSE).
POST /api/models/{id}/load → 200 Model. Loads into VRAM, unloading any other model first (one model resident at a time).
POST /api/models/{id}/unload → 200 Model
DELETE /api/models/{id}/weights → 204. Deletes cached weights. A model
whose weights are your own local files (weights_path, below) has none to
delete and returns 400.
Artwork
Cover art for music and avatars for voices, off by default. The endpoints and
modes are documented in ARTWORK.md.
GET /api/image/status reports what is configured; POST /api/sounds/{id}/artwork
and POST /api/voices/{id}/artwork draw one. A Sound and a Voice each
carry an artwork field: a served /artwork/<file>.png URL, or null.
Models (audio.cpp catalogue)
GET /api/models/audiocpp/catalog → AudioCppCatalogEntry[]. Every model the
audio.cpp adapter knows, enabled or not.
{
"id": "audiocpp-ace-step",
"name": "ACE-Step (audio.cpp)",
"vendor": "ACE Studio",
"description": "…",
"license": { "name": "MIT License", "commercial": "yes", "url": "…", "note": "…" },
"verified": true,
"always_on": false,
"enabled": true,
"category": "music",
"source": "builtin" | "custom"
}always_on models are in GET /api/models regardless; the rest appear only
once their id is in enabled_audiocpp_models (PATCH /api/settings), which
takes effect without a restart. source: "custom" marks an entry from your own
<data dir>/models/custom.json; these are always on and never verified. Use
that file to add a model Soundbox doesn't ship, including one whose weights are
already on your disk; see AUDIOCPP.md.
Generation
POST /api/generate
{ "model": "audiocpp-stable-audio-3-sfx", "prompt": "heavy wooden door slam, dry room",
"negative_prompt": null, "duration_s": 3.0, "seed": null | 1234,
"steps": null, "cfg": null, "variations": 1, "tags": ["door","impact"],
"session_id": null | "uuid" }→ 202 { "job_id": "..." }. If the model isn't loaded, the job loads it first (progress phase loading).
negative_prompt is ignored by models whose supports.negative_prompt is false.
POST /api/separate
{ "model": "audiocpp-htdemucs-sep", "source_sound_id": "uuid",
"stems": null | { "keep": ["vocals"], "rest": "discard" | "merge" } }→ 202 { "job_id": "..." }. Splits a library sound into stems, each saved as
its own sound; the job's result.sounds lists them. model is any model
whose task is sep. stems.keep keeps only the named stems, and
rest: "merge" sums the others into one "rest" sound.
POST /api/repaint
{ "source_sound_id": "uuid", "start_s": 12.0, "end_s": 18.5,
"prompt": null | "brass stabs instead of the pad", "lyrics": null,
"model": null | "audiocpp-ace-step", "strength": null | 0.0..1.0,
"mode": null | "conservative" | "balanced" | "aggressive",
"seed": null, "variations": 1 }→ 202 { "job_id": "..." }. Regenerates start_s..end_s of an existing sound
and keeps the rest of it sample-for-sample. This is the editor's Repaint tool,
applied to the highlighted range. Everything outside the window is inherited
from the source: its model, its length and its lyrics. Omit prompt to reroll
the source's own description.
The same render is available as POST /api/generate with edit: "repaint",
source_sound_id and edit_options.repaint_start_s/repaint_end_s. Use that
form to change the length, the model or the lyrics at the same time. Both
reject the request up front (before rendering) for a window past the end of the
clip, a model that can't repaint (only ACE-Step can today), or a clip longer
than that model renders in one pass.
GET /api/jobs/{job_id} → Job
{ "id": "...", "kind": "generate" | "download" | "separate" | "speak" | "convert" | "segment" | "export", "status": "queued" | "running" | "done" | "error" | "cancelled",
"phase": "queued" | "loading" | "generating" | "encoding" | "done",
"progress": 0.0..1.0, "message": "step 4/8", "error": null,
"request": { ...the request body... },
"result": null | { "sounds": ["sound_id", ...] },
"created_at": "iso", "started_at": "iso|null", "finished_at": "iso|null",
"queue_position": null | 3, "eta_s": null | 210.5 }queue_position is a 1-indexed position among jobs still waiting, and is null
once a job is running or in a terminal state. eta_s is a rough estimate of
how many seconds until this job starts running, derived from a rolling average
of recent successful jobs for the same model; null when there's no timing
history yet for that model, or the job isn't queued.
GET /api/jobs/{job_id}/events → SSE, data: <Job JSON> on every change, closes on terminal status. Send Accept: text/event-stream. Poll GET /api/jobs/{id} if you can't do SSE.
POST /api/jobs/{job_id}/cancel → 200 Job. Cancels a queued or running job.
GET /api/jobs?limit=50 → Job[] newest first (in-memory ring, not persisted).
Logs
GET /api/logs?lines=200 → { "lines": ["...", ...] }. The tail of the persistent log file
(<data_dir>/soundbox-<port>.log; see INSTALL.md), oldest first. The app and
MCP clients use it to show recent server activity. For a full trail, grep the log file itself.
Library
GET /api/sounds?limit=100&offset=0&q=door&tag=impact&favorite=true&model=...&session_id=... → { "items": Sound[], "total": n }
{ "id": "uuid", "created_at": "iso", "model": "audiocpp-stable-audio-3-sfx",
"prompt": "...", "negative_prompt": "...", "duration_s": 3.0, "seed": 1234,
"steps": 8, "cfg": 1.0, "sample_rate": 44100, "channels": 2,
"file": "/files/uuid.wav", "peaks": [0.0, ...], // 256 normalised peaks for the waveform
"favorite": false, "tags": ["door","impact"], "name": "Door slam 01",
"size_bytes": 529244, "session_id": "uuid" | null }GET /api/sounds/{id} → Sound
PATCH /api/sounds/{id} { "name"?, "favorite"?, "tags"? } → Sound
DELETE /api/sounds/{id} → 204 (deletes file too)
GET /api/tags → [{ "tag": "impact", "count": 12 }]
GET /files/{id}.wav: static. ?format=mp3 transcodes on the fly (cached).
Sessions
A session groups a run of related generations so the app can show them as one thread. Some sounds have no session: anything generated before sessions existed, and anything made directly through this API or MCP. The Library view lists every sound regardless of session.
A generation submitted without a session_id creates a session titled from
the prompt. Every new sound bumps its session's updated_at, so the sidebar
can order by last worked on. The resolved id comes back on the job's
request. Pass it on later generations to keep adding to the same thread.
A session_id that no longer exists starts a fresh session; the generation
still runs.
Sessions that produce no audio (the job was cancelled or errored) are pruned automatically.
GET /api/sessions?limit=50 → Session[], newest activity first
{ "id": "uuid", "title": "Glass bottle shattering on tile",
"created_at": "iso", "updated_at": "iso", "sound_count": 2 }POST /api/sessions { "title": "..." } → 201 Session. Rarely needed; generating creates one.
GET /api/sessions/{id} → Session
PATCH /api/sessions/{id} { "title": "..." } → Session
DELETE /api/sessions/{id} → 204. Deletes the session and every sound in it, audio files included.
To read one session's thread, filter the library: GET /api/sounds?session_id={id}.
Settings
GET /api/settings / PATCH /api/settings
{ "data_dir": "C:\\Users\\x\\.soundbox", "hf_token_set": true, "output_format": "wav" | "mp3",
"normalize_lufs": -16.0 | null, "trim_silence": true, "auto_unload_minutes": 10,
"sounds_dir": "C:\\Users\\x\\.soundbox\\sounds",
"llm_mode": "off" | "managed" | "external", "llm_model": "qwen3-1.7b",
"llm_external_base_url": "", "llm_external_model": "", "llm_external_api_key_set": false }PATCH accepts hf_token and llm_external_api_key. Both are write-only and
never echoed; send "" to clear one. enabled_audiocpp_models replaces the
whole list: send every id that should stay enabled.
sounds_dir is where generated audio is written. It is set independently of
audiocpp_models_dir, so weights and sounds can live on different disks. It is
always returned resolved, never null; PATCH it with "" to go back to
<data_dir>/sounds. Changing it moves the existing library to the new folder
(the database stores bare filenames), so the request is refused with 400 while
anything is queued or generating.
GET /api/fs/dirs?path= lists the folders inside path on the server's own
disk. The "Browse…" buttons in Settings use it. It returns
{ path, parent, separator, home, writable, roots, entries }, where entries
is subdirectories only and roots is the drive letters (Windows) or /.
Omit path to start at the server user's home directory. A path that no
longer exists resolves to its nearest existing ancestor.
Read-only, directory names only, and reachable on the same loopback bind as
everything else. It is a server route because the chosen path is one the
server writes to, which a browser file input cannot provide.
Profiles
Several libraries in one install. A profile is a data directory with its own
soundbox.db, sounds/, artwork/ and settings.json, so switching profiles
changes everything the library endpoints return. Model weights (models/) and
the runtime binaries (bin/) live at the install root and are shared by every
profile.
The default profile is the data directory itself (~/.soundbox). An install
that predates profiles is therefore the default profile. Named profiles live in
~/.soundbox/profiles/{id}.
GET /api/profiles
{ "active": "default",
"profiles": [
{ "id": "default", "name": "Default", "created_at": "",
"data_dir": "C:\\Users\\x\\.soundbox", "active": true },
{ "id": "demo", "name": "Demo", "created_at": "2026-02-01T10:00:00Z",
"data_dir": "C:\\Users\\x\\.soundbox\\profiles\\demo", "active": false }
] }POST /api/profiles { "name": "Demo" } → 201 Profile. Creates the
profile without switching to it. The id is a slug of the name, fixed at creation.
PATCH /api/profiles/{id} { "name": "..." } → Profile. Renames. The id and
its directory do not change.
POST /api/profiles/{id}/activate → the GET payload, with the new active id.
Switches the running server in place: no restart, and the loaded model stays in
VRAM. Returns 400 while a job is queued or running, because a generation
writes its file and database row at the end and would land in the wrong library.
DELETE /api/profiles/{id} → 204. Removes it from the list and leaves its
folder on disk. ?delete_data=1 also deletes the directory and every sound in
it. The default profile and the active profile cannot be deleted.
Server-side, --profile NAME opens one at startup; without it the last active
profile is reopened.
Prompt assistant
An optional local LLM that rewrites a terse prompt ("scary sound") into a
descriptive one, off by default (llm_mode: "off"). managed runs a small
model Soundbox installs and manages (llama-server + a curated GGUF);
external talks to any OpenAI-compatible endpoint already running on this
machine. Check detected (below) before pointing someone at the managed install.
POST /api/enhance { "prompt": "scary sound", "n"?: 1..5, "category"?: "sfx" | "music", "lyrics"?: false }
→ { "variations": ["heavy wooden door slamming shut in a stone corridor"] }. 400 with an actionable message
when the assistant is off or unconfigured.
POST /api/assistant/chat: the multi-turn studio assistant. A conversation that
explains the installed models (licence, limits, how each family wants to be prompted) and ends
in a request ready to generate. Stateless: send the whole thread each time.
{ "messages": [{ "role": "user", "content": "I need a door slam for a horror game" },
{ "role": "assistant", "content": "Wooden or metal?" },
{ "role": "user", "content": "wooden, huge" }],
"draft"?: { "model": "audiocpp-stable-audio-3-sfx", "prompt": "..." } }→ { "reply": "Going with Stable Audio 3.0 SFX — commercial use is conditional on its licence.", "proposal": { "model": "audiocpp-stable-audio-3-sfx", "prompt": "a massive oak door slamming shut in a cold stone corridor, close mic", "negative_prompt": null, "lyrics": null, "track_name": null, "duration_s": 4, "tags": ["door", "slam", "oak", "stone"] } }. proposal is null while the
assistant is still asking; when present its model is guaranteed to be an installed generation
model, duration_s fits that model, and lyrics only ride on a model that sings. draft is the
composer's current contents, so "make it longer" has something to refer to.
GET /api/llm/status → engine/model install state plus discovery:
{ "mode": "off", "engine": { "installed": false, "version": null, "backend": null },
"model": null,
"catalog": [{ "id": "qwen3-1.7b", "name": "Qwen3 1.7B", "vendor": "Alibaba", "license": "Apache-2.0",
"size_bytes": 1834426016, "description": "...", "recommended": true, "installed": false }],
"external": { "base_url": "", "model": "", "api_key_set": false },
"running": false,
"detected": [{ "label": "Ollama", "base_url": "http://127.0.0.1:11434", "models": ["llama3.1"] }] }detected is a best-effort probe of the well-known local ports for Ollama, LM Studio and a bare
llama-server. Each one that answers its OpenAI-compat /v1/models is listed, so the app can offer
to use it instead of installing a second local server.
POST /api/llm/engine/install { "version"? } → 202 { "job_id": "..." }. Installs llama-server.
POST /api/llm/models/{id}/download → 202 { "job_id": "..." }. Pulls one catalog model's GGUF.
DELETE /api/llm/models/{id} → 204. Deletes cached weights.
MCP
Streamable-HTTP MCP endpoint at /mcp (same port). Twenty-one tools; server/src/mcp/server.ts is the authority on their signatures, and MCP.md documents them in full. Summary:
Sounds:
soundbox.generate(prompt, duration_s?, model?, negative_prompt?, seed?, variations?, tags?, lyrics?, source_sound_id?, edit?)→ waits for the job, returnsSound[]with absolute file paths.soundbox.enhance_prompt(prompt, n?, category?, lyrics?, model?)→{ variations: string[] }, or a plain-text explanation when the assistant is off.soundbox.separate(sound_id, model?, stems?, rest?)→ waits for the job, returns the stems asSound[]with absolute file paths.soundbox.list_models()→Model[]soundbox.list_sounds(q?, tag?, limit?)→Sound[]soundbox.get_sound(sound_id)→Sound+ absolute pathsoundbox.delete_sound(sound_id)
Voices:
soundbox.list_voices()→ the saved voices a script can be cast to.soundbox.speak(lines, model?, gap_s?, name?, delivery?)→ waits, returns aSound.
Stories (a script that persists, with lines placed in time):
soundbox.create_story(title?, delivery?),soundbox.list_stories(limit?),soundbox.get_story(story_id)soundbox.add_lines(story_id, lines),soundbox.update_line(line_id, …),soundbox.delete_line(line_id),soundbox.split_line(line_id, at_s)soundbox.speak_story(story_id, model?, regenerate?)→ reads every silent line; the slow one.soundbox.export_story(story_id)→ mixes down to oneSound.
Arrangement:
soundbox.open_in_studio(sound_id, separate?, stems?, rest?)soundbox.mix_track(story_id, track_index, name?, muted?, soloed?, gain_db?),soundbox.remove_track(story_id, track_index)
Stdio bridge for clients that can't do HTTP: node <checkout>/server/dist/mcp/stdio.js, which proxies to the running server. The file exists only after npm run build, and the bridge covers the sound tools (generate, enhance_prompt, list_models, list_sounds, get_sound, delete_sound). Use HTTP for separate, speech and stories. The Agents page prints the line with the path resolved.