sonilo 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +148 -4
- package/dist/index.cjs +71 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +123 -11
- package/dist/index.d.ts +123 -11
- package/dist/index.js +70 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ npm install sonilo
|
|
|
11
11
|
|
|
12
12
|
## Authentication
|
|
13
13
|
|
|
14
|
-
Create an API key in your [Sonilo dashboard](https://platform.sonilo.com/dashboard/api-keys),
|
|
14
|
+
Create an API key in your [Sonilo dashboard](https://platform.sonilo.com/dashboard/api-keys?utm_source=sonilo_js&utm_medium=readme&utm_campaign=sdk_quickstart),
|
|
15
15
|
then give it to the client either as an environment variable (recommended) or
|
|
16
16
|
inline:
|
|
17
17
|
|
|
@@ -107,6 +107,46 @@ if (result.ducked) {
|
|
|
107
107
|
}
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
+
### Variants (async)
|
|
111
|
+
|
|
112
|
+
`variantsNum` generates several distinct music variants in one request (1-10,
|
|
113
|
+
default 1) — each is its own creative direction, with its own title. It's
|
|
114
|
+
available on `textToMusic`, `videoToMusic`, `videoToVideoMusic`,
|
|
115
|
+
`videoToSound` and `videoToVideoSound`. Cost scales linearly with the count,
|
|
116
|
+
and **values above 1 are never covered by the free trial**.
|
|
117
|
+
|
|
118
|
+
On `textToMusic`/`videoToMusic`, `variantsNum` above 1 requires the async task
|
|
119
|
+
API — same as `preserveSpeech` above — so it implies `mode: "async"` if you
|
|
120
|
+
don't set `mode` yourself; `stream()`/`generate()` never send it, since they
|
|
121
|
+
always request a plain stream. `videoToVideoMusic`, `videoToSound` and
|
|
122
|
+
`videoToVideoSound` are already async-only, so no extra `mode` handling is
|
|
123
|
+
needed there.
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
const task = await client.textToMusic.submit({
|
|
127
|
+
prompt: "warm lo-fi piano",
|
|
128
|
+
duration: 30,
|
|
129
|
+
variantsNum: 3,
|
|
130
|
+
});
|
|
131
|
+
const result = await client.tasks.wait<MusicTaskResult>(task.task_id);
|
|
132
|
+
|
|
133
|
+
// audio has one entry per variant; each entry may carry its own `title`.
|
|
134
|
+
for (const variant of result.audio ?? []) {
|
|
135
|
+
console.log(variant.title?.title, variant.url);
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`videoToVideoMusic` returns one video per variant in `videos[]`, with `video`
|
|
140
|
+
kept as a permanent alias for `videos[0]`. `videoToSound`/`videoToVideoSound`
|
|
141
|
+
return one entry per variant in `outputs[]`, each shaped like the top-level
|
|
142
|
+
result (`output_url`, `output_type`, `output_bytes`, `music`,
|
|
143
|
+
`music_processed?`, `sfx`) — the top-level fields remain permanent aliases for
|
|
144
|
+
`outputs[0]`. All of these arrays are present even at the default
|
|
145
|
+
`variantsNum` of 1, as a single-entry array; every other field is unchanged.
|
|
146
|
+
|
|
147
|
+
`GET /v1/tasks/{id}` (`tasks.get`/`tasks.wait`) echoes the request's
|
|
148
|
+
`variantsNum` back as `variants_num`, but only when it was above 1.
|
|
149
|
+
|
|
110
150
|
## Video to video
|
|
111
151
|
|
|
112
152
|
Generate a soundtrack or sound effects and get back a **re-hosted video** with
|
|
@@ -175,6 +215,57 @@ Input videos may be at most 180 seconds long.
|
|
|
175
215
|
Use `submit()` instead of `generate()` to get a `task_id` back immediately and
|
|
176
216
|
poll it yourself with `client.tasks.wait<SoundResult>(taskId)`.
|
|
177
217
|
|
|
218
|
+
## Dubbing
|
|
219
|
+
|
|
220
|
+
`client.dubbing.submit()` / `.generate()` dub a video into one or more target
|
|
221
|
+
languages in a single async call — one call, one task, one dubbed video per
|
|
222
|
+
language.
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
import { SoniloClient } from "sonilo";
|
|
226
|
+
import type { DubbingResult } from "sonilo";
|
|
227
|
+
|
|
228
|
+
const client = new SoniloClient();
|
|
229
|
+
|
|
230
|
+
const task = await client.dubbing.submit({
|
|
231
|
+
videoUrl: "https://example.com/clip.mp4",
|
|
232
|
+
languages: ["es", "fr"],
|
|
233
|
+
});
|
|
234
|
+
const result = await client.tasks.wait<DubbingResult>(task.task_id);
|
|
235
|
+
for (const [language, url] of Object.entries(result.outputs ?? {})) {
|
|
236
|
+
console.log(language, url);
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
`generate()` wraps submit + poll, same as the other async endpoints, and
|
|
241
|
+
accepts a `{ timeout }` option to override the default 10-minute wait. The
|
|
242
|
+
dubbing pipeline can take much longer than that, especially with several
|
|
243
|
+
languages in one call, so pass a longer timeout for anything but the shortest
|
|
244
|
+
clips. 7,200,000 ms matches the backend's own ceiling for a dubbing job and is
|
|
245
|
+
what the CLI defaults to. For long jobs you can also use `submit()` plus your
|
|
246
|
+
own `client.tasks.wait()`, as above:
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
const result = await client.dubbing.generate(
|
|
250
|
+
{ videoUrl: "https://example.com/clip.mp4", languages: ["es", "fr"] },
|
|
251
|
+
{ timeout: 7_200_000 }, // 2 hours, the backend's own ceiling
|
|
252
|
+
);
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Params: exactly one of `video` / `videoUrl` (`videoUrl` must be **https** —
|
|
256
|
+
the dubbing pipeline fetches the source itself and rejects plain http). The
|
|
257
|
+
optional `languages` array defaults to `["zh_cn", "es", "fr"]`; supported
|
|
258
|
+
codes are `en, zh_cn, ja, ko, pt, es, de, fr, it, ru`.
|
|
259
|
+
|
|
260
|
+
Dubbing is async-only, and the source video may be at most 180 seconds long.
|
|
261
|
+
You are billed per language. Dubbing has **no free trial allowance** — unlike
|
|
262
|
+
every other endpoint, every call bills from the first one (see
|
|
263
|
+
[Free trial](#free-trial)).
|
|
264
|
+
|
|
265
|
+
The result is a `DubbingResult`, whose `outputs` is a map of language code to
|
|
266
|
+
dubbed `.mp4` URL — not the `audio`/`video`/`output_url` shape the other
|
|
267
|
+
endpoints use.
|
|
268
|
+
|
|
178
269
|
## Configuration
|
|
179
270
|
|
|
180
271
|
```ts
|
|
@@ -258,15 +349,25 @@ are presigned and expire; download promptly or re-fetch via `tasks.get`.
|
|
|
258
349
|
|
|
259
350
|
## Free trial
|
|
260
351
|
|
|
261
|
-
Accounts created through self-serve signup start with free runs on
|
|
262
|
-
|
|
352
|
+
Accounts created through self-serve signup start with free runs on most
|
|
353
|
+
endpoints — no card required:
|
|
263
354
|
|
|
264
355
|
| Free runs | Endpoints |
|
|
265
356
|
| --- | --- |
|
|
266
357
|
| 2 each | text-to-music, text-to-sfx, audio-ducking |
|
|
267
358
|
| 1 each | video-to-music, video-to-sfx, video-to-video-music, video-to-video-sfx, video-to-sound, video-to-video-sound |
|
|
359
|
+
| 0 | dubbing |
|
|
268
360
|
|
|
269
361
|
Once an endpoint's free runs are used up, calls to it bill at the normal rate.
|
|
362
|
+
**Dubbing has no free trial allowance at all** — it bills every call from the
|
|
363
|
+
first one. This is deliberate: dubbing charges `video_duration ×
|
|
364
|
+
number_of_languages`, so a single "free" run could easily cost more than the
|
|
365
|
+
free allowance on every other endpoint combined.
|
|
366
|
+
|
|
367
|
+
The table above is the current default. Read the live numbers from
|
|
368
|
+
`account.services()` rather than hard-coding them — see
|
|
369
|
+
[Account](#account) below, and [Errors](#errors) for what a spent trial
|
|
370
|
+
looks like at the call site.
|
|
270
371
|
|
|
271
372
|
## Account
|
|
272
373
|
|
|
@@ -275,10 +376,27 @@ const services = await sonilo.account.services();
|
|
|
275
376
|
const usage = await sonilo.account.usage({ days: 7 });
|
|
276
377
|
```
|
|
277
378
|
|
|
379
|
+
`services.trial` reports the free-trial allowance per service, so an
|
|
380
|
+
integration can degrade gracefully *before* a call fails:
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
const { trial } = await sonilo.account.services();
|
|
384
|
+
const quota = trial?.text_to_music;
|
|
385
|
+
if (quota && quota.remaining === 0) {
|
|
386
|
+
// Prompt for a payment method instead of firing a call that will 402.
|
|
387
|
+
console.log(`Free trial spent (${quota.used}/${quota.granted}).`);
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
`trial` is present only for self-serve accounts, so always treat it as
|
|
392
|
+
optional; a service missing from the map has no trial allowance rather than
|
|
393
|
+
an unlimited one.
|
|
394
|
+
|
|
278
395
|
## Errors
|
|
279
396
|
|
|
280
397
|
All errors extend `SoniloError`: `AuthenticationError` (401),
|
|
281
|
-
`PaymentRequiredError` (402), `
|
|
398
|
+
`PaymentRequiredError` (402), `TrialExhaustedError` (402, a subclass of
|
|
399
|
+
`PaymentRequiredError`), `RateLimitError` (429, `.retryAfter`),
|
|
282
400
|
`BadRequestError` (400/413/422, `.detail`), `APIError` (anything else),
|
|
283
401
|
`GenerationError` for failures mid-stream, `TaskFailedError` (`.code`,
|
|
284
402
|
`.taskId`, `.refunded`) for a failed SFX task, `TaskTimeoutError`
|
|
@@ -292,3 +410,29 @@ Every `APIError` also carries `.status`, `.body` (the parsed response),
|
|
|
292
410
|
`.code` (the API's error code, e.g. `"rate_limit_exceeded"`), and `.errors`
|
|
293
411
|
(the validation detail array on a 422), in addition to any subclass-specific
|
|
294
412
|
properties above.
|
|
413
|
+
|
|
414
|
+
### The three 402s
|
|
415
|
+
|
|
416
|
+
A `402` is not one condition. Branch on the class (or equivalently on
|
|
417
|
+
`.code`), never on the message text:
|
|
418
|
+
|
|
419
|
+
```ts
|
|
420
|
+
try {
|
|
421
|
+
await sonilo.textToMusic.generate({ prompt: "lofi", duration: 30 });
|
|
422
|
+
} catch (err) {
|
|
423
|
+
if (err instanceof TrialExhaustedError) {
|
|
424
|
+
// code: "trial_exhausted" — the free trial for this service is spent and
|
|
425
|
+
// the account has never been funded. Prompt for a payment method; a retry
|
|
426
|
+
// can never succeed.
|
|
427
|
+
} else if (err instanceof PaymentRequiredError) {
|
|
428
|
+
// code: "insufficient_balance" — a funded wallet ran dry. Add balance and
|
|
429
|
+
// retry the same request.
|
|
430
|
+
// code: "payment_required" — anything else, e.g. a suspended account.
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
`TrialExhaustedError` extends `PaymentRequiredError`, so an existing
|
|
436
|
+
`catch (err) { if (err instanceof PaymentRequiredError) ... }` keeps
|
|
437
|
+
catching every 402 — order the checks most-specific-first if you want to
|
|
438
|
+
tell them apart.
|
package/dist/index.cjs
CHANGED
|
@@ -32,6 +32,7 @@ __export(index_exports, {
|
|
|
32
32
|
SoniloError: () => SoniloError,
|
|
33
33
|
TaskFailedError: () => TaskFailedError,
|
|
34
34
|
TaskTimeoutError: () => TaskTimeoutError,
|
|
35
|
+
TrialExhaustedError: () => TrialExhaustedError,
|
|
35
36
|
VERSION: () => VERSION,
|
|
36
37
|
download: () => download,
|
|
37
38
|
isAudioChunkEvent: () => isAudioChunkEvent,
|
|
@@ -60,6 +61,8 @@ var AuthenticationError = class extends APIError {
|
|
|
60
61
|
};
|
|
61
62
|
var PaymentRequiredError = class extends APIError {
|
|
62
63
|
};
|
|
64
|
+
var TrialExhaustedError = class extends PaymentRequiredError {
|
|
65
|
+
};
|
|
63
66
|
var BadRequestError = class extends APIError {
|
|
64
67
|
get detail() {
|
|
65
68
|
const body = this.body;
|
|
@@ -129,8 +132,13 @@ async function errorFromResponse(res) {
|
|
|
129
132
|
switch (res.status) {
|
|
130
133
|
case 401:
|
|
131
134
|
return new AuthenticationError(message, res.status, body);
|
|
132
|
-
case 402:
|
|
135
|
+
case 402: {
|
|
136
|
+
const code = body?.code;
|
|
137
|
+
if (code === "trial_exhausted") {
|
|
138
|
+
return new TrialExhaustedError(message, res.status, body);
|
|
139
|
+
}
|
|
133
140
|
return new PaymentRequiredError(message, res.status, body);
|
|
141
|
+
}
|
|
134
142
|
case 429: {
|
|
135
143
|
const ra = res.headers.get("retry-after");
|
|
136
144
|
const retryAfter = ra !== null && ra !== "" && !Number.isNaN(Number(ra)) ? Number(ra) : void 0;
|
|
@@ -339,7 +347,8 @@ var TextToMusic = class {
|
|
|
339
347
|
/**
|
|
340
348
|
* Submit an async text-to-music task; poll with
|
|
341
349
|
* `client.tasks.wait<MusicTaskResult>(task.task_id)`. Required for
|
|
342
|
-
* `outputFormat: "wav"
|
|
350
|
+
* `outputFormat: "wav"` and `variantsNum` above 1. `stream()`/`generate()`
|
|
351
|
+
* remain the streaming path.
|
|
343
352
|
*/
|
|
344
353
|
async submit(params) {
|
|
345
354
|
const mode = params.mode ?? "async";
|
|
@@ -356,6 +365,9 @@ var TextToMusic = class {
|
|
|
356
365
|
if (params.outputFormat !== void 0) {
|
|
357
366
|
form.set("output_format", params.outputFormat);
|
|
358
367
|
}
|
|
368
|
+
if (params.variantsNum !== void 0) {
|
|
369
|
+
form.set("variants_num", String(params.variantsNum));
|
|
370
|
+
}
|
|
359
371
|
const res = await this.client.request("/v1/text-to-music", {
|
|
360
372
|
method: "POST",
|
|
361
373
|
body: form
|
|
@@ -436,19 +448,20 @@ var VideoToMusic = class {
|
|
|
436
448
|
/**
|
|
437
449
|
* Submit an async video-to-music task; poll its result with
|
|
438
450
|
* `client.tasks.wait<MusicTaskResult>(task.task_id)`. Required for
|
|
439
|
-
* `isolateVocals`
|
|
440
|
-
*
|
|
451
|
+
* `isolateVocals`/`preserveSpeech`, `outputFormat: "wav"`, and
|
|
452
|
+
* `variantsNum` above 1 — the backend rejects all of these on the plain
|
|
453
|
+
* stream, and they only ever run in async mode.
|
|
441
454
|
*/
|
|
442
455
|
async submit(params) {
|
|
443
456
|
if (params.video === void 0 === (params.videoUrl === void 0)) {
|
|
444
457
|
throw new SoniloError("Provide exactly one of video or videoUrl");
|
|
445
458
|
}
|
|
446
459
|
let mode = params.mode;
|
|
447
|
-
const needsAsync = params.isolateVocals || params.preserveSpeech || params.ducking !== void 0 || params.outputFormat === "wav";
|
|
460
|
+
const needsAsync = params.isolateVocals || params.preserveSpeech || params.ducking !== void 0 || params.outputFormat === "wav" || params.variantsNum !== void 0 && params.variantsNum > 1;
|
|
448
461
|
if (mode === void 0) mode = "async";
|
|
449
462
|
if (needsAsync && mode !== "async") {
|
|
450
463
|
throw new SoniloError(
|
|
451
|
-
'isolateVocals/preserveSpeech/ducking/outputFormat "wav" require mode: "async"'
|
|
464
|
+
'isolateVocals/preserveSpeech/ducking/outputFormat "wav"/variantsNum > 1 require mode: "async"'
|
|
452
465
|
);
|
|
453
466
|
}
|
|
454
467
|
const form = new FormData();
|
|
@@ -475,6 +488,9 @@ var VideoToMusic = class {
|
|
|
475
488
|
if (params.ducking !== void 0) {
|
|
476
489
|
form.set("ducking", String(params.ducking));
|
|
477
490
|
}
|
|
491
|
+
if (params.variantsNum !== void 0) {
|
|
492
|
+
form.set("variants_num", String(params.variantsNum));
|
|
493
|
+
}
|
|
478
494
|
const res = await this.client.request("/v1/video-to-music", {
|
|
479
495
|
method: "POST",
|
|
480
496
|
body: form
|
|
@@ -561,6 +577,9 @@ var VideoToVideoMusic = class {
|
|
|
561
577
|
if (params.isolateVocals !== void 0) {
|
|
562
578
|
form.set("isolate_vocals", String(params.isolateVocals));
|
|
563
579
|
}
|
|
580
|
+
if (params.variantsNum !== void 0) {
|
|
581
|
+
form.set("variants_num", String(params.variantsNum));
|
|
582
|
+
}
|
|
564
583
|
const res = await this.client.request("/v1/video-to-video-music", {
|
|
565
584
|
method: "POST",
|
|
566
585
|
body: form
|
|
@@ -626,6 +645,9 @@ async function buildSoundForm(params) {
|
|
|
626
645
|
form.set("preserve_speech", String(params.preserveSpeech));
|
|
627
646
|
}
|
|
628
647
|
if (params.ducking !== void 0) form.set("ducking", String(params.ducking));
|
|
648
|
+
if (params.variantsNum !== void 0) {
|
|
649
|
+
form.set("variants_num", String(params.variantsNum));
|
|
650
|
+
}
|
|
629
651
|
return form;
|
|
630
652
|
}
|
|
631
653
|
|
|
@@ -665,8 +687,48 @@ var VideoToVideoSound = class {
|
|
|
665
687
|
}
|
|
666
688
|
};
|
|
667
689
|
|
|
690
|
+
// src/resources/dubbing.ts
|
|
691
|
+
async function buildDubbingForm(params) {
|
|
692
|
+
if (params.video === void 0 === (params.videoUrl === void 0)) {
|
|
693
|
+
throw new SoniloError("Provide exactly one of video or videoUrl");
|
|
694
|
+
}
|
|
695
|
+
const form = new FormData();
|
|
696
|
+
if (params.video !== void 0) {
|
|
697
|
+
const { blob, filename } = await toUploadBlob(params.video);
|
|
698
|
+
form.set("video", blob, filename);
|
|
699
|
+
} else {
|
|
700
|
+
const url = params.videoUrl;
|
|
701
|
+
if (!url.toLowerCase().startsWith("https://")) {
|
|
702
|
+
throw new SoniloError(
|
|
703
|
+
"videoUrl must use https \u2014 the dubbing pipeline requires an https URL"
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
form.set("video_url", url);
|
|
707
|
+
}
|
|
708
|
+
if (params.languages !== void 0) {
|
|
709
|
+
form.set("languages", JSON.stringify(params.languages));
|
|
710
|
+
}
|
|
711
|
+
return form;
|
|
712
|
+
}
|
|
713
|
+
var Dubbing = class {
|
|
714
|
+
constructor(client) {
|
|
715
|
+
this.client = client;
|
|
716
|
+
}
|
|
717
|
+
async submit(params) {
|
|
718
|
+
const res = await this.client.request("/v1/dubbing", {
|
|
719
|
+
method: "POST",
|
|
720
|
+
body: await buildDubbingForm(params)
|
|
721
|
+
});
|
|
722
|
+
return await res.json();
|
|
723
|
+
}
|
|
724
|
+
async generate(params, opts) {
|
|
725
|
+
const task = await this.submit(params);
|
|
726
|
+
return this.client.tasks.wait(task.task_id, opts);
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
|
|
668
730
|
// src/version.ts
|
|
669
|
-
var VERSION = "0.
|
|
731
|
+
var VERSION = "0.8.0";
|
|
670
732
|
|
|
671
733
|
// src/client.ts
|
|
672
734
|
var DEFAULT_BASE_URL = "https://api.sonilo.com";
|
|
@@ -697,6 +759,7 @@ var SoniloClient = class {
|
|
|
697
759
|
this.videoToVideoSfx = new VideoToVideoSfx(this);
|
|
698
760
|
this.videoToSound = new VideoToSound(this);
|
|
699
761
|
this.videoToVideoSound = new VideoToVideoSound(this);
|
|
762
|
+
this.dubbing = new Dubbing(this);
|
|
700
763
|
}
|
|
701
764
|
/**
|
|
702
765
|
* Perform an authenticated request; throws a typed error on non-2xx.
|
|
@@ -769,6 +832,7 @@ function isErrorEvent(event) {
|
|
|
769
832
|
SoniloError,
|
|
770
833
|
TaskFailedError,
|
|
771
834
|
TaskTimeoutError,
|
|
835
|
+
TrialExhaustedError,
|
|
772
836
|
VERSION,
|
|
773
837
|
download,
|
|
774
838
|
isAudioChunkEvent,
|