sonilo 0.16.5 → 0.18.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
@@ -379,6 +379,15 @@ boolean (default off, free) ducks the background music/effects bed under the
379
379
  dubbed voice while it speaks; when off the bed is kept at a constant level.
380
380
  Every endpoint's `ducking` is default-off, so this one is no exception.
381
381
 
382
+ The optional `lipsync` boolean is the one parameter here that defaults **on**:
383
+ the speaker's mouth is re-rendered to match the dubbed speech. Pass
384
+ `lipsync: false` to leave the picture completely untouched instead — the video
385
+ comes back at its original resolution and frame rate rather than re-rendered,
386
+ and only the audio is replaced, so the mouths keep moving to the original
387
+ language. Reach for it on footage with no on-camera speaker, or when
388
+ preserving the exact original picture matters more than matching lip movement.
389
+ The background bed is rebuilt either way, so `ducking` behaves the same.
390
+
382
391
  Dubbing is async-only, and the source video may be at most 300 seconds long.
383
392
  You are billed per language. Dubbing has **no free trial allowance** — unlike
384
393
  every other endpoint, every call bills from the first one (see
@@ -388,6 +397,56 @@ The result is a `DubbingResult`, whose `outputs` is a map of language code to
388
397
  dubbed `.mp4` URL — not the `audio`/`video`/`output_url` shape the other
389
398
  endpoints use.
390
399
 
400
+ ### Subtitle scripts
401
+
402
+ `subtitles` supplies the lines to speak, one script per target language:
403
+
404
+ ```ts
405
+ const result = await client.dubbing.generate(
406
+ {
407
+ videoUrl: "https://example.com/clip.mp4",
408
+ languages: ["ja", "es"],
409
+ subtitles: { ja: "./ja.srt", es: "https://example.com/es.vtt" },
410
+ exportSrt: true,
411
+ },
412
+ { timeout: 7_200_000 },
413
+ );
414
+ console.log(result.subtitles?.ja); // re-timed .srt URL
415
+ ```
416
+
417
+ Rules:
418
+
419
+ - These are **target-language** scripts, not source-language transcripts.
420
+ - The keys must match `languages` exactly — a missing or extra code is a 422,
421
+ checked server-side before anything is charged.
422
+ - A value starting with `https://` is a URL; any other string is a local file
423
+ path (Node.js only). A browser can pass a `File` instead. The filename must
424
+ end in `.srt` or `.vtt`, and an uploaded script is at most 1 MiB.
425
+ - `exportSrt` requires `subtitles`. With it, each language's delivered audio is
426
+ force-aligned against its script and a re-timed SRT is returned under
427
+ `subtitles`, keeping your lines verbatim.
428
+
429
+ `subtitle_preflight` (what the pipeline made of each script) and
430
+ `subtitle_export` (how each re-timed SRT came out) are maps keyed by language.
431
+ Their numeric fields — `cue_count`, `changes_count`, `alignment_loss` — are
432
+ typed `number | string`, because a finished task carries them as strings; read
433
+ them through `Number(...)`. `report_url` is `string | null` — the key is always
434
+ written, so test the value, not the key. An export whose `status` is `blocked`
435
+ does not fail the task: the dubbed videos are still delivered and `subtitles`
436
+ simply lacks that language.
437
+
438
+ `submit()` returns a `DubbingTask`: the shared `SfxTask` plus the
439
+ acknowledgement's own `subtitle_preflight`. A `review_required` status there
440
+ means the pipeline **changed lines in the script you submitted**, and
441
+ `changes_count` says how many — worth checking without waiting for the dub:
442
+
443
+ ```ts
444
+ const task = await client.dubbing.submit({ videoUrl, languages, subtitles });
445
+ for (const [language, report] of Object.entries(task.subtitle_preflight ?? {})) {
446
+ if (report.status !== "ok") console.warn(language, report.status, report.issues);
447
+ }
448
+ ```
449
+
391
450
  ## Video analysis
392
451
 
393
452
  `client.videoAnalysis` analyzes a video and returns a **creative brief** for
package/dist/index.cjs CHANGED
@@ -756,6 +756,17 @@ var AudioDucking = class {
756
756
  };
757
757
 
758
758
  // src/resources/dubbing.ts
759
+ var SUBTITLE_EXTENSIONS = [".srt", ".vtt"];
760
+ async function toSubtitleBlob(language, value) {
761
+ const name = typeof value === "string" ? value : value?.name ?? "";
762
+ const lowered = name.toLowerCase();
763
+ if (!SUBTITLE_EXTENSIONS.some((ext) => lowered.endsWith(ext))) {
764
+ throw new SoniloError(
765
+ `subtitles[${language}]: "${name}" must be an .srt or .vtt file, or an https:// URL`
766
+ );
767
+ }
768
+ return toUploadBlob(value);
769
+ }
759
770
  async function buildDubbingForm(params) {
760
771
  if (params.video === void 0 === (params.videoUrl === void 0)) {
761
772
  throw new SoniloError("Provide exactly one of video or videoUrl");
@@ -779,12 +790,37 @@ async function buildDubbingForm(params) {
779
790
  if (params.ducking !== void 0) {
780
791
  form.set("ducking", String(params.ducking));
781
792
  }
793
+ if (params.lipsync !== void 0) {
794
+ form.set("lipsync", String(params.lipsync));
795
+ }
796
+ if (params.subtitles !== void 0) {
797
+ for (const [language, value] of Object.entries(params.subtitles)) {
798
+ const field = `subtitles[${language}]`;
799
+ if (typeof value === "string" && value.toLowerCase().startsWith("https://")) {
800
+ form.set(field, value);
801
+ continue;
802
+ }
803
+ const { blob, filename } = await toSubtitleBlob(language, value);
804
+ form.set(field, blob, filename);
805
+ }
806
+ }
807
+ if (params.exportSrt !== void 0) {
808
+ if (params.exportSrt && Object.keys(params.subtitles ?? {}).length === 0) {
809
+ throw new SoniloError("exportSrt requires subtitles \u2014 there is nothing to align against");
810
+ }
811
+ form.set("export_srt", String(params.exportSrt));
812
+ }
782
813
  return form;
783
814
  }
784
815
  var Dubbing = class {
785
816
  constructor(client) {
786
817
  this.client = client;
787
818
  }
819
+ /** The acknowledgement carries `subtitle_preflight` when scripts were sent,
820
+ * which is why this returns `DubbingTask` rather than the shared `SfxTask`:
821
+ * a `review_required` preflight means the pipeline altered lines in the
822
+ * script that was submitted, and typing it away hides that from every
823
+ * caller who only ever sees the 202. */
788
824
  async submit(params) {
789
825
  const res = await this.client.request("/v1/dubbing", {
790
826
  method: "POST",
@@ -831,7 +867,7 @@ var VideoAnalysis = class {
831
867
  };
832
868
 
833
869
  // src/version.ts
834
- var VERSION = "0.16.5";
870
+ var VERSION = "0.18.0";
835
871
 
836
872
  // src/client.ts
837
873
  var DEFAULT_BASE_URL = "https://api.sonilo.com";