video-editing 2.0.0 → 3.1.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.
@@ -1,126 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/ffmpeg.ts
4
- import { spawn, spawnSync } from "child_process";
5
- import { createWriteStream, mkdirSync } from "fs";
6
- import { dirname } from "path";
7
- function shquote(value) {
8
- if (value.length === 0) return "''";
9
- if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(value)) return value;
10
- return "'" + value.replace(/'/g, `'"'"'`) + "'";
11
- }
12
- function shown(cmd) {
13
- return cmd.map(shquote).join(" ");
14
- }
15
- var CommandError = class extends Error {
16
- constructor(message, code, result) {
17
- super(message);
18
- this.code = code;
19
- this.result = result;
20
- this.name = "CommandError";
21
- }
22
- code;
23
- result;
24
- };
25
- function run(cmd, opts = {}) {
26
- const { capture = false, stdoutPath, cwd, check = true, echo = true } = opts;
27
- if (echo) process.stderr.write(`+ ${shown(cmd)}
28
- `);
29
- return new Promise((resolve3, reject) => {
30
- const [bin, ...args] = cmd;
31
- if (!bin) {
32
- reject(new Error("empty command"));
33
- return;
34
- }
35
- let fileStream;
36
- const outChunks = [];
37
- const errChunks = [];
38
- const stdio = ["ignore", "pipe", "pipe"];
39
- if (!capture && !stdoutPath) {
40
- stdio[1] = "inherit";
41
- stdio[2] = "inherit";
42
- }
43
- if (stdoutPath) {
44
- mkdirSync(dirname(stdoutPath), { recursive: true });
45
- fileStream = createWriteStream(stdoutPath, { encoding: "utf8" });
46
- }
47
- const child = spawn(bin, args, { cwd, stdio });
48
- child.stdout?.on("data", (d) => {
49
- if (fileStream) fileStream.write(d);
50
- else if (capture) outChunks.push(d);
51
- });
52
- child.stderr?.on("data", (d) => {
53
- if (fileStream) fileStream.write(d);
54
- else if (capture) errChunks.push(d);
55
- });
56
- child.on("error", (err) => reject(err));
57
- child.on("close", (code) => {
58
- if (fileStream) fileStream.end();
59
- const result = {
60
- code: code ?? -1,
61
- stdout: Buffer.concat(outChunks).toString("utf8"),
62
- stderr: Buffer.concat(errChunks).toString("utf8")
63
- };
64
- if (check && result.code !== 0) {
65
- if (capture) {
66
- if (result.stdout) process.stderr.write(result.stdout + "\n");
67
- if (result.stderr) process.stderr.write(result.stderr + "\n");
68
- }
69
- reject(new CommandError(`command failed with exit ${result.code}: ${shown(cmd)}`, result.code, result));
70
- return;
71
- }
72
- resolve3(result);
73
- });
74
- });
75
- }
76
- function runSync(cmd, opts = {}) {
77
- const { check = true, echo = true } = opts;
78
- if (echo) process.stderr.write(`+ ${shown(cmd)}
79
- `);
80
- const [bin, ...args] = cmd;
81
- const proc = spawnSync(bin, args, { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
82
- const result = { code: proc.status ?? -1, stdout: proc.stdout ?? "", stderr: proc.stderr ?? "" };
83
- if (check && result.code !== 0) {
84
- throw new CommandError(`command failed with exit ${result.code}: ${shown(cmd)}`, result.code, result);
85
- }
86
- return result;
87
- }
88
- var FFPROBE_ENTRIES = "stream=index,codec_type,codec_name,width,height,r_frame_rate,avg_frame_rate,duration,color_transfer:stream_side_data=rotation:format=duration,size,format_name";
89
- async function ffprobe(path) {
90
- const res = await run(
91
- ["ffprobe", "-v", "error", "-show_entries", FFPROBE_ENTRIES, "-of", "json", path],
92
- { capture: true }
93
- );
94
- try {
95
- return JSON.parse(res.stdout || "{}");
96
- } catch (e) {
97
- throw new Error(`ffprobe returned invalid JSON for ${path}: ${e.message}`);
98
- }
99
- }
100
- function videoStream(probe) {
101
- for (const s of probe.streams ?? []) {
102
- if (s.codec_type === "video") return s;
103
- }
104
- throw new Error("no video stream found");
105
- }
106
- async function mediaDuration(path) {
107
- const probe = await ffprobe(path);
108
- const d = probe.format?.duration;
109
- const n = d ? Number(d) : 0;
110
- return Number.isFinite(n) ? n : 0;
111
- }
112
- function requireFfmpeg() {
113
- for (const bin of ["ffmpeg", "ffprobe"]) {
114
- const r = spawnSync(bin, ["-version"], { encoding: "utf8" });
115
- if (r.error) {
116
- throw new Error("ffmpeg and ffprobe must be on PATH");
117
- }
118
- }
119
- }
120
-
121
3
  // src/cloud/client.ts
122
- import { createWriteStream as createWriteStream2, mkdirSync as mkdirSync2, statSync, writeFileSync } from "fs";
123
- import { dirname as dirname2, join as pathJoin } from "path";
4
+ import { createWriteStream, mkdirSync, statSync, writeFileSync } from "fs";
5
+ import { dirname, join as pathJoin } from "path";
124
6
  import { Readable } from "stream";
125
7
  import { pipeline } from "stream/promises";
126
8
  var DEFAULT_TIMEOUT_MS = 3e4;
@@ -131,8 +13,9 @@ function firstNonEmpty(...vals) {
131
13
  }
132
14
  return void 0;
133
15
  }
16
+ var DEFAULT_API_BASE = "https://clipready-usewhip.vercel.app";
134
17
  function resolveApiBase(flag, env = process.env) {
135
- return firstNonEmpty(flag, env.CLIPREADY_API_BASE, env.API_BASE);
18
+ return firstNonEmpty(flag, env.CLIPREADY_API_BASE, env.API_BASE) ?? DEFAULT_API_BASE;
136
19
  }
137
20
  function resolveApiKey(flag, env = process.env) {
138
21
  return firstNonEmpty(flag, env.CLIPREADY_API_KEY, env.RUN_TOKEN);
@@ -186,9 +69,19 @@ function projectsUrl(base) {
186
69
  function projectVideosUrl(base, projectId) {
187
70
  return `${stripTrailingSlash(base)}/api/v1/projects/${encodeURIComponent(projectId)}/videos`;
188
71
  }
72
+ function webVideoUrl(base, projectId, videoId) {
73
+ return `${stripTrailingSlash(base)}/projects/${encodeURIComponent(projectId)}/videos/${encodeURIComponent(videoId)}`;
74
+ }
189
75
  function runUrl(base, runId) {
190
76
  return `${stripTrailingSlash(base)}/api/v1/runs/${encodeURIComponent(runId)}`;
191
77
  }
78
+ function multipartUrl(base, tail) {
79
+ const root = `${stripTrailingSlash(base)}/api/v1/uploads/multipart`;
80
+ return tail ? `${root}/${tail}` : root;
81
+ }
82
+ function multipartRelayUrl(base, uploadId, n, i, of) {
83
+ return `${multipartUrl(base, "relay")}?upload_id=${encodeURIComponent(uploadId)}&n=${n}&i=${i}&of=${of}`;
84
+ }
192
85
  function videoUrl(base, videoId, tail) {
193
86
  return `${stripTrailingSlash(base)}/api/v1/videos/${encodeURIComponent(videoId)}/${tail}`;
194
87
  }
@@ -199,6 +92,9 @@ function filesUrl(base, videoId, path) {
199
92
  function transcribeUrl(base, videoId) {
200
93
  return videoUrl(base, videoId, "transcribe");
201
94
  }
95
+ function ingestUrl(base, videoId) {
96
+ return videoUrl(base, videoId, "ingest");
97
+ }
202
98
  function compileUrl(base, videoId) {
203
99
  return videoUrl(base, videoId, "compile");
204
100
  }
@@ -208,6 +104,9 @@ function resolveUrl(base, videoId) {
208
104
  function verifyUrl(base, videoId) {
209
105
  return videoUrl(base, videoId, "verify");
210
106
  }
107
+ function smoothJoinsUrl(base, videoId) {
108
+ return videoUrl(base, videoId, "smooth-joins");
109
+ }
211
110
  function composeUrl(base, videoId) {
212
111
  return videoUrl(base, videoId, "compose");
213
112
  }
@@ -279,6 +178,18 @@ function parseQaWaveform(data) {
279
178
  }) : [];
280
179
  return { url: o.url, words };
281
180
  }
181
+ function parseQaInspect(data) {
182
+ const o = asRecord(data, "qa-inspect");
183
+ if (typeof o.url !== "string") throw new Error(`qa inspect response missing url: ${JSON.stringify(data)}`);
184
+ const cuts = Array.isArray(o.cuts) ? o.cuts.filter((c) => c !== null && typeof c === "object").map((c) => ({ start: Number(c.start ?? 0), end: Number(c.end ?? 0) })) : [];
185
+ return {
186
+ url: o.url,
187
+ tStart: Number(o.tStart ?? 0),
188
+ tEnd: Number(o.tEnd ?? 0),
189
+ cuts,
190
+ cached: o.cached === true
191
+ };
192
+ }
282
193
  function parseFileList(data) {
283
194
  const o = asRecord(data, "file-list");
284
195
  const arr = o.files;
@@ -411,7 +322,10 @@ function extractErrorMessage(data) {
411
322
  }
412
323
  return void 0;
413
324
  }
414
- var CloudClient = class {
325
+ function backoffDelayMs(attempt) {
326
+ return 500 * 2 ** (attempt - 1);
327
+ }
328
+ var CloudClient = class _CloudClient {
415
329
  constructor(cfg, opts = {}) {
416
330
  this.cfg = cfg;
417
331
  this.fetchImpl = opts.fetchImpl ?? globalThis.fetch;
@@ -425,6 +339,61 @@ var CloudClient = class {
425
339
  if (!this.cfg.videoId) throw new Error("missing video id \u2014 pass --video-id or set VIDEO_ID");
426
340
  return this.cfg.videoId;
427
341
  }
342
+ /** Same client (same fetch/timeout) bound to a video id — for flows that
343
+ * create the video mid-run (init). */
344
+ withVideoId(videoId) {
345
+ return new _CloudClient({ ...this.cfg, videoId }, { fetchImpl: this.fetchImpl, timeoutMs: this.timeoutMs });
346
+ }
347
+ /** The injected fetch — for helpers outside the class (multipart part PUTs)
348
+ * that need raw Response access (e.g. the ETag header). */
349
+ get fetch() {
350
+ return this.fetchImpl;
351
+ }
352
+ /** POST a JSON body to an absolute API URL with auth — public wrapper over
353
+ * the private transport for helpers living outside the class (multipart). */
354
+ async postJson(url, body) {
355
+ return this.request("POST", url, body);
356
+ }
357
+ /** POST raw binary bytes to an authenticated API URL (multipart relay
358
+ * sub-chunks). Content-Type application/octet-stream; JSON response. */
359
+ async postBinary(url, bytes, timeoutMs = DEFAULT_DOWNLOAD_TIMEOUT_MS) {
360
+ const ac = new AbortController();
361
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
362
+ try {
363
+ const res = await this.fetchImpl(url, {
364
+ method: "POST",
365
+ headers: {
366
+ Authorization: `Bearer ${this.cfg.credential}`,
367
+ Accept: "application/json",
368
+ "Content-Type": "application/octet-stream"
369
+ },
370
+ body: bytes,
371
+ signal: ac.signal
372
+ });
373
+ const text = await res.text();
374
+ let data = void 0;
375
+ if (text) {
376
+ try {
377
+ data = JSON.parse(text);
378
+ } catch {
379
+ data = text;
380
+ }
381
+ }
382
+ if (!res.ok) {
383
+ const detail = extractErrorMessage(data) ?? `${res.status} ${res.statusText}`;
384
+ throw new CloudHttpError(res.status, `POST ${url} failed: ${detail}`, data);
385
+ }
386
+ return data;
387
+ } catch (err) {
388
+ if (err instanceof CloudHttpError) throw err;
389
+ if (err instanceof Error && err.name === "AbortError") {
390
+ throw new Error(`request timed out after ${timeoutMs}ms: POST ${url}`);
391
+ }
392
+ throw err;
393
+ } finally {
394
+ clearTimeout(timer);
395
+ }
396
+ }
428
397
  async request(method, url, body) {
429
398
  const ac = new AbortController();
430
399
  const timer = setTimeout(() => ac.abort(), this.timeoutMs);
@@ -491,6 +460,12 @@ var CloudClient = class {
491
460
  });
492
461
  return parseQaWaveform(data);
493
462
  }
463
+ async qaInspect(args) {
464
+ const body = { kind: "inspect", start: args.start, end: args.end };
465
+ if (args.nFrames !== void 0) body.n_frames = args.nFrames;
466
+ const data = await this.request("POST", qaUrl(this.cfg.base, this.requireVideoId()), body);
467
+ return parseQaInspect(data);
468
+ }
494
469
  // ---- artifacts ----------------------------------------------------------
495
470
  async listArtifacts() {
496
471
  const data = await this.request("GET", artifactsUrl(this.cfg.base, this.requireVideoId()));
@@ -512,28 +487,45 @@ var CloudClient = class {
512
487
  return parseAssetCreate(data);
513
488
  }
514
489
  /** PUT raw bytes to a presigned upload URL. No Authorization header — the
515
- * signed URL carries its own credentials and may be a different host. */
516
- async upload(url, bytes, contentType, timeoutMs = DEFAULT_DOWNLOAD_TIMEOUT_MS) {
517
- const ac = new AbortController();
518
- const timer = setTimeout(() => ac.abort(), timeoutMs);
519
- try {
520
- const res = await this.fetchImpl(url, {
521
- method: "PUT",
522
- headers: { "Content-Type": contentType },
523
- body: bytes,
524
- signal: ac.signal
525
- });
526
- if (!res.ok) {
527
- throw new Error(`asset upload failed: HTTP ${res.status} ${res.statusText} for ${url}`);
528
- }
529
- } catch (err) {
530
- if (err instanceof Error && err.name === "AbortError") {
531
- throw new Error(`asset upload timed out after ${timeoutMs}ms: ${url}`);
490
+ * signed URL carries its own credentials and may be a different host.
491
+ * Mid-stream network failures (connection resets, TLS record errors) and
492
+ * 5xx responses are retried with exponential backoff — the whole body is
493
+ * an in-memory buffer, so every attempt is a clean full replay. */
494
+ async upload(url, bytes, contentType, timeoutMs = DEFAULT_DOWNLOAD_TIMEOUT_MS, maxAttempts = 5) {
495
+ let lastErr;
496
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
497
+ const ac = new AbortController();
498
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
499
+ try {
500
+ const res = await this.fetchImpl(url, {
501
+ method: "PUT",
502
+ headers: { "Content-Type": contentType },
503
+ body: bytes,
504
+ signal: ac.signal
505
+ });
506
+ if (res.ok) return;
507
+ const failure = new Error(`asset upload failed: HTTP ${res.status} ${res.statusText} for ${url}`);
508
+ if (res.status < 500 || attempt === maxAttempts) throw failure;
509
+ lastErr = failure;
510
+ } catch (err) {
511
+ if (err instanceof Error && err.name === "AbortError") {
512
+ throw new Error(`asset upload timed out after ${timeoutMs}ms: ${url}`);
513
+ }
514
+ const cause = err.cause;
515
+ const detail = cause?.code ?? cause?.message;
516
+ const network = err instanceof TypeError;
517
+ if (!network || attempt === maxAttempts) {
518
+ throw err instanceof TypeError ? new Error(
519
+ `upload network failure after ${attempt} attempt${attempt === 1 ? "" : "s"}${detail ? ` (${detail})` : ""} PUTting ${bytes.length} bytes to ${new URL(url).host}`
520
+ ) : err;
521
+ }
522
+ lastErr = err;
523
+ } finally {
524
+ clearTimeout(timer);
532
525
  }
533
- throw err;
534
- } finally {
535
- clearTimeout(timer);
526
+ await new Promise((r) => setTimeout(r, backoffDelayMs(attempt)));
536
527
  }
528
+ throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
537
529
  }
538
530
  // ---- review session -----------------------------------------------------
539
531
  async getReview() {
@@ -641,8 +633,22 @@ var CloudClient = class {
641
633
  }
642
634
  return { runId: o.run_id };
643
635
  }
644
- /** POST /videos/{id}/verify → 202 {run_id}. */
645
- async verifyStart(body) {
636
+ /** POST /videos/{id}/ingest {stem?} → 202 {run_id}. Server-side probe + audio
637
+ * extraction + transcription + screening over the uploaded source. The stem
638
+ * names every derived artifact (transcripts/<stem>.json, audio/<stem>.wav) —
639
+ * omit it and the server defaults to "source". */
640
+ async ingestStart(stem) {
641
+ const body = stem !== void 0 && stem !== "" ? { stem } : {};
642
+ const data = await this.request("POST", ingestUrl(this.cfg.base, this.requireVideoId()), body);
643
+ const o = asRecord(data, "ingest");
644
+ if (typeof o.run_id !== "string" || o.run_id === "") {
645
+ throw new Error(`ingest response missing run_id: ${JSON.stringify(data)}`);
646
+ }
647
+ return { runId: o.run_id };
648
+ }
649
+ /** POST /videos/{id}/verify {mode?, window_s?} → 202 {run_id}. The server
650
+ * sources the preview audio itself — nothing is uploaded. */
651
+ async verifyStart(body = {}) {
646
652
  const data = await this.request("POST", verifyUrl(this.cfg.base, this.requireVideoId()), body);
647
653
  const o = asRecord(data, "verify");
648
654
  if (typeof o.run_id !== "string" || o.run_id === "") {
@@ -650,6 +656,18 @@ var CloudClient = class {
650
656
  }
651
657
  return { runId: o.run_id };
652
658
  }
659
+ async smoothJoinsStart(body = {}) {
660
+ const data = await this.request(
661
+ "POST",
662
+ smoothJoinsUrl(this.cfg.base, this.requireVideoId()),
663
+ body
664
+ );
665
+ const o = asRecord(data, "smooth-joins");
666
+ if (typeof o.run_id !== "string" || o.run_id === "") {
667
+ throw new Error(`smooth-joins response missing run_id: ${JSON.stringify(data)}`);
668
+ }
669
+ return { runId: o.run_id };
670
+ }
653
671
  /** GET /runs/{id} — run status + events. */
654
672
  async getRun(runId) {
655
673
  const data = await this.request("GET", runUrl(this.cfg.base, runId));
@@ -686,14 +704,14 @@ async function downloadTo(url, dest, opts = {}) {
686
704
  try {
687
705
  const res = await f(url, { signal: ac.signal });
688
706
  if (!res.ok) throw new Error(`download failed: HTTP ${res.status} ${res.statusText} for ${url}`);
689
- mkdirSync2(dirname2(dest), { recursive: true });
707
+ mkdirSync(dirname(dest), { recursive: true });
690
708
  const body = res.body;
691
709
  if (!body) {
692
710
  const buf = Buffer.from(await res.arrayBuffer());
693
711
  writeFileSync(dest, buf);
694
712
  return { path: dest, bytes: buf.length };
695
713
  }
696
- await pipeline(Readable.fromWeb(body), createWriteStream2(dest));
714
+ await pipeline(Readable.fromWeb(body), createWriteStream(dest));
697
715
  return { path: dest, bytes: statSync(dest).size };
698
716
  } catch (err) {
699
717
  if (err instanceof Error && err.name === "AbortError") {
@@ -733,10 +751,10 @@ async function pollRender(client, renderId, opts = {}) {
733
751
  // src/cloud/skill.ts
734
752
  import { cpSync, existsSync, readdirSync, rmSync, statSync as statSync2 } from "fs";
735
753
  import { homedir } from "os";
736
- import { dirname as dirname3, join as join2, relative, resolve } from "path";
754
+ import { dirname as dirname2, join as join2, relative, resolve } from "path";
737
755
  import { fileURLToPath } from "url";
738
756
  function bundledSkillDir() {
739
- const here = dirname3(fileURLToPath(import.meta.url));
757
+ const here = dirname2(fileURLToPath(import.meta.url));
740
758
  const candidates = [
741
759
  join2(here, "skills", "video-editing"),
742
760
  join2(here, "..", "skills", "video-editing"),
@@ -770,13 +788,13 @@ function installSkill(opts = {}) {
770
788
  }
771
789
 
772
790
  // src/cloud/jobfile.ts
773
- import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync, writeFileSync as writeFileSync2 } from "fs";
791
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync as writeFileSync2 } from "fs";
774
792
  import { join as join3 } from "path";
775
793
  function jobJsonPath(jobDir) {
776
794
  return join3(jobDir, "job.json");
777
795
  }
778
796
  function writeJobFile(jobDir, job) {
779
- mkdirSync3(jobDir, { recursive: true });
797
+ mkdirSync2(jobDir, { recursive: true });
780
798
  const path = jobJsonPath(jobDir);
781
799
  writeFileSync2(path, JSON.stringify(job, null, 2) + "\n", "utf8");
782
800
  return path;
@@ -792,7 +810,8 @@ function readJobFile(jobDir) {
792
810
  project_id: typeof raw.project_id === "string" ? raw.project_id : "",
793
811
  api_base: typeof raw.api_base === "string" ? raw.api_base : "",
794
812
  stem: typeof raw.stem === "string" ? raw.stem : "source",
795
- duration_s: typeof raw.duration_s === "number" ? raw.duration_s : 0
813
+ duration_s: typeof raw.duration_s === "number" ? raw.duration_s : 0,
814
+ ...typeof raw.source_path === "string" && raw.source_path !== "" ? { source_path: raw.source_path } : {}
796
815
  };
797
816
  } catch {
798
817
  return null;
@@ -805,184 +824,276 @@ function videoIdFromJob(jobDir, resolved) {
805
824
  throw new Error(`missing video id \u2014 pass --video-id, set VIDEO_ID, or run \`init\` first (no ${jobJsonPath(jobDir)})`);
806
825
  }
807
826
 
808
- // src/cloud/init.ts
809
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync2 } from "fs";
810
- import { join as join5, parse as parsePath, resolve as resolvePath } from "path";
811
-
812
- // src/master.ts
813
- import { writeFileSync as writeFileSync3 } from "fs";
814
- import { existsSync as existsSync3 } from "fs";
815
- import { join as join4 } from "path";
816
-
817
- // src/probe.ts
818
- function rotationDegrees(stream) {
819
- for (const sideData of stream.side_data_list ?? []) {
820
- if (sideData && "rotation" in sideData) {
821
- const v = Number(sideData.rotation);
822
- return Number.isFinite(v) ? Math.trunc(v) : 0;
827
+ // src/cloud/multipart.ts
828
+ import { open } from "fs/promises";
829
+ async function runPool(tasks, concurrency) {
830
+ let next = 0;
831
+ const workers = Array.from({ length: Math.min(concurrency, tasks.length) }, async () => {
832
+ while (next < tasks.length) {
833
+ const task = tasks[next++];
834
+ if (task) await task();
823
835
  }
824
- }
825
- const tags = stream.tags ?? {};
826
- if ("rotate" in tags) {
827
- const v = Number(tags.rotate);
828
- return Number.isFinite(v) ? Math.trunc(v) : 0;
829
- }
830
- return 0;
836
+ });
837
+ await Promise.all(workers);
838
+ }
839
+ var MULTIPART_THRESHOLD_BYTES = 8 * 1024 * 1024;
840
+ var PART_CONCURRENCY = 3;
841
+ var PART_MAX_ATTEMPTS = 8;
842
+ var RELAY_DIRECT_ATTEMPTS = 3;
843
+ var RELAY_SUBCHUNK_BYTES = 3670016;
844
+ function jitteredBackoffMs(attempt) {
845
+ const base = Math.min(500 * 2 ** (attempt - 1), 8e3);
846
+ return Math.round(base / 2 + Math.random() * (base / 2));
847
+ }
848
+ function shouldMultipart(sizeOrBytes) {
849
+ const size = typeof sizeOrBytes === "number" ? sizeOrBytes : sizeOrBytes.length;
850
+ return size > MULTIPART_THRESHOLD_BYTES;
851
+ }
852
+ function bytesSource(bytes) {
853
+ return {
854
+ size: bytes.length,
855
+ readSlice: async (offset, len) => Buffer.from(bytes.subarray(offset, Math.min(offset + len, bytes.length)))
856
+ };
857
+ }
858
+ async function openFileSource(path) {
859
+ const fh = await open(path, "r");
860
+ const size = (await fh.stat()).size;
861
+ return {
862
+ size,
863
+ async readSlice(offset, len) {
864
+ const want = Math.min(len, Math.max(0, size - offset));
865
+ const buf = Buffer.allocUnsafe(want);
866
+ let done = 0;
867
+ while (done < want) {
868
+ const { bytesRead } = await fh.read(buf, done, want - done, offset + done);
869
+ if (bytesRead <= 0) throw new Error(`short read at byte ${offset + done} of ${path}`);
870
+ done += bytesRead;
871
+ }
872
+ return buf;
873
+ },
874
+ close: () => fh.close()
875
+ };
831
876
  }
832
- function displayedDimensions(stream) {
833
- const width = Math.trunc(Number(stream.width ?? 0)) || 0;
834
- const height = Math.trunc(Number(stream.height ?? 0)) || 0;
835
- const rotation = rotationDegrees(stream);
836
- if (Math.abs(rotation) % 180 === 90) {
837
- return [height, width, rotation];
877
+ function asRecord2(data, ctx) {
878
+ if (data === null || typeof data !== "object") throw new Error(`unexpected ${ctx} response: ${JSON.stringify(data)}`);
879
+ return data;
880
+ }
881
+ function parsePartUrls(data, ctx) {
882
+ const o = asRecord2(data, ctx);
883
+ if (!Array.isArray(o.parts)) throw new Error(`${ctx} response missing parts[]: ${JSON.stringify(data)}`);
884
+ return o.parts.filter((p) => p !== null && typeof p === "object").map((p) => ({ n: Number(p.n ?? 0), url: String(p.url ?? "") })).filter((p) => Number.isInteger(p.n) && p.n >= 1 && p.url !== "");
885
+ }
886
+ function parseMultipartCreate(data) {
887
+ const o = asRecord2(data, "multipart-create");
888
+ const uploadId = o.upload_id;
889
+ if (typeof uploadId !== "string" || uploadId === "") {
890
+ throw new Error(`multipart create response missing upload_id: ${JSON.stringify(data)}`);
838
891
  }
839
- return [width, height, rotation];
892
+ const partSize = Number(o.part_size ?? 0);
893
+ if (!(partSize > 0)) throw new Error(`multipart create response missing part_size: ${JSON.stringify(data)}`);
894
+ return {
895
+ uploadId,
896
+ assetId: typeof o.asset_id === "string" && o.asset_id !== "" ? o.asset_id : void 0,
897
+ partSize,
898
+ parts: parsePartUrls(data, "multipart-create")
899
+ };
840
900
  }
841
-
842
- // src/util.ts
843
- function isoNow() {
844
- return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "+00:00");
845
- }
846
- function pyFixed(x, prec) {
847
- if (!Number.isFinite(x)) return String(x);
848
- const neg = x < 0 || Object.is(x, -0);
849
- const a = Math.abs(x);
850
- const m = Math.pow(10, prec);
851
- const scaled = a * m;
852
- const floor = Math.floor(scaled);
853
- const diff = scaled - floor;
854
- const EPS2 = 1e-9;
855
- let rounded;
856
- if (Math.abs(diff - 0.5) < EPS2) {
857
- rounded = floor % 2 === 0 ? floor : floor + 1;
858
- } else {
859
- rounded = Math.round(scaled);
901
+ function isMultipartUnavailable(err) {
902
+ if (!(err instanceof CloudHttpError)) return false;
903
+ if (err.status === 404 || err.status === 405) return true;
904
+ if (err.status !== 503) return false;
905
+ const body = err.body;
906
+ return typeof body === "object" && body !== null && typeof body.error === "object" && body.error !== null ? body.error.code === "multipart_unavailable" : false;
907
+ }
908
+ function stripEtagQuotes(etag) {
909
+ return etag.trim().replace(/^"+|"+$/g, "");
910
+ }
911
+ function fmtMb(n) {
912
+ return `${(n / (1024 * 1024)).toFixed(1)} MB`;
913
+ }
914
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
915
+ async function putPart(client, url, chunk, timeoutMs) {
916
+ const ac = new AbortController();
917
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
918
+ try {
919
+ return await client.fetch(url, { method: "PUT", body: chunk, signal: ac.signal });
920
+ } finally {
921
+ clearTimeout(timer);
860
922
  }
861
- let s = (rounded / m).toFixed(prec);
862
- if (neg && Number(s) !== 0) s = "-" + s;
863
- return s;
864
923
  }
865
-
866
- // src/master.ts
867
- var X264_BAKE = [
868
- "-c:v",
869
- "libx264",
870
- "-preset",
871
- "medium",
872
- "-crf",
873
- "18",
874
- "-pix_fmt",
875
- "yuv420p",
876
- "-c:a",
877
- "aac",
878
- "-b:a",
879
- "192k",
880
- "-movflags",
881
- "+faststart"
882
- ];
883
- async function ensureVerticalMaster(jobDir, source, opts = {}) {
884
- const out = join4(jobDir, "vertical_src.mp4");
885
- if (existsSync3(out) && !opts.force) return out;
886
- const probe = await ffprobe(source);
887
- const stream = videoStream(probe);
888
- const [displayW, displayH, rotation] = displayedDimensions(stream);
889
- const longEdge = Math.max(displayW, displayH);
890
- let vf = displayH >= displayW && longEdge > 1920 ? "scale=-2:1920" : null;
891
- if (displayW > displayH && longEdge > 1920) vf = "scale=1920:-2";
892
- const cmd = ["ffmpeg", "-y", "-i", source];
893
- if (vf) cmd.push("-vf", vf);
894
- cmd.push(...X264_BAKE, out);
895
- await run(cmd);
896
- const outProbe = await ffprobe(out);
897
- const outStream = videoStream(outProbe);
898
- const outW = Math.trunc(Number(outStream.width ?? 0)) || 0;
899
- const outH = Math.trunc(Number(outStream.height ?? 0)) || 0;
900
- let retriedWith = null;
901
- if (displayH > displayW && outW > outH) {
902
- retriedWith = vf ? `transpose=1,${vf}` : "transpose=1";
903
- await run(["ffmpeg", "-y", "-i", source, "-vf", retriedWith, ...X264_BAKE, out]);
904
- }
905
- writeFileSync3(
906
- join4(jobDir, "master.json"),
907
- JSON.stringify(
908
- {
909
- source,
910
- master: out,
911
- display_width: displayW,
912
- display_height: displayH,
913
- rotation,
914
- scale_filter: vf,
915
- retry_filter: retriedWith,
916
- created: isoNow()
917
- },
918
- null,
919
- 2
920
- ) + "\n",
921
- "utf8"
922
- );
923
- return out;
924
+ async function refreshPartUrl(client, uploadId, n) {
925
+ const data = await client.postJson(multipartUrl(client.cfg.base, "parts"), { upload_id: uploadId, ns: [n] });
926
+ const url = parsePartUrls(data, "multipart-parts").find((p) => p.n === n)?.url;
927
+ if (!url) throw new Error(`multipart parts refresh did not return a URL for part ${n}: ${JSON.stringify(data)}`);
928
+ return url;
924
929
  }
925
-
926
- // src/cloud/init.ts
927
- function contentTypeFor(path) {
928
- const ext = parsePath(path).ext.toLowerCase();
929
- const map = { ".mp4": "video/mp4", ".m4v": "video/mp4", ".mov": "video/quicktime", ".mkv": "video/x-matroska", ".avi": "video/x-msvideo" };
930
- return map[ext] ?? "video/mp4";
930
+ async function relayOnePart(client, uploadId, n, chunk) {
931
+ const of = Math.max(1, Math.ceil(chunk.length / RELAY_SUBCHUNK_BYTES));
932
+ for (let i = 1; i <= of; i++) {
933
+ const sub = chunk.subarray((i - 1) * RELAY_SUBCHUNK_BYTES, Math.min(i * RELAY_SUBCHUNK_BYTES, chunk.length));
934
+ const data = await client.postBinary(multipartRelayUrl(client.cfg.base, uploadId, n, i, of), sub);
935
+ const o = data;
936
+ if (!o || o.ok !== true) {
937
+ throw new Error(`relay sub-chunk ${i}/${of} of part ${n} did not return {ok:true}: ${JSON.stringify(data)}`);
938
+ }
939
+ }
940
+ const done = asRecord2(
941
+ await client.postJson(multipartUrl(client.cfg.base, "relay-complete"), { upload_id: uploadId, n }),
942
+ "relay-complete"
943
+ );
944
+ if (typeof done.etag !== "string" || done.etag === "") {
945
+ throw new Error(`relay-complete for part ${n} returned no etag: ${JSON.stringify(done)}`);
946
+ }
947
+ return stripEtagQuotes(done.etag);
948
+ }
949
+ function isRelayUnavailable(err) {
950
+ return err instanceof CloudHttpError && (err.status === 404 || err.status === 405);
951
+ }
952
+ function transportDetail(err) {
953
+ const cause = err.cause;
954
+ return cause?.code ?? cause?.message;
955
+ }
956
+ async function uploadOnePart(client, uploadId, n, chunk, initialUrl, timeoutMs, backoffMs, diag) {
957
+ let url = initialUrl;
958
+ let lastErr;
959
+ let transportFailures = 0;
960
+ let relayUnavailable = false;
961
+ for (let attempt = 1; attempt <= PART_MAX_ATTEMPTS; attempt++) {
962
+ const useRelay = !relayUnavailable && transportFailures >= RELAY_DIRECT_ATTEMPTS;
963
+ const mode = useRelay ? "relay" : "direct";
964
+ const t0 = Date.now();
965
+ const report = (error) => {
966
+ if (!error && mode === "direct") return;
967
+ diag?.(
968
+ `part ${n} attempt ${attempt} mode ${mode} bytes ${chunk.length} elapsed ${Date.now() - t0}ms` + (error ? ` error ${error}` : " ok")
969
+ );
970
+ };
971
+ if (useRelay) {
972
+ try {
973
+ const etag = await relayOnePart(client, uploadId, n, chunk);
974
+ report();
975
+ return etag;
976
+ } catch (err) {
977
+ if (isRelayUnavailable(err)) {
978
+ relayUnavailable = true;
979
+ report("relay_unavailable");
980
+ } else {
981
+ lastErr = err instanceof Error ? err : new Error(String(err));
982
+ report(err instanceof CloudHttpError ? `http_${err.status}` : lastErr.message.slice(0, 80));
983
+ }
984
+ }
985
+ } else {
986
+ try {
987
+ if (!url) url = await refreshPartUrl(client, uploadId, n);
988
+ const res = await putPart(client, url, chunk, timeoutMs);
989
+ if (res.ok) {
990
+ const etag = res.headers.get("etag");
991
+ if (!etag) throw new Error(`part ${n} uploaded but the response had no ETag header \u2014 cannot complete the multipart upload`);
992
+ report();
993
+ return stripEtagQuotes(etag);
994
+ }
995
+ lastErr = new Error(`part ${n}: HTTP ${res.status} ${res.statusText}`);
996
+ report(`http_${res.status}`);
997
+ if (res.status >= 400 && res.status < 500) url = void 0;
998
+ } catch (err) {
999
+ if (err instanceof Error && err.name === "AbortError") {
1000
+ lastErr = new Error(`part ${n}: timed out after ${timeoutMs}ms`);
1001
+ transportFailures += 1;
1002
+ report("timeout");
1003
+ } else if (err instanceof TypeError) {
1004
+ const detail = transportDetail(err);
1005
+ lastErr = new Error(`part ${n}: network failure${detail ? ` (${detail})` : ""}`);
1006
+ transportFailures += 1;
1007
+ report(detail ?? "fetch_failed");
1008
+ } else {
1009
+ throw err;
1010
+ }
1011
+ }
1012
+ }
1013
+ if (attempt < PART_MAX_ATTEMPTS) await sleep2(backoffMs(attempt));
1014
+ }
1015
+ throw new Error(
1016
+ `multipart upload failed at part ${n} (${chunk.length} bytes) after ${PART_MAX_ATTEMPTS} attempts: ${lastErr?.message ?? "unknown error"}`
1017
+ );
931
1018
  }
932
- async function initCloudJob(client, opts) {
933
- const source = resolvePath(opts.source);
934
- if (!existsSync4(source)) throw new Error(`source not found: ${source}`);
935
- const stem = parsePath(source).name;
936
- const jobDir = resolvePath(opts.jobDir ?? join5(process.cwd(), `job_${stem}`));
937
- mkdirSync4(jobDir, { recursive: true });
938
- const log = (line) => {
939
- if (opts.verbose !== false) process.stderr.write(line + "\n");
1019
+ async function uploadMultipart(client, opts, timeoutMs = DEFAULT_DOWNLOAD_TIMEOUT_MS) {
1020
+ const source = opts.source ?? (opts.bytes !== void 0 ? bytesSource(opts.bytes) : void 0);
1021
+ if (!source) throw new Error("uploadMultipart: pass exactly one of bytes or source");
1022
+ const createBody = {
1023
+ kind: opts.kind,
1024
+ size: source.size,
1025
+ content_type: opts.contentType
940
1026
  };
941
- await client.me();
942
- const probe = await ffprobe(source);
943
- const durationS = Number(probe.format?.duration ?? 0) || 0;
944
- if (!(durationS > 0)) throw new Error(`could not read a duration from ${source} (is it a video?)`);
945
- let uploadPath = source;
946
- if (opts.master) {
947
- log(" baking vertical 9:16 master (local ffmpeg)");
948
- uploadPath = await ensureVerticalMaster(jobDir, source);
949
- }
950
- const title = opts.title ?? stem;
951
- const { projectId } = await client.createProject(title);
952
- log(` project: ${projectId}`);
953
- const asset = await client.createSourceAsset({
954
- contentType: contentTypeFor(uploadPath),
955
- filename: parsePath(uploadPath).base
956
- });
957
- const bytes = readFileSync2(uploadPath);
958
- log(` uploading ${parsePath(uploadPath).base} (${(bytes.length / (1024 * 1024)).toFixed(1)} MB)`);
959
- await client.upload(asset.uploadUrl, bytes, contentTypeFor(uploadPath));
960
- const video = await client.createVideo(projectId, { assetId: asset.assetId, title });
961
- log(` video: ${video.videoId}`);
962
- const job = {
963
- video_id: video.videoId,
964
- project_id: video.projectId,
965
- api_base: client.cfg.base,
966
- stem,
967
- duration_s: durationS
968
- };
969
- const jobJson = writeJobFile(jobDir, job);
970
- return {
971
- jobDir,
972
- jobJson,
973
- videoId: video.videoId,
974
- projectId: video.projectId,
975
- stem,
976
- durationS,
977
- uploaded: uploadPath,
978
- uploadedBytes: bytes.length
1027
+ if (opts.filename !== void 0) createBody.filename = opts.filename;
1028
+ if (opts.videoId !== void 0) createBody.video_id = opts.videoId;
1029
+ if (opts.path !== void 0) createBody.path = opts.path;
1030
+ let session;
1031
+ try {
1032
+ session = parseMultipartCreate(await client.postJson(multipartUrl(client.cfg.base), createBody));
1033
+ } catch (err) {
1034
+ if (isMultipartUnavailable(err)) return "unavailable";
1035
+ throw err;
1036
+ }
1037
+ const abort = async () => {
1038
+ try {
1039
+ await client.postJson(multipartUrl(client.cfg.base, "abort"), { upload_id: session.uploadId });
1040
+ } catch {
1041
+ }
979
1042
  };
1043
+ if (opts.kind === "asset" && !session.assetId) {
1044
+ await abort();
1045
+ throw new Error("multipart create (kind asset) returned no asset_id \u2014 cannot create the video afterwards");
1046
+ }
1047
+ const total = Math.max(1, Math.ceil(source.size / session.partSize));
1048
+ const urls = new Map(session.parts.map((p) => [p.n, p.url]));
1049
+ const marks = new Set([1, 2, 3, 4].map((k) => Math.ceil(total * k / 4)));
1050
+ const etags = new Array(total);
1051
+ let doneParts = 0;
1052
+ let doneBytes = 0;
1053
+ try {
1054
+ await runPool(
1055
+ Array.from({ length: total }, (_, i) => async () => {
1056
+ const n = i + 1;
1057
+ const offset = i * session.partSize;
1058
+ const chunk = await source.readSlice(offset, session.partSize);
1059
+ etags[i] = await uploadOnePart(
1060
+ client,
1061
+ session.uploadId,
1062
+ n,
1063
+ chunk,
1064
+ urls.get(n),
1065
+ timeoutMs,
1066
+ opts.backoffMs ?? jitteredBackoffMs,
1067
+ opts.log
1068
+ );
1069
+ doneParts += 1;
1070
+ doneBytes += chunk.length;
1071
+ if (opts.log && marks.has(doneParts)) {
1072
+ opts.log(`upload: ${doneParts}/${total} parts (${fmtMb(doneBytes)} / ${fmtMb(source.size)})`);
1073
+ }
1074
+ }),
1075
+ PART_CONCURRENCY
1076
+ );
1077
+ const completeBody = {
1078
+ upload_id: session.uploadId,
1079
+ parts: etags.map((etag, i) => ({ n: i + 1, etag }))
1080
+ };
1081
+ if (opts.kind === "jobfile") {
1082
+ if (opts.sha256 !== void 0) completeBody.sha256 = opts.sha256;
1083
+ completeBody.bytes = source.size;
1084
+ }
1085
+ await client.postJson(multipartUrl(client.cfg.base, "complete"), completeBody);
1086
+ } catch (err) {
1087
+ await abort();
1088
+ throw err;
1089
+ }
1090
+ return opts.kind === "asset" ? { assetId: session.assetId } : {};
980
1091
  }
981
1092
 
982
1093
  // src/cloud/files.ts
983
1094
  import { createHash } from "crypto";
984
- import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
985
- import { dirname as dirname4, join as join6, sep } from "path";
1095
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync2, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
1096
+ import { dirname as dirname3, join as join4, sep } from "path";
986
1097
  var INLINE_MAX_BYTES = 5 * 1024 * 1024;
987
1098
  function sha256Hex(bytes) {
988
1099
  return createHash("sha256").update(bytes).digest("hex");
@@ -995,10 +1106,10 @@ function sameSha(a, b) {
995
1106
  function vfsLocalPath(jobDir, path) {
996
1107
  const parts = path.split("/").filter((p) => p !== "" && p !== "." && p !== "..");
997
1108
  if (parts.length === 0) throw new Error(`bad VFS path: ${JSON.stringify(path)}`);
998
- return join6(jobDir, ...parts);
1109
+ return join4(jobDir, ...parts);
999
1110
  }
1000
1111
  function toVfsPath(jobDir, localPath) {
1001
- const abs = localPath.startsWith(sep) ? localPath : join6(jobDir, localPath);
1112
+ const abs = localPath.startsWith(sep) ? localPath : join4(jobDir, localPath);
1002
1113
  const rel = abs.startsWith(jobDir + sep) ? abs.slice(jobDir.length + 1) : localPath;
1003
1114
  return rel.split(sep).join("/");
1004
1115
  }
@@ -1036,18 +1147,18 @@ async function pullFiles(client, jobDir, opts = {}) {
1036
1147
  const entry = byPath.get(path);
1037
1148
  if (!entry) continue;
1038
1149
  const dest = vfsLocalPath(jobDir, path);
1039
- if (existsSync5(dest) && sameSha(sha256Hex(readFileSync3(dest)), entry.sha256)) {
1150
+ if (existsSync3(dest) && sameSha(sha256Hex(readFileSync2(dest)), entry.sha256)) {
1040
1151
  const f2 = { path, localPath: dest, bytes: statSync3(dest).size, skipped: true };
1041
1152
  pulled.push(f2);
1042
1153
  opts.onFile?.(f2);
1043
1154
  continue;
1044
1155
  }
1045
1156
  const got = await client.filesGet(path);
1046
- mkdirSync5(dirname4(dest), { recursive: true });
1157
+ mkdirSync3(dirname3(dest), { recursive: true });
1047
1158
  let bytes;
1048
1159
  if (got.content !== void 0) {
1049
1160
  const buf = Buffer.from(got.content, "utf8");
1050
- writeFileSync4(dest, buf);
1161
+ writeFileSync3(dest, buf);
1051
1162
  bytes = buf.length;
1052
1163
  if (got.sha256 && !sameSha(sha256Hex(buf), got.sha256)) {
1053
1164
  throw new Error(`pulled ${path} but sha256 mismatch vs server (${got.sha256}) \u2014 refusing to trust the copy`);
@@ -1055,7 +1166,7 @@ async function pullFiles(client, jobDir, opts = {}) {
1055
1166
  } else {
1056
1167
  const dl = await client.download(got.downloadUrl, dest);
1057
1168
  bytes = dl.bytes;
1058
- if (got.sha256 && !sameSha(sha256Hex(readFileSync3(dest)), got.sha256)) {
1169
+ if (got.sha256 && !sameSha(sha256Hex(readFileSync2(dest)), got.sha256)) {
1059
1170
  throw new Error(`pulled ${path} but sha256 mismatch vs server (${got.sha256}) \u2014 refusing to trust the copy`);
1060
1171
  }
1061
1172
  }
@@ -1073,8 +1184,8 @@ async function pushFiles(client, jobDir, paths, opts = {}) {
1073
1184
  for (const rel of paths) {
1074
1185
  const vfsPath = toVfsPath(jobDir, rel);
1075
1186
  const local = vfsLocalPath(jobDir, vfsPath);
1076
- if (!existsSync5(local)) throw new Error(`files push: local file not found: ${local}`);
1077
- const bytes = readFileSync3(local);
1187
+ if (!existsSync3(local)) throw new Error(`files push: local file not found: ${local}`);
1188
+ const bytes = readFileSync2(local);
1078
1189
  const sha = sha256Hex(bytes);
1079
1190
  const existing = byPath.get(vfsPath);
1080
1191
  if (existing && sameSha(sha, existing.sha256)) {
@@ -1094,9 +1205,7 @@ async function pushFiles(client, jobDir, paths, opts = {}) {
1094
1205
  out.push(f);
1095
1206
  opts.onFile?.(f);
1096
1207
  } else {
1097
- const presign = await client.filesPresign({ path: vfsPath, contentType, bytes: bytes.length, sha256: sha });
1098
- await client.upload(presign.uploadUrl, bytes, contentType);
1099
- await client.filesCommit(vfsPath);
1208
+ await uploadVfsPayload(client, vfsPath, bytes, contentType, { sha256: sha, log: opts.log });
1100
1209
  const f = { path: vfsPath, bytes: bytes.length, skipped: false, mode: "presign" };
1101
1210
  out.push(f);
1102
1211
  opts.onFile?.(f);
@@ -1104,20 +1213,32 @@ async function pushFiles(client, jobDir, paths, opts = {}) {
1104
1213
  }
1105
1214
  return out;
1106
1215
  }
1107
- async function uploadVfsBytes(client, vfsPath, bytes, contentType) {
1108
- const presign = await client.filesPresign({
1109
- path: vfsPath,
1110
- contentType,
1111
- bytes: bytes.length,
1112
- sha256: sha256Hex(bytes)
1113
- });
1216
+ async function uploadVfsPayload(client, vfsPath, bytes, contentType, opts = {}) {
1217
+ const sha = opts.sha256 ?? sha256Hex(bytes);
1218
+ if (shouldMultipart(bytes)) {
1219
+ const res = await uploadMultipart(client, {
1220
+ kind: "jobfile",
1221
+ bytes,
1222
+ contentType,
1223
+ videoId: client.cfg.videoId,
1224
+ path: vfsPath,
1225
+ sha256: sha,
1226
+ log: opts.log
1227
+ });
1228
+ if (res !== "unavailable") return;
1229
+ opts.log?.(" chunked upload unavailable on this server; using single-request upload");
1230
+ }
1231
+ const presign = await client.filesPresign({ path: vfsPath, contentType, bytes: bytes.length, sha256: sha });
1114
1232
  await client.upload(presign.uploadUrl, bytes, contentType);
1115
1233
  await client.filesCommit(vfsPath);
1116
1234
  }
1235
+ async function uploadVfsBytes(client, vfsPath, bytes, contentType, log) {
1236
+ await uploadVfsPayload(client, vfsPath, bytes, contentType, { log });
1237
+ }
1117
1238
 
1118
1239
  // src/cloud/runs.ts
1119
1240
  var MAX_RUN_POLL_MS = 30 * 60 * 1e3;
1120
- var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
1241
+ var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
1121
1242
  function formatRunEvent(ev) {
1122
1243
  const p = ev.payload;
1123
1244
  const message = p !== null && typeof p === "object" && typeof p.message === "string" ? p.message : "";
@@ -1126,7 +1247,7 @@ function formatRunEvent(ev) {
1126
1247
  async function pollRun(client, runId, opts = {}) {
1127
1248
  const intervalMs = opts.intervalMs ?? 5e3;
1128
1249
  const maxMs = opts.maxMs ?? MAX_RUN_POLL_MS;
1129
- const nap = opts.sleepImpl ?? sleep2;
1250
+ const nap = opts.sleepImpl ?? sleep3;
1130
1251
  const now = opts.now ?? Date.now;
1131
1252
  const start = now();
1132
1253
  let seen = 0;
@@ -1147,115 +1268,136 @@ function assertRunCompleted(state, what) {
1147
1268
  throw new Error(`${what} run ${state.id || ""} ${state.status}: ${detail}`);
1148
1269
  }
1149
1270
 
1150
- // src/cloud/transcribe2.ts
1151
- import { mkdtempSync, readFileSync as readFileSync4, rmSync as rmSync2 } from "fs";
1152
- import { tmpdir } from "os";
1153
- import { join as join7 } from "path";
1154
- function transcribeArtifactSet(stem) {
1155
- return [`transcripts/${stem}.json`, "takes_packed.md", "flags.txt", "word_dump.txt", "coverage.json"];
1271
+ // src/cloud/init.ts
1272
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync3 } from "fs";
1273
+ import { join as join5, parse as parsePath, resolve as resolvePath } from "path";
1274
+ function sanitizeStem(name) {
1275
+ const safe = name.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
1276
+ return safe === "" ? "source" : safe;
1156
1277
  }
1157
- async function extractGuardedWav(media) {
1158
- const tmp = mkdtempSync(join7(tmpdir(), "ve-cloud-wav-"));
1278
+ function ingestArtifactSet(stem) {
1279
+ return [`transcripts/${stem}.json`, "takes_packed.md", "flags.txt", "word_dump.txt", "coverage.json", "probe.json"];
1280
+ }
1281
+ function readProbeDuration(jobDir) {
1282
+ const path = join5(jobDir, "probe.json");
1283
+ if (!existsSync4(path)) throw new Error(`ingest completed but probe.json was not pulled: ${path}`);
1284
+ let raw;
1159
1285
  try {
1160
- const wav = join7(tmp, "audio.wav");
1161
- await run(
1162
- ["ffmpeg", "-y", "-i", media, "-vn", "-ac", "1", "-ar", "16000", "-c:a", "pcm_s16le", wav],
1163
- { capture: true, echo: false }
1164
- );
1165
- const audioS = await mediaDuration(wav);
1166
- const videoS = await mediaDuration(media);
1167
- if (Number.isFinite(audioS) && Number.isFinite(videoS) && Math.abs(audioS - videoS) > 0.15) {
1168
- throw new Error(
1169
- `source A/V clocks diverge: decoded audio is ${audioS.toFixed(3)}s but video is ${videoS.toFixed(3)}s (\u0394 ${(audioS - videoS).toFixed(3)}s). Word timestamps would drift against the picture by up to that much. Re-export the source with a clean muxer (e.g. \`ffmpeg -i in -c:v copy -af aresample=async=1 out.mp4\`) and retry.`
1170
- );
1171
- }
1172
- return { bytes: readFileSync4(wav), durationS: audioS };
1173
- } finally {
1174
- rmSync2(tmp, { recursive: true, force: true });
1286
+ raw = JSON.parse(readFileSync3(path, "utf8"));
1287
+ } catch (err) {
1288
+ throw new Error(`probe.json is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
1175
1289
  }
1290
+ const durationS = Number(raw?.duration_s ?? NaN);
1291
+ if (!(durationS > 0)) throw new Error(`probe.json has no usable duration_s: ${JSON.stringify(raw)}`);
1292
+ return durationS;
1176
1293
  }
1177
- async function transcribeCloud(client, jobDir, opts) {
1178
- const verbose = opts.verbose !== false;
1294
+ function contentTypeFor(path) {
1295
+ const ext = parsePath(path).ext.toLowerCase();
1296
+ const map = { ".mp4": "video/mp4", ".m4v": "video/mp4", ".mov": "video/quicktime", ".mkv": "video/x-matroska", ".avi": "video/x-msvideo" };
1297
+ return map[ext] ?? "video/mp4";
1298
+ }
1299
+ async function initCloudJob(client, opts) {
1300
+ const source = resolvePath(opts.source);
1301
+ if (!existsSync4(source)) throw new Error(`source not found: ${source}`);
1302
+ const rawName = parsePath(source).name;
1303
+ const stem = sanitizeStem(rawName);
1304
+ const jobDir = resolvePath(opts.jobDir ?? join5(process.cwd(), `job_${stem}`));
1305
+ mkdirSync4(jobDir, { recursive: true });
1179
1306
  const log = (line) => {
1180
- if (verbose) process.stderr.write(line + "\n");
1307
+ if (opts.verbose !== false) process.stderr.write(line + "\n");
1181
1308
  };
1182
- log(` extracting audio from ${opts.media}`);
1183
- const { bytes, durationS } = await extractGuardedWav(opts.media);
1184
- const wavPath = `audio/${opts.stem}.wav`;
1185
- log(` uploading ${wavPath} (${(bytes.length / (1024 * 1024)).toFixed(1)} MB)`);
1186
- await uploadVfsBytes(client, wavPath, bytes, "audio/wav");
1187
- const { runId } = await client.transcribeStart({
1188
- wav_path: wavPath,
1189
- stem: opts.stem,
1190
- duration_s: durationS,
1191
- ...opts.language !== void 0 ? { language: opts.language } : {},
1192
- ...opts.numSpeakers !== void 0 ? { num_speakers: opts.numSpeakers } : {}
1193
- });
1194
- log(` transcribe run: ${runId}`);
1195
- const state = await pollRun(client, runId, {
1309
+ await client.me();
1310
+ const title = opts.title ?? rawName;
1311
+ const { projectId } = await client.createProject(title);
1312
+ log(` project: ${projectId}`);
1313
+ const contentType = contentTypeFor(source);
1314
+ const filename = parsePath(source).base;
1315
+ let assetId;
1316
+ let uploadedBytes = 0;
1317
+ const fileSrc = await openFileSource(source);
1318
+ try {
1319
+ uploadedBytes = fileSrc.size;
1320
+ log(` uploading ${filename} (${(fileSrc.size / (1024 * 1024)).toFixed(1)} MB)`);
1321
+ if (shouldMultipart(fileSrc.size)) {
1322
+ const mp = await uploadMultipart(client, { kind: "asset", source: fileSrc, contentType, filename, log });
1323
+ if (mp === "unavailable") {
1324
+ log(" chunked upload unavailable on this server; using single-request upload");
1325
+ } else {
1326
+ assetId = mp.assetId;
1327
+ }
1328
+ }
1329
+ if (!assetId) {
1330
+ const bytes = await fileSrc.readSlice(0, fileSrc.size);
1331
+ const asset = await client.createSourceAsset({ contentType, filename });
1332
+ await client.upload(asset.uploadUrl, bytes, contentType);
1333
+ assetId = asset.assetId;
1334
+ }
1335
+ } finally {
1336
+ await fileSrc.close();
1337
+ }
1338
+ const video = await client.createVideo(projectId, { assetId, title });
1339
+ log(` video: ${video.videoId}`);
1340
+ const videoClient = client.cfg.videoId ? client : client.withVideoId(video.videoId);
1341
+ const { runId } = await videoClient.ingestStart(stem);
1342
+ log(` ingest run: ${runId}`);
1343
+ const state = await pollRun(videoClient, runId, {
1196
1344
  intervalMs: opts.pollIntervalMs,
1197
1345
  onEvent: (ev) => log(` ${formatRunEvent(ev)}`)
1198
1346
  });
1199
- assertRunCompleted(state, "transcribe");
1200
- const pulled = await pullFiles(client, jobDir, { only: transcribeArtifactSet(opts.stem), prefixes: ["prompts/"] });
1201
- return { runId, wavPath, durationS, pulled };
1347
+ assertRunCompleted(state, "ingest");
1348
+ const pulled = await pullFiles(videoClient, jobDir, { only: ingestArtifactSet(stem), prefixes: ["prompts/"] });
1349
+ const durationS = readProbeDuration(jobDir);
1350
+ const job = {
1351
+ video_id: video.videoId,
1352
+ project_id: video.projectId,
1353
+ api_base: client.cfg.base,
1354
+ stem,
1355
+ duration_s: durationS,
1356
+ source_path: source
1357
+ };
1358
+ const jobJson = writeJobFile(jobDir, job);
1359
+ return {
1360
+ jobDir,
1361
+ jobJson,
1362
+ videoId: video.videoId,
1363
+ projectId: video.projectId,
1364
+ stem,
1365
+ durationS,
1366
+ uploaded: source,
1367
+ uploadedBytes,
1368
+ runId,
1369
+ pulled,
1370
+ webUrl: webVideoUrl(client.cfg.base, video.projectId, video.videoId)
1371
+ };
1202
1372
  }
1203
1373
 
1204
- // src/silence-detect.ts
1205
- var DEFAULT_DESILENCE = {
1206
- noiseDb: -36,
1207
- minSilence: 0.14,
1208
- targetGap: 0.09,
1209
- leadKeep: 0.04,
1210
- trailKeep: 0.05,
1211
- minSegment: 0.1
1212
- };
1213
- function f3(x) {
1214
- return pyFixed(x, 3);
1215
- }
1216
- function silenceDetectArgs(source, noiseDb, minSilenceS) {
1217
- return [
1218
- "ffmpeg",
1219
- "-hide_banner",
1220
- "-nostats",
1221
- "-i",
1222
- source,
1223
- "-af",
1224
- `silencedetect=noise=${noiseDb}dB:d=${f3(minSilenceS)}`,
1225
- "-vn",
1226
- "-f",
1227
- "null",
1228
- "-"
1229
- ];
1374
+ // src/cloud/transcribe2.ts
1375
+ function transcribeArtifactSet(stem) {
1376
+ return [`transcripts/${stem}.json`, "takes_packed.md", "flags.txt", "word_dump.txt", "coverage.json", "probe.json"];
1230
1377
  }
1231
- function parseSilenceDetect(stderr, sourceDurationS = 0) {
1232
- const intervals = [];
1233
- let pendingStart = null;
1234
- for (const line of stderr.split(/\r?\n/)) {
1235
- const mStart = line.match(/silence_start:\s*(-?[0-9]+(?:\.[0-9]+)?)/);
1236
- if (mStart) {
1237
- pendingStart = Number(mStart[1]);
1238
- continue;
1239
- }
1240
- const mEnd = line.match(/silence_end:\s*(-?[0-9]+(?:\.[0-9]+)?)/);
1241
- if (mEnd && pendingStart !== null) {
1242
- const end = Number(mEnd[1]);
1243
- if (end > pendingStart) intervals.push({ start: pendingStart, end });
1244
- pendingStart = null;
1245
- }
1246
- }
1247
- if (pendingStart !== null && sourceDurationS > pendingStart) {
1248
- intervals.push({ start: pendingStart, end: sourceDurationS });
1378
+ async function transcribeCloud(client, jobDir, opts) {
1379
+ const verbose = opts.verbose !== false;
1380
+ const log = (line) => {
1381
+ if (verbose) process.stderr.write(line + "\n");
1382
+ };
1383
+ const transcriptPath = `transcripts/${opts.stem}.json`;
1384
+ const listing = await client.filesList();
1385
+ const haveTranscript = listing.some((f) => f.path === transcriptPath);
1386
+ let runId = null;
1387
+ if (haveTranscript) {
1388
+ log(` ${transcriptPath} already exists server-side \u2014 pulling artifacts`);
1389
+ } else {
1390
+ const started = await client.ingestStart(opts.stem);
1391
+ runId = started.runId;
1392
+ log(` ingest run: ${runId}`);
1393
+ const state = await pollRun(client, runId, {
1394
+ intervalMs: opts.pollIntervalMs,
1395
+ onEvent: (ev) => log(` ${formatRunEvent(ev)}`)
1396
+ });
1397
+ assertRunCompleted(state, "ingest");
1249
1398
  }
1250
- return intervals;
1251
- }
1252
- async function detectSilences(source, noiseDb, minSilenceS, sourceDurationS) {
1253
- const res = await run(silenceDetectArgs(source, noiseDb, minSilenceS), {
1254
- capture: true,
1255
- check: false,
1256
- echo: false
1257
- });
1258
- return parseSilenceDetect(res.stderr, sourceDurationS);
1399
+ const pulled = await pullFiles(client, jobDir, { only: transcribeArtifactSet(opts.stem), prefixes: ["prompts/"] });
1400
+ return { runId, pulled };
1259
1401
  }
1260
1402
 
1261
1403
  // src/timeline-v2/schema.ts
@@ -1728,20 +1870,20 @@ function checkCapabilities(uses, registry = CAPABILITIES) {
1728
1870
  }
1729
1871
 
1730
1872
  // src/cloud/compile2.ts
1731
- import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
1732
- import { isAbsolute, join as join8, resolve as resolve2 } from "path";
1873
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
1874
+ import { isAbsolute, join as join6, resolve as resolve2 } from "path";
1733
1875
  function timelineJsonPath2(jobDir) {
1734
- return join8(jobDir, "timeline.json");
1876
+ return join6(jobDir, "timeline.json");
1735
1877
  }
1736
1878
  function planPath2(jobDir) {
1737
- return join8(jobDir, "resolved", "plan.json");
1879
+ return join6(jobDir, "resolved", "plan.json");
1738
1880
  }
1739
1881
  function diagnosticsPath2(jobDir) {
1740
- return join8(jobDir, "resolved", "diagnostics.json");
1882
+ return join6(jobDir, "resolved", "diagnostics.json");
1741
1883
  }
1742
1884
  function loadTimeline2(jobDir) {
1743
1885
  const path = timelineJsonPath2(jobDir);
1744
- if (!existsSync6(path)) {
1886
+ if (!existsSync5(path)) {
1745
1887
  return {
1746
1888
  doc: null,
1747
1889
  raw: null,
@@ -1750,7 +1892,7 @@ function loadTimeline2(jobDir) {
1750
1892
  }
1751
1893
  let raw;
1752
1894
  try {
1753
- raw = JSON.parse(readFileSync5(path, "utf8"));
1895
+ raw = JSON.parse(readFileSync4(path, "utf8"));
1754
1896
  } catch (err) {
1755
1897
  return {
1756
1898
  doc: null,
@@ -1775,22 +1917,11 @@ function validateLocal(doc) {
1775
1917
  const warnings = [...sem.warnings, ...cap.warnings];
1776
1918
  return { ok: errors.length === 0, uses, errors, warnings };
1777
1919
  }
1778
- async function detectSilencesForDoc(jobDir, doc) {
1779
- const dopts = { ...DEFAULT_DESILENCE, ...doc.settings.desilence ?? {} };
1780
- const out = {};
1781
- for (const [name, src] of Object.entries(doc.sources)) {
1782
- const srcPath = isAbsolute(src.path) ? src.path : resolve2(jobDir, src.path);
1783
- if (!existsSync6(srcPath)) continue;
1784
- const dur = src.durationS ?? await mediaDuration(srcPath);
1785
- out[name] = await detectSilences(srcPath, dopts.noiseDb, dopts.minSilence, dur);
1786
- }
1787
- return out;
1788
- }
1789
1920
  function statAssetsForDoc(jobDir, doc) {
1790
1921
  const out = {};
1791
1922
  const statRef = (ref) => {
1792
1923
  if (ref !== void 0 && !(ref in out)) {
1793
- out[ref] = existsSync6(isAbsolute(ref) ? ref : resolve2(jobDir, ref));
1924
+ out[ref] = existsSync5(isAbsolute(ref) ? ref : resolve2(jobDir, ref));
1794
1925
  }
1795
1926
  };
1796
1927
  for (const ov of doc.overlays) statRef(ov.asset?.ref);
@@ -1799,60 +1930,37 @@ function statAssetsForDoc(jobDir, doc) {
1799
1930
  }
1800
1931
  async function compileRemote(client, jobDir, loaded) {
1801
1932
  if (!loaded.doc) throw new Error("compileRemote requires a parsed timeline");
1802
- const silences = await detectSilencesForDoc(jobDir, loaded.doc);
1803
1933
  const assets = statAssetsForDoc(jobDir, loaded.doc);
1804
1934
  const res = await client.compileRemote({
1805
1935
  timeline: loaded.raw,
1806
- ...Object.keys(silences).length > 0 ? { silences_by_source: silences } : {},
1807
1936
  ...Object.keys(assets).length > 0 ? { assets_present: assets } : {}
1808
1937
  });
1809
- const dir = join8(jobDir, "resolved");
1810
- mkdirSync6(dir, { recursive: true });
1938
+ const dir = join6(jobDir, "resolved");
1939
+ mkdirSync5(dir, { recursive: true });
1811
1940
  const out = { ok: res.ok === true, diagnosticsPath: diagnosticsPath2(jobDir), response: res };
1812
- writeFileSync5(out.diagnosticsPath, JSON.stringify(res.diagnostics ?? { errors: [], warnings: [], dropped: [] }, null, 2) + "\n", "utf8");
1941
+ writeFileSync4(out.diagnosticsPath, JSON.stringify(res.diagnostics ?? { errors: [], warnings: [], dropped: [] }, null, 2) + "\n", "utf8");
1813
1942
  if (res.ok === true && res.plan !== void 0 && res.plan !== null) {
1814
1943
  out.planPath = planPath2(jobDir);
1815
- writeFileSync5(out.planPath, JSON.stringify(res.plan, null, 2) + "\n", "utf8");
1944
+ writeFileSync4(out.planPath, JSON.stringify(res.plan, null, 2) + "\n", "utf8");
1816
1945
  }
1817
1946
  if (res.ok === true && res.legacy_edl !== void 0 && res.legacy_edl !== null) {
1818
- out.edlPath = join8(jobDir, "edl.json");
1819
- writeFileSync5(out.edlPath, JSON.stringify(res.legacy_edl, null, 2) + "\n", "utf8");
1947
+ out.edlPath = join6(jobDir, "edl.json");
1948
+ writeFileSync4(out.edlPath, JSON.stringify(res.legacy_edl, null, 2) + "\n", "utf8");
1820
1949
  }
1821
1950
  return out;
1822
1951
  }
1823
1952
 
1824
1953
  // src/cloud/verify2.ts
1825
- import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
1826
- import { join as join9 } from "path";
1954
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
1955
+ import { join as join7 } from "path";
1827
1956
  async function verifyCloud(client, jobDir, opts = {}) {
1828
1957
  const verbose = opts.verbose !== false;
1829
1958
  const log = (line) => {
1830
1959
  if (verbose) process.stderr.write(line + "\n");
1831
1960
  };
1832
- const preview = join9(jobDir, "preview.mp4");
1833
- if (!existsSync7(preview)) {
1834
- throw new Error(`preview not found (render first, e.g. \`video-editing cloud render --mode preview --wait\`): ${preview}`);
1835
- }
1836
- log(" extracting preview audio");
1837
- const { bytes, durationS } = await extractGuardedWav(preview);
1838
- const wavPath = "audio/preview.wav";
1839
- log(` uploading ${wavPath} (${(bytes.length / (1024 * 1024)).toFixed(1)} MB)`);
1840
- await uploadVfsBytes(client, wavPath, bytes, "audio/wav");
1841
- let edl;
1842
- const edlPath = join9(jobDir, "edl.json");
1843
- if (existsSync7(edlPath)) {
1844
- try {
1845
- edl = JSON.parse(readFileSync6(edlPath, "utf8"));
1846
- } catch {
1847
- edl = void 0;
1848
- }
1849
- }
1850
1961
  const { runId } = await client.verifyStart({
1851
- wav_path: wavPath,
1852
- preview_duration_s: durationS,
1853
1962
  ...opts.full ? { mode: "full" } : {},
1854
- ...opts.windowS !== void 0 ? { window_s: opts.windowS } : {},
1855
- ...edl !== void 0 ? { edl } : {}
1963
+ ...opts.windowS !== void 0 ? { window_s: opts.windowS } : {}
1856
1964
  });
1857
1965
  log(` verify run: ${runId}`);
1858
1966
  const state = await pollRun(client, runId, {
@@ -1861,12 +1969,12 @@ async function verifyCloud(client, jobDir, opts = {}) {
1861
1969
  });
1862
1970
  assertRunCompleted(state, "verify");
1863
1971
  await pullFiles(client, jobDir, { only: ["verify.json"] });
1864
- const verifyPath = join9(jobDir, "verify.json");
1972
+ const verifyPath = join7(jobDir, "verify.json");
1865
1973
  let verdict = null;
1866
1974
  let findings = null;
1867
- if (existsSync7(verifyPath)) {
1975
+ if (existsSync6(verifyPath)) {
1868
1976
  try {
1869
- const v = JSON.parse(readFileSync6(verifyPath, "utf8"));
1977
+ const v = JSON.parse(readFileSync5(verifyPath, "utf8"));
1870
1978
  verdict = typeof v.verdict === "string" ? v.verdict : null;
1871
1979
  findings = Array.isArray(v.joins) ? v.joins.length : null;
1872
1980
  } catch {
@@ -1875,302 +1983,128 @@ async function verifyCloud(client, jobDir, opts = {}) {
1875
1983
  return { runId, verifyPath, verdict, findings };
1876
1984
  }
1877
1985
 
1878
- // src/cloud/compose2.ts
1879
- import { copyFileSync, existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync7, statSync as statSync4, writeFileSync as writeFileSync6 } from "fs";
1880
- import { dirname as dirname5, join as join10, resolve as resolvePath2 } from "path";
1881
- function stale(out, src) {
1882
- return !existsSync8(out) || statSync4(out).mtimeMs < statSync4(src).mtimeMs;
1883
- }
1884
- async function buildMediaEntry(jobDir, compDir, entry, opts) {
1885
- const dest = join10(compDir, ...entry.src.split("/").filter((p) => p !== "" && p !== "." && p !== ".."));
1886
- mkdirSync7(dirname5(dest), { recursive: true });
1887
- if (entry.role === "video" || entry.role === "source") {
1888
- const master = join10(jobDir, "vertical_src.mp4");
1889
- if (!existsSync8(master)) {
1890
- throw new Error(
1891
- `vertical master not found: ${master} \u2014 bake it locally (\`video-editing init --source <video> --master\`) or pull it`
1892
- );
1893
- }
1894
- if (stale(dest, master)) {
1895
- await run([
1896
- "ffmpeg",
1897
- "-y",
1898
- "-hide_banner",
1899
- "-loglevel",
1900
- "error",
1901
- "-stats",
1902
- "-i",
1903
- master,
1904
- "-c:v",
1905
- "libx264",
1906
- "-preset",
1907
- "veryfast",
1908
- "-crf",
1909
- String(opts.proxyCrf),
1910
- // Dense keyframes: HyperFrames frame-extracts per clip; sparse GOPs
1911
- // (default ~8s) break seeks on sub-second segments.
1912
- "-r",
1913
- "30",
1914
- "-g",
1915
- "30",
1916
- "-keyint_min",
1917
- "30",
1918
- "-movflags",
1919
- "+faststart",
1920
- "-maxrate",
1921
- "6M",
1922
- "-bufsize",
1923
- "12M",
1924
- "-c:a",
1925
- "aac",
1926
- "-b:a",
1927
- "160k",
1928
- "-movflags",
1929
- "+faststart",
1930
- dest
1931
- ]);
1932
- } else {
1933
- process.stderr.write(" proxy up to date; skipping re-encode\n");
1934
- }
1935
- return dest;
1936
- }
1937
- if (entry.role === "audio") {
1938
- const preview = join10(jobDir, "preview.mp4");
1939
- if (existsSync8(preview)) {
1940
- if (stale(dest, preview)) {
1941
- await run([
1942
- "ffmpeg",
1943
- "-y",
1944
- "-hide_banner",
1945
- "-loglevel",
1946
- "error",
1947
- "-stats",
1948
- "-i",
1949
- preview,
1950
- "-vn",
1951
- "-c:a",
1952
- "aac",
1953
- "-b:a",
1954
- "160k",
1955
- dest
1956
- ]);
1957
- } else {
1958
- process.stderr.write(" cut audio up to date; skipping re-extract\n");
1959
- }
1960
- return dest;
1961
- }
1962
- const master = join10(jobDir, "vertical_src.mp4");
1963
- const planPath = join10(jobDir, "resolved", "plan.json");
1964
- if (!existsSync8(master) || !existsSync8(planPath)) {
1965
- throw new Error(
1966
- `cut audio needs either <job>/preview.mp4 or vertical_src.mp4 + resolved/plan.json (compile first)`
1967
- );
1986
+ // src/cloud/qa.ts
1987
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
1988
+ import { join as join8 } from "path";
1989
+ function qaDir(jobDir) {
1990
+ return jobDir ? join8(jobDir, "qa") : join8(process.cwd(), "qa");
1991
+ }
1992
+ function tag(start, end) {
1993
+ return `${start}_${end}`;
1994
+ }
1995
+ function framesPngPath(dir, start, end, suffix = "") {
1996
+ return join8(dir, `frames_${tag(start, end)}${suffix}.png`);
1997
+ }
1998
+ function waveformPngPath(dir, start, end) {
1999
+ return join8(dir, `waveform_${tag(start, end)}.png`);
2000
+ }
2001
+ function inspectPngPath(dir, start, end, suffix = "") {
2002
+ return join8(dir, `inspect_${tag(start, end)}${suffix}.png`);
2003
+ }
2004
+ function waveformWordsPath(dir, start, end) {
2005
+ return join8(dir, `waveform_${tag(start, end)}.words.json`);
2006
+ }
2007
+ function readPlanSegments(jobDir) {
2008
+ const path = join8(jobDir, "resolved", "plan.json");
2009
+ if (!existsSync7(path)) {
2010
+ throw new Error(`no compiled plan at ${path} \u2014 run \`video-editing timeline compile\` first`);
2011
+ }
2012
+ let raw;
2013
+ try {
2014
+ raw = JSON.parse(readFileSync6(path, "utf8"));
2015
+ } catch (err) {
2016
+ throw new Error(`resolved/plan.json is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
2017
+ }
2018
+ const segments = raw?.segments;
2019
+ if (!Array.isArray(segments) || segments.length === 0) {
2020
+ throw new Error(`resolved/plan.json has no segments[]: ${path}`);
2021
+ }
2022
+ return segments.map((s, i) => {
2023
+ const r = s;
2024
+ const seg = {
2025
+ srcStart: Number(r.srcStart ?? NaN),
2026
+ srcEnd: Number(r.srcEnd ?? NaN),
2027
+ outStart: Number(r.outStart ?? NaN),
2028
+ outEnd: Number(r.outEnd ?? NaN)
2029
+ };
2030
+ if (![seg.srcStart, seg.srcEnd, seg.outStart, seg.outEnd].every(Number.isFinite)) {
2031
+ throw new Error(`plan segment ${i} is missing src/out times: ${JSON.stringify(s)}`);
1968
2032
  }
1969
- const plan = JSON.parse(readFileSync7(planPath, "utf8"));
1970
- const segs = (plan.segments ?? []).filter(
1971
- (x) => typeof x.srcStart === "number" && typeof x.srcEnd === "number" && x.srcEnd > x.srcStart
2033
+ return seg;
2034
+ });
2035
+ }
2036
+ var round3 = (n) => Math.round(n * 1e3) / 1e3;
2037
+ var SUFFIXES = "abcdefghijklmnopqrstuvwxyz";
2038
+ function suffixed(windows) {
2039
+ return windows.map((w, i) => ({
2040
+ start: round3(w.start),
2041
+ end: round3(w.end),
2042
+ suffix: windows.length > 1 ? `-${SUFFIXES[i] ?? String(i)}` : ""
2043
+ }));
2044
+ }
2045
+ function joinSourceWindows(segments, joinIndex, spanS) {
2046
+ if (!Number.isInteger(joinIndex) || joinIndex < 1 || joinIndex > segments.length - 1) {
2047
+ throw new Error(
2048
+ `--join must be a 1-based join index between 1 and ${segments.length - 1} (the plan has ${segments.length} segment(s))`
1972
2049
  );
1973
- if (segs.length === 0) throw new Error("resolved/plan.json has no usable segments for cut audio");
1974
- const trims = segs.map((x, i) => `[0:a]atrim=start=${x.srcStart}:end=${x.srcEnd},asetpts=PTS-STARTPTS[a${i}]`).join(";");
1975
- const labels = segs.map((_, i) => `[a${i}]`).join("");
1976
- const filter = `${trims};${labels}concat=n=${segs.length}:v=0:a=1[out]`;
1977
- await run([
1978
- "ffmpeg",
1979
- "-y",
1980
- "-hide_banner",
1981
- "-loglevel",
1982
- "error",
1983
- "-stats",
1984
- "-i",
1985
- master,
1986
- "-filter_complex",
1987
- filter,
1988
- "-map",
1989
- "[out]",
1990
- "-c:a",
1991
- "aac",
1992
- "-b:a",
1993
- "160k",
1994
- dest
1995
- ]);
1996
- return dest;
1997
- }
1998
- const ref = entry.src.replace(/^media\//, "");
1999
- const srcPath = resolvePath2(jobDir, ref);
2000
- if (!existsSync8(srcPath)) {
2001
- throw new Error(`asset not found: ${ref} (expected at ${srcPath}) \u2014 asset refs are job-dir-relative paths`);
2002
- }
2003
- copyFileSync(srcPath, dest);
2004
- return dest;
2005
- }
2006
- async function composeCloud(client, jobDir, opts = {}) {
2007
- const quality = opts.quality ?? "standard";
2008
- if (!["draft", "standard", "high"].includes(quality)) throw new Error("--quality must be draft|standard|high");
2009
- const fps = opts.fps ?? 30;
2010
- const proxyCrf = opts.proxyCrf ?? 23;
2011
- const res = await client.getCompose();
2012
- const compDir = join10(jobDir, "composition");
2013
- mkdirSync7(join10(compDir, "media"), { recursive: true });
2014
- const entry = join10(compDir, "index.html");
2015
- writeFileSync6(entry, res.html, "utf8");
2016
- for (const m of res.media) {
2017
- await buildMediaEntry(jobDir, compDir, m, { proxyCrf });
2018
- }
2019
- const out = resolvePath2(opts.out ?? join10(jobDir, "preview.mp4"));
2020
- const renderCmd = ["hyperframes", "render", "--quality", quality, "--fps", String(fps), "--output", out];
2021
- const result = {
2022
- compositionDir: compDir,
2023
- entry,
2024
- counts: res.counts,
2025
- media: res.media,
2026
- renderCmd,
2027
- rendered: false
2028
- };
2029
- if (!opts.render) {
2030
- if (opts.verbose !== false) {
2031
- process.stderr.write(`render locally: cd ${shown([compDir])} && ${shown(renderCmd)}
2032
- `);
2033
- }
2034
- return result;
2035
2050
  }
2036
- const probe = runSync(["hyperframes", "--version"], { check: false, echo: false });
2037
- if (probe.code !== 0) {
2038
- throw new Error("`hyperframes` CLI not found on PATH \u2014 install it with `npm install -g hyperframes`");
2051
+ if (!(spanS > 0)) throw new Error("--span must be > 0 seconds");
2052
+ const a = segments[joinIndex - 1];
2053
+ const b = segments[joinIndex];
2054
+ return suffixed([
2055
+ { start: Math.max(a.srcStart, a.srcEnd - spanS), end: a.srcEnd },
2056
+ { start: b.srcStart, end: Math.min(b.srcEnd, b.srcStart + spanS) }
2057
+ ]);
2058
+ }
2059
+ function joinSpanWindow(segments, joinIndex, spanS) {
2060
+ if (!Number.isInteger(joinIndex) || joinIndex < 1 || joinIndex > segments.length - 1) {
2061
+ throw new Error(
2062
+ `--join must be a 1-based join index between 1 and ${segments.length - 1} (the plan has ${segments.length} segment(s))`
2063
+ );
2039
2064
  }
2040
- const render = await run(renderCmd, { cwd: compDir, check: false });
2041
- result.renderExit = render.code;
2042
- result.rendered = render.code === 0;
2043
- if (render.code === 0) result.out = out;
2044
- return result;
2045
- }
2046
-
2047
- // src/cloud/qa-local.ts
2048
- import { existsSync as existsSync9, mkdirSync as mkdirSync8, readdirSync as readdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
2049
- import { join as join11 } from "path";
2050
- function qaFramesArgs(win, opts = {}) {
2051
- const dur = Math.max(0.05, win.end - win.start);
2052
- const frames = Math.max(1, Math.round(opts.frames ?? 10));
2053
- const cols = Math.max(1, Math.round(opts.cols ?? 5));
2054
- const rows = Math.ceil(frames / cols);
2055
- const fps = frames / dur;
2056
- const width = Math.max(64, Math.round(opts.frameWidth ?? 216));
2057
- return [
2058
- "ffmpeg",
2059
- "-y",
2060
- "-hide_banner",
2061
- "-loglevel",
2062
- "error",
2063
- "-ss",
2064
- win.start.toFixed(3),
2065
- "-t",
2066
- dur.toFixed(3),
2067
- "-i",
2068
- win.video,
2069
- "-frames:v",
2070
- "1",
2071
- "-vf",
2072
- `fps=${fps.toFixed(6)},scale=${width}:-2,tile=${cols}x${rows}`,
2073
- win.output
2074
- ];
2065
+ if (!(spanS > 0)) throw new Error("--span must be > 0 seconds");
2066
+ const a = segments[joinIndex - 1];
2067
+ const b = segments[joinIndex];
2068
+ return { start: round3(Math.max(0, a.srcEnd - spanS)), end: round3(b.srcStart + spanS), suffix: "" };
2075
2069
  }
2076
- function qaWaveformArgs(win, opts = {}) {
2077
- const dur = Math.max(0.05, win.end - win.start);
2078
- const w = Math.max(64, Math.round(opts.width ?? 1200));
2079
- const h = Math.max(32, Math.round(opts.height ?? 240));
2080
- return [
2081
- "ffmpeg",
2082
- "-y",
2083
- "-hide_banner",
2084
- "-loglevel",
2085
- "error",
2086
- "-ss",
2087
- win.start.toFixed(3),
2088
- "-t",
2089
- dur.toFixed(3),
2090
- "-i",
2091
- win.video,
2092
- "-filter_complex",
2093
- `showwavespic=s=${w}x${h}:split_channels=0`,
2094
- "-frames:v",
2095
- "1",
2096
- win.output
2097
- ];
2098
- }
2099
- function qaWordsForWindow(transcriptJson, start, end) {
2100
- const data = JSON.parse(transcriptJson);
2101
- const out = [];
2102
- for (const w of data.words ?? []) {
2103
- if ((w.type ?? "word") !== "word") continue;
2104
- if (typeof w.start !== "number" || typeof w.end !== "number") continue;
2105
- if (w.end < start || w.start > end) continue;
2106
- const text = typeof w.text === "string" ? w.text.trim() : "";
2107
- if (!text) continue;
2108
- out.push({ t: w.start, end: w.end, text });
2070
+ function outWindowToSourceWindows(segments, outStart, outEnd) {
2071
+ if (!(outEnd > outStart)) throw new Error("--out-end must be greater than --out-start");
2072
+ const windows = [];
2073
+ for (const seg of segments) {
2074
+ const s = Math.max(outStart, seg.outStart);
2075
+ const e = Math.min(outEnd, seg.outEnd);
2076
+ if (e - s <= 5e-4) continue;
2077
+ windows.push({ start: seg.srcStart + (s - seg.outStart), end: seg.srcStart + (e - seg.outStart) });
2109
2078
  }
2110
- return out;
2111
- }
2112
- function findLocalTranscript(jobDir, stem) {
2113
- const dir = join11(jobDir, "transcripts");
2114
- if (stem) {
2115
- const direct = join11(dir, `${stem}.json`);
2116
- if (existsSync9(direct)) return direct;
2117
- }
2118
- if (!existsSync9(dir)) return null;
2119
- const all = readdirSync2(dir).filter((f) => f.endsWith(".json"));
2120
- return all.length === 1 ? join11(dir, all[0]) : null;
2121
- }
2122
- async function qaFramesLocal(jobDir, args) {
2123
- const dir = join11(jobDir, "qa");
2124
- mkdirSync8(dir, { recursive: true });
2125
- const png = join11(dir, `frames_${args.start}_${args.end}.png`);
2126
- await run(qaFramesArgs({ video: args.video, start: args.start, end: args.end, output: png }, args), {
2127
- capture: true,
2128
- echo: false
2129
- });
2130
- return { png };
2131
- }
2132
- async function qaWaveformLocal(jobDir, args) {
2133
- const dir = join11(jobDir, "qa");
2134
- mkdirSync8(dir, { recursive: true });
2135
- const png = join11(dir, `waveform_${args.start}_${args.end}.png`);
2136
- await run(qaWaveformArgs({ video: args.video, start: args.start, end: args.end, output: png }), {
2137
- capture: true,
2138
- echo: false
2139
- });
2140
- const result = { png };
2141
- const tpath = findLocalTranscript(jobDir, args.stem);
2142
- if (tpath) {
2143
- const words = qaWordsForWindow(readFileSync8(tpath, "utf8"), args.start, args.end);
2144
- const wordsPath = join11(dir, `waveform_${args.start}_${args.end}.words.json`);
2145
- writeFileSync7(wordsPath, JSON.stringify(words, null, 2) + "\n", "utf8");
2146
- result.wordsPath = wordsPath;
2147
- result.words = words.length;
2079
+ if (windows.length === 0) {
2080
+ const total = segments.length ? Math.max(...segments.map((s) => s.outEnd)) : 0;
2081
+ throw new Error(`output window ${outStart}s\u2013${outEnd}s does not overlap the plan (output duration ${total}s)`);
2148
2082
  }
2149
- return result;
2083
+ return suffixed(windows);
2150
2084
  }
2151
2085
 
2152
2086
  // src/cloud/review2.ts
2153
2087
  import { createHash as createHash2 } from "crypto";
2154
- import { existsSync as existsSync10, mkdirSync as mkdirSync9, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "fs";
2155
- import { basename, isAbsolute as isAbsolute2, join as join12 } from "path";
2088
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
2089
+ import { basename, isAbsolute as isAbsolute2, join as join9 } from "path";
2156
2090
  function reviewDir2(jobDir) {
2157
- return join12(jobDir, "review");
2091
+ return join9(jobDir, "review");
2158
2092
  }
2159
2093
  function roundJsonPath2(jobDir) {
2160
- return join12(reviewDir2(jobDir), "round.json");
2094
+ return join9(reviewDir2(jobDir), "round.json");
2161
2095
  }
2162
2096
  function instructionPath2(jobDir) {
2163
- return join12(reviewDir2(jobDir), "instruction.md");
2097
+ return join9(reviewDir2(jobDir), "instruction.md");
2164
2098
  }
2165
2099
  function assetsCachePath2(jobDir) {
2166
- return join12(reviewDir2(jobDir), "assets.json");
2100
+ return join9(reviewDir2(jobDir), "assets.json");
2167
2101
  }
2168
2102
  async function pullReview2(client, jobDir, opts = {}) {
2169
2103
  const session = await client.getReview();
2170
- mkdirSync9(reviewDir2(jobDir), { recursive: true });
2104
+ mkdirSync6(reviewDir2(jobDir), { recursive: true });
2171
2105
  const round = session.activeRound;
2172
2106
  if (round) {
2173
- writeFileSync8(
2107
+ writeFileSync5(
2174
2108
  roundJsonPath2(jobDir),
2175
2109
  JSON.stringify(
2176
2110
  {
@@ -2188,20 +2122,20 @@ async function pullReview2(client, jobDir, opts = {}) {
2188
2122
  ) + "\n",
2189
2123
  "utf8"
2190
2124
  );
2191
- writeFileSync8(instructionPath2(jobDir), round.instruction + "\n", "utf8");
2125
+ writeFileSync5(instructionPath2(jobDir), round.instruction + "\n", "utf8");
2192
2126
  } else {
2193
- writeFileSync8(instructionPath2(jobDir), "No active review round for this video.\n", "utf8");
2127
+ writeFileSync5(instructionPath2(jobDir), "No active review round for this video.\n", "utf8");
2194
2128
  }
2195
2129
  let wroteTimeline = false;
2196
2130
  const tlPath = timelineJsonPath2(jobDir);
2197
- if (session.timeline !== null && session.timeline !== void 0 && !existsSync10(tlPath)) {
2198
- writeFileSync8(tlPath, JSON.stringify(session.timeline, null, 2) + "\n", "utf8");
2131
+ if (session.timeline !== null && session.timeline !== void 0 && !existsSync8(tlPath)) {
2132
+ writeFileSync5(tlPath, JSON.stringify(session.timeline, null, 2) + "\n", "utf8");
2199
2133
  wroteTimeline = true;
2200
2134
  }
2201
2135
  let downloadedSource = null;
2202
2136
  if (opts.withSource && session.sourceUrl) {
2203
- const dest = join12(jobDir, "source.mp4");
2204
- if (!existsSync10(dest)) {
2137
+ const dest = join9(jobDir, "source.mp4");
2138
+ if (!existsSync8(dest)) {
2205
2139
  const dl = await client.download(session.sourceUrl, dest);
2206
2140
  downloadedSource = dl.path;
2207
2141
  }
@@ -2211,9 +2145,9 @@ async function pullReview2(client, jobDir, opts = {}) {
2211
2145
  function resolveRunId2(jobDir, flagOverride) {
2212
2146
  if (typeof flagOverride === "string" && flagOverride.trim() !== "") return flagOverride.trim();
2213
2147
  const path = roundJsonPath2(jobDir);
2214
- if (existsSync10(path)) {
2148
+ if (existsSync8(path)) {
2215
2149
  try {
2216
- const runId = JSON.parse(readFileSync9(path, "utf8")).run_id;
2150
+ const runId = JSON.parse(readFileSync7(path, "utf8")).run_id;
2217
2151
  if (typeof runId === "string" && runId !== "") return runId;
2218
2152
  } catch {
2219
2153
  }
@@ -2273,8 +2207,8 @@ async function syncAssets2(client, jobDir, plan) {
2273
2207
  if (refs.length === 0) return {};
2274
2208
  const paths = /* @__PURE__ */ new Map();
2275
2209
  for (const ref of refs) {
2276
- const abs = isAbsolute2(ref) ? ref : join12(jobDir, ref);
2277
- if (!existsSync10(abs)) {
2210
+ const abs = isAbsolute2(ref) ? ref : join9(jobDir, ref);
2211
+ if (!existsSync8(abs)) {
2278
2212
  throw new Error(
2279
2213
  `asset not found: ${ref} (expected at ${abs}) \u2014 asset refs are job-dir-relative paths; place the file before pushing`
2280
2214
  );
@@ -2282,9 +2216,9 @@ async function syncAssets2(client, jobDir, plan) {
2282
2216
  paths.set(ref, abs);
2283
2217
  }
2284
2218
  let cache = {};
2285
- if (existsSync10(assetsCachePath2(jobDir))) {
2219
+ if (existsSync8(assetsCachePath2(jobDir))) {
2286
2220
  try {
2287
- const raw = JSON.parse(readFileSync9(assetsCachePath2(jobDir), "utf8"));
2221
+ const raw = JSON.parse(readFileSync7(assetsCachePath2(jobDir), "utf8"));
2288
2222
  for (const [ref, v] of Object.entries(raw ?? {})) {
2289
2223
  const e = v;
2290
2224
  if (e && typeof e.sha256 === "string" && typeof e.asset_id === "string") {
@@ -2298,7 +2232,7 @@ async function syncAssets2(client, jobDir, plan) {
2298
2232
  const manifest = {};
2299
2233
  let dirty = false;
2300
2234
  for (const ref of refs) {
2301
- const bytes = readFileSync9(paths.get(ref));
2235
+ const bytes = readFileSync7(paths.get(ref));
2302
2236
  const sha256 = "sha256:" + createHash2("sha256").update(bytes).digest("hex");
2303
2237
  const hit = cache[ref];
2304
2238
  if (hit !== void 0 && hit.sha256 === sha256) {
@@ -2313,8 +2247,8 @@ async function syncAssets2(client, jobDir, plan) {
2313
2247
  dirty = true;
2314
2248
  }
2315
2249
  if (dirty) {
2316
- mkdirSync9(reviewDir2(jobDir), { recursive: true });
2317
- writeFileSync8(assetsCachePath2(jobDir), JSON.stringify(cache, null, 2) + "\n", "utf8");
2250
+ mkdirSync6(reviewDir2(jobDir), { recursive: true });
2251
+ writeFileSync5(assetsCachePath2(jobDir), JSON.stringify(cache, null, 2) + "\n", "utf8");
2318
2252
  }
2319
2253
  return manifest;
2320
2254
  }
@@ -2347,10 +2281,10 @@ async function pushTimeline2(client, jobDir, runId) {
2347
2281
  return { ok: true, plan: compiled.plan, timelineHash: compiled.timelineHash, assets };
2348
2282
  }
2349
2283
  function readOutcomesFile2(path) {
2350
- if (!existsSync10(path)) throw new Error(`outcomes file not found: ${path}`);
2284
+ if (!existsSync8(path)) throw new Error(`outcomes file not found: ${path}`);
2351
2285
  let raw;
2352
2286
  try {
2353
- raw = JSON.parse(readFileSync9(path, "utf8"));
2287
+ raw = JSON.parse(readFileSync7(path, "utf8"));
2354
2288
  } catch (err) {
2355
2289
  throw new Error(`outcomes file is not valid JSON: ${path}: ${err instanceof Error ? err.message : String(err)}`);
2356
2290
  }
@@ -2387,16 +2321,16 @@ async function pushComplete2(client, jobDir, runId, opts) {
2387
2321
  }
2388
2322
 
2389
2323
  // src/public/config.ts
2390
- import { existsSync as existsSync11, mkdirSync as mkdirSync10, readFileSync as readFileSync10, writeFileSync as writeFileSync9 } from "fs";
2324
+ import { existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
2391
2325
  import { homedir as homedir2 } from "os";
2392
- import { join as join13 } from "path";
2326
+ import { join as join10 } from "path";
2393
2327
  function publicConfigDir() {
2394
2328
  const xdg = process.env.XDG_CONFIG_HOME;
2395
- const base = xdg && xdg.trim() ? xdg : join13(homedir2(), ".config");
2396
- return join13(base, "video-editing");
2329
+ const base = xdg && xdg.trim() ? xdg : join10(homedir2(), ".config");
2330
+ return join10(base, "video-editing");
2397
2331
  }
2398
2332
  function publicConfigEnvPath() {
2399
- return join13(publicConfigDir(), ".env");
2333
+ return join10(publicConfigDir(), ".env");
2400
2334
  }
2401
2335
  function parseEnvLines(text) {
2402
2336
  return text.split(/\r?\n/).map((raw) => {
@@ -2412,10 +2346,10 @@ function parseEnvLines(text) {
2412
2346
  var MANAGED_KEYS = ["CLIPREADY_API_KEY", "CLIPREADY_API_BASE"];
2413
2347
  function loadPublicConfigEnv(env = process.env) {
2414
2348
  const path = publicConfigEnvPath();
2415
- if (!existsSync11(path)) return;
2349
+ if (!existsSync9(path)) return;
2416
2350
  let text;
2417
2351
  try {
2418
- text = readFileSync10(path, "utf8");
2352
+ text = readFileSync8(path, "utf8");
2419
2353
  } catch {
2420
2354
  return;
2421
2355
  }
@@ -2425,22 +2359,31 @@ function loadPublicConfigEnv(env = process.env) {
2425
2359
  if (!env[line.key] || !String(env[line.key]).trim()) env[line.key] = line.value;
2426
2360
  }
2427
2361
  }
2362
+ function removePublicConfigKey(key) {
2363
+ const path = publicConfigEnvPath();
2364
+ if (!existsSync9(path)) return path;
2365
+ const lines = readFileSync8(path, "utf8").split(/\r?\n/).filter((l) => l.trim() !== "").filter((l) => {
2366
+ const eq = l.indexOf("=");
2367
+ return eq < 0 || l.slice(0, eq).trim() !== key;
2368
+ });
2369
+ writeFileSync6(path, lines.length ? lines.join("\n") + "\n" : "", { mode: 384 });
2370
+ return path;
2371
+ }
2428
2372
  function savePublicConfigKey(key, value) {
2429
- mkdirSync10(publicConfigDir(), { recursive: true });
2373
+ mkdirSync7(publicConfigDir(), { recursive: true });
2430
2374
  const path = publicConfigEnvPath();
2431
- const existing = existsSync11(path) ? readFileSync10(path, "utf8") : "";
2375
+ const existing = existsSync9(path) ? readFileSync8(path, "utf8") : "";
2432
2376
  const lines = existing.split(/\r?\n/).filter((l) => l.trim() !== "");
2433
2377
  const kept = lines.filter((l) => {
2434
2378
  const eq = l.indexOf("=");
2435
2379
  return eq < 0 || l.slice(0, eq).trim() !== key;
2436
2380
  });
2437
2381
  kept.push(`${key}=${value}`);
2438
- writeFileSync9(path, kept.join("\n") + "\n", { mode: 384 });
2382
+ writeFileSync6(path, kept.join("\n") + "\n", { mode: 384 });
2439
2383
  return path;
2440
2384
  }
2441
2385
 
2442
2386
  export {
2443
- requireFfmpeg,
2444
2387
  DEFAULT_TIMEOUT_MS,
2445
2388
  DEFAULT_DOWNLOAD_TIMEOUT_MS,
2446
2389
  resolveApiBase,
@@ -2455,9 +2398,13 @@ export {
2455
2398
  meUrl,
2456
2399
  projectsUrl,
2457
2400
  projectVideosUrl,
2401
+ webVideoUrl,
2458
2402
  runUrl,
2403
+ multipartUrl,
2404
+ multipartRelayUrl,
2459
2405
  filesUrl,
2460
2406
  transcribeUrl,
2407
+ ingestUrl,
2461
2408
  compileUrl,
2462
2409
  resolveUrl,
2463
2410
  verifyUrl,
@@ -2483,7 +2430,13 @@ export {
2483
2430
  writeJobFile,
2484
2431
  readJobFile,
2485
2432
  videoIdFromJob,
2486
- initCloudJob,
2433
+ MULTIPART_THRESHOLD_BYTES,
2434
+ RELAY_DIRECT_ATTEMPTS,
2435
+ RELAY_SUBCHUNK_BYTES,
2436
+ shouldMultipart,
2437
+ bytesSource,
2438
+ openFileSource,
2439
+ uploadMultipart,
2487
2440
  INLINE_MAX_BYTES,
2488
2441
  sha256Hex,
2489
2442
  sameSha,
@@ -2492,18 +2445,18 @@ export {
2492
2445
  vfsContentType,
2493
2446
  pullFiles,
2494
2447
  pushFiles,
2448
+ uploadVfsPayload,
2495
2449
  uploadVfsBytes,
2496
2450
  MAX_RUN_POLL_MS,
2497
2451
  formatRunEvent,
2498
2452
  pollRun,
2499
2453
  assertRunCompleted,
2454
+ sanitizeStem,
2455
+ ingestArtifactSet,
2456
+ readProbeDuration,
2457
+ initCloudJob,
2500
2458
  transcribeArtifactSet,
2501
- extractGuardedWav,
2502
2459
  transcribeCloud,
2503
- DEFAULT_DESILENCE,
2504
- silenceDetectArgs,
2505
- parseSilenceDetect,
2506
- detectSilences,
2507
2460
  CAPTION_STYLE_DEFAULTS,
2508
2461
  timelineV2Schema,
2509
2462
  emptyDiagnostics,
@@ -2517,18 +2470,18 @@ export {
2517
2470
  diagnosticsPath2,
2518
2471
  loadTimeline2,
2519
2472
  validateLocal,
2520
- detectSilencesForDoc,
2521
2473
  statAssetsForDoc,
2522
2474
  compileRemote,
2523
2475
  verifyCloud,
2524
- buildMediaEntry,
2525
- composeCloud,
2526
- qaFramesArgs,
2527
- qaWaveformArgs,
2528
- qaWordsForWindow,
2529
- findLocalTranscript,
2530
- qaFramesLocal,
2531
- qaWaveformLocal,
2476
+ qaDir,
2477
+ framesPngPath,
2478
+ waveformPngPath,
2479
+ inspectPngPath,
2480
+ waveformWordsPath,
2481
+ readPlanSegments,
2482
+ joinSourceWindows,
2483
+ joinSpanWindow,
2484
+ outWindowToSourceWindows,
2532
2485
  reviewDir2,
2533
2486
  roundJsonPath2,
2534
2487
  instructionPath2,
@@ -2545,5 +2498,6 @@ export {
2545
2498
  publicConfigDir,
2546
2499
  publicConfigEnvPath,
2547
2500
  loadPublicConfigEnv,
2501
+ removePublicConfigKey,
2548
2502
  savePublicConfigKey
2549
2503
  };