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/README.md +133 -8
- package/dist/index.cjs +178 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +128 -4
- package/dist/index.d.ts +128 -4
- package/dist/index.js +178 -8
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -9,6 +9,25 @@ Works in Node.js ≥ 18 and modern browsers. Zero runtime dependencies.
|
|
|
9
9
|
npm install sonilo
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
+
## Authentication
|
|
13
|
+
|
|
14
|
+
Create an API key in your [Sonilo dashboard](https://platform.sonilo.com/dashboard/api-keys),
|
|
15
|
+
then give it to the client either as an environment variable (recommended) or
|
|
16
|
+
inline:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
export SONILO_API_KEY=sk_...
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
const sonilo = new SoniloClient(); // reads SONILO_API_KEY
|
|
24
|
+
// or pass it directly:
|
|
25
|
+
const sonilo = new SoniloClient({ apiKey: "sk_..." });
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Keep your key secret — use it only server-side, never commit it, and prefer the
|
|
29
|
+
environment variable over hardcoding it.
|
|
30
|
+
|
|
12
31
|
## Quickstart
|
|
13
32
|
|
|
14
33
|
```ts
|
|
@@ -36,12 +55,14 @@ const track = await sonilo.videoToMusic.generate({
|
|
|
36
55
|
await sonilo.videoToMusic.generate({ videoUrl: "https://example.com/clip.mp4" });
|
|
37
56
|
```
|
|
38
57
|
|
|
39
|
-
###
|
|
58
|
+
### Preserve speech (async)
|
|
40
59
|
|
|
41
|
-
|
|
42
|
-
stream above doesn't support it
|
|
43
|
-
implies `mode: "async"` if you don't set `mode` yourself
|
|
44
|
-
`client.tasks.wait<MusicTaskResult>()
|
|
60
|
+
Set `preserveSpeech: true` to keep the source speech/vocals in the result.
|
|
61
|
+
This requires the async task API — the plain stream above doesn't support it —
|
|
62
|
+
so it implies `mode: "async"` if you don't set `mode` yourself. Submit, then
|
|
63
|
+
poll with `client.tasks.wait<MusicTaskResult>()`. The result carries the
|
|
64
|
+
generated `audio` plus a separate speech stem (`vocals`) and a `mux` (the
|
|
65
|
+
generated music mixed with the preserved speech):
|
|
45
66
|
|
|
46
67
|
```ts
|
|
47
68
|
import { SoniloClient, download } from "sonilo";
|
|
@@ -52,16 +73,108 @@ const client = new SoniloClient();
|
|
|
52
73
|
const task = await client.videoToMusic.submit({
|
|
53
74
|
video: "./my_video.mp4",
|
|
54
75
|
prompt: "upbeat, energetic",
|
|
55
|
-
|
|
76
|
+
preserveSpeech: true,
|
|
56
77
|
});
|
|
57
78
|
const result = await client.tasks.wait<MusicTaskResult>(task.task_id);
|
|
58
79
|
|
|
59
|
-
// `audio` is always an array for async video-to-music (one entry per
|
|
60
|
-
//
|
|
80
|
+
// `audio` is always an array for async video-to-music (one entry per output
|
|
81
|
+
// stream); `vocals` and `mux` are only present when preserveSpeech is set.
|
|
61
82
|
await writeFile("mix.m4a", await download(result.audio[0]!));
|
|
62
83
|
await writeFile("vocals.m4a", await download(result.vocals!));
|
|
63
84
|
```
|
|
64
85
|
|
|
86
|
+
### Ducking, speech & output format (async video-to-music)
|
|
87
|
+
|
|
88
|
+
The async `submit()` path also accepts:
|
|
89
|
+
|
|
90
|
+
- `preserveSpeech` — keep the source speech/vocals in the result (see
|
|
91
|
+
[Preserve speech](#preserve-speech-async) above).
|
|
92
|
+
- `ducking` — duck the generated music under the source voice. It is **on by
|
|
93
|
+
default** in async mode; pass `ducking: false` to opt out. When it runs, the
|
|
94
|
+
result gains a `ducked` array alongside `audio`.
|
|
95
|
+
- `outputFormat` — `"m4a"` (default) or `"wav"` (requires async mode).
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
const task = await client.videoToMusic.submit({
|
|
99
|
+
video: "./my_video.mp4",
|
|
100
|
+
preserveSpeech: true,
|
|
101
|
+
outputFormat: "wav",
|
|
102
|
+
// ducking is on by default in async — set `false` to disable
|
|
103
|
+
});
|
|
104
|
+
const result = await client.tasks.wait<MusicTaskResult>(task.task_id);
|
|
105
|
+
if (result.ducked) {
|
|
106
|
+
await writeFile("ducked.wav", await download(result.ducked[0]!));
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Video to video
|
|
111
|
+
|
|
112
|
+
Generate a soundtrack or sound effects and get back a **re-hosted video** with
|
|
113
|
+
the audio muxed in — not just an audio file. Both endpoints are async; poll to
|
|
114
|
+
a `VideoResult`:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { SoniloClient, download } from "sonilo";
|
|
118
|
+
import { writeFile } from "node:fs/promises";
|
|
119
|
+
|
|
120
|
+
const client = new SoniloClient();
|
|
121
|
+
|
|
122
|
+
// Score music into the video (optionally keep the original speech)
|
|
123
|
+
const music = await client.videoToVideoMusic.generate({
|
|
124
|
+
video: "./my_video.mp4", // Node path; File/Blob in the browser, or `videoUrl`
|
|
125
|
+
prompt: "cinematic orchestral swell",
|
|
126
|
+
preserveSpeech: true,
|
|
127
|
+
});
|
|
128
|
+
await writeFile("scored.mp4", await download(music.video!));
|
|
129
|
+
|
|
130
|
+
// Sound effects for the video, optionally per time segment
|
|
131
|
+
const sfx = await client.videoToVideoSfx.generate({
|
|
132
|
+
video: "./my_video.mp4",
|
|
133
|
+
segments: [{ start: 0, end: 2, prompt: "footsteps on gravel" }],
|
|
134
|
+
});
|
|
135
|
+
await writeFile("with_sfx.mp4", await download(sfx.video!));
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Video to sound
|
|
139
|
+
|
|
140
|
+
`videoToSound` and `videoToVideoSound` generate a music bed and sound effects
|
|
141
|
+
for the same clip and return them mixed into a single soundtrack — one call,
|
|
142
|
+
one charge, instead of chaining two requests. `videoToSound` returns the mixed
|
|
143
|
+
audio; `videoToVideoSound` returns the source video with that audio muxed in.
|
|
144
|
+
Both are async-only, and both accept the same options.
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import { SoniloClient, download } from "sonilo";
|
|
148
|
+
import { writeFile } from "node:fs/promises";
|
|
149
|
+
|
|
150
|
+
const client = new SoniloClient();
|
|
151
|
+
|
|
152
|
+
const result = await client.videoToSound.generate({
|
|
153
|
+
videoUrl: "https://example.com/clip.mp4",
|
|
154
|
+
musicPrompt: "uplifting orchestral score",
|
|
155
|
+
sfxPrompt: "match the on-screen action",
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
await writeFile("soundtrack.wav", await download(result.output_url));
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The mixed result is `output_url` (`output_type` is `"audio"` here, `"video"`
|
|
162
|
+
for `videoToVideoSound`). The individual stems come back alongside it, so you
|
|
163
|
+
can re-balance the mix yourself:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
await writeFile("music.m4a", await download(result.music));
|
|
167
|
+
await writeFile("sfx.wav", await download(result.sfx));
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`preserveSpeech: true` keeps the speech from the source video, and `ducking`
|
|
171
|
+
(on by default) dips the music under it — pass `ducking: false` to opt out.
|
|
172
|
+
`segments` takes the same `{ start, end, prompt }` list as `videoToSfx`.
|
|
173
|
+
Input videos may be at most 180 seconds long.
|
|
174
|
+
|
|
175
|
+
Use `submit()` instead of `generate()` to get a `task_id` back immediately and
|
|
176
|
+
poll it yourself with `client.tasks.wait<SoundResult>(taskId)`.
|
|
177
|
+
|
|
65
178
|
## Configuration
|
|
66
179
|
|
|
67
180
|
```ts
|
|
@@ -143,6 +256,18 @@ const result = await client.tasks.wait(task.task_id, { pollInterval: 2000, timeo
|
|
|
143
256
|
task keeps running server-side and can still be polled afterwards. Result URLs
|
|
144
257
|
are presigned and expire; download promptly or re-fetch via `tasks.get`.
|
|
145
258
|
|
|
259
|
+
## Free trial
|
|
260
|
+
|
|
261
|
+
Accounts created through self-serve signup start with free runs on every
|
|
262
|
+
endpoint — no card required:
|
|
263
|
+
|
|
264
|
+
| Free runs | Endpoints |
|
|
265
|
+
| --- | --- |
|
|
266
|
+
| 2 each | text-to-music, text-to-sfx, audio-ducking |
|
|
267
|
+
| 1 each | video-to-music, video-to-sfx, video-to-video-music, video-to-video-sfx, video-to-sound, video-to-video-sound |
|
|
268
|
+
|
|
269
|
+
Once an endpoint's free runs are used up, calls to it bill at the normal rate.
|
|
270
|
+
|
|
146
271
|
## Account
|
|
147
272
|
|
|
148
273
|
```ts
|
package/dist/index.cjs
CHANGED
|
@@ -336,6 +336,32 @@ var TextToMusic = class {
|
|
|
336
336
|
generate(params) {
|
|
337
337
|
return collectTrack(this.stream(params));
|
|
338
338
|
}
|
|
339
|
+
/**
|
|
340
|
+
* Submit an async text-to-music task; poll with
|
|
341
|
+
* `client.tasks.wait<MusicTaskResult>(task.task_id)`. Required for
|
|
342
|
+
* `outputFormat: "wav"`. `stream()`/`generate()` remain the streaming path.
|
|
343
|
+
*/
|
|
344
|
+
async submit(params) {
|
|
345
|
+
const mode = params.mode ?? "async";
|
|
346
|
+
if (mode !== "async") {
|
|
347
|
+
throw new SoniloError('submit() requires mode: "async"');
|
|
348
|
+
}
|
|
349
|
+
const form = new FormData();
|
|
350
|
+
form.set("prompt", params.prompt);
|
|
351
|
+
form.set("duration", String(params.duration));
|
|
352
|
+
if (params.segments !== void 0) {
|
|
353
|
+
form.set("segments", JSON.stringify(params.segments));
|
|
354
|
+
}
|
|
355
|
+
form.set("mode", mode);
|
|
356
|
+
if (params.outputFormat !== void 0) {
|
|
357
|
+
form.set("output_format", params.outputFormat);
|
|
358
|
+
}
|
|
359
|
+
const res = await this.client.request("/v1/text-to-music", {
|
|
360
|
+
method: "POST",
|
|
361
|
+
body: form
|
|
362
|
+
});
|
|
363
|
+
return await res.json();
|
|
364
|
+
}
|
|
339
365
|
};
|
|
340
366
|
|
|
341
367
|
// src/upload.ts
|
|
@@ -418,9 +444,12 @@ var VideoToMusic = class {
|
|
|
418
444
|
throw new SoniloError("Provide exactly one of video or videoUrl");
|
|
419
445
|
}
|
|
420
446
|
let mode = params.mode;
|
|
421
|
-
|
|
422
|
-
if (
|
|
423
|
-
|
|
447
|
+
const needsAsync = params.isolateVocals || params.preserveSpeech || params.ducking !== void 0 || params.outputFormat === "wav";
|
|
448
|
+
if (mode === void 0) mode = "async";
|
|
449
|
+
if (needsAsync && mode !== "async") {
|
|
450
|
+
throw new SoniloError(
|
|
451
|
+
'isolateVocals/preserveSpeech/ducking/outputFormat "wav" require mode: "async"'
|
|
452
|
+
);
|
|
424
453
|
}
|
|
425
454
|
const form = new FormData();
|
|
426
455
|
if (params.video !== void 0) {
|
|
@@ -433,10 +462,19 @@ var VideoToMusic = class {
|
|
|
433
462
|
if (params.segments !== void 0) {
|
|
434
463
|
form.set("segments", JSON.stringify(params.segments));
|
|
435
464
|
}
|
|
436
|
-
|
|
465
|
+
form.set("mode", mode);
|
|
466
|
+
if (params.preserveSpeech !== void 0) {
|
|
467
|
+
form.set("preserve_speech", String(params.preserveSpeech));
|
|
468
|
+
}
|
|
437
469
|
if (params.isolateVocals !== void 0) {
|
|
438
470
|
form.set("isolate_vocals", String(params.isolateVocals));
|
|
439
471
|
}
|
|
472
|
+
if (params.outputFormat !== void 0) {
|
|
473
|
+
form.set("output_format", params.outputFormat);
|
|
474
|
+
}
|
|
475
|
+
if (params.ducking !== void 0) {
|
|
476
|
+
form.set("ducking", String(params.ducking));
|
|
477
|
+
}
|
|
440
478
|
const res = await this.client.request("/v1/video-to-music", {
|
|
441
479
|
method: "POST",
|
|
442
480
|
body: form
|
|
@@ -500,8 +538,135 @@ var VideoToSfx = class {
|
|
|
500
538
|
}
|
|
501
539
|
};
|
|
502
540
|
|
|
541
|
+
// src/resources/videoToVideoMusic.ts
|
|
542
|
+
var VideoToVideoMusic = class {
|
|
543
|
+
constructor(client) {
|
|
544
|
+
this.client = client;
|
|
545
|
+
}
|
|
546
|
+
async submit(params) {
|
|
547
|
+
if (params.video === void 0 === (params.videoUrl === void 0)) {
|
|
548
|
+
throw new SoniloError("Provide exactly one of video or videoUrl");
|
|
549
|
+
}
|
|
550
|
+
const form = new FormData();
|
|
551
|
+
if (params.video !== void 0) {
|
|
552
|
+
const { blob, filename } = await toUploadBlob(params.video);
|
|
553
|
+
form.set("video", blob, filename);
|
|
554
|
+
} else {
|
|
555
|
+
form.set("video_url", params.videoUrl);
|
|
556
|
+
}
|
|
557
|
+
if (params.prompt !== void 0) form.set("prompt", params.prompt);
|
|
558
|
+
if (params.preserveSpeech !== void 0) {
|
|
559
|
+
form.set("preserve_speech", String(params.preserveSpeech));
|
|
560
|
+
}
|
|
561
|
+
if (params.isolateVocals !== void 0) {
|
|
562
|
+
form.set("isolate_vocals", String(params.isolateVocals));
|
|
563
|
+
}
|
|
564
|
+
const res = await this.client.request("/v1/video-to-video-music", {
|
|
565
|
+
method: "POST",
|
|
566
|
+
body: form
|
|
567
|
+
});
|
|
568
|
+
return await res.json();
|
|
569
|
+
}
|
|
570
|
+
async generate(params, opts) {
|
|
571
|
+
const task = await this.submit(params);
|
|
572
|
+
return this.client.tasks.wait(task.task_id, opts);
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
// src/resources/videoToVideoSfx.ts
|
|
577
|
+
var VideoToVideoSfx = class {
|
|
578
|
+
constructor(client) {
|
|
579
|
+
this.client = client;
|
|
580
|
+
}
|
|
581
|
+
async submit(params) {
|
|
582
|
+
if (params.video === void 0 === (params.videoUrl === void 0)) {
|
|
583
|
+
throw new SoniloError("Provide exactly one of video or videoUrl");
|
|
584
|
+
}
|
|
585
|
+
const form = new FormData();
|
|
586
|
+
if (params.video !== void 0) {
|
|
587
|
+
const { blob, filename } = await toUploadBlob(params.video);
|
|
588
|
+
form.set("video", blob, filename);
|
|
589
|
+
} else {
|
|
590
|
+
form.set("video_url", params.videoUrl);
|
|
591
|
+
}
|
|
592
|
+
if (params.prompt !== void 0) form.set("prompt", params.prompt);
|
|
593
|
+
if (params.segments !== void 0) {
|
|
594
|
+
form.set("segments", JSON.stringify(params.segments));
|
|
595
|
+
}
|
|
596
|
+
const res = await this.client.request("/v1/video-to-video-sfx", {
|
|
597
|
+
method: "POST",
|
|
598
|
+
body: form
|
|
599
|
+
});
|
|
600
|
+
return await res.json();
|
|
601
|
+
}
|
|
602
|
+
async generate(params, opts) {
|
|
603
|
+
const task = await this.submit(params);
|
|
604
|
+
return this.client.tasks.wait(task.task_id, opts);
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
// src/resources/soundForm.ts
|
|
609
|
+
async function buildSoundForm(params) {
|
|
610
|
+
if (params.video === void 0 === (params.videoUrl === void 0)) {
|
|
611
|
+
throw new SoniloError("Provide exactly one of video or videoUrl");
|
|
612
|
+
}
|
|
613
|
+
const form = new FormData();
|
|
614
|
+
if (params.video !== void 0) {
|
|
615
|
+
const { blob, filename } = await toUploadBlob(params.video);
|
|
616
|
+
form.set("video", blob, filename);
|
|
617
|
+
} else {
|
|
618
|
+
form.set("video_url", params.videoUrl);
|
|
619
|
+
}
|
|
620
|
+
if (params.musicPrompt !== void 0) form.set("music_prompt", params.musicPrompt);
|
|
621
|
+
if (params.sfxPrompt !== void 0) form.set("sfx_prompt", params.sfxPrompt);
|
|
622
|
+
if (params.segments !== void 0) {
|
|
623
|
+
form.set("segments", JSON.stringify(params.segments));
|
|
624
|
+
}
|
|
625
|
+
if (params.preserveSpeech !== void 0) {
|
|
626
|
+
form.set("preserve_speech", String(params.preserveSpeech));
|
|
627
|
+
}
|
|
628
|
+
if (params.ducking !== void 0) form.set("ducking", String(params.ducking));
|
|
629
|
+
return form;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// src/resources/videoToSound.ts
|
|
633
|
+
var VideoToSound = class {
|
|
634
|
+
constructor(client) {
|
|
635
|
+
this.client = client;
|
|
636
|
+
}
|
|
637
|
+
async submit(params) {
|
|
638
|
+
const res = await this.client.request("/v1/video-to-sound", {
|
|
639
|
+
method: "POST",
|
|
640
|
+
body: await buildSoundForm(params)
|
|
641
|
+
});
|
|
642
|
+
return await res.json();
|
|
643
|
+
}
|
|
644
|
+
async generate(params, opts) {
|
|
645
|
+
const task = await this.submit(params);
|
|
646
|
+
return this.client.tasks.wait(task.task_id, opts);
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
// src/resources/videoToVideoSound.ts
|
|
651
|
+
var VideoToVideoSound = class {
|
|
652
|
+
constructor(client) {
|
|
653
|
+
this.client = client;
|
|
654
|
+
}
|
|
655
|
+
async submit(params) {
|
|
656
|
+
const res = await this.client.request("/v1/video-to-video-sound", {
|
|
657
|
+
method: "POST",
|
|
658
|
+
body: await buildSoundForm(params)
|
|
659
|
+
});
|
|
660
|
+
return await res.json();
|
|
661
|
+
}
|
|
662
|
+
async generate(params, opts) {
|
|
663
|
+
const task = await this.submit(params);
|
|
664
|
+
return this.client.tasks.wait(task.task_id, opts);
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
|
|
503
668
|
// src/version.ts
|
|
504
|
-
var VERSION = "0.
|
|
669
|
+
var VERSION = "0.4.0";
|
|
505
670
|
|
|
506
671
|
// src/client.ts
|
|
507
672
|
var DEFAULT_BASE_URL = "https://api.sonilo.com";
|
|
@@ -525,6 +690,10 @@ var SoniloClient = class {
|
|
|
525
690
|
this.videoToMusic = new VideoToMusic(this);
|
|
526
691
|
this.textToSfx = new TextToSfx(this);
|
|
527
692
|
this.videoToSfx = new VideoToSfx(this);
|
|
693
|
+
this.videoToVideoMusic = new VideoToVideoMusic(this);
|
|
694
|
+
this.videoToVideoSfx = new VideoToVideoSfx(this);
|
|
695
|
+
this.videoToSound = new VideoToSound(this);
|
|
696
|
+
this.videoToVideoSound = new VideoToVideoSound(this);
|
|
528
697
|
}
|
|
529
698
|
/**
|
|
530
699
|
* Perform an authenticated request; throws a typed error on non-2xx.
|
|
@@ -557,15 +726,16 @@ var SoniloClient = class {
|
|
|
557
726
|
|
|
558
727
|
// src/download.ts
|
|
559
728
|
async function download(media, fetchFn = globalThis.fetch, timeout = DEFAULT_TIMEOUT_MS) {
|
|
560
|
-
|
|
729
|
+
const url = typeof media === "string" ? media : media?.url;
|
|
730
|
+
if (!url) {
|
|
561
731
|
throw new SoniloError("No media to download");
|
|
562
732
|
}
|
|
563
733
|
let res;
|
|
564
734
|
try {
|
|
565
|
-
res = await fetchFn(
|
|
735
|
+
res = await fetchFn(url, { signal: AbortSignal.timeout(timeout) });
|
|
566
736
|
} catch (err) {
|
|
567
737
|
if (isTimeoutSignalError(err)) {
|
|
568
|
-
throw new RequestTimeoutError(`Download of ${
|
|
738
|
+
throw new RequestTimeoutError(`Download of ${url} timed out after ${timeout}ms`);
|
|
569
739
|
}
|
|
570
740
|
throw err;
|
|
571
741
|
}
|