Skip to content
Search docs

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.

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.

Terminal window
pnpm install --frozen-lockfile
uv sync --project services/api --locked --python 3.12.13
pnpm client:check
pnpm --filter @llm-music/api-client build

client: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.

First start an isolated fake API in one terminal:

Terminal window
$env:MUSIC_API_RUNTIME_MODE = "fake"
uv run --project services/api --no-sync music-api serve --data-dir data/client-example-fake --port 8000

In another terminal run the complete generate-and-save example:

Terminal window
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-01

The 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 ↗

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.

OperationTransport and checks
UploadPOST /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.
TranscribeCurrent 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 ScorePOST /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.
DownloadUse 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 retryBoth 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.
SaveA 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.
ErrorRead 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.

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.

Terminal window
pnpm test:client

This 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.

Source version: 1ce3268Page source: docs/guides/api-client.en.md