sonilo 0.17.0 → 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
@@ -397,6 +397,56 @@ The result is a `DubbingResult`, whose `outputs` is a map of language code to
397
397
  dubbed `.mp4` URL — not the `audio`/`video`/`output_url` shape the other
398
398
  endpoints use.
399
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
+
400
450
  ## Video analysis
401
451
 
402
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");
@@ -782,12 +793,34 @@ async function buildDubbingForm(params) {
782
793
  if (params.lipsync !== void 0) {
783
794
  form.set("lipsync", String(params.lipsync));
784
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
+ }
785
813
  return form;
786
814
  }
787
815
  var Dubbing = class {
788
816
  constructor(client) {
789
817
  this.client = client;
790
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. */
791
824
  async submit(params) {
792
825
  const res = await this.client.request("/v1/dubbing", {
793
826
  method: "POST",
@@ -834,7 +867,7 @@ var VideoAnalysis = class {
834
867
  };
835
868
 
836
869
  // src/version.ts
837
- var VERSION = "0.17.0";
870
+ var VERSION = "0.18.0";
838
871
 
839
872
  // src/client.ts
840
873
  var DEFAULT_BASE_URL = "https://api.sonilo.com";