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, voice_id, source, limit, offset). voice_id lists what one saved voice has read: single-voice reads and conversions, not multi-voice dialogues |
| 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. Each carries read_count and, for a clone, samples: [{ id, file, duration_s, created_at }] |
| GET | /api/voices/{id} |
One voice |
| POST | /api/voices |
Body { name, kind: "clone" | "builtin", voice_id?, model_id?, notes?, source_sound_id?, tidy? } |
| POST | /api/voices/upload |
Clone from a WAV. Body is the file; ?name, notes, model_id, tidy |
| PATCH | /api/voices/{id} |
Body { name?, notes? } |
| DELETE | /api/voices/{id} |
Delete the voice and every clip file |
| POST | /api/voices/{id}/samples |
Add a clip to a clone. Body is a WAV (?tidy), or JSON { source_sound_id, tidy? } |
| DELETE | /api/voices/{id}/samples/{sample_id} |
Remove a clip. Refused for a voice's last one |
| GET | /api/voices/{id}/export |
The voice as a .soundbox-voice.json download |
| POST | /api/voices/import |
Body is an exported file. Makes a copy under a new id |
A clone voice holds one to five clips of the same speaker. audio.cpp takes a
single --voice-ref, so with more than one the server joins them: each made
mono, levelled to the median loudness of the set, with 0.3 s between them.
reference_file is that joined file, or the only clip. A clip is refused if it
is under 1 s, over 60 s or almost silent, and a voice's clips together stay
under 90 s — past that, models read the start of the reference and ignore the
rest.
tidy (off by default over the API, on by default in the app) takes out DC
offset, cuts near-silence off both ends with a little padding left on, and pulls
a peak above full scale back under it. It never makes a clip louder or changes
its tone, because that is the voice.
An export is JSON: { format: "soundbox-voice", version: 1, voice: { name, kind, voice_id, model_id, notes }, samples: [{ wav_base64, duration_s }] }. The clips
travel and the joined file does not; import rebuilds it. The avatar is not
included and can be redrawn from the notes. On import, a name already in the
library gets " (2)", and a model_id this Soundbox does not know is dropped
rather than failing the import.
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": "NVIDIA GeForce RTX 3080", "vram_total_mb": 10240, "vram_free_mb": 9800,
"loaded_model": "audiocpp-stable-audio-3-sfx" | null, "queue_length": 0 }device comes from the same backend detection audio.cpp uses. On NVIDIA
hardware gpu_name and the vram_* fields come from nvidia-smi, for the
first GPU in CUDA_VISIBLE_DEVICES or GPU 0 when that is unset. Without
nvidia-smi (Metal, AMD, Intel, CPU) they are 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.