sonilo 0.3.0 → 0.5.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
@@ -51,6 +51,10 @@ interface TextToMusicParams {
51
51
  prompt: string;
52
52
  duration: number;
53
53
  segments?: Segment[];
54
+ /** "stream" (default) or "async" (required by `submit()` and `output_format: "wav"`). */
55
+ mode?: "stream" | "async";
56
+ /** Container for the async result. `wav` requires `mode: "async"`. Defaults to m4a server-side. */
57
+ outputFormat?: "m4a" | "wav";
54
58
  /** Bounds the stream: aborting this cancels the in-flight generation.
55
59
  * Passed straight through to `fetch` — it is never rewrapped as
56
60
  * RequestTimeoutError, since the client's own absolute timeout does not
@@ -79,6 +83,16 @@ interface VideoToMusicParams {
79
83
  * "async" automatically. Only usable via `submit()` — the backend
80
84
  * rejects it on the plain stream. */
81
85
  isolateVocals?: boolean;
86
+ /** Keep the source speech/vocals in the async result. Current name for
87
+ * `isolateVocals`; both are accepted and OR'd server-side. Requires
88
+ * `mode: "async"` (auto-selected by `submit()`). */
89
+ preserveSpeech?: boolean;
90
+ /** Container for the async result. `wav` requires async. Defaults to m4a. */
91
+ outputFormat?: "m4a" | "wav";
92
+ /** Duck the generated music under the source voice at finalize time.
93
+ * Default-ON server-side in async mode: leave unset to keep it on, pass
94
+ * `false` to opt out. Free, best-effort; only valid on `submit()`. */
95
+ ducking?: boolean;
82
96
  }
