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/dist/index.d.ts CHANGED
@@ -55,6 +55,12 @@ interface TextToMusicParams {
55
55
  mode?: "stream" | "async";
56
56
  /** Container for the async result. `wav` requires `mode: "async"`. Defaults to m4a server-side. */
57
57
  outputFormat?: "m4a" | "wav";
58
+ /** How many distinct music variants to generate in one request (1-10,
59
+ * default 1). Cost scales linearly, and values above 1 are never covered
60
+ * by the free trial. Values above 1 require `mode: "async"` — only
61
+ * meaningful via `submit()`; `stream()`/`generate()` never send it, since
62
+ * they always request a plain stream. */
63
+ variantsNum?: number;
58
64
  /** Bounds the stream: aborting this cancels the in-flight generation.
59
65
  * Passed straight through to `fetch` — it is never rewrapped as
60
66
  * RequestTimeoutError, since the client's own absolute timeout does not
@@ -93,7 +99,15 @@ interface VideoToMusicParams {
93
99
  * Default-ON server-side in async mode: leave unset to keep it on, pass
94
100
  * `false` to opt out. Free, best-effort; only valid on `submit()`. */
95
101
  ducking?: boolean;
96
- }
102
+ /** How many distinct music variants to generate in one request (1-10,
103
+ * default 1). Cost scales linearly, and values above 1 are never covered
104
+ * by the free trial. Values above 1 require `mode: "async"` (auto-selected
105
+ * by `submit()`) — only meaningful via `submit()`; `stream()`/`generate()`
106
+ * never send it, since they always request a plain stream. */
107
+ variantsNum?: number;
108
+ }
109
+ /** One service's free-trial allowance. `remaining` is already floored at 0,
110
+ * so it is safe to compare directly. */
97
111
  interface TrialQuota {
98
112
  granted: number;
99
113
  used: number;
@@ -105,8 +119,10 @@ interface AccountServices {
105
119
  concurrency_limit: number;
106
120
  discount_factor: number | string;
107
121
  max_upload_size_mb: number | null;
108
- /** Free-trial allowance keyed by service. Returned only for self-serve
109
- * accounts; absent entirely for invoiced accounts. */
122
+ /** Free-trial allowance keyed by service (`granted` / `used` /
123
+ * `remaining`). Present only for self-serve accounts — always treat it as
124
+ * possibly absent, and treat a service missing from the map as "no trial
125
+ * allowance", not as an error. */
110
126
  trial?: Record<string, TrialQuota>;
111
127
  }
112
128
  interface UsageSummary {
@@ -167,6 +183,11 @@ interface BaseTaskResult {
167
183
  cost?: number;
168
184
  error?: SfxError;
169
185
  refunded?: boolean;
186
+ /** Echoes the request's `variantsNum`. Present regardless of task status
187
+ * (so a `processing`/`failed` poll explains the charge too), but only
188
+ * when it was above 1 — a default (single-variant) request sees the same
189
+ * shape it always has. */
190
+ variants_num?: number;
170
191
  [key: string]: unknown;
171
192
  }
172
193
  /** State of an SFX task (`tasks.get`) or its final result (`wait`/`generate`). */
@@ -194,6 +215,11 @@ interface MusicMediaEntry extends SfxMedia {
194
215
  stream_index: number;
195
216
  sample_rate?: number;
196
217
  channels?: number;
218
+ /** This entry's own title, present when `variantsNum` was above 1 (and
219
+ * titles are visible on the account). Each variant is a distinct creative
220
+ * direction, so it can carry its own title rather than sharing the
221
+ * top-level `MusicTaskResult.title`, which always names variant 0. */
222
+ title?: MusicTitle;
197
223
  }
198
224
  /** One muxed audio+video-aligned output, present only when `isolateVocals`
199
225
  * is set. */
@@ -209,13 +235,20 @@ interface MusicTitle {
209
235
  * (`tasks.wait<MusicTaskResult>()`). Only reachable via `videoToMusic.submit()`
210
236
  * with `mode: "async"`. */
211
237
  interface MusicTaskResult extends BaseTaskResult {
238
+ /** One entry per stream, or one entry per variant when `variantsNum` was
239
+ * greater than 1 — each variant entry may carry its own `title`. */
212
240
  audio?: MusicMediaEntry[];
213
241
  /** Vocals-only stem; present only when `isolateVocals` was requested. */
214
242
  vocals?: SfxMedia;
215
- /** Muxed output per stream; present only when `isolateVocals` was requested. */
243
+ /** Muxed output per stream (or per variant); present only when
244
+ * `isolateVocals` was requested. */
216
245
  mux?: MusicMuxEntry[];
217
- /** Music ducked under the source voice; present only when `ducking` ran. */
246
+ /** Music ducked under the source voice (per variant when `variantsNum` is
247
+ * above 1); present only when `ducking` ran. */
218
248
  ducked?: MusicMediaEntry[];
249
+ /** Variant 0's title — the top-level field always names the first variant,
250
+ * even when `variantsNum` produced others with their own titles on
251
+ * `audio[]`. */
219
252
  title?: MusicTitle;
220
253
  duration_seconds?: number;
221
254
  }
@@ -228,6 +261,11 @@ interface WaitOptions {
228
261
  /** Result of an async video-to-video task (`videoToVideoMusic`/`videoToVideoSfx`):
229
262
  * a re-hosted video with generated music or SFX muxed in. */
230
263
  interface VideoResult extends BaseTaskResult {
264
+ /** One re-hosted video per variant. On `videoToVideoMusic` this is
265
+ * populated even at the default `variantsNum` of 1 (as a single-entry
266
+ * array); `videoToVideoSfx` has no variants knob and always sends one. */
267
+ videos?: SfxMedia[];
268
+ /** Permanent alias for `videos[0]`. */
231
269
  video?: SfxMedia;
232
270
  duration_seconds?: number;
233
271
  }
@@ -240,6 +278,11 @@ interface VideoToVideoMusicParams {
240
278
  preserveSpeech?: boolean;
241
279
  /** @deprecated Legacy alias for `preserveSpeech`. */
242
280
  isolateVocals?: boolean;
281
+ /** How many distinct music variants to generate in one request (1-10,
282
+ * default 1). Cost scales linearly, and values above 1 are never covered
283
+ * by the free trial. This endpoint is always async, so no extra `mode`
284
+ * gating applies. The result's `videos[]` gets one entry per variant. */
285
+ variantsNum?: number;
243
286
  }
244
287
  interface VideoToVideoSfxParams {
245
288
  video?: VideoInput;
@@ -263,6 +306,24 @@ interface VideoToSoundParams {
263
306
  /** Duck the generated music under the source speech. Default-ON
264
307
  * server-side: leave unset to keep it on, pass `false` to opt out. */
265
308
  ducking?: boolean;
309
+ /** How many distinct variants to generate in one request (1-10, default
310
+ * 1). Cost scales linearly, and values above 1 are never covered by the
311
+ * free trial. Both `videoToSound` and `videoToVideoSound` are always
312
+ * async, so no extra `mode` gating applies. The result's `outputs[]` gets
313
+ * one entry per variant. */
314
+ variantsNum?: number;
315
+ }
316
+ /** One variant's outputs on a `videoToSound` / `videoToVideoSound` result.
317
+ * Present even at the default `variantsNum` of 1, as a single-entry array. */
318
+ interface SoundOutputEntry {
319
+ variant_index: number;
320
+ output_url: string;
321
+ output_type: "audio" | "video";
322
+ output_bytes: number;
323
+ music?: SfxMedia;
324
+ /** Present only when `preserveSpeech`/`ducking` altered this variant's music bed. */
325
+ music_processed?: SfxMedia;
326
+ sfx?: SfxMedia;
266
327
  }
267
328
  /** Result of a `videoToSound` / `videoToVideoSound` task (`tasks.get`) or its
268
329
  * final state (`generate`).
@@ -271,7 +332,11 @@ interface VideoToSoundParams {
271
332
  * than a media object, since these endpoints render one artifact whose kind is
272
333
  * announced by `output_type` ("audio" for video-to-sound, "video" for
273
334
  * video-to-video-sound). `music`, `music_processed` and `sfx` are the
274
- * individual stems; pass any of them, or `output_url` itself, to `download()`. */
335
+ * individual stems; pass any of them, or `output_url` itself, to `download()`.
336
+ *
337
+ * `outputs` carries the same fields per variant when `variantsNum` was
338
+ * greater than 1; `output_url`/`output_type`/`output_bytes`/`music`/
339
+ * `music_processed`/`sfx` remain permanent aliases for `outputs[0]`. */
275
340
  interface SoundResult extends BaseTaskResult {
276
341
  output_url?: string;
277
342
  output_type?: "audio" | "video";
@@ -281,6 +346,33 @@ interface SoundResult extends BaseTaskResult {
281
346
  music_processed?: SfxMedia;
282
347
  sfx?: SfxMedia;
283
348
  duration_seconds?: number;
349
+ /** One entry per variant; see `SoundOutputEntry`. */
350
+ outputs?: SoundOutputEntry[];
351
+ }
352
+ /**
353
+ * A target language for /v1/dubbing. The union stays open (`string & {}`) so a
354
+ * language added server-side still type-checks against an older SDK — the
355
+ * backend, not this list, is the authority on what is supported.
356
+ */
357
+ type DubbingLanguage = "en" | "zh_cn" | "ja" | "ko" | "pt" | "es" | "de" | "fr" | "it" | "ru" | (string & {});
358
+ interface DubbingParams {
359
+ /** Exactly one of `video` / `videoUrl`. */
360
+ video?: VideoInput;
361
+ /**
362
+ * Exactly one of `video` / `videoUrl`. Must be an `https://` URL — the
363
+ * dubbing pipeline fetches the source itself and rejects plain http.
364
+ */
365
+ videoUrl?: string;
366
+ /** Omit to get the server default, `["zh_cn", "es", "fr"]`. */
367
+ languages?: DubbingLanguage[];
368
+ }
369
+ interface DubbingResult extends BaseTaskResult {
370
+ /**
371
+ * One dubbed video URL per requested language, keyed by language code.
372
+ * Unlike every other endpoint's envelope this is a map, not an `audio`/
373
+ * `video` slot — a dubbing task renders N artifacts, one per language.
374
+ */
375
+ outputs?: Record<string, string>;
284
376
  }
285
377
 
286
378
  declare class Account {
@@ -323,7 +415,8 @@ declare class TextToMusic {
323
415
  /**
324
416
  * Submit an async text-to-music task; poll with
325
417
  * `client.tasks.wait<MusicTaskResult>(task.task_id)`. Required for
326
- * `outputFormat: "wav"`. `stream()`/`generate()` remain the streaming path.
418
+ * `outputFormat: "wav"` and `variantsNum` above 1. `stream()`/`generate()`
419
+ * remain the streaming path.
327
420
  */
328
421
  submit(params: TextToMusicParams): Promise<SfxTask>;
329
422
  }
@@ -336,8 +429,9 @@ declare class VideoToMusic {
336
429
  /**
337
430
  * Submit an async video-to-music task; poll its result with
338
431
  * `client.tasks.wait<MusicTaskResult>(task.task_id)`. Required for
339
- * `isolateVocals` the backend rejects vocal isolation on the plain
340
- * stream, and it only ever runs in async mode.
432
+ * `isolateVocals`/`preserveSpeech`, `outputFormat: "wav"`, and
433
+ * `variantsNum` above 1 the backend rejects all of these on the plain
434
+ * stream, and they only ever run in async mode.
341
435
  */
342
436
  submit(params: VideoToMusicParams): Promise<SfxTask>;
343
437
  }
@@ -393,6 +487,15 @@ declare class VideoToVideoSound {
393
487
  generate(params: VideoToSoundParams, opts?: WaitOptions): Promise<SoundResult>;
394
488
  }
395
489
 
490
+ /** Dub a video into one or more target languages. Async only; the result
491
+ * carries a language → dubbed-video-URL map under `outputs`. */
492
+ declare class Dubbing {
493
+ private readonly client;
494
+ constructor(client: SoniloClient);
495
+ submit(params: DubbingParams): Promise<SfxTask>;
496
+ generate(params: DubbingParams, opts?: WaitOptions): Promise<DubbingResult>;
497
+ }
498
+
396
499
  interface SoniloClientOptions {
397
500
  /** Defaults to the SONILO_API_KEY environment variable (Node.js only). */
398
501
  apiKey?: string;
@@ -430,6 +533,7 @@ declare class SoniloClient {
430
533
  readonly videoToVideoSfx: VideoToVideoSfx;
431
534
  readonly videoToSound: VideoToSound;
432
535
  readonly videoToVideoSound: VideoToVideoSound;
536
+ readonly dubbing: Dubbing;
433
537
  constructor(options?: SoniloClientOptions);
434
538
  /**
435
539
  * Perform an authenticated request; throws a typed error on non-2xx.
@@ -460,6 +564,14 @@ declare class AuthenticationError extends APIError {
460
564
  }
461
565
  declare class PaymentRequiredError extends APIError {
462
566
  }
567
+ /** The account's free trial for this service is spent and it has never been
568
+ * funded — the caller should add a payment method rather than retry. A
569
+ * subclass of `PaymentRequiredError`, so code that already catches every 402
570
+ * keeps working; catch this first to tell "you haven't paid us yet" apart
571
+ * from a funded wallet that ran dry (`PaymentRequiredError` with
572
+ * `code === "insufficient_balance"`). */
573
+ declare class TrialExhaustedError extends PaymentRequiredError {
574
+ }
463
575
  declare class BadRequestError extends APIError {
464
576
  get detail(): string | undefined;
465
577
  }
@@ -503,6 +615,6 @@ declare class RequestTimeoutError extends SoniloError {
503
615
  declare function download(media: SfxMedia | string | undefined, fetchFn?: typeof globalThis.fetch, timeout?: number): Promise<Uint8Array>;
504
616
 
505
617
  /** The SDK's own version. Generated by scripts/sync-versions.mjs — do not edit. */
506
- declare const VERSION = "0.6.0";
618
+ declare const VERSION = "0.8.0";
507
619
 
508
- export { APIError, type AccountServices, type AudioChunkEvent, AuthenticationError, BadRequestError, type BaseTaskResult, type CompleteEvent, type CostEvent, type CostInfo, DEFAULT_TIMEOUT_MS, type DailyUsage, type ErrorEvent, GenerationError, type MusicMediaEntry, type MusicMuxEntry, type MusicTaskResult, type MusicTitle, PaymentRequiredError, RateLimitError, RequestTimeoutError, type Segment, type SegmentLabel, type SfxAudioFormat, type SfxError, type SfxMedia, type SfxResult, type SfxSegment, type SfxTask, SoniloClient, type SoniloClientOptions, SoniloError, type SoundResult, type StreamEvent, TaskFailedError, TaskTimeoutError, type TextToMusicParams, type TextToSfxParams, type TitleEvent, type Track, type TrialQuota, type UnknownEvent, type UsageResponse, type UsageSummary, VERSION, type VideoInput, type VideoResult, type VideoToMusicParams, type VideoToSfxParams, type VideoToSoundParams, type VideoToVideoMusicParams, type VideoToVideoSfxParams, type WaitOptions, download, isAudioChunkEvent, isErrorEvent };
620
+ export { APIError, type AccountServices, type AudioChunkEvent, AuthenticationError, BadRequestError, type BaseTaskResult, type CompleteEvent, type CostEvent, type CostInfo, DEFAULT_TIMEOUT_MS, type DailyUsage, type DubbingLanguage, type DubbingParams, type DubbingResult, type ErrorEvent, GenerationError, type MusicMediaEntry, type MusicMuxEntry, type MusicTaskResult, type MusicTitle, PaymentRequiredError, RateLimitError, RequestTimeoutError, type Segment, type SegmentLabel, type SfxAudioFormat, type SfxError, type SfxMedia, type SfxResult, type SfxSegment, type SfxTask, SoniloClient, type SoniloClientOptions, SoniloError, type SoundOutputEntry, type SoundResult, type StreamEvent, TaskFailedError, TaskTimeoutError, type TextToMusicParams, type TextToSfxParams, type TitleEvent, type Track, TrialExhaustedError, type TrialQuota, type UnknownEvent, type UsageResponse, type UsageSummary, VERSION, type VideoInput, type VideoResult, type VideoToMusicParams, type VideoToSfxParams, type VideoToSoundParams, type VideoToVideoMusicParams, type VideoToVideoSfxParams, type WaitOptions, download, isAudioChunkEvent, isErrorEvent };
package/dist/index.js CHANGED
@@ -19,6 +19,8 @@ var AuthenticationError = class extends APIError {
19
19
  };
20
20
  var PaymentRequiredError = class extends APIError {
21
21
  };
22
+ var TrialExhaustedError = class extends PaymentRequiredError {
23
+ };
22
24
  var BadRequestError = class extends APIError {
23
25
  get detail() {
24
26
  const body = this.body;
@@ -88,8 +90,13 @@ async function errorFromResponse(res) {
88
90
  switch (res.status) {
89
91
  case 401:
90
92
  return new AuthenticationError(message, res.status, body);
91
- case 402:
93
+ case 402: {
94
+ const code = body?.code;
95
+ if (code === "trial_exhausted") {
96
+ return new TrialExhaustedError(message, res.status, body);
97
+ }
92
98
  return new PaymentRequiredError(message, res.status, body);
99
+ }
93
100
  case 429: {
94
101
  const ra = res.headers.get("retry-after");
95
102
  const retryAfter = ra !== null && ra !== "" && !Number.isNaN(Number(ra)) ? Number(ra) : void 0;
@@ -298,7 +305,8 @@ var TextToMusic = class {
298
305
  /**
299
306
  * Submit an async text-to-music task; poll with
300
307
  * `client.tasks.wait<MusicTaskResult>(task.task_id)`. Required for
301
- * `outputFormat: "wav"`. `stream()`/`generate()` remain the streaming path.
308
+ * `outputFormat: "wav"` and `variantsNum` above 1. `stream()`/`generate()`
309
+ * remain the streaming path.
302
310
  */
303
311
  async submit(params) {
304
312
  const mode = params.mode ?? "async";
@@ -315,6 +323,9 @@ var TextToMusic = class {
315
323
  if (params.outputFormat !== void 0) {
316
324
  form.set("output_format", params.outputFormat);
317
325
  }
326
+ if (params.variantsNum !== void 0) {
327
+ form.set("variants_num", String(params.variantsNum));
328
+ }
318
329
  const res = await this.client.request("/v1/text-to-music", {
319
330
  method: "POST",
320
331
  body: form
@@ -395,19 +406,20 @@ var VideoToMusic = class {
395
406
  /**
396
407
  * Submit an async video-to-music task; poll its result with
397
408
  * `client.tasks.wait<MusicTaskResult>(task.task_id)`. Required for
398
- * `isolateVocals` the backend rejects vocal isolation on the plain
399
- * stream, and it only ever runs in async mode.
409
+ * `isolateVocals`/`preserveSpeech`, `outputFormat: "wav"`, and
410
+ * `variantsNum` above 1 the backend rejects all of these on the plain
411
+ * stream, and they only ever run in async mode.
400
412
  */
401
413
  async submit(params) {
402
414
  if (params.video === void 0 === (params.videoUrl === void 0)) {
403
415
  throw new SoniloError("Provide exactly one of video or videoUrl");
404
416
  }
405
417
  let mode = params.mode;
406
- const needsAsync = params.isolateVocals || params.preserveSpeech || params.ducking !== void 0 || params.outputFormat === "wav";
418
+ const needsAsync = params.isolateVocals || params.preserveSpeech || params.ducking !== void 0 || params.outputFormat === "wav" || params.variantsNum !== void 0 && params.variantsNum > 1;
407
419
  if (mode === void 0) mode = "async";
408
420
  if (needsAsync && mode !== "async") {
409
421
  throw new SoniloError(
410
- 'isolateVocals/preserveSpeech/ducking/outputFormat "wav" require mode: "async"'
422
+ 'isolateVocals/preserveSpeech/ducking/outputFormat "wav"/variantsNum > 1 require mode: "async"'
411
423
  );
412
424
  }
413
425
  const form = new FormData();
@@ -434,6 +446,9 @@ var VideoToMusic = class {
434
446
  if (params.ducking !== void 0) {
435
447
  form.set("ducking", String(params.ducking));
436
448
  }
449
+ if (params.variantsNum !== void 0) {
450
+ form.set("variants_num", String(params.variantsNum));
451
+ }
437
452
  const res = await this.client.request("/v1/video-to-music", {
438
453
  method: "POST",
439
454
  body: form
@@ -520,6 +535,9 @@ var VideoToVideoMusic = class {
520
535
  if (params.isolateVocals !== void 0) {
521
536
  form.set("isolate_vocals", String(params.isolateVocals));
522
537
  }
538
+ if (params.variantsNum !== void 0) {
539
+ form.set("variants_num", String(params.variantsNum));
540
+ }
523
541
  const res = await this.client.request("/v1/video-to-video-music", {
524
542
  method: "POST",
525
543
  body: form
@@ -585,6 +603,9 @@ async function buildSoundForm(params) {
585
603
  form.set("preserve_speech", String(params.preserveSpeech));
586
604
  }
587
605
  if (params.ducking !== void 0) form.set("ducking", String(params.ducking));
606
+ if (params.variantsNum !== void 0) {
607
+ form.set("variants_num", String(params.variantsNum));
608
+ }
588
609
  return form;
589
610
  }
590
611
 
@@ -624,8 +645,48 @@ var VideoToVideoSound = class {
624
645
  }
625
646
  };
626
647
 
648
+ // src/resources/dubbing.ts
649
+ async function buildDubbingForm(params) {
650
+ if (params.video === void 0 === (params.videoUrl === void 0)) {
651
+ throw new SoniloError("Provide exactly one of video or videoUrl");
652
+ }
653
+ const form = new FormData();
654
+ if (params.video !== void 0) {
655
+ const { blob, filename } = await toUploadBlob(params.video);
656
+ form.set("video", blob, filename);
657
+ } else {
658
+ const url = params.videoUrl;
659
+ if (!url.toLowerCase().startsWith("https://")) {
660
+ throw new SoniloError(
661
+ "videoUrl must use https \u2014 the dubbing pipeline requires an https URL"
662
+ );
663
+ }
664
+ form.set("video_url", url);
665
+ }
666
+ if (params.languages !== void 0) {
667
+ form.set("languages", JSON.stringify(params.languages));
668
+ }
669
+ return form;
670
+ }
671
+ var Dubbing = class {
672
+ constructor(client) {
673
+ this.client = client;
674
+ }
675
+ async submit(params) {
676
+ const res = await this.client.request("/v1/dubbing", {
677
+ method: "POST",
678
+ body: await buildDubbingForm(params)
679
+ });
680
+ return await res.json();
681
+ }
682
+ async generate(params, opts) {
683
+ const task = await this.submit(params);
684
+ return this.client.tasks.wait(task.task_id, opts);
685
+ }
686
+ };
687
+
627
688
  // src/version.ts
628
- var VERSION = "0.6.0";
689
+ var VERSION = "0.8.0";
629
690
 
630
691
  // src/client.ts
631
692
  var DEFAULT_BASE_URL = "https://api.sonilo.com";
@@ -656,6 +717,7 @@ var SoniloClient = class {
656
717
  this.videoToVideoSfx = new VideoToVideoSfx(this);
657
718
  this.videoToSound = new VideoToSound(this);
658
719
  this.videoToVideoSound = new VideoToVideoSound(this);
720
+ this.dubbing = new Dubbing(this);
659
721
  }
660
722
  /**
661
723
  * Perform an authenticated request; throws a typed error on non-2xx.
@@ -727,6 +789,7 @@ export {
727
789
  SoniloError,
728
790
  TaskFailedError,
729
791
  TaskTimeoutError,
792
+ TrialExhaustedError,
730
793
  VERSION,
731
794
  download,
732
795
  isAudioChunkEvent,