sonilo
v0.3.0
Published
Official TypeScript/JavaScript client for the Sonilo API
Downloads
422
Maintainers
Readme
sonilo
Official TypeScript/JavaScript client for the Sonilo API. Works in Node.js ≥ 18 and modern browsers. Zero runtime dependencies.
Installation
npm install soniloQuickstart
import { SoniloClient } from "sonilo";
const sonilo = new SoniloClient(); // reads SONILO_API_KEY
const track = await sonilo.textToMusic.generate({
prompt: "cinematic orchestral score",
duration: 60,
});
// track.audio is a Uint8Array of MP3 bytesVideo to music
// Node: file path, browser: File/Blob from an <input type="file">
const track = await sonilo.videoToMusic.generate({
video: "./my_video.mp4",
prompt: "upbeat, energetic",
});
// Or point at a hosted video
await sonilo.videoToMusic.generate({ videoUrl: "https://example.com/clip.mp4" });Vocal isolation (async)
Splitting out a vocals-only stem requires the async task API — the plain
stream above doesn't support it. Submit with isolateVocals: true (this
implies mode: "async" if you don't set mode yourself) and poll with
client.tasks.wait<MusicTaskResult>():
import { SoniloClient, download } from "sonilo";
import type { MusicTaskResult } from "sonilo";
import { writeFile } from "node:fs/promises";
const client = new SoniloClient();
const task = await client.videoToMusic.submit({
video: "./my_video.mp4",
prompt: "upbeat, energetic",
isolateVocals: true,
});
const result = await client.tasks.wait<MusicTaskResult>(task.task_id);
// `audio` is always an array for async video-to-music (one entry per
// output stream); `vocals` and `mux` are only present with isolateVocals.
await writeFile("mix.m4a", await download(result.audio[0]!));
await writeFile("vocals.m4a", await download(result.vocals!));Configuration
const client = new SoniloClient({
apiKey: "sk_...", // defaults to SONILO_API_KEY
baseUrl: "https://api.sonilo.com",
timeout: 600_000, // milliseconds, default 600000 (10 minutes)
});timeout bounds one-shot requests (account, tasks, SFX submits) and
download() — it protects against a stalled connection hanging forever.
It does not bound streaming music generation
(textToMusic/videoToMusic .stream()/.generate()): those hold the
response body open for as long as generation takes, so an absolute timeout
would kill a healthy long-running stream. Pass your own signal in
TextToMusicParams/VideoToMusicParams (e.g. from an AbortController,
or AbortSignal.timeout(ms) for an absolute cap) to bound or cancel a
music stream instead — it's forwarded to fetch as-is and never
rewrapped as RequestTimeoutError.
Streaming
import { SoniloClient, isAudioChunkEvent } from "sonilo";
for await (const event of sonilo.textToMusic.stream({ prompt: "lofi", duration: 30 })) {
if (isAudioChunkEvent(event)) {
// event.data is a Uint8Array — feed it to your player as it arrives
}
}Segments
Shape the composition with start-only contiguous segments (each ends where the next begins):
await sonilo.textToMusic.generate({
prompt: "epic trailer",
duration: 60,
segments: [
{ start: 0, prompt: "soft intro", label: "intro" },
{ start: 20, prompt: "building tension", label: "verse" },
{ start: 40, prompt: "full orchestra", label: "chorus" },
],
});Sound effects (async tasks)
SFX endpoints are asynchronous: submitting returns a task_id, and the result
is fetched by polling. generate() wraps submit + poll:
import { SoniloClient, download } from "sonilo";
import { writeFile } from "node:fs/promises";
const client = new SoniloClient();
const result = await client.textToSfx.generate({ prompt: "glass shattering", duration: 5 });
await writeFile("sfx.m4a", await download(result.audio));Or control polling yourself:
const task = await client.videoToSfx.submit({
video: "clip.mp4", // Node.js path; pass File/Blob in the browser
segments: [{ start: 0, end: 2.5, prompt: "footsteps on gravel" }],
audioFormat: "wav",
});
const result = await client.tasks.wait(task.task_id, { pollInterval: 2000, timeout: 600000 });tasks.get(taskId) fetches state once and never throws on a failed task;
tasks.wait() / generate() throw TaskFailedError (with .code,
.refunded) on failure and TaskTimeoutError if the deadline passes — the
task keeps running server-side and can still be polled afterwards. Result URLs
are presigned and expire; download promptly or re-fetch via tasks.get.
Account
const services = await sonilo.account.services();
const usage = await sonilo.account.usage({ days: 7 });Errors
All errors extend SoniloError: AuthenticationError (401),
PaymentRequiredError (402), RateLimitError (429, .retryAfter),
BadRequestError (400/413/422, .detail), APIError (anything else),
GenerationError for failures mid-stream, TaskFailedError (.code,
.taskId, .refunded) for a failed SFX task, TaskTimeoutError
(.taskId) when tasks.wait() / generate() hits its deadline, and
RequestTimeoutError when a one-shot request or download() is aborted
by its own timeout (a caller-supplied AbortSignal is never rewrapped
this way, and streaming music generation is never subject to this timeout
at all).
Every APIError also carries .status, .body (the parsed response),
.code (the API's error code, e.g. "rate_limit_exceeded"), and .errors
(the validation detail array on a 422), in addition to any subclass-specific
properties above.
