sonilo 0.14.0 → 0.16.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 CHANGED
@@ -127,6 +127,53 @@ if (result.ducked) {
127
127
  }
128
128
  ```
129
129
 
130
+ ### Stems (async)
131
+
132
+ Set `stems: true` to also split the generated track into four separated
133
+ stems — `drums`, `bass`, `vocals`, `other`. It is **free of charge**, and
134
+ available on `textToMusic` and `videoToMusic`. It requires the async task API
135
+ (`mode: "async"` — the backend rejects it on the plain stream with a 400), so
136
+ it is only meaningful via `submit()`; on `videoToMusic` it splits the
137
+ **generated** music, never the source video's own audio.
138
+
139
+ When separation succeeds, the result gains a `stems` array with one entry per
140
+ stream: `{ stream_index, drums, bass, vocals, other }`, each stem an ordinary
141
+ media object (`url`, `content_type`, `file_size`) you can pass to
142
+ `download()`. **Look entries up by `stream_index`, never by array position** —
143
+ `stems` carries only the streams that separated successfully, so it can be
144
+ shorter than `audio`.
145
+
146
+ Failures land in `stems_error`, a string present when separation failed wholly
147
+ or in part, or was skipped. It can appear **alongside a partial `stems`
148
+ array**, so never treat it as "no stems" — check `stems` itself for what did
149
+ arrive. Either way the generated `audio` is unaffected.
150
+
151
+ Separation runs after generation and typically adds 2-6 minutes to the wait
152
+ (it gives up after 30), so raise `tasks.wait`'s `timeout` beyond the 10-minute
153
+ default. The stems normally follow the request's `outputFormat`; each stem's
154
+ own `content_type` reports what was actually delivered.
155
+
156
+ ```ts
157
+ const task = await client.textToMusic.submit({
158
+ prompt: "warm lo-fi piano",
159
+ duration: 30,
160
+ stems: true, // free — adds drums/bass/vocals/other alongside the mix
161
+ });
162
+ const result = await client.tasks.wait<MusicTaskResult>(task.task_id, {
163
+ timeout: 2_400_000, // separation can add up to 30 minutes
164
+ });
165
+
166
+ if (result.stems_error) console.warn(result.stems_error); // may be partial
167
+ for (const track of result.audio ?? []) {
168
+ const split = result.stems?.find((s) => s.stream_index === track.stream_index);
169
+ if (!split) continue; // this stream did not separate — see stems_error
170
+ await writeFile("drums.m4a", await download(split.drums));
171
+ await writeFile("bass.m4a", await download(split.bass));
172
+ await writeFile("vocals.m4a", await download(split.vocals));
173
+ await writeFile("other.m4a", await download(split.other));
174
+ }
175
+ ```
176
+
130
177
  ### Variants (async)
131
178
 
132
179
  `variantsNum` generates several distinct music variants in one request (1-10,
@@ -338,6 +385,47 @@ The result is a `DubbingResult`, whose `outputs` is a map of language code to
338
385
  dubbed `.mp4` URL — not the `audio`/`video`/`output_url` shape the other
339
386
  endpoints use.
340
387
 
388
+ ## Video analysis
389
+
390
+ `client.videoAnalysis` analyzes a video and returns a **creative brief** for
391
+ scoring it. Nothing is generated: no audio, no video, no file to download.
392
+ The result is the work order — a time-aligned `segments` plan plus one
393
+ `prompt` per requested variation, each ready to hand straight to
394
+ `videoToMusic`, `videoToSfx`, `videoToSound` or their video-to-video
395
+ counterparts.
396
+
397
+ Pass exactly one of `video` / `videoUrl`, plus optional `prompt` (guidance
398
+ for the analysis, at most 2000 characters) and `variantsNum` (1-5, default
399
+ 1 — billed per brief). Source videos may be at most 600 seconds long, and
400
+ billing has a 10-second floor, so a very short clip still costs the same as a
401
+ 10-second one.
402
+
403
+ ```ts
404
+ const brief = await client.videoAnalysis.analyze({
405
+ video: "trailer.mp4",
406
+ prompt: "focus on the chase",
407
+ variantsNum: 2,
408
+ });
409
+
410
+ for (const segment of brief.segments ?? []) {
411
+ console.log(`${segment.start}-${segment.end}s [${segment.label}] ${segment.prompt}`);
412
+ }
413
+
414
+ // Feed a variation's prompt straight into a generation call.
415
+ const task = await client.videoToMusic.submit({
416
+ video: "trailer.mp4",
417
+ prompt: brief.variations![0]!.prompt,
418
+ });
419
+ ```
420
+
421
+ The method is `analyze`, not `generate`, for the same reason there is no
422
+ download helper on the result: every other resource returns something you
423
+ save, and this one never does. Both `segments` and `variations` are optional
424
+ on the type because a `processing` or `failed` poll carries neither. Use
425
+ `submit()` instead of `analyze()` to get a `task_id` back immediately and
426
+ poll it yourself with
427
+ `client.tasks.wait<VideoAnalysisResult>(taskId)`.
428
+
341
429
  ## Configuration
342
430
 
343
431
  ```ts
@@ -426,7 +514,7 @@ endpoints — no card required:
426
514
 
427
515
  | Free runs | Endpoints |
428
516
  | --- | --- |
429
- | 2 each | text-to-music, text-to-sfx, audio-ducking |
517
+ | 2 each | text-to-music, text-to-sfx, audio-ducking, video-analysis |
430
518
  | 1 each | video-to-music, video-to-sfx, video-to-video-music, video-to-video-sfx, video-to-sound, video-to-video-sound |
431
519
  | 0 | dubbing |
