TypeScript client
This is a secondary guide for maintainers and client developers. Music creation tutorials remain the main documentation entry. The current client uses FastAPI to create Projects, upload Reference Audio, track Jobs, inspect Candidates and explicitly save Versions. It does not connect to ComfyUI.
Prepare the independent environment
Section titled “Prepare the independent environment”Run from the repository root. Use Node 24.18.0, pnpm 11.22.0, uv 0.11.28 and Python 3.12.13. FastAPI and Runtime retain separate environments and lockfiles.
pnpm install --frozen-lockfileuv sync --project services/api --locked --python 3.12.13pnpm client:checkpnpm --filter @llm-music/api-client buildclient:check runs music-api openapi in two isolated directories, compares the generated output with the versioned schema.ts, then checks TypeScript. Export does not create an application database, start Runtime or read models. A failed generation prints the retained temporary evidence directory. After changing the Pydantic contract, run pnpm client:generate, commit and review its generated diff.
Get an inspectable result
Section titled “Get an inspectable result”First start an isolated fake API in one terminal:
$env:MUSIC_API_RUNTIME_MODE = "fake"uv run --project services/api --no-sync music-api serve --data-dir data/client-example-fake --port 8000In another terminal run the complete generate-and-save example:
node packages/api-client/dist/examples/generate-save.js --base-url http://127.0.0.1:8000 --expect-mode fake --output-dir data/client-example-output-01The example creates a Project, submits Generate once, reads the Candidate and Score, downloads audio and ABC, checks SHA256 and byte counts, then explicitly selects this Candidate and saves one Version. The terminal returns actual application ids. The new output directory retains receipt.json, song.flac and score.abc. Fake audio is a test tone; it does not demonstrate music quality or real inference. The command refuses to overwrite an existing output directory.
Real Runtime first requires application diagnostics and a separate real data directory. Only the GPU resource owner performs real inference. The example’s --expect-mode comfyui requires the connected FastAPI to report that mode and readiness. This option does not start Runtime or refresh readiness receipts.
import { createHash } from 'node:crypto';import { mkdir, writeFile } from 'node:fs/promises';import { dirname, resolve } from 'node:path';import { setTimeout as delay } from 'node:timers/promises';import { parseArgs } from 'node:util';import { createMusicClient, type GenerateCreate } from '../src/index.js';
const { values } = parseArgs({ options: { 'base-url': { type: 'string', default: 'http://127.0.0.1:8000' }, 'output-dir': { type: 'string' }, 'expect-mode': { type: 'string', default: 'fake' },} });if (!values['output-dir'] || !['fake', 'comfyui'].includes(values['expect-mode'])) { throw new Error('Pass a new --output-dir and --expect-mode fake|comfyui');}const output = resolve(values['output-dir']);await mkdir(dirname(output), { recursive: true });await mkdir(output); // Retain previous attempts; never overwrite or automatically resubmit.const client = createMusicClient({ baseUrl: values['base-url'], fetch: request => fetch(request, { signal: AbortSignal.any([request.signal, AbortSignal.timeout(15_000)]),}) });function read<T>(result: { data?: T; error?: unknown; response: Response }): T { if (!result.response.ok || result.data === undefined) { throw new Error(`HTTP ${result.response.status}: ${JSON.stringify(result.error)}`); } return result.data;}const input: GenerateCreate = { style: 'gentle folk pop, warm acoustic guitar, soft female vocal, relaxed tempo', lyrics: '[verse]\nMorning gathers on the window\nI follow sunlight down the street', seed: 2026192201, max_seconds: 35,};if (!Number.isSafeInteger(input.seed)) throw new Error('Choose a nonnegative safe integer seed');const journal: Record<string, unknown> = { baseUrl: values['base-url'], input, expectedMode: values['expect-mode'] };const record = () => writeFile(resolve(output, 'receipt.json'), `${JSON.stringify(journal, null, 2)}\n`);await record();try { const health = read(await client.GET('/health')); if (health.runtime.mode !== values['expect-mode'] || !health.runtime.ready) { throw new Error(`Runtime mode/readiness differs: ${JSON.stringify(health.runtime)}`); } journal.scope = health.runtime.mode === 'fake' ? 'CPU test tone; no model inference or music quality claim' : 'Application API to owned real Runtime'; const project = read(await client.POST('/projects', { body: { name: 'Morning song client example' } })); journal.project = project; await record(); const params = { path: { project_id: project.id } }; const submitted = read(await client.POST('/projects/{project_id}/jobs/generate', { params, body: input })); journal.submitted = submitted; await record(); const deadline = Date.now() + 1_800_000; let job = submitted; while (job.status === 'queued' || job.status === 'running') { if (Date.now() >= deadline) throw new Error(`Wait expired. Read the existing Job ${job.id} before a new attempt`); await delay(200); job = read(await client.GET('/projects/{project_id}/jobs/{job_id}', { params: { path: { project_id: project.id, job_id: submitted.id } }, })); } journal.job = job; await record(); const candidateId = job.result?.candidate_id; if (job.status !== 'completed' || !candidateId) throw new Error(`Job failed: ${JSON.stringify(job.error)}`); const candidate = read(await client.GET('/projects/{project_id}/candidates/{candidate_id}', { params: { path: { project_id: project.id, candidate_id: candidateId } }, })); const score = read(await client.GET('/projects/{project_id}/scores/{score_id}', { params: { path: { project_id: project.id, score_id: candidate.score_id } }, })); for (const [role, id] of [['song', candidate.audio_asset_id], ['score', score.abc_asset_id]]) { if (!id) throw new Error(`Missing ${role} Asset`); const params = { path: { project_id: project.id, asset_id: id } }; const asset = read(await client.GET('/projects/{project_id}/assets/{asset_id}', { params })); const bytes = read(await client.GET('/projects/{project_id}/assets/{asset_id}/content', { params, parseAs: 'arrayBuffer' })); const hash = createHash('sha256').update(new Uint8Array(bytes)).digest('hex'); if (hash !== asset.sha256 || bytes.byteLength !== asset.size_bytes) throw new Error(`Asset bytes differ: ${id}`); await writeFile(resolve(output, `${role}.${asset.format}`), new Uint8Array(bytes), { flag: 'wx' }); } // This executable example explicitly chooses and saves its inspected Candidate. const version = read(await client.POST('/projects/{project_id}/versions', { params, body: { candidate_id: candidate.id, name: 'Selected morning client example' }, })); journal.candidate = candidate; journal.version = version; await record(); process.stdout.write(`${JSON.stringify({ projectId: project.id, jobId: job.id, candidateId, versionId: version.id, output, scope: journal.scope })}\n`);} catch (error) { journal.failure = String(error); await record(); throw error;}Complete example: packages/api-client/examples/generate-save.ts ↗
Requests and results
Section titled “Requests and results”The client entry only provides typed createMusicClient, jobEventsUrl and aliases from the generated schema. Project, Asset, Job, Candidate, Version and diagnostic types come from components; HTTP paths and methods come from paths.
| Operation | Transport and checks |
|---|---|
| Upload | POST /projects/{project_id}/assets; generated file is a Blob. Use a native File and FormData with this request’s bodySerializer; let fetch set the boundary instead of writing Content-Type. |
| Transcribe | Current support is 16-second PCM16 mono24k or stereo48k. Successful upload does not prove support for arbitrary 600-second inference. After completion, read the Score, ABC and MIDI application ids returned by the Job. |
| Generate from a selected Score | POST /projects/{project_id}/jobs/generate-from-score uses generated GenerateFromScoreCreate, with explicit ABC, same-Project source Score, optional source parent and generation settings. Original and effective inference text are retained separately. See the complete operation and limits. |
| Download | Use parseAs: 'arrayBuffer' or 'blob'. Success MIME can be WAV, FLAC, ABC or MIDI. Check byte count and SHA256 against Asset metadata. Failed responses still return typed JSON error. |
| Cancel and retry | Both POSTs have no body. Cancel may return 202 intent or 200 terminal state; keep reading the original Job. Explicit retry returns 202 and a new Job. The client never retries automatically. |
| Save | A Candidate does not automatically become a Version. The first explicit save returns 201; repeating a save for that Candidate returns 200 and the existing Version id. |
| Error | Read response.status and error.error.code/message/recovery. Error codes are open strings. A network failure throws a transport error instead of fabricating a domain 503. |
The current server accepts JSON integer seed from 0 through 2^63−1. JavaScript Number can represent only 0 through Number.MAX_SAFE_INTEGER (2^53−1) exactly. Client callers must choose a nonnegative safe integer; the example uses 2026192201. This release has no string-seed contract and does not promise exact JSON numbers above the safe range.
Recover lost events and failures
Section titled “Recover lost events and failures”jobEventsUrl(baseUrl, projectId, jobId) derives the URL from the registered WebSocket route and preserves an application deployment prefix. FastAPI’s OpenAPI x-websockets describes this channel and its Pydantic JobEventRead payload. Generated TypeScript types do not validate arbitrary WebSocket JSON. Clients that face an untrusted service still need runtime validation at that boundary.
A connection first receives a persisted Job snapshot; later messages use job.updated. sequence orders events in the current process and is not a durable replay cursor. After losing a connection, first GET /projects/{project_id}/jobs/{job_id}, then reconnect. A terminal connection sends the same durable result and closes. A missing Job or a Job in another Project is rejected during upgrade; its current HTTP status is 403.
On example failure, retain the Project/Job ids in the receipt and read the original Job. Correct invalid inputs; restore readiness when Runtime is unavailable; inspect the original task after a wait or connection failure. POSTs are never sent again automatically, which prevents duplicate generation. Completed data remains readable after API restart. For active Job recovery, see the Job recovery guide.
Verify without a GPU
Section titled “Verify without a GPU”pnpm test:clientThis command compiles a real Node consumer and sends native fetch, FormData and WebSocket requests to production FastAPI on an isolated port. The inherited Fake Runtime only controls completion, failure and cancellation timing. It does not replace business HTTP routes. Checks cover both loops, queue/current Job, file hashes, explicit 201/200 save, cancel/retry, 404/409/422/503, disconnect recovery and completed data reopen. Tests own their API processes and data and never connect to the retained Runtime 8188.
Default evidence is under packages/api-client/.artifacts/. Set MUSIC_CLIENT_ARTIFACTS for an isolated directory. Each run retains the port, actual API leaf PID, creation time, process logs and stop receipt. Completed data remains after a graceful API stop. This CPU result cannot replace real GPU smoke. The actual P1 evidence and limits are recorded in the stage acceptance maintenance report.
1ce3268Page source: docs/guides/api-client.en.md