83
97
  interface AccountServices {
84
98
  available_services: string[];
@@ -192,6 +206,8 @@ interface MusicTaskResult extends BaseTaskResult {
192
206
  vocals?: SfxMedia;
193
207
  /** Muxed output per stream; present only when `isolateVocals` was requested. */
194
208
  mux?: MusicMuxEntry[];
209
+ /** Music ducked under the source voice; present only when `ducking` ran. */
210
+ ducked?: MusicMediaEntry[];
195
211
  title?: MusicTitle;
196
212
  duration_seconds?: number;
197
213
  }
@@ -201,6 +217,63 @@ interface WaitOptions {
201
217
  /** Overall deadline in milliseconds. Default 600000. */
202
218
  timeout?: number;
203
219
  }
220
+ /** Result of an async video-to-video task (`videoToVideoMusic`/`videoToVideoSfx`):
221
+ * a re-hosted video with generated music or SFX muxed in. */
222
+ interface VideoResult extends BaseTaskResult {
223
+ video?: SfxMedia;
224
+ duration_seconds?: number;
225
+ }
226
+ interface VideoToVideoMusicParams {
227
+ video?: VideoInput;
228
+ videoUrl?: string;
229
+ prompt?: string;
230
+ /** Keep the source speech/vocals in the output. Both this and the legacy
231
+ * `isolateVocals` are accepted and OR'd server-side. */
232
+ preserveSpeech?: boolean;
233
+ /** @deprecated Legacy alias for `preserveSpeech`. */
234
+ isolateVocals?: boolean;
235
+ }
236
+ interface VideoToVideoSfxParams {
237
+ video?: VideoInput;
238
+ videoUrl?: string;
239
+ prompt?: string;
240
+ segments?: SfxSegment[];
241
+ }
242
+ /** Params for `videoToSound` and `videoToVideoSound`. Both endpoints take the
243
+ * identical form, so they share one params type. */
244
+ interface VideoToSoundParams {
245
+ video?: VideoInput;
246
+ videoUrl?: string;
247
+ /** Style hint for the generated music bed. */
248
+ musicPrompt?: string;
249
+ /** Description of the sound effects layered over the music. */
250
+ sfxPrompt?: string;
251
+ /** Per-segment SFX descriptions; must start at 0 and be contiguous. */
252
+ segments?: SfxSegment[];
253
+ /** Keep the source speech in the result. */
254
+ preserveSpeech?: boolean;
255
+ /** Duck the generated music under the source speech. Default-ON
256
+ * server-side: leave unset to keep it on, pass `false` to opt out. */
257
+ ducking?: boolean;
258
+ }
259
+ /** Result of a `videoToSound` / `videoToVideoSound` task (`tasks.get`) or its
260
+ * final state (`generate`).
261
+ *
262
+ * The combined music+SFX result is `output_url` — a bare presigned URL rather
263
+ * than a media object, since these endpoints render one artifact whose kind is
264
+ * announced by `output_type` ("audio" for video-to-sound, "video" for
265
+ * video-to-video-sound). `music`, `music_processed` and `sfx` are the
266
+ * individual stems; pass any of them, or `output_url` itself, to `download()`. */
267
+ interface SoundResult extends BaseTaskResult {
268
+ output_url?: string;
269
+ output_type?: "audio" | "video";
270
+ output_bytes?: number;
271
+ music?: SfxMedia;
272
+ /** Present only when `preserveSpeech`/`ducking` altered the music bed. */
273
+ music_processed?: SfxMedia;
274
+ sfx?: SfxMedia;
275
+ duration_seconds?: number;
276
+ }
204
277
 
205
278
  declare class Account {
206
279
  private readonly client;
@@ -239,6 +312,12 @@ declare class TextToMusic {
239
312
  stream(params: TextToMusicParams): AsyncGenerator<StreamEvent, void, undefined>;
240
313
  /** Generate and buffer the whole track; throws GenerationError on stream errors. */
241
314
  generate(params: TextToMusicParams): Promise<Track>;
315
+ /**
316
+ * Submit an async text-to-music task; poll with
317
+ * `client.tasks.wait<MusicTaskResult>(task.task_id)`. Required for
318
+ * `outputFormat: "wav"`. `stream()`/`generate()` remain the streaming path.
319
+ */
320
+ submit(params: TextToMusicParams): Promise<SfxTask>;
242
321
  }
243
322
 
244
323
  declare class VideoToMusic {
@@ -269,6 +348,43 @@ declare class VideoToSfx {
269
348
  generate(params: VideoToSfxParams, opts?: WaitOptions): Promise<SfxResult>;
270
349
  }
271
350
 
351
+ /** Generate an original score for a video and get back a re-hosted video with
352
+ * the music muxed in. Async only: `submit()` returns a task ack; poll with
353
+ * `client.tasks.wait<VideoResult>(id)`, or use `generate()` to do both. */
354
+ declare class VideoToVideoMusic {
355
+ private readonly client;
356
+ constructor(client: SoniloClient);
357
+ submit(params: VideoToVideoMusicParams): Promise<SfxTask>;
358
+ generate(params: VideoToVideoMusicParams, opts?: WaitOptions): Promise<VideoResult>;
359
+ }
360
+
361
+ /** Generate sound effects for a video and get back a re-hosted video with the
362
+ * SFX muxed in. Async only. */
363
+ declare class VideoToVideoSfx {
364
+ private readonly client;
365
+ constructor(client: SoniloClient);
366
+ submit(params: VideoToVideoSfxParams): Promise<SfxTask>;
367
+ generate(params: VideoToVideoSfxParams, opts?: WaitOptions): Promise<VideoResult>;
368
+ }
369
+
370
+ /** Generate a combined music + sound-effects track for a video and get back
371
+ * the mixed audio. Async only. */
372
+ declare class VideoToSound {
373
+ private readonly client;
374
+ constructor(client: SoniloClient);
375
+ submit(params: VideoToSoundParams): Promise<SfxTask>;
376
+ generate(params: VideoToSoundParams, opts?: WaitOptions): Promise<SoundResult>;
377
+ }
378
+
379
+ /** Generate a combined music + sound-effects track for a video and get back a
380
+ * re-hosted video with that track muxed in. Async only. */
381
+ declare class VideoToVideoSound {
382
+ private readonly client;
383
+ constructor(client: SoniloClient);
384
+ submit(params: VideoToSoundParams): Promise<SfxTask>;
385
+ generate(params: VideoToSoundParams, opts?: WaitOptions): Promise<SoundResult>;
386
+ }
387
+
272
388
  interface SoniloClientOptions {
273
389
  /** Defaults to the SONILO_API_KEY environment variable (Node.js only). */
274
390
  apiKey?: string;
@@ -292,6 +408,10 @@ declare class SoniloClient {
292
408
  readonly videoToMusic: VideoToMusic;
293
409
  readonly textToSfx: TextToSfx;
294
410
  readonly videoToSfx: VideoToSfx;
411
+ readonly videoToVideoMusic: VideoToVideoMusic;
412
+ readonly videoToVideoSfx: VideoToVideoSfx;
413
+ readonly videoToSound: VideoToSound;
414
+ readonly videoToVideoSound: VideoToVideoSound;
295
415
  constructor(options?: SoniloClientOptions);
296
416
  /**
297
417
  * Perform an authenticated request; throws a typed error on non-2xx.
@@ -357,9 +477,13 @@ declare class TaskTimeoutError extends SoniloError {
357
477
  declare class RequestTimeoutError extends SoniloError {
358
478
  }
359
479
 
360
- /** Fetch a result media file. The URL is presigned — no API key is sent. */
361
- declare function download(media: SfxMedia | undefined, fetchFn?: typeof globalThis.fetch, timeout?: number): Promise<Uint8Array>;
480
+ /** Fetch a result media file. The URL is presigned — no API key is sent.
481
+ *
482
+ * Accepts either a media object (`result.audio`, `result.music`, …) or a bare
483
+ * URL string, which is what the combined video-to-sound endpoints return as
484
+ * `output_url`. */
485
+ declare function download(media: SfxMedia | string | undefined, fetchFn?: typeof globalThis.fetch, timeout?: number): Promise<Uint8Array>;
362
486
 
363
- declare const VERSION = "0.2.0";
487
+ declare const VERSION = "0.4.0";
364
488
 
365
- 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 StreamEvent, TaskFailedError, TaskTimeoutError, type TextToMusicParams, type TextToSfxParams, type TitleEvent, type Track, type UnknownEvent, type UsageResponse, type UsageSummary, VERSION, type VideoInput, type VideoToMusicParams, type VideoToSfxParams, type WaitOptions, download, isAudioChunkEvent, isErrorEvent };
489
+ 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 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
@@ -295,6 +295,32 @@ var TextToMusic = class {
295
295
  generate(params) {
296
296
  return collectTrack(this.stream(params));
297
297
  }
298
+ /**
299
+ * Submit an async text-to-music task; poll with
300
+ * `client.tasks.wait<MusicTaskResult>(task.task_id)`. Required for
301
+ * `outputFormat: "wav"`. `stream()`/`generate()` remain the streaming path.
302
+ */
303
+ async submit(params) {
304
+ const mode = params.mode ?? "async";
305
+ if (mode !== "async") {
306
+ throw new SoniloError('submit() requires mode: "async"');
307
+ }
308
+ const form = new FormData();
309
+ form.set("prompt", params.prompt);
310
+ form.set("duration", String(params.duration));
311
+ if (params.segments !== void 0) {
312
+ form.set("segments", JSON.stringify(params.segments));
313
+ }
314
+ form.set("mode", mode);
315
+ if (params.outputFormat !== void 0) {
316
+ form.set("output_format", params.outputFormat);
317
+ }
318
+ const res = await this.client.request("/v1/text-to-music", {
319
+ method: "POST",
320
+ body: form
321
+ });
322
+ return await res.json();
323
+ }
298
324
  };
299
325
 
300
326
  // src/upload.ts
@@ -377,9 +403,12 @@ var VideoToMusic = class {
377
403
  throw new SoniloError("Provide exactly one of video or videoUrl");
378
404
  }
379
405
  let mode = params.mode;
380
- if (params.isolateVocals && mode === void 0) mode = "async";
381
- if (params.isolateVocals && mode !== "async") {
382
- throw new SoniloError('isolateVocals requires mode: "async"');
406
+ const needsAsync = params.isolateVocals || params.preserveSpeech || params.ducking !== void 0 || params.outputFormat === "wav";
407
+ if (mode === void 0) mode = "async";
408
+ if (needsAsync && mode !== "async") {
409
+ throw new SoniloError(
410
+ 'isolateVocals/preserveSpeech/ducking/outputFormat "wav" require mode: "async"'
411
+ );
383
412
  }
384
413
  const form = new FormData();
385
414
  if (params.video !== void 0) {
@@ -392,10 +421,19 @@ var VideoToMusic = class {
392
421
  if (params.segments !== void 0) {
393
422
  form.set("segments", JSON.stringify(params.segments));
394
423
  }
395
- if (mode !== void 0) form.set("mode", mode);
424
+ form.set("mode", mode);
425
+ if (params.preserveSpeech !== void 0) {
426
+ form.set("preserve_speech", String(params.preserveSpeech));
427
+ }
396
428
  if (params.isolateVocals !== void 0) {
397
429
  form.set("isolate_vocals", String(params.isolateVocals));
398
430
  }
431
+ if (params.outputFormat !== void 0) {
432
+ form.set("output_format", params.outputFormat);
433
+ }
434
+ if (params.ducking !== void 0) {
435
+ form.set("ducking", String(params.ducking));
436
+ }
399
437
  const res = await this.client.request("/v1/video-to-music", {
400
438
  method: "POST",
401
439
  body: form
@@ -459,8 +497,135 @@ var VideoToSfx = class {
459
497
  }
460
498
  };
461
499
 
500
+ // src/resources/videoToVideoMusic.ts
501
+ var VideoToVideoMusic = class {
502
+ constructor(client) {
503
+ this.client = client;
504
+ }
505
+ async submit(params) {
506
+ if (params.video === void 0 === (params.videoUrl === void 0)) {
507
+ throw new SoniloError("Provide exactly one of video or videoUrl");
508
+ }
509
+ const form = new FormData();
510
+ if (params.video !== void 0) {
511
+ const { blob, filename } = await toUploadBlob(params.video);
512
+ form.set("video", blob, filename);
513
+ } else {
514
+ form.set("video_url", params.videoUrl);
515
+ }
516
+ if (params.prompt !== void 0) form.set("prompt", params.prompt);
517
+ if (params.preserveSpeech !== void 0) {
518
+ form.set("preserve_speech", String(params.preserveSpeech));
519
+ }
520
+ if (params.isolateVocals !== void 0) {
521
+ form.set("isolate_vocals", String(params.isolateVocals));
522
+ }
523
+ const res = await this.client.request("/v1/video-to-video-music", {
524
+ method: "POST",
525
+ body: form
526
+ });
527
+ return await res.json();
528
+ }
529
+ async generate(params, opts) {
530
+ const task = await this.submit(params);
531
+ return this.client.tasks.wait(task.task_id, opts);
532
+ }
533
+ };
534
+
535
+ // src/resources/videoToVideoSfx.ts
536
+ var VideoToVideoSfx = class {
537
+ constructor(client) {
538
+ this.client = client;
539
+ }
540
+ async submit(params) {
541
+ if (params.video === void 0 === (params.videoUrl === void 0)) {
542
+ throw new SoniloError("Provide exactly one of video or videoUrl");
543
+ }
544
+ const form = new FormData();
545
+ if (params.video !== void 0) {
546
+ const { blob, filename } = await toUploadBlob(params.video);
547
+ form.set("video", blob, filename);
548
+ } else {
549
+ form.set("video_url", params.videoUrl);
550
+ }
551
+ if (params.prompt !== void 0) form.set("prompt", params.prompt);
552
+ if (params.segments !== void 0) {
553
+ form.set("segments", JSON.stringify(params.segments));
554
+ }
555
+ const res = await this.client.request("/v1/video-to-video-sfx", {
556
+ method: "POST",
557
+ body: form
558
+ });
559
+ return await res.json();
560
+ }
561
+ async generate(params, opts) {
562
+ const task = await this.submit(params);
563
+ return this.client.tasks.wait(task.task_id, opts);
564
+ }
565
+ };
566
+
567
+ // src/resources/soundForm.ts
568
+ async function buildSoundForm(params) {
569
+ if (params.video === void 0 === (params.videoUrl === void 0)) {
570
+ throw new SoniloError("Provide exactly one of video or videoUrl");
571
+ }
572
+ const form = new FormData();
573
+ if (params.video !== void 0) {
574
+ const { blob, filename } = await toUploadBlob(params.video);
575
+ form.set("video", blob, filename);
576
+ } else {
577
+ form.set("video_url", params.videoUrl);
578
+ }
579
+ if (params.musicPrompt !== void 0) form.set("music_prompt", params.musicPrompt);
580
+ if (params.sfxPrompt !== void 0) form.set("sfx_prompt", params.sfxPrompt);
581
+ if (params.segments !== void 0) {
582
+ form.set("segments", JSON.stringify(params.segments));
583
+ }
584
+ if (params.preserveSpeech !== void 0) {
585
+ form.set("preserve_speech", String(params.preserveSpeech));
586
+ }
587
+ if (params.ducking !== void 0) form.set("ducking", String(params.ducking));
588
+ return form;
589
+ }
590
+
591
+ // src/resources/videoToSound.ts
592
+ var VideoToSound = class {
593
+ constructor(client) {
594
+ this.client = client;
595
+ }
596
+ async submit(params) {
597
+ const res = await this.client.request("/v1/video-to-sound", {
598
+ method: "POST",
599
+ body: await buildSoundForm(params)
600
+ });
601
+ return await res.json();
602
+ }
603
+ async generate(params, opts) {
604
+ const task = await this.submit(params);
605
+ return this.client.tasks.wait(task.task_id, opts);
606
+ }
607
+ };
608
+
609
+ // src/resources/videoToVideoSound.ts
610
+ var VideoToVideoSound = class {
611
+ constructor(client) {
612
+ this.client = client;
613
+ }
614
+ async submit(params) {
615
+ const res = await this.client.request("/v1/video-to-video-sound", {
616
+ method: "POST",
617
+ body: await buildSoundForm(params)
618
+ });
619
+ return await res.json();
620
+ }
621
+ async generate(params, opts) {
622
+ const task = await this.submit(params);
623
+ return this.client.tasks.wait(task.task_id, opts);
624
+ }
625
+ };
626
+
462
627
  // src/version.ts
463
- var VERSION = "0.2.0";
628
+ var VERSION = "0.4.0";
464
629
 
465
630
  // src/client.ts
466
631
  var DEFAULT_BASE_URL = "https://api.sonilo.com";
@@ -484,6 +649,10 @@ var SoniloClient = class {
484
649
  this.videoToMusic = new VideoToMusic(this);
485
650
  this.textToSfx = new TextToSfx(this);
486
651
  this.videoToSfx = new VideoToSfx(this);
652
+ this.videoToVideoMusic = new VideoToVideoMusic(this);
653
+ this.videoToVideoSfx = new VideoToVideoSfx(this);
654
+ this.videoToSound = new VideoToSound(this);
655
+ this.videoToVideoSound = new VideoToVideoSound(this);
487
656
  }
488
657
  /**
489
658
  * Perform an authenticated request; throws a typed error on non-2xx.
@@ -516,15 +685,16 @@ var SoniloClient = class {
516
685
 
517
686
  // src/download.ts
518
687
  async function download(media, fetchFn = globalThis.fetch, timeout = DEFAULT_TIMEOUT_MS) {
519
- if (!media?.url) {
688
+ const url = typeof media === "string" ? media : media?.url;
689
+ if (!url) {
520
690
  throw new SoniloError("No media to download");
521
691
  }
522
692
  let res;
523
693
  try {
524
- res = await fetchFn(media.url, { signal: AbortSignal.timeout(timeout) });
694
+ res = await fetchFn(url, { signal: AbortSignal.timeout(timeout) });
525
695
  } catch (err) {
526
696
  if (isTimeoutSignalError(err)) {
527
- throw new RequestTimeoutError(`Download of ${media.url} timed out after ${timeout}ms`);
697
+ throw new RequestTimeoutError(`Download of ${url} timed out after ${timeout}ms`);
528
698
  }
529
699
  throw err;
530
700
  }