432
520
 
package/dist/index.cjs CHANGED
@@ -347,8 +347,8 @@ var TextToMusic = class {
347
347
  /**
348
348
  * Submit an async text-to-music task; poll with
349
349
  * `client.tasks.wait<MusicTaskResult>(task.task_id)`. Required for
350
- * a non-m4a `outputFormat` and `variantsNum` above 1. `stream()`/`generate()`
351
- * remain the streaming path.
350
+ * a non-m4a `outputFormat`, `variantsNum` above 1, and `stems`.
351
+ * `stream()`/`generate()` remain the streaming path.
352
352
  */
353
353
  async submit(params) {
354
354
  const mode = params.mode ?? "async";
@@ -368,6 +368,9 @@ var TextToMusic = class {
368
368
  if (params.variantsNum !== void 0) {
369
369
  form.set("variants_num", String(params.variantsNum));
370
370
  }
371
+ if (params.stems !== void 0) {
372
+ form.set("stems", String(params.stems));
373
+ }
371
374
  const res = await this.client.request("/v1/text-to-music", {
372
375
  method: "POST",
373
376
  body: form
@@ -451,9 +454,9 @@ var VideoToMusic = class {
451
454
  /**
452
455
  * Submit an async video-to-music task; poll its result with
453
456
  * `client.tasks.wait<MusicTaskResult>(task.task_id)`. Required for
454
- * `isolateVocals`/`preserveSpeech`, a non-m4a `outputFormat`, and
455
- * `variantsNum` above 1 — the backend rejects all of these on the plain
456
- * stream, and they only ever run in async mode.
457
+ * `isolateVocals`/`preserveSpeech`, a non-m4a `outputFormat`,
458
+ * `variantsNum` above 1, and `stems` — the backend rejects all of these on
459
+ * the plain stream, and they only ever run in async mode.
457
460
  */
458
461
  async submit(params) {
459
462
  if (params.video === void 0 === (params.videoUrl === void 0)) {
@@ -463,11 +466,11 @@ var VideoToMusic = class {
463
466
  const needsAsync = params.isolateVocals || params.preserveSpeech || params.ducking !== void 0 || // Any non-m4a container is a finalize-time transcode, so it needs
464
467
  // async. Checking != "m4a" rather than == "wav" keeps this correct
465
468
  // as formats are added (mp3 landed after the original check).
466
- params.outputFormat !== void 0 && params.outputFormat !== "m4a" || params.variantsNum !== void 0 && params.variantsNum > 1;
469
+ params.outputFormat !== void 0 && params.outputFormat !== "m4a" || params.variantsNum !== void 0 && params.variantsNum > 1 || params.stems !== void 0;
467
470
  if (mode === void 0) mode = "async";
468
471
  if (needsAsync && mode !== "async") {
469
472
  throw new SoniloError(
470
- 'isolateVocals/preserveSpeech/ducking/outputFormat other than "m4a"/variantsNum > 1 require mode: "async"'
473
+ 'isolateVocals/preserveSpeech/ducking/stems/outputFormat other than "m4a"/variantsNum > 1 require mode: "async"'
471
474
  );
472
475
  }
473
476
  const form = new FormData();
@@ -497,6 +500,9 @@ var VideoToMusic = class {
497
500
  if (params.variantsNum !== void 0) {
498
501
  form.set("variants_num", String(params.variantsNum));
499
502
  }
503
+ if (params.stems !== void 0) {
504
+ form.set("stems", String(params.stems));
505
+ }
500
506
  if (params.promptInfluence !== void 0) {
501
507
  form.set("prompt_influence", String(params.promptInfluence));
502
508
  }
@@ -792,8 +798,40 @@ var Dubbing = class {
792
798
  }
793
799
  };
794
800
 
801
+ // src/resources/videoAnalysis.ts
802
+ var VideoAnalysis = class {
803
+ constructor(client) {
804
+ this.client = client;
805
+ }
806
+ async submit(params) {
807
+ if (params.video === void 0 === (params.videoUrl === void 0)) {
808
+ throw new SoniloError("Provide exactly one of video or videoUrl");
809
+ }
810
+ const form = new FormData();
811
+ if (params.video !== void 0) {
812
+ const { blob, filename } = await toUploadBlob(params.video);
813
+ form.set("video", blob, filename);
814
+ } else {
815
+ form.set("video_url", params.videoUrl);
816
+ }
817
+ if (params.prompt !== void 0) form.set("prompt", params.prompt);
818
+ if (params.variantsNum !== void 0) {
819
+ form.set("variants_num", String(params.variantsNum));
820
+ }
821
+ const res = await this.client.request("/v1/video-analysis", {
822
+ method: "POST",
823
+ body: form
824
+ });
825
+ return await res.json();
826
+ }
827
+ async analyze(params, opts) {
828
+ const task = await this.submit(params);
829
+ return this.client.tasks.wait(task.task_id, opts);
830
+ }
831
+ };
832
+
795
833
  // src/version.ts
796
- var VERSION = "0.14.0";
834
+ var VERSION = "0.16.0";
797
835
 
798
836
  // src/client.ts
799
837
  var DEFAULT_BASE_URL = "https://api.sonilo.com";
@@ -826,6 +864,7 @@ var SoniloClient = class {
826
864
  this.videoToVideoSound = new VideoToVideoSound(this);
827
865
  this.audioDucking = new AudioDucking(this);
828
866
  this.dubbing = new Dubbing(this);
867
+ this.videoAnalysis = new VideoAnalysis(this);
829
868
  }
830
869
  /**
831
870
  * Perform an authenticated request; throws a typed error on non-2xx.