TypeScript client
这是供维护者和客户端开发者查阅的次级指南。音乐创作教程保留为文档主入口。当前客户端通过 FastAPI 创建 Project、上传 Reference Audio、跟踪 Job、取得 Candidate 和显式保存 Version。它不连接 ComfyUI。
准备独立环境
Section titled “准备独立环境”从仓库根目录运行。需要 Node 24.18.0、pnpm 11.22.0、uv 0.11.28 和 Python 3.12.13。FastAPI 与 Runtime 继续使用独立环境和锁文件。
pnpm install --frozen-lockfileuv sync --project services/api --locked --python 3.12.13pnpm client:checkpnpm --filter @llm-music/api-client buildclient:check 在两个隔离目录执行 music-api openapi,比较生成结果与受版本控制的 schema.ts,然后检查 TypeScript。导出不创建应用数据库、不启动 Runtime、不读取模型。生成失败时,终端给出保留的临时证据目录。修改 Pydantic 接口后运行 pnpm client:generate,提交并审阅生成差异。
得到一个可检查的结果
Section titled “得到一个可检查的结果”先在一个终端启动隔离的 fake API:
$env:MUSIC_API_RUNTIME_MODE = "fake"uv run --project services/api --no-sync music-api serve --data-dir data/client-example-fake --port 8000在另一个终端执行完整 生成与保存示例:
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示例创建一个 Project,提交一次 Generate,读取 Candidate 与 Score,下载音频和 ABC,核对 SHA256 与字节数,再显式选择该 Candidate 并保存一次 Version。终端返回真实应用 id;新输出目录保留 receipt.json、song.flac 和 score.abc。fake 音频是测试音,不能说明音乐质量或真实推理。输出目录已存在时,命令拒绝覆盖。
真实 Runtime 仍需先通过 应用诊断 并使用独立的真实数据目录。只有 GPU 资源 owner 执行真实推理;示例的 --expect-mode comfyui 会要求连接的 FastAPI 报告该模式与 readiness。此参数不会启动 Runtime或更新就绪凭据。
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;}完整示例: packages/api-client/examples/generate-save.ts ↗
客户端入口 只提供 typed createMusicClient、jobEventsUrl 和生成 schema 的别名。Project、Asset、Job、Candidate、Version 与诊断类型来自 components;HTTP 路径和方法来自 paths。
| 操作 | 传输与检查 |
|---|---|
| 上传 | POST /projects/{project_id}/assets;生成的 file 类型是 Blob。用原生 File 与 FormData,通过该请求的 bodySerializer 设置 multipart body;由 fetch 生成 boundary,勿手写 Content-Type。 |
| 转谱 | 当前支持 16 秒 PCM16 mono24k 或 stereo48k。上传成功不表示任意 600 秒音频可推理。完成后读取 Job 返回的 Score、ABC 和 MIDI 应用 id。 |
| 从选定乐谱生成 | POST /projects/{project_id}/jobs/generate-from-score 使用生成的 GenerateFromScoreCreate,提交明确 ABC、同 Project 来源 Score、可选来源 parent 与生成设置。原文与有效推理文本分别保留;完整操作与限制。 |
| 下载 | 用 parseAs: 'arrayBuffer' 或 'blob'。成功的 MIME 可以是 WAV、FLAC、ABC 或 MIDI;与 Asset 元数据核对字节数和 SHA256。失败响应仍为 typed JSON error。 |
| 取消与重试 | 两个 POST 都没有 body。取消可返回 202 意图或 200 终态;继续查询原 Job。显式 retry 返回 202 和新 Job,客户端不会自动重试。 |
| 保存 | Candidate 不自动成为 Version。第一次显式保存返回 201;相同 Candidate 的重复保存返回 200 和原 Version id。 |
| 错误 | 读取 response.status 和 error.error.code/message/recovery。错误码是开放的 string;网络断开会抛出传输错误,不伪造成业务 503。 |
当前 seed 的服务端范围是 0 到 2^63−1 的 JSON 整数。JavaScript Number 只能精确表达 0 到 Number.MAX_SAFE_INTEGER(2^53−1)。客户端必须选择非负 safe integer;示例用 2026192201。此版本没有字符串 seed 合同,也不承诺超出安全范围的 JSON 数字保持精确。
事件丢失与失败恢复
Section titled “事件丢失与失败恢复”jobEventsUrl(baseUrl, projectId, jobId) 从实际注册的 WebSocket 路由生成 URL,并保留应用部署前缀。FastAPI 的 OpenAPI x-websockets 描述该通道及同源 JobEventRead payload。生成 TypeScript 类型不等于校验任意 WebSocket JSON;面对不可信服务时仍需在客户端边界做运行时校验。
连接后先取得持久 Job 快照,后续消息是 job.updated。sequence 只表示本进程事件顺序,不能当持久 replay cursor。连接丢失时先 GET /projects/{project_id}/jobs/{job_id},再重连;终态连接发送同一持久结果后关闭。不存在或其他 Project 的 Job 在 upgrade 阶段拒绝,当前 HTTP 状态是 403。
示例失败时保留 receipt 中的 Project/Job id,并读取原 Job。输入无效时修正输入;Runtime 不可用时先恢复 readiness;等待或连接失败时核对原任务。POST 不会自动重发,避免重复生成。已完成数据可在 API 重启后继续读取;正在执行任务的恢复见 Job 恢复指南。
无 GPU 验证
Section titled “无 GPU 验证”pnpm test:client该命令编译真实 Node consumer,并向生产 FastAPI 的隔离端口发送原生 fetch、FormData 和 WebSocket 请求。继承的 Fake Runtime 只控制完成、失败和取消时机;不替换业务 HTTP 路由。验证两条闭环、队列/当前任务、文件哈希、显式保存 201/200、取消/重试、404/409/422/503、断开恢复及已完成数据重新打开。测试使用自己的 API 进程和数据;不会连接保留的 Runtime 8188。
默认证据位于 packages/api-client/.artifacts/。MUSIC_CLIENT_ARTIFACTS 可指定隔离目录。每次执行保留端口、实际 API leaf PID、创建时间、进程日志和停止回执;API 正常停止后,已完成数据仍保留。这个 CPU 结果不能替代真实 GPU smoke;P1 的实际证据与限制记录于 阶段验收维护报告。
1ce3268正文来源: docs/guides/api-client.md