video-editing 2.0.0 → 3.0.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
  }
@@ -279,6 +175,18 @@ function parseQaWaveform(data) {
279
175
  }) : [];
280
176
  return { url: o.url, words };
281
177
  }
178
+ function parseQaInspect(data) {
179
+ const o = asRecord(data, "qa-inspect");
180
+ if (typeof o.url !== "string") throw new Error(`qa inspect response missing url: ${JSON.stringify(data)}`);
181
+ 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) })) : [];
182
+ return {
183
+ url: o.url,
184
+ tStart: Number(o.tStart ?? 0),
185
+ tEnd: Number(o.tEnd ?? 0),
186
+ cuts,
187
+ cached: o.cached === true
188
+ };
189
+ }
282
190
  function parseFileList(data) {
283
191
  const o = asRecord(data, "file-list");
284
192
  const arr = o.files;
@@ -411,7 +319,10 @@ function extractErrorMessage(data) {
411
319
  }
412
320
  return void 0;
413
321
  }
414
- var CloudClient = class {
322
+ function backoffDelayMs(attempt) {
323
+ return 500 * 2 ** (attempt - 1);
324
+ }
325
+ var CloudClient = class _CloudClient {
415
326
  constructor(cfg, opts = {}) {
416
327
  this.cfg = cfg;
417
328
  this.fetchImpl = opts.fetchImpl ?? globalThis.fetch;
@@ -425,6 +336,61 @@ var CloudClient = class {
425
336
  if (!this.cfg.videoId) throw new Error("missing video id \u2014 pass --video-id or set VIDEO_ID");
426
337
  return this.cfg.videoId;
427
338
  }
339
+ /** Same client (same fetch/timeout) bound to a video id — for flows that
340
+ * create the video mid-run (init). */
341
+ withVideoId(videoId) {
342
+ return new _CloudClient({ ...this.cfg, videoId }, { fetchImpl: this.fetchImpl, timeoutMs: this.timeoutMs });
343
+ }
344
+ /** The injected fetch — for helpers outside the class (multipart part PUTs)
345
+ * that need raw Response access (e.g. the ETag header). */
346
+ get fetch() {
347
+ return this.fetchImpl;
348
+ }
349
+ /** POST a JSON body to an absolute API URL with auth — public wrapper over
350
+ * the private transport for helpers living outside the class (multipart). */
351
+ async postJson(url, body) {
352
+ return this.request("POST", url, body);
353
+ }
354
+ /** POST raw binary bytes to an authenticated API URL (multipart relay
355
+ * sub-chunks). Content-Type application/octet-stream; JSON response. */
356
+ async postBinary(url, bytes, timeoutMs = DEFAULT_DOWNLOAD_TIMEOUT_MS) {
357
+ const ac = new AbortController();
358
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
359
+ try {
360
+ const res = await this.fetchImpl(url, {
361
+ method: "POST",
362
+ headers: {
363
+ Authorization: `Bearer ${this.cfg.credential}`,
364
+ Accept: "application/json",
365
+ "Content-Type": "application/octet-stream"
366
+ },
367
+ body: bytes,
368
+ signal: ac.signal
369
+ });
370
+ const text = await res.text();
371
+ let data = void 0;
372
+ if (text) {
373
+ try {
374
+ data = JSON.parse(text);
375
+ } catch {
376
+ data = text;
377
+ }
378
+ }
379
+ if (!res.ok) {
380
+ const detail = extractErrorMessage(data) ?? `${res.status} ${res.statusText}`;
381
+ throw new CloudHttpError(res.status, `POST ${url} failed: ${detail}`, data);
382
+ }
383
+ return data;
384
+ } catch (err) {
385
+ if (err instanceof CloudHttpError) throw err;
386
+ if (err instanceof Error && err.name === "AbortError") {
387
+ throw new Error(`request timed out after ${timeoutMs}ms: POST ${url}`);
388
+ }
389
+ throw err;
390
+ } finally {
391
+ clearTimeout(timer);
392
+ }
393
+ }
428
394
  async request(method, url, body) {
429
395
  const ac = new AbortController();
430
396
  const timer = setTimeout(() => ac.abort(), this.timeoutMs);
@@ -491,6 +457,12 @@ var CloudClient = class {
491
457
  });
492
458
  return parseQaWaveform(data);
493
459
  }
460
+ async qaInspect(args) {
461
+ const body = { kind: "inspect", start: args.start, end: args.end };
462
+ if (args.nFrames !== void 0) body.n_frames = args.nFrames;
463
+ const data = await this.request("POST", qaUrl(this.cfg.base, this.requireVideoId()), body);
464
+ return parseQaInspect(data);
465
+ }
494
466
  // ---- artifacts ----------------------------------------------------------
495
467
  async listArtifacts() {
496
468
  const data = await this.request("GET", artifactsUrl(this.cfg.base, this.requireVideoId()));
@@ -512,28 +484,45 @@ var CloudClient = class {
512
484
  return parseAssetCreate(data);
513
485
  }
514
486
  /** 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}`);
487
+ * signed URL carries its own credentials and may be a different host.
488
+ * Mid-stream network failures (connection resets, TLS record errors) and
489
+ * 5xx responses are retried with exponential backoff — the whole body is
490
+ * an in-memory buffer, so every attempt is a clean full replay. */
491
+ async upload(url, bytes, contentType, timeoutMs = DEFAULT_DOWNLOAD_TIMEOUT_MS, maxAttempts = 5) {
492
+ let lastErr;
493
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
494
+ const ac = new AbortController();
495
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
496
+ try {
497
+ const res = await this.fetchImpl(url, {
498
+ method: "PUT",
499
+ headers: { "Content-Type": contentType },
500
+ body: bytes,
501
+ signal: ac.signal
502
+ });
503
+ if (res.ok) return;
504
+ const failure = new Error(`asset upload failed: HTTP ${res.status} ${res.statusText} for ${url}`);
505
+ if (res.status < 500 || attempt === maxAttempts) throw failure;
506
+ lastErr = failure;
507
+ } catch (err) {
508
+ if (err instanceof Error && err.name === "AbortError") {
509
+ throw new Error(`asset upload timed out after ${timeoutMs}ms: ${url}`);
510
+ }
511
+ const cause = err.cause;
512
+ const detail = cause?.code ?? cause?.message;
513
+ const network = err instanceof TypeError;
514
+ if (!network || attempt === maxAttempts) {
515
+ throw err instanceof TypeError ? new Error(
516
+ `upload network failure after ${attempt} attempt${attempt === 1 ? "" : "s"}${detail ? ` (${detail})` : ""} PUTting ${bytes.length} bytes to ${new URL(url).host}`
517
+ ) : err;
518
+ }
519
+ lastErr = err;
520
+ } finally {
521
+ clearTimeout(timer);
532
522
  }
533
- throw err;
534
- } finally {
535
- clearTimeout(timer);
523
+ await new Promise((r) => setTimeout(r, backoffDelayMs(attempt)));
536
524
  }
525
+ throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
537
526
  }
538
527
  // ---- review session -----------------------------------------------------
539
528
  async getReview() {
@@ -641,8 +630,22 @@ var CloudClient = class {
641
630
  }
642
631
  return { runId: o.run_id };
643
632
  }
644
- /** POST /videos/{id}/verify → 202 {run_id}. */
645
- async verifyStart(body) {
633
+ /** POST /videos/{id}/ingest {stem?} → 202 {run_id}. Server-side probe + audio
634
+ * extraction + transcription + screening over the uploaded source. The stem
635
+ * names every derived artifact (transcripts/<stem>.json, audio/<stem>.wav) —
636
+ * omit it and the server defaults to "source". */
637
+ async ingestStart(stem) {
638
+ const body = stem !== void 0 && stem !== "" ? { stem } : {};
639
+ const data = await this.request("POST", ingestUrl(this.cfg.base, this.requireVideoId()), body);
640
+ const o = asRecord(data, "ingest");
641
+ if (typeof o.run_id !== "string" || o.run_id === "") {
642
+ throw new Error(`ingest response missing run_id: ${JSON.stringify(data)}`);
643
+ }
644
+ return { runId: o.run_id };
645
+ }
646
+ /** POST /videos/{id}/verify {mode?, window_s?} → 202 {run_id}. The server
647
+ * sources the preview audio itself — nothing is uploaded. */
648
+ async verifyStart(body = {}) {
646
649
  const data = await this.request("POST", verifyUrl(this.cfg.base, this.requireVideoId()), body);
647
650
  const o = asRecord(data, "verify");
648
651
  if (typeof o.run_id !== "string" || o.run_id === "") {
@@ -686,14 +689,14 @@ async function downloadTo(url, dest, opts = {}) {
686
689
  try {
687
690
  const res = await f(url, { signal: ac.signal });
688
691
  if (!res.ok) throw new Error(`download failed: HTTP ${res.status} ${res.statusText} for ${url}`);
689
- mkdirSync2(dirname2(dest), { recursive: true });
692
+ mkdirSync(dirname(dest), { recursive: true });
690
693
  const body = res.body;
691
694
  if (!body) {
692
695
  const buf = Buffer.from(await res.arrayBuffer());
693
696
  writeFileSync(dest, buf);
694
697
  return { path: dest, bytes: buf.length };
695
698
  }
696
- await pipeline(Readable.fromWeb(body), createWriteStream2(dest));
699
+ await pipeline(Readable.fromWeb(body), createWriteStream(dest));
697
700
  return { path: dest, bytes: statSync(dest).size };
698
701
  } catch (err) {
699
702
  if (err instanceof Error && err.name === "AbortError") {
@@ -733,10 +736,10 @@ async function pollRender(client, renderId, opts = {}) {
733
736
  // src/cloud/skill.ts
734
737
  import { cpSync, existsSync, readdirSync, rmSync, statSync as statSync2 } from "fs";
735
738
  import { homedir } from "os";
736
- import { dirname as dirname3, join as join2, relative, resolve } from "path";
739
+ import { dirname as dirname2, join as join2, relative, resolve } from "path";
737
740
  import { fileURLToPath } from "url";
738
741
  function bundledSkillDir() {
739
- const here = dirname3(fileURLToPath(import.meta.url));
742
+ const here = dirname2(fileURLToPath(import.meta.url));
740
743
  const candidates = [
741
744
  join2(here, "skills", "video-editing"),
742
745
  join2(here, "..", "skills", "video-editing"),
@@ -770,13 +773,13 @@ function installSkill(opts = {}) {
770
773
  }
771
774
 
772
775
  // src/cloud/jobfile.ts
773
- import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync, writeFileSync as writeFileSync2 } from "fs";
776
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync as writeFileSync2 } from "fs";
774
777
  import { join as join3 } from "path";
775
778
  function jobJsonPath(jobDir) {
776
779
  return join3(jobDir, "job.json");
777
780
  }
778
781
  function writeJobFile(jobDir, job) {
779
- mkdirSync3(jobDir, { recursive: true });
782
+ mkdirSync2(jobDir, { recursive: true });
780
783
  const path = jobJsonPath(jobDir);
781
784
  writeFileSync2(path, JSON.stringify(job, null, 2) + "\n", "utf8");
782
785
  return path;
@@ -792,7 +795,8 @@ function readJobFile(jobDir) {
792
795
  project_id: typeof raw.project_id === "string" ? raw.project_id : "",
793
796
  api_base: typeof raw.api_base === "string" ? raw.api_base : "",
794
797
  stem: typeof raw.stem === "string" ? raw.stem : "source",
795
- duration_s: typeof raw.duration_s === "number" ? raw.duration_s : 0
798
+ duration_s: typeof raw.duration_s === "number" ? raw.duration_s : 0,
799
+ ...typeof raw.source_path === "string" && raw.source_path !== "" ? { source_path: raw.source_path } : {}
796
800
  };
797
801
  } catch {
798
802
  return null;
@@ -805,184 +809,276 @@ function videoIdFromJob(jobDir, resolved) {
805
809
  throw new Error(`missing video id \u2014 pass --video-id, set VIDEO_ID, or run \`init\` first (no ${jobJsonPath(jobDir)})`);
806
810
  }
807
811
 
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;
812
+ // src/cloud/multipart.ts
813
+ import { open } from "fs/promises";
814
+ async function runPool(tasks, concurrency) {
815
+ let next = 0;
816
+ const workers = Array.from({ length: Math.min(concurrency, tasks.length) }, async () => {
817
+ while (next < tasks.length) {
818
+ const task = tasks[next++];
819
+ if (task) await task();
823
820
  }
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;
821
+ });
822
+ await Promise.all(workers);
823
+ }
824
+ var MULTIPART_THRESHOLD_BYTES = 8 * 1024 * 1024;
825
+ var PART_CONCURRENCY = 3;
826
+ var PART_MAX_ATTEMPTS = 8;
827
+ var RELAY_DIRECT_ATTEMPTS = 3;
828
+ var RELAY_SUBCHUNK_BYTES = 3670016;
829
+ function jitteredBackoffMs(attempt) {
830
+ const base = Math.min(500 * 2 ** (attempt - 1), 8e3);
831
+ return Math.round(base / 2 + Math.random() * (base / 2));
832
+ }
833
+ function shouldMultipart(sizeOrBytes) {
834
+ const size = typeof sizeOrBytes === "number" ? sizeOrBytes : sizeOrBytes.length;
835
+ return size > MULTIPART_THRESHOLD_BYTES;
836
+ }
837
+ function bytesSource(bytes) {
838
+ return {
839
+ size: bytes.length,
840
+ readSlice: async (offset, len) => Buffer.from(bytes.subarray(offset, Math.min(offset + len, bytes.length)))
841
+ };
842
+ }
843
+ async function openFileSource(path) {
844
+ const fh = await open(path, "r");
845
+ const size = (await fh.stat()).size;
846
+ return {
847
+ size,
848
+ async readSlice(offset, len) {
849
+ const want = Math.min(len, Math.max(0, size - offset));
850
+ const buf = Buffer.allocUnsafe(want);
851
+ let done = 0;
852
+ while (done < want) {
853
+ const { bytesRead } = await fh.read(buf, done, want - done, offset + done);
854
+ if (bytesRead <= 0) throw new Error(`short read at byte ${offset + done} of ${path}`);
855
+ done += bytesRead;
856
+ }
857
+ return buf;
858
+ },
859
+ close: () => fh.close()
860
+ };
861
+ }
862
+ function asRecord2(data, ctx) {
863
+ if (data === null || typeof data !== "object") throw new Error(`unexpected ${ctx} response: ${JSON.stringify(data)}`);
864
+ return data;
865
+ }
866
+ function parsePartUrls(data, ctx) {
867
+ const o = asRecord2(data, ctx);
868
+ if (!Array.isArray(o.parts)) throw new Error(`${ctx} response missing parts[]: ${JSON.stringify(data)}`);
869
+ 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 !== "");
831
870
  }
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];
871
+ function parseMultipartCreate(data) {
872
+ const o = asRecord2(data, "multipart-create");
873
+ const uploadId = o.upload_id;
874
+ if (typeof uploadId !== "string" || uploadId === "") {
875
+ throw new Error(`multipart create response missing upload_id: ${JSON.stringify(data)}`);
838
876
  }
839
- return [width, height, rotation];
877
+ const partSize = Number(o.part_size ?? 0);
878
+ if (!(partSize > 0)) throw new Error(`multipart create response missing part_size: ${JSON.stringify(data)}`);
879
+ return {
880
+ uploadId,
881
+ assetId: typeof o.asset_id === "string" && o.asset_id !== "" ? o.asset_id : void 0,
882
+ partSize,
883
+ parts: parsePartUrls(data, "multipart-create")
884
+ };
840
885
  }
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);
886
+ function isMultipartUnavailable(err) {
887
+ if (!(err instanceof CloudHttpError)) return false;
888
+ if (err.status === 404 || err.status === 405) return true;
889
+ if (err.status !== 503) return false;
890
+ const body = err.body;
891
+ return typeof body === "object" && body !== null && typeof body.error === "object" && body.error !== null ? body.error.code === "multipart_unavailable" : false;
892
+ }
893
+ function stripEtagQuotes(etag) {
894
+ return etag.trim().replace(/^"+|"+$/g, "");
895
+ }
896
+ function fmtMb(n) {
897
+ return `${(n / (1024 * 1024)).toFixed(1)} MB`;
898
+ }
899
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
900
+ async function putPart(client, url, chunk, timeoutMs) {
901
+ const ac = new AbortController();
902
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
903
+ try {
904
+ return await client.fetch(url, { method: "PUT", body: chunk, signal: ac.signal });
905
+ } finally {
906
+ clearTimeout(timer);
860
907
  }
861
- let s = (rounded / m).toFixed(prec);
862
- if (neg && Number(s) !== 0) s = "-" + s;
863
- return s;
864
908
  }
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;
909
+ async function refreshPartUrl(client, uploadId, n) {
910
+ const data = await client.postJson(multipartUrl(client.cfg.base, "parts"), { upload_id: uploadId, ns: [n] });
911
+ const url = parsePartUrls(data, "multipart-parts").find((p) => p.n === n)?.url;
912
+ if (!url) throw new Error(`multipart parts refresh did not return a URL for part ${n}: ${JSON.stringify(data)}`);
913
+ return url;
924
914
  }
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";
915
+ async function relayOnePart(client, uploadId, n, chunk) {
916
+ const of = Math.max(1, Math.ceil(chunk.length / RELAY_SUBCHUNK_BYTES));
917
+ for (let i = 1; i <= of; i++) {
918
+ const sub = chunk.subarray((i - 1) * RELAY_SUBCHUNK_BYTES, Math.min(i * RELAY_SUBCHUNK_BYTES, chunk.length));
919
+ const data = await client.postBinary(multipartRelayUrl(client.cfg.base, uploadId, n, i, of), sub);
920
+ const o = data;
921
+ if (!o || o.ok !== true) {
922
+ throw new Error(`relay sub-chunk ${i}/${of} of part ${n} did not return {ok:true}: ${JSON.stringify(data)}`);
923
+ }
924
+ }
925
+ const done = asRecord2(
926
+ await client.postJson(multipartUrl(client.cfg.base, "relay-complete"), { upload_id: uploadId, n }),
927
+ "relay-complete"
928
+ );
929
+ if (typeof done.etag !== "string" || done.etag === "") {
930
+ throw new Error(`relay-complete for part ${n} returned no etag: ${JSON.stringify(done)}`);
931
+ }
932
+ return stripEtagQuotes(done.etag);
933
+ }
934
+ function isRelayUnavailable(err) {
935
+ return err instanceof CloudHttpError && (err.status === 404 || err.status === 405);
936
+ }
937
+ function transportDetail(err) {
938
+ const cause = err.cause;
939
+ return cause?.code ?? cause?.message;
940
+ }
941
+ async function uploadOnePart(client, uploadId, n, chunk, initialUrl, timeoutMs, backoffMs, diag) {
942
+ let url = initialUrl;
943
+ let lastErr;
944
+ let transportFailures = 0;
945
+ let relayUnavailable = false;
946
+ for (let attempt = 1; attempt <= PART_MAX_ATTEMPTS; attempt++) {
947
+ const useRelay = !relayUnavailable && transportFailures >= RELAY_DIRECT_ATTEMPTS;
948
+ const mode = useRelay ? "relay" : "direct";
949
+ const t0 = Date.now();
950
+ const report = (error) => {
951
+ if (!error && mode === "direct") return;
952
+ diag?.(
953
+ `part ${n} attempt ${attempt} mode ${mode} bytes ${chunk.length} elapsed ${Date.now() - t0}ms` + (error ? ` error ${error}` : " ok")
954
+ );
955
+ };
956
+ if (useRelay) {
957
+ try {
958
+ const etag = await relayOnePart(client, uploadId, n, chunk);
959
+ report();
960
+ return etag;
961
+ } catch (err) {
962
+ if (isRelayUnavailable(err)) {
963
+ relayUnavailable = true;
964
+ report("relay_unavailable");
965
+ } else {
966
+ lastErr = err instanceof Error ? err : new Error(String(err));
967
+ report(err instanceof CloudHttpError ? `http_${err.status}` : lastErr.message.slice(0, 80));
968
+ }
969
+ }
970
+ } else {
971
+ try {
972
+ if (!url) url = await refreshPartUrl(client, uploadId, n);
973
+ const res = await putPart(client, url, chunk, timeoutMs);
974
+ if (res.ok) {
975
+ const etag = res.headers.get("etag");
976
+ if (!etag) throw new Error(`part ${n} uploaded but the response had no ETag header \u2014 cannot complete the multipart upload`);
977
+ report();
978
+ return stripEtagQuotes(etag);
979
+ }
980
+ lastErr = new Error(`part ${n}: HTTP ${res.status} ${res.statusText}`);
981
+ report(`http_${res.status}`);
982
+ if (res.status >= 400 && res.status < 500) url = void 0;
983
+ } catch (err) {
984
+ if (err instanceof Error && err.name === "AbortError") {
985
+ lastErr = new Error(`part ${n}: timed out after ${timeoutMs}ms`);
986
+ transportFailures += 1;
987
+ report("timeout");
988
+ } else if (err instanceof TypeError) {
989
+ const detail = transportDetail(err);
990
+ lastErr = new Error(`part ${n}: network failure${detail ? ` (${detail})` : ""}`);
991
+ transportFailures += 1;
992
+ report(detail ?? "fetch_failed");
993
+ } else {
994
+ throw err;
995
+ }
996
+ }
997
+ }
998
+ if (attempt < PART_MAX_ATTEMPTS) await sleep2(backoffMs(attempt));
999
+ }
1000
+ throw new Error(
1001
+ `multipart upload failed at part ${n} (${chunk.length} bytes) after ${PART_MAX_ATTEMPTS} attempts: ${lastErr?.message ?? "unknown error"}`
1002
+ );
931
1003
  }
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");
940
- };
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
1004
+ async function uploadMultipart(client, opts, timeoutMs = DEFAULT_DOWNLOAD_TIMEOUT_MS) {
1005
+ const source = opts.source ?? (opts.bytes !== void 0 ? bytesSource(opts.bytes) : void 0);
1006
+ if (!source) throw new Error("uploadMultipart: pass exactly one of bytes or source");
1007
+ const createBody = {
1008
+ kind: opts.kind,
1009
+ size: source.size,
1010
+ content_type: opts.contentType
968
1011
  };
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
1012
+ if (opts.filename !== void 0) createBody.filename = opts.filename;
1013
+ if (opts.videoId !== void 0) createBody.video_id = opts.videoId;
1014
+ if (opts.path !== void 0) createBody.path = opts.path;
1015
+ let session;
1016
+ try {
1017
+ session = parseMultipartCreate(await client.postJson(multipartUrl(client.cfg.base), createBody));
1018
+ } catch (err) {
1019
+ if (isMultipartUnavailable(err)) return "unavailable";
1020
+ throw err;
1021
+ }
1022
+ const abort = async () => {
1023
+ try {
1024
+ await client.postJson(multipartUrl(client.cfg.base, "abort"), { upload_id: session.uploadId });
1025
+ } catch {
1026
+ }
979
1027
  };
1028
+ if (opts.kind === "asset" && !session.assetId) {
1029
+ await abort();
1030
+ throw new Error("multipart create (kind asset) returned no asset_id \u2014 cannot create the video afterwards");
1031
+ }
1032
+ const total = Math.max(1, Math.ceil(source.size / session.partSize));
1033
+ const urls = new Map(session.parts.map((p) => [p.n, p.url]));
1034
+ const marks = new Set([1, 2, 3, 4].map((k) => Math.ceil(total * k / 4)));
1035
+ const etags = new Array(total);
1036
+ let doneParts = 0;
1037
+ let doneBytes = 0;
1038
+ try {
1039
+ await runPool(
1040
+ Array.from({ length: total }, (_, i) => async () => {
1041
+ const n = i + 1;
1042
+ const offset = i * session.partSize;
1043
+ const chunk = await source.readSlice(offset, session.partSize);
1044
+ etags[i] = await uploadOnePart(
1045
+ client,
1046
+ session.uploadId,
1047
+ n,
1048
+ chunk,
1049
+ urls.get(n),
1050
+ timeoutMs,
1051
+ opts.backoffMs ?? jitteredBackoffMs,
1052
+ opts.log
1053
+ );
1054
+ doneParts += 1;
1055
+ doneBytes += chunk.length;
1056
+ if (opts.log && marks.has(doneParts)) {
1057
+ opts.log(`upload: ${doneParts}/${total} parts (${fmtMb(doneBytes)} / ${fmtMb(source.size)})`);
1058
+ }
1059
+ }),
1060
+ PART_CONCURRENCY
1061
+ );
1062
+ const completeBody = {
1063
+ upload_id: session.uploadId,
1064
+ parts: etags.map((etag, i) => ({ n: i + 1, etag }))
1065
+ };
1066
+ if (opts.kind === "jobfile") {
1067
+ if (opts.sha256 !== void 0) completeBody.sha256 = opts.sha256;
1068
+ completeBody.bytes = source.size;
1069
+ }
1070
+ await client.postJson(multipartUrl(client.cfg.base, "complete"), completeBody);
1071
+ } catch (err) {
1072
+ await abort();
1073
+ throw err;
1074
+ }
1075
+ return opts.kind === "asset" ? { assetId: session.assetId } : {};
980
1076
  }
981
1077
 
982
1078
  // src/cloud/files.ts
983
1079
  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";
1080
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync2, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
1081
+ import { dirname as dirname3, join as join4, sep } from "path";
986
1082
  var INLINE_MAX_BYTES = 5 * 1024 * 1024;
987
1083
  function sha256Hex(bytes) {
988
1084
  return createHash("sha256").update(bytes).digest("hex");
@@ -995,10 +1091,10 @@ function sameSha(a, b) {
995
1091
  function vfsLocalPath(jobDir, path) {
996
1092
  const parts = path.split("/").filter((p) => p !== "" && p !== "." && p !== "..");
997
1093
  if (parts.length === 0) throw new Error(`bad VFS path: ${JSON.stringify(path)}`);
998
- return join6(jobDir, ...parts);
1094
+ return join4(jobDir, ...parts);
999
1095
  }
1000
1096
  function toVfsPath(jobDir, localPath) {
1001
- const abs = localPath.startsWith(sep) ? localPath : join6(jobDir, localPath);
1097
+ const abs = localPath.startsWith(sep) ? localPath : join4(jobDir, localPath);
1002
1098
  const rel = abs.startsWith(jobDir + sep) ? abs.slice(jobDir.length + 1) : localPath;
1003
1099
  return rel.split(sep).join("/");
1004
1100
  }
@@ -1036,18 +1132,18 @@ async function pullFiles(client, jobDir, opts = {}) {
1036
1132
  const entry = byPath.get(path);
1037
1133
  if (!entry) continue;
1038
1134
  const dest = vfsLocalPath(jobDir, path);
1039
- if (existsSync5(dest) && sameSha(sha256Hex(readFileSync3(dest)), entry.sha256)) {
1135
+ if (existsSync3(dest) && sameSha(sha256Hex(readFileSync2(dest)), entry.sha256)) {
1040
1136
  const f2 = { path, localPath: dest, bytes: statSync3(dest).size, skipped: true };
1041
1137
  pulled.push(f2);
1042
1138
  opts.onFile?.(f2);
1043
1139
  continue;
1044
1140
  }
1045
1141
  const got = await client.filesGet(path);
1046
- mkdirSync5(dirname4(dest), { recursive: true });
1142
+ mkdirSync3(dirname3(dest), { recursive: true });
1047
1143
  let bytes;
1048
1144
  if (got.content !== void 0) {
1049
1145
  const buf = Buffer.from(got.content, "utf8");
1050
- writeFileSync4(dest, buf);
1146
+ writeFileSync3(dest, buf);
1051
1147
  bytes = buf.length;
1052
1148
  if (got.sha256 && !sameSha(sha256Hex(buf), got.sha256)) {
1053
1149
  throw new Error(`pulled ${path} but sha256 mismatch vs server (${got.sha256}) \u2014 refusing to trust the copy`);
@@ -1055,7 +1151,7 @@ async function pullFiles(client, jobDir, opts = {}) {
1055
1151
  } else {
1056
1152
  const dl = await client.download(got.downloadUrl, dest);
1057
1153
  bytes = dl.bytes;
1058
- if (got.sha256 && !sameSha(sha256Hex(readFileSync3(dest)), got.sha256)) {
1154
+ if (got.sha256 && !sameSha(sha256Hex(readFileSync2(dest)), got.sha256)) {
1059
1155
  throw new Error(`pulled ${path} but sha256 mismatch vs server (${got.sha256}) \u2014 refusing to trust the copy`);
1060
1156
  }
1061
1157
  }
@@ -1073,8 +1169,8 @@ async function pushFiles(client, jobDir, paths, opts = {}) {
1073
1169
  for (const rel of paths) {
1074
1170
  const vfsPath = toVfsPath(jobDir, rel);
1075
1171
  const local = vfsLocalPath(jobDir, vfsPath);
1076
- if (!existsSync5(local)) throw new Error(`files push: local file not found: ${local}`);
1077
- const bytes = readFileSync3(local);
1172
+ if (!existsSync3(local)) throw new Error(`files push: local file not found: ${local}`);
1173
+ const bytes = readFileSync2(local);
1078
1174
  const sha = sha256Hex(bytes);
1079
1175
  const existing = byPath.get(vfsPath);
1080
1176
  if (existing && sameSha(sha, existing.sha256)) {
@@ -1094,9 +1190,7 @@ async function pushFiles(client, jobDir, paths, opts = {}) {
1094
1190
  out.push(f);
1095
1191
  opts.onFile?.(f);
1096
1192
  } 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);
1193
+ await uploadVfsPayload(client, vfsPath, bytes, contentType, { sha256: sha, log: opts.log });
1100
1194
  const f = { path: vfsPath, bytes: bytes.length, skipped: false, mode: "presign" };
1101
1195
  out.push(f);
1102
1196
  opts.onFile?.(f);
@@ -1104,20 +1198,32 @@ async function pushFiles(client, jobDir, paths, opts = {}) {
1104
1198
  }
1105
1199
  return out;
1106
1200
  }
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
- });
1201
+ async function uploadVfsPayload(client, vfsPath, bytes, contentType, opts = {}) {
1202
+ const sha = opts.sha256 ?? sha256Hex(bytes);
1203
+ if (shouldMultipart(bytes)) {
1204
+ const res = await uploadMultipart(client, {
1205
+ kind: "jobfile",
1206
+ bytes,
1207
+ contentType,
1208
+ videoId: client.cfg.videoId,
1209
+ path: vfsPath,
1210
+ sha256: sha,
1211
+ log: opts.log
1212
+ });
1213
+ if (res !== "unavailable") return;
1214
+ opts.log?.(" chunked upload unavailable on this server; using single-request upload");
1215
+ }
1216
+ const presign = await client.filesPresign({ path: vfsPath, contentType, bytes: bytes.length, sha256: sha });
1114
1217
  await client.upload(presign.uploadUrl, bytes, contentType);
1115
1218
  await client.filesCommit(vfsPath);
1116
1219
  }
1220
+ async function uploadVfsBytes(client, vfsPath, bytes, contentType, log) {
1221
+ await uploadVfsPayload(client, vfsPath, bytes, contentType, { log });
1222
+ }
1117
1223
 
1118
1224
  // src/cloud/runs.ts
1119
1225
  var MAX_RUN_POLL_MS = 30 * 60 * 1e3;
1120
- var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
1226
+ var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
1121
1227
  function formatRunEvent(ev) {
1122
1228
  const p = ev.payload;
1123
1229
  const message = p !== null && typeof p === "object" && typeof p.message === "string" ? p.message : "";
@@ -1126,7 +1232,7 @@ function formatRunEvent(ev) {
1126
1232
  async function pollRun(client, runId, opts = {}) {
1127
1233
  const intervalMs = opts.intervalMs ?? 5e3;
1128
1234
  const maxMs = opts.maxMs ?? MAX_RUN_POLL_MS;
1129
- const nap = opts.sleepImpl ?? sleep2;
1235
+ const nap = opts.sleepImpl ?? sleep3;
1130
1236
  const now = opts.now ?? Date.now;
1131
1237
  const start = now();
1132
1238
  let seen = 0;
@@ -1147,115 +1253,136 @@ function assertRunCompleted(state, what) {
1147
1253
  throw new Error(`${what} run ${state.id || ""} ${state.status}: ${detail}`);
1148
1254
  }
1149
1255
 
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"];
1256
+ // src/cloud/init.ts
1257
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync3 } from "fs";
1258
+ import { join as join5, parse as parsePath, resolve as resolvePath } from "path";
1259
+ function sanitizeStem(name) {
1260
+ const safe = name.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
1261
+ return safe === "" ? "source" : safe;
1262
+ }
1263
+ function ingestArtifactSet(stem) {
1264
+ return [`transcripts/${stem}.json`, "takes_packed.md", "flags.txt", "word_dump.txt", "coverage.json", "probe.json"];
1156
1265
  }
1157
- async function extractGuardedWav(media) {
1158
- const tmp = mkdtempSync(join7(tmpdir(), "ve-cloud-wav-"));
1266
+ function readProbeDuration(jobDir) {
1267
+ const path = join5(jobDir, "probe.json");
1268
+ if (!existsSync4(path)) throw new Error(`ingest completed but probe.json was not pulled: ${path}`);
1269
+ let raw;
1159
1270
  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 });
1271
+ raw = JSON.parse(readFileSync3(path, "utf8"));
1272
+ } catch (err) {
1273
+ throw new Error(`probe.json is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
1175
1274
  }
1275
+ const durationS = Number(raw?.duration_s ?? NaN);
1276
+ if (!(durationS > 0)) throw new Error(`probe.json has no usable duration_s: ${JSON.stringify(raw)}`);
1277
+ return durationS;
1176
1278
  }
1177
- async function transcribeCloud(client, jobDir, opts) {
1178
- const verbose = opts.verbose !== false;
1279
+ function contentTypeFor(path) {
1280
+ const ext = parsePath(path).ext.toLowerCase();
1281
+ const map = { ".mp4": "video/mp4", ".m4v": "video/mp4", ".mov": "video/quicktime", ".mkv": "video/x-matroska", ".avi": "video/x-msvideo" };
1282
+ return map[ext] ?? "video/mp4";
1283
+ }
1284
+ async function initCloudJob(client, opts) {
1285
+ const source = resolvePath(opts.source);
1286
+ if (!existsSync4(source)) throw new Error(`source not found: ${source}`);
1287
+ const rawName = parsePath(source).name;
1288
+ const stem = sanitizeStem(rawName);
1289
+ const jobDir = resolvePath(opts.jobDir ?? join5(process.cwd(), `job_${stem}`));
1290
+ mkdirSync4(jobDir, { recursive: true });
1179
1291
  const log = (line) => {
1180
- if (verbose) process.stderr.write(line + "\n");
1292
+ if (opts.verbose !== false) process.stderr.write(line + "\n");
1181
1293
  };
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, {
1294
+ await client.me();
1295
+ const title = opts.title ?? rawName;
1296
+ const { projectId } = await client.createProject(title);
1297
+ log(` project: ${projectId}`);
1298
+ const contentType = contentTypeFor(source);
1299
+ const filename = parsePath(source).base;
1300
+ let assetId;
1301
+ let uploadedBytes = 0;
1302
+ const fileSrc = await openFileSource(source);
1303
+ try {
1304
+ uploadedBytes = fileSrc.size;
1305
+ log(` uploading ${filename} (${(fileSrc.size / (1024 * 1024)).toFixed(1)} MB)`);
1306
+ if (shouldMultipart(fileSrc.size)) {
1307
+ const mp = await uploadMultipart(client, { kind: "asset", source: fileSrc, contentType, filename, log });
1308
+ if (mp === "unavailable") {
1309
+ log(" chunked upload unavailable on this server; using single-request upload");
1310
+ } else {
1311
+ assetId = mp.assetId;
1312
+ }
1313
+ }
1314
+ if (!assetId) {
1315
+ const bytes = await fileSrc.readSlice(0, fileSrc.size);
1316
+ const asset = await client.createSourceAsset({ contentType, filename });
1317
+ await client.upload(asset.uploadUrl, bytes, contentType);
1318
+ assetId = asset.assetId;
1319
+ }
1320
+ } finally {
1321
+ await fileSrc.close();
1322
+ }
1323
+ const video = await client.createVideo(projectId, { assetId, title });
1324
+ log(` video: ${video.videoId}`);
1325
+ const videoClient = client.cfg.videoId ? client : client.withVideoId(video.videoId);
1326
+ const { runId } = await videoClient.ingestStart(stem);
1327
+ log(` ingest run: ${runId}`);
1328
+ const state = await pollRun(videoClient, runId, {
1196
1329
  intervalMs: opts.pollIntervalMs,
1197
1330
  onEvent: (ev) => log(` ${formatRunEvent(ev)}`)
1198
1331
  });
1199
- assertRunCompleted(state, "transcribe");
1200
- const pulled = await pullFiles(client, jobDir, { only: transcribeArtifactSet(opts.stem), prefixes: ["prompts/"] });
1201
- return { runId, wavPath, durationS, pulled };
1332
+ assertRunCompleted(state, "ingest");
1333
+ const pulled = await pullFiles(videoClient, jobDir, { only: ingestArtifactSet(stem), prefixes: ["prompts/"] });
1334
+ const durationS = readProbeDuration(jobDir);
1335
+ const job = {
1336
+ video_id: video.videoId,
1337
+ project_id: video.projectId,
1338
+ api_base: client.cfg.base,
1339
+ stem,
1340
+ duration_s: durationS,
1341
+ source_path: source
1342
+ };
1343
+ const jobJson = writeJobFile(jobDir, job);
1344
+ return {
1345
+ jobDir,
1346
+ jobJson,
1347
+ videoId: video.videoId,
1348
+ projectId: video.projectId,
1349
+ stem,
1350
+ durationS,
1351
+ uploaded: source,
1352
+ uploadedBytes,
1353
+ runId,
1354
+ pulled,
1355
+ webUrl: webVideoUrl(client.cfg.base, video.projectId, video.videoId)
1356
+ };
1202
1357
  }
1203
1358
 
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
- ];
1359
+ // src/cloud/transcribe2.ts
1360
+ function transcribeArtifactSet(stem) {
1361
+ return [`transcripts/${stem}.json`, "takes_packed.md", "flags.txt", "word_dump.txt", "coverage.json", "probe.json"];
1230
1362
  }
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 });
1363
+ async function transcribeCloud(client, jobDir, opts) {
1364
+ const verbose = opts.verbose !== false;
1365
+ const log = (line) => {
1366
+ if (verbose) process.stderr.write(line + "\n");
1367
+ };
1368
+ const transcriptPath = `transcripts/${opts.stem}.json`;
1369
+ const listing = await client.filesList();
1370
+ const haveTranscript = listing.some((f) => f.path === transcriptPath);
1371
+ let runId = null;
1372
+ if (haveTranscript) {
1373
+ log(` ${transcriptPath} already exists server-side \u2014 pulling artifacts`);
1374
+ } else {
1375
+ const started = await client.ingestStart(opts.stem);
1376
+ runId = started.runId;
1377
+ log(` ingest run: ${runId}`);
1378
+ const state = await pollRun(client, runId, {
1379
+ intervalMs: opts.pollIntervalMs,
1380
+ onEvent: (ev) => log(` ${formatRunEvent(ev)}`)
1381
+ });
1382
+ assertRunCompleted(state, "ingest");
1249
1383
  }
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);
1384
+ const pulled = await pullFiles(client, jobDir, { only: transcribeArtifactSet(opts.stem), prefixes: ["prompts/"] });
1385
+ return { runId, pulled };
1259
1386
  }
1260
1387
 
1261
1388
  // src/timeline-v2/schema.ts
@@ -1728,20 +1855,20 @@ function checkCapabilities(uses, registry = CAPABILITIES) {
1728
1855
  }
1729
1856
 
1730
1857
  // 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";
1858
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
1859
+ import { isAbsolute, join as join6, resolve as resolve2 } from "path";
1733
1860
  function timelineJsonPath2(jobDir) {
1734
- return join8(jobDir, "timeline.json");
1861
+ return join6(jobDir, "timeline.json");
1735
1862
  }
1736
1863
  function planPath2(jobDir) {
1737
- return join8(jobDir, "resolved", "plan.json");
1864
+ return join6(jobDir, "resolved", "plan.json");
1738
1865
  }
1739
1866
  function diagnosticsPath2(jobDir) {
1740
- return join8(jobDir, "resolved", "diagnostics.json");
1867
+ return join6(jobDir, "resolved", "diagnostics.json");
1741
1868
  }
1742
1869
  function loadTimeline2(jobDir) {
1743
1870
  const path = timelineJsonPath2(jobDir);
1744
- if (!existsSync6(path)) {
1871
+ if (!existsSync5(path)) {
1745
1872
  return {
1746
1873
  doc: null,
1747
1874
  raw: null,
@@ -1750,7 +1877,7 @@ function loadTimeline2(jobDir) {
1750
1877
  }
1751
1878
  let raw;
1752
1879
  try {
1753
- raw = JSON.parse(readFileSync5(path, "utf8"));
1880
+ raw = JSON.parse(readFileSync4(path, "utf8"));
1754
1881
  } catch (err) {
1755
1882
  return {
1756
1883
  doc: null,
@@ -1775,22 +1902,11 @@ function validateLocal(doc) {
1775
1902
  const warnings = [...sem.warnings, ...cap.warnings];
1776
1903
  return { ok: errors.length === 0, uses, errors, warnings };
1777
1904
  }
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
1905
  function statAssetsForDoc(jobDir, doc) {
1790
1906
  const out = {};
1791
1907
  const statRef = (ref) => {
1792
1908
  if (ref !== void 0 && !(ref in out)) {
1793
- out[ref] = existsSync6(isAbsolute(ref) ? ref : resolve2(jobDir, ref));
1909
+ out[ref] = existsSync5(isAbsolute(ref) ? ref : resolve2(jobDir, ref));
1794
1910
  }
1795
1911
  };
1796
1912
  for (const ov of doc.overlays) statRef(ov.asset?.ref);
@@ -1799,60 +1915,37 @@ function statAssetsForDoc(jobDir, doc) {
1799
1915
  }
1800
1916
  async function compileRemote(client, jobDir, loaded) {
1801
1917
  if (!loaded.doc) throw new Error("compileRemote requires a parsed timeline");
1802
- const silences = await detectSilencesForDoc(jobDir, loaded.doc);
1803
1918
  const assets = statAssetsForDoc(jobDir, loaded.doc);
1804
1919
  const res = await client.compileRemote({
1805
1920
  timeline: loaded.raw,
1806
- ...Object.keys(silences).length > 0 ? { silences_by_source: silences } : {},
1807
1921
  ...Object.keys(assets).length > 0 ? { assets_present: assets } : {}
1808
1922
  });
1809
- const dir = join8(jobDir, "resolved");
1810
- mkdirSync6(dir, { recursive: true });
1923
+ const dir = join6(jobDir, "resolved");
1924
+ mkdirSync5(dir, { recursive: true });
1811
1925
  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");
1926
+ writeFileSync4(out.diagnosticsPath, JSON.stringify(res.diagnostics ?? { errors: [], warnings: [], dropped: [] }, null, 2) + "\n", "utf8");
1813
1927
  if (res.ok === true && res.plan !== void 0 && res.plan !== null) {
1814
1928
  out.planPath = planPath2(jobDir);
1815
- writeFileSync5(out.planPath, JSON.stringify(res.plan, null, 2) + "\n", "utf8");
1929
+ writeFileSync4(out.planPath, JSON.stringify(res.plan, null, 2) + "\n", "utf8");
1816
1930
  }
1817
1931
  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");
1932
+ out.edlPath = join6(jobDir, "edl.json");
1933
+ writeFileSync4(out.edlPath, JSON.stringify(res.legacy_edl, null, 2) + "\n", "utf8");
1820
1934
  }
1821
1935
  return out;
1822
1936
  }
1823
1937
 
1824
1938
  // src/cloud/verify2.ts
1825
- import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
1826
- import { join as join9 } from "path";
1939
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
1940
+ import { join as join7 } from "path";
1827
1941
  async function verifyCloud(client, jobDir, opts = {}) {
1828
1942
  const verbose = opts.verbose !== false;
1829
1943
  const log = (line) => {
1830
1944
  if (verbose) process.stderr.write(line + "\n");
1831
1945
  };
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
1946
  const { runId } = await client.verifyStart({
1851
- wav_path: wavPath,
1852
- preview_duration_s: durationS,
1853
1947
  ...opts.full ? { mode: "full" } : {},
1854
- ...opts.windowS !== void 0 ? { window_s: opts.windowS } : {},
1855
- ...edl !== void 0 ? { edl } : {}
1948
+ ...opts.windowS !== void 0 ? { window_s: opts.windowS } : {}
1856
1949
  });
1857
1950
  log(` verify run: ${runId}`);
1858
1951
  const state = await pollRun(client, runId, {
@@ -1861,12 +1954,12 @@ async function verifyCloud(client, jobDir, opts = {}) {
1861
1954
  });
1862
1955
  assertRunCompleted(state, "verify");
1863
1956
  await pullFiles(client, jobDir, { only: ["verify.json"] });
1864
- const verifyPath = join9(jobDir, "verify.json");
1957
+ const verifyPath = join7(jobDir, "verify.json");
1865
1958
  let verdict = null;
1866
1959
  let findings = null;
1867
- if (existsSync7(verifyPath)) {
1960
+ if (existsSync6(verifyPath)) {
1868
1961
  try {
1869
- const v = JSON.parse(readFileSync6(verifyPath, "utf8"));
1962
+ const v = JSON.parse(readFileSync5(verifyPath, "utf8"));
1870
1963
  verdict = typeof v.verdict === "string" ? v.verdict : null;
1871
1964
  findings = Array.isArray(v.joins) ? v.joins.length : null;
1872
1965
  } catch {
@@ -1875,302 +1968,128 @@ async function verifyCloud(client, jobDir, opts = {}) {
1875
1968
  return { runId, verifyPath, verdict, findings };
1876
1969
  }
1877
1970
 
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
- );
1971
+ // src/cloud/qa.ts
1972
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
1973
+ import { join as join8 } from "path";
1974
+ function qaDir(jobDir) {
1975
+ return jobDir ? join8(jobDir, "qa") : join8(process.cwd(), "qa");
1976
+ }
1977
+ function tag(start, end) {
1978
+ return `${start}_${end}`;
1979
+ }
1980
+ function framesPngPath(dir, start, end, suffix = "") {
1981
+ return join8(dir, `frames_${tag(start, end)}${suffix}.png`);
1982
+ }
1983
+ function waveformPngPath(dir, start, end) {
1984
+ return join8(dir, `waveform_${tag(start, end)}.png`);
1985
+ }
1986
+ function inspectPngPath(dir, start, end, suffix = "") {
1987
+ return join8(dir, `inspect_${tag(start, end)}${suffix}.png`);
1988
+ }
1989
+ function waveformWordsPath(dir, start, end) {
1990
+ return join8(dir, `waveform_${tag(start, end)}.words.json`);
1991
+ }
1992
+ function readPlanSegments(jobDir) {
1993
+ const path = join8(jobDir, "resolved", "plan.json");
1994
+ if (!existsSync7(path)) {
1995
+ throw new Error(`no compiled plan at ${path} \u2014 run \`video-editing timeline compile\` first`);
1996
+ }
1997
+ let raw;
1998
+ try {
1999
+ raw = JSON.parse(readFileSync6(path, "utf8"));
2000
+ } catch (err) {
2001
+ throw new Error(`resolved/plan.json is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
2002
+ }
2003
+ const segments = raw?.segments;
2004
+ if (!Array.isArray(segments) || segments.length === 0) {
2005
+ throw new Error(`resolved/plan.json has no segments[]: ${path}`);
2006
+ }
2007
+ return segments.map((s, i) => {
2008
+ const r = s;
2009
+ const seg = {
2010
+ srcStart: Number(r.srcStart ?? NaN),
2011
+ srcEnd: Number(r.srcEnd ?? NaN),
2012
+ outStart: Number(r.outStart ?? NaN),
2013
+ outEnd: Number(r.outEnd ?? NaN)
2014
+ };
2015
+ if (![seg.srcStart, seg.srcEnd, seg.outStart, seg.outEnd].every(Number.isFinite)) {
2016
+ throw new Error(`plan segment ${i} is missing src/out times: ${JSON.stringify(s)}`);
1968
2017
  }
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
2018
+ return seg;
2019
+ });
2020
+ }
2021
+ var round3 = (n) => Math.round(n * 1e3) / 1e3;
2022
+ var SUFFIXES = "abcdefghijklmnopqrstuvwxyz";
2023
+ function suffixed(windows) {
2024
+ return windows.map((w, i) => ({
2025
+ start: round3(w.start),
2026
+ end: round3(w.end),
2027
+ suffix: windows.length > 1 ? `-${SUFFIXES[i] ?? String(i)}` : ""
2028
+ }));
2029
+ }
2030
+ function joinSourceWindows(segments, joinIndex, spanS) {
2031
+ if (!Number.isInteger(joinIndex) || joinIndex < 1 || joinIndex > segments.length - 1) {
2032
+ throw new Error(
2033
+ `--join must be a 1-based join index between 1 and ${segments.length - 1} (the plan has ${segments.length} segment(s))`
1972
2034
  );
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
2035
  }
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`");
2036
+ if (!(spanS > 0)) throw new Error("--span must be > 0 seconds");
2037
+ const a = segments[joinIndex - 1];
2038
+ const b = segments[joinIndex];
2039
+ return suffixed([
2040
+ { start: Math.max(a.srcStart, a.srcEnd - spanS), end: a.srcEnd },
2041
+ { start: b.srcStart, end: Math.min(b.srcEnd, b.srcStart + spanS) }
2042
+ ]);
2043
+ }
2044
+ function joinSpanWindow(segments, joinIndex, spanS) {
2045
+ if (!Number.isInteger(joinIndex) || joinIndex < 1 || joinIndex > segments.length - 1) {
2046
+ throw new Error(
2047
+ `--join must be a 1-based join index between 1 and ${segments.length - 1} (the plan has ${segments.length} segment(s))`
2048
+ );
2039
2049
  }
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;
2050
+ if (!(spanS > 0)) throw new Error("--span must be > 0 seconds");
2051
+ const a = segments[joinIndex - 1];
2052
+ const b = segments[joinIndex];
2053
+ return { start: round3(Math.max(0, a.srcEnd - spanS)), end: round3(b.srcStart + spanS), suffix: "" };
2045
2054
  }
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
- ];
2075
- }
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 });
2055
+ function outWindowToSourceWindows(segments, outStart, outEnd) {
2056
+ if (!(outEnd > outStart)) throw new Error("--out-end must be greater than --out-start");
2057
+ const windows = [];
2058
+ for (const seg of segments) {
2059
+ const s = Math.max(outStart, seg.outStart);
2060
+ const e = Math.min(outEnd, seg.outEnd);
2061
+ if (e - s <= 5e-4) continue;
2062
+ windows.push({ start: seg.srcStart + (s - seg.outStart), end: seg.srcStart + (e - seg.outStart) });
2109
2063
  }
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;
2064
+ if (windows.length === 0) {
2065
+ const total = segments.length ? Math.max(...segments.map((s) => s.outEnd)) : 0;
2066
+ throw new Error(`output window ${outStart}s\u2013${outEnd}s does not overlap the plan (output duration ${total}s)`);
2148
2067
  }
2149
- return result;
2068
+ return suffixed(windows);
2150
2069
  }
2151
2070
 
2152
2071
  // src/cloud/review2.ts
2153
2072
  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";
2073
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
2074
+ import { basename, isAbsolute as isAbsolute2, join as join9 } from "path";
2156
2075
  function reviewDir2(jobDir) {
2157
- return join12(jobDir, "review");
2076
+ return join9(jobDir, "review");
2158
2077
  }
2159
2078
  function roundJsonPath2(jobDir) {
2160
- return join12(reviewDir2(jobDir), "round.json");
2079
+ return join9(reviewDir2(jobDir), "round.json");
2161
2080
  }
2162
2081
  function instructionPath2(jobDir) {
2163
- return join12(reviewDir2(jobDir), "instruction.md");
2082
+ return join9(reviewDir2(jobDir), "instruction.md");
2164
2083
  }
2165
2084
  function assetsCachePath2(jobDir) {
2166
- return join12(reviewDir2(jobDir), "assets.json");
2085
+ return join9(reviewDir2(jobDir), "assets.json");
2167
2086
  }
2168
2087
  async function pullReview2(client, jobDir, opts = {}) {
2169
2088
  const session = await client.getReview();
2170
- mkdirSync9(reviewDir2(jobDir), { recursive: true });
2089
+ mkdirSync6(reviewDir2(jobDir), { recursive: true });
2171
2090
  const round = session.activeRound;
2172
2091
  if (round) {
2173
- writeFileSync8(
2092
+ writeFileSync5(
2174
2093
  roundJsonPath2(jobDir),
2175
2094
  JSON.stringify(
2176
2095
  {
@@ -2188,20 +2107,20 @@ async function pullReview2(client, jobDir, opts = {}) {
2188
2107
  ) + "\n",
2189
2108
  "utf8"
2190
2109
  );
2191
- writeFileSync8(instructionPath2(jobDir), round.instruction + "\n", "utf8");
2110
+ writeFileSync5(instructionPath2(jobDir), round.instruction + "\n", "utf8");
2192
2111
  } else {
2193
- writeFileSync8(instructionPath2(jobDir), "No active review round for this video.\n", "utf8");
2112
+ writeFileSync5(instructionPath2(jobDir), "No active review round for this video.\n", "utf8");
2194
2113
  }
2195
2114
  let wroteTimeline = false;
2196
2115
  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");
2116
+ if (session.timeline !== null && session.timeline !== void 0 && !existsSync8(tlPath)) {
2117
+ writeFileSync5(tlPath, JSON.stringify(session.timeline, null, 2) + "\n", "utf8");
2199
2118
  wroteTimeline = true;
2200
2119
  }
2201
2120
  let downloadedSource = null;
2202
2121
  if (opts.withSource && session.sourceUrl) {
2203
- const dest = join12(jobDir, "source.mp4");
2204
- if (!existsSync10(dest)) {
2122
+ const dest = join9(jobDir, "source.mp4");
2123
+ if (!existsSync8(dest)) {
2205
2124
  const dl = await client.download(session.sourceUrl, dest);
2206
2125
  downloadedSource = dl.path;
2207
2126
  }
@@ -2211,9 +2130,9 @@ async function pullReview2(client, jobDir, opts = {}) {
2211
2130
  function resolveRunId2(jobDir, flagOverride) {
2212
2131
  if (typeof flagOverride === "string" && flagOverride.trim() !== "") return flagOverride.trim();
2213
2132
  const path = roundJsonPath2(jobDir);
2214
- if (existsSync10(path)) {
2133
+ if (existsSync8(path)) {
2215
2134
  try {
2216
- const runId = JSON.parse(readFileSync9(path, "utf8")).run_id;
2135
+ const runId = JSON.parse(readFileSync7(path, "utf8")).run_id;
2217
2136
  if (typeof runId === "string" && runId !== "") return runId;
2218
2137
  } catch {
2219
2138
  }
@@ -2273,8 +2192,8 @@ async function syncAssets2(client, jobDir, plan) {
2273
2192
  if (refs.length === 0) return {};
2274
2193
  const paths = /* @__PURE__ */ new Map();
2275
2194
  for (const ref of refs) {
2276
- const abs = isAbsolute2(ref) ? ref : join12(jobDir, ref);
2277
- if (!existsSync10(abs)) {
2195
+ const abs = isAbsolute2(ref) ? ref : join9(jobDir, ref);
2196
+ if (!existsSync8(abs)) {
2278
2197
  throw new Error(
2279
2198
  `asset not found: ${ref} (expected at ${abs}) \u2014 asset refs are job-dir-relative paths; place the file before pushing`
2280
2199
  );
@@ -2282,9 +2201,9 @@ async function syncAssets2(client, jobDir, plan) {
2282
2201
  paths.set(ref, abs);
2283
2202
  }
2284
2203
  let cache = {};
2285
- if (existsSync10(assetsCachePath2(jobDir))) {
2204
+ if (existsSync8(assetsCachePath2(jobDir))) {
2286
2205
  try {
2287
- const raw = JSON.parse(readFileSync9(assetsCachePath2(jobDir), "utf8"));
2206
+ const raw = JSON.parse(readFileSync7(assetsCachePath2(jobDir), "utf8"));
2288
2207
  for (const [ref, v] of Object.entries(raw ?? {})) {
2289
2208
  const e = v;
2290
2209
  if (e && typeof e.sha256 === "string" && typeof e.asset_id === "string") {
@@ -2298,7 +2217,7 @@ async function syncAssets2(client, jobDir, plan) {
2298
2217
  const manifest = {};
2299
2218
  let dirty = false;
2300
2219
  for (const ref of refs) {
2301
- const bytes = readFileSync9(paths.get(ref));
2220
+ const bytes = readFileSync7(paths.get(ref));
2302
2221
  const sha256 = "sha256:" + createHash2("sha256").update(bytes).digest("hex");
2303
2222
  const hit = cache[ref];
2304
2223
  if (hit !== void 0 && hit.sha256 === sha256) {
@@ -2313,8 +2232,8 @@ async function syncAssets2(client, jobDir, plan) {
2313
2232
  dirty = true;
2314
2233
  }
2315
2234
  if (dirty) {
2316
- mkdirSync9(reviewDir2(jobDir), { recursive: true });
2317
- writeFileSync8(assetsCachePath2(jobDir), JSON.stringify(cache, null, 2) + "\n", "utf8");
2235
+ mkdirSync6(reviewDir2(jobDir), { recursive: true });
2236
+ writeFileSync5(assetsCachePath2(jobDir), JSON.stringify(cache, null, 2) + "\n", "utf8");
2318
2237
  }
2319
2238
  return manifest;
2320
2239
  }
@@ -2347,10 +2266,10 @@ async function pushTimeline2(client, jobDir, runId) {
2347
2266
  return { ok: true, plan: compiled.plan, timelineHash: compiled.timelineHash, assets };
2348
2267
  }
2349
2268
  function readOutcomesFile2(path) {
2350
- if (!existsSync10(path)) throw new Error(`outcomes file not found: ${path}`);
2269
+ if (!existsSync8(path)) throw new Error(`outcomes file not found: ${path}`);
2351
2270
  let raw;
2352
2271
  try {
2353
- raw = JSON.parse(readFileSync9(path, "utf8"));
2272
+ raw = JSON.parse(readFileSync7(path, "utf8"));
2354
2273
  } catch (err) {
2355
2274
  throw new Error(`outcomes file is not valid JSON: ${path}: ${err instanceof Error ? err.message : String(err)}`);
2356
2275
  }
@@ -2387,16 +2306,16 @@ async function pushComplete2(client, jobDir, runId, opts) {
2387
2306
  }
2388
2307
 
2389
2308
  // src/public/config.ts
2390
- import { existsSync as existsSync11, mkdirSync as mkdirSync10, readFileSync as readFileSync10, writeFileSync as writeFileSync9 } from "fs";
2309
+ import { existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
2391
2310
  import { homedir as homedir2 } from "os";
2392
- import { join as join13 } from "path";
2311
+ import { join as join10 } from "path";
2393
2312
  function publicConfigDir() {
2394
2313
  const xdg = process.env.XDG_CONFIG_HOME;
2395
- const base = xdg && xdg.trim() ? xdg : join13(homedir2(), ".config");
2396
- return join13(base, "video-editing");
2314
+ const base = xdg && xdg.trim() ? xdg : join10(homedir2(), ".config");
2315
+ return join10(base, "video-editing");
2397
2316
  }
2398
2317
  function publicConfigEnvPath() {
2399
- return join13(publicConfigDir(), ".env");
2318
+ return join10(publicConfigDir(), ".env");
2400
2319
  }
2401
2320
  function parseEnvLines(text) {
2402
2321
  return text.split(/\r?\n/).map((raw) => {
@@ -2412,10 +2331,10 @@ function parseEnvLines(text) {
2412
2331
  var MANAGED_KEYS = ["CLIPREADY_API_KEY", "CLIPREADY_API_BASE"];
2413
2332
  function loadPublicConfigEnv(env = process.env) {
2414
2333
  const path = publicConfigEnvPath();
2415
- if (!existsSync11(path)) return;
2334
+ if (!existsSync9(path)) return;
2416
2335
  let text;
2417
2336
  try {
2418
- text = readFileSync10(path, "utf8");
2337
+ text = readFileSync8(path, "utf8");
2419
2338
  } catch {
2420
2339
  return;
2421
2340
  }
@@ -2425,22 +2344,31 @@ function loadPublicConfigEnv(env = process.env) {
2425
2344
  if (!env[line.key] || !String(env[line.key]).trim()) env[line.key] = line.value;
2426
2345
  }
2427
2346
  }
2347
+ function removePublicConfigKey(key) {
2348
+ const path = publicConfigEnvPath();
2349
+ if (!existsSync9(path)) return path;
2350
+ const lines = readFileSync8(path, "utf8").split(/\r?\n/).filter((l) => l.trim() !== "").filter((l) => {
2351
+ const eq = l.indexOf("=");
2352
+ return eq < 0 || l.slice(0, eq).trim() !== key;
2353
+ });
2354
+ writeFileSync6(path, lines.length ? lines.join("\n") + "\n" : "", { mode: 384 });
2355
+ return path;
2356
+ }
2428
2357
  function savePublicConfigKey(key, value) {
2429
- mkdirSync10(publicConfigDir(), { recursive: true });
2358
+ mkdirSync7(publicConfigDir(), { recursive: true });
2430
2359
  const path = publicConfigEnvPath();
2431
- const existing = existsSync11(path) ? readFileSync10(path, "utf8") : "";
2360
+ const existing = existsSync9(path) ? readFileSync8(path, "utf8") : "";
2432
2361
  const lines = existing.split(/\r?\n/).filter((l) => l.trim() !== "");
2433
2362
  const kept = lines.filter((l) => {
2434
2363
  const eq = l.indexOf("=");
2435
2364
  return eq < 0 || l.slice(0, eq).trim() !== key;
2436
2365
  });
2437
2366
  kept.push(`${key}=${value}`);
2438
- writeFileSync9(path, kept.join("\n") + "\n", { mode: 384 });
2367
+ writeFileSync6(path, kept.join("\n") + "\n", { mode: 384 });
2439
2368
  return path;
2440
2369
  }
2441
2370
 
2442
2371
  export {
2443
- requireFfmpeg,
2444
2372
  DEFAULT_TIMEOUT_MS,
2445
2373
  DEFAULT_DOWNLOAD_TIMEOUT_MS,
2446
2374
  resolveApiBase,
@@ -2455,9 +2383,13 @@ export {
2455
2383
  meUrl,
2456
2384
  projectsUrl,
2457
2385
  projectVideosUrl,
2386
+ webVideoUrl,
2458
2387
  runUrl,
2388
+ multipartUrl,
2389
+ multipartRelayUrl,
2459
2390
  filesUrl,
2460
2391
  transcribeUrl,
2392
+ ingestUrl,
2461
2393
  compileUrl,
2462
2394
  resolveUrl,
2463
2395
  verifyUrl,
@@ -2483,7 +2415,13 @@ export {
2483
2415
  writeJobFile,
2484
2416
  readJobFile,
2485
2417
  videoIdFromJob,
2486
- initCloudJob,
2418
+ MULTIPART_THRESHOLD_BYTES,
2419
+ RELAY_DIRECT_ATTEMPTS,
2420
+ RELAY_SUBCHUNK_BYTES,
2421
+ shouldMultipart,
2422
+ bytesSource,
2423
+ openFileSource,
2424
+ uploadMultipart,
2487
2425
  INLINE_MAX_BYTES,
2488
2426
  sha256Hex,
2489
2427
  sameSha,
@@ -2492,18 +2430,18 @@ export {
2492
2430
  vfsContentType,
2493
2431
  pullFiles,
2494
2432
  pushFiles,
2433
+ uploadVfsPayload,
2495
2434
  uploadVfsBytes,
2496
2435
  MAX_RUN_POLL_MS,
2497
2436
  formatRunEvent,
2498
2437
  pollRun,
2499
2438
  assertRunCompleted,
2439
+ sanitizeStem,
2440
+ ingestArtifactSet,
2441
+ readProbeDuration,
2442
+ initCloudJob,
2500
2443
  transcribeArtifactSet,
2501
- extractGuardedWav,
2502
2444
  transcribeCloud,
2503
- DEFAULT_DESILENCE,
2504
- silenceDetectArgs,
2505
- parseSilenceDetect,
2506
- detectSilences,
2507
2445
  CAPTION_STYLE_DEFAULTS,
2508
2446
  timelineV2Schema,
2509
2447
  emptyDiagnostics,
@@ -2517,18 +2455,18 @@ export {
2517
2455
  diagnosticsPath2,
2518
2456
  loadTimeline2,
2519
2457
  validateLocal,
2520
- detectSilencesForDoc,
2521
2458
  statAssetsForDoc,
2522
2459
  compileRemote,
2523
2460
  verifyCloud,
2524
- buildMediaEntry,
2525
- composeCloud,
2526
- qaFramesArgs,
2527
- qaWaveformArgs,
2528
- qaWordsForWindow,
2529
- findLocalTranscript,
2530
- qaFramesLocal,
2531
- qaWaveformLocal,
2461
+ qaDir,
2462
+ framesPngPath,
2463
+ waveformPngPath,
2464
+ inspectPngPath,
2465
+ waveformWordsPath,
2466
+ readPlanSegments,
2467
+ joinSourceWindows,
2468
+ joinSpanWindow,
2469
+ outWindowToSourceWindows,
2532
2470
  reviewDir2,
2533
2471
  roundJsonPath2,
2534
2472
  instructionPath2,
@@ -2545,5 +2483,6 @@ export {
2545
2483
  publicConfigDir,
2546
2484
  publicConfigEnvPath,
2547
2485
  loadPublicConfigEnv,
2486
+ removePublicConfigKey,
2548
2487
  savePublicConfigKey
2549
2488
  };