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 +89 -1
- package/dist/index.cjs +47 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +120 -7
- package/dist/index.d.ts +120 -7
- package/dist/index.js +47 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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`,
|
|
455
|
-
* `variantsNum` above 1 — the backend rejects all of these on
|
|
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.
|
|
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.
|