@pixerate/editor 0.159.0 → 0.161.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.
Files changed (63) hide show
  1. package/dist/{chunk-VKF3PWKR.cjs → chunk-3OSXHLIT.cjs} +259 -28
  2. package/dist/chunk-6DM3PUQD.cjs +767 -0
  3. package/dist/{chunk-IQ6KKXOU.js → chunk-YN4AYYBJ.js} +243 -12
  4. package/dist/chunk-ZGOHKS3X.js +767 -0
  5. package/dist/effects/index.cjs +16 -767
  6. package/dist/effects/index.js +5 -756
  7. package/dist/image-editor/controller.d.cts +7 -0
  8. package/dist/image-editor/controller.d.ts +7 -0
  9. package/dist/image-editor/index.cjs +49 -10
  10. package/dist/image-editor/index.js +48 -9
  11. package/dist/image-editor/layers/transform-ui.d.cts +14 -6
  12. package/dist/image-editor/layers/transform-ui.d.ts +14 -6
  13. package/dist/paint-editor/index.cjs +2 -2
  14. package/dist/paint-editor/index.js +1 -1
  15. package/dist/video-editor/audio.d.cts +9 -0
  16. package/dist/video-editor/audio.d.ts +9 -0
  17. package/dist/video-editor/check.d.cts +1 -1
  18. package/dist/video-editor/check.d.ts +1 -1
  19. package/dist/video-editor/controller.d.cts +47 -0
  20. package/dist/video-editor/controller.d.ts +47 -0
  21. package/dist/video-editor/dialogue-tools.d.cts +19 -0
  22. package/dist/video-editor/dialogue-tools.d.ts +19 -0
  23. package/dist/video-editor/effect-authoring.d.cts +54 -0
  24. package/dist/video-editor/effect-authoring.d.ts +54 -0
  25. package/dist/video-editor/evaluate.d.cts +11 -0
  26. package/dist/video-editor/evaluate.d.ts +11 -0
  27. package/dist/video-editor/export.d.cts +7 -1
  28. package/dist/video-editor/export.d.ts +7 -1
  29. package/dist/video-editor/index.cjs +1853 -528
  30. package/dist/video-editor/index.d.cts +7 -0
  31. package/dist/video-editor/index.d.ts +7 -0
  32. package/dist/video-editor/index.js +1512 -187
  33. package/dist/video-editor/lut.d.cts +12 -2
  34. package/dist/video-editor/lut.d.ts +12 -2
  35. package/dist/video-editor/mask.d.cts +48 -0
  36. package/dist/video-editor/mask.d.ts +48 -0
  37. package/dist/video-editor/mix.d.cts +107 -0
  38. package/dist/video-editor/mix.d.ts +107 -0
  39. package/dist/video-editor/music-tools.d.cts +19 -0
  40. package/dist/video-editor/music-tools.d.ts +19 -0
  41. package/dist/video-editor/parse.d.cts +3 -1
  42. package/dist/video-editor/parse.d.ts +3 -1
  43. package/dist/video-editor/plan.d.cts +75 -0
  44. package/dist/video-editor/plan.d.ts +75 -0
  45. package/dist/video-editor/render.d.cts +1 -1
  46. package/dist/video-editor/render.d.ts +1 -1
  47. package/dist/video-editor/timeline.d.cts +2 -0
  48. package/dist/video-editor/timeline.d.ts +2 -0
  49. package/dist/video-editor/tools.d.cts +90 -0
  50. package/dist/video-editor/tools.d.ts +90 -0
  51. package/dist/video-editor/transcript-controller.d.cts +106 -0
  52. package/dist/video-editor/transcript-controller.d.ts +106 -0
  53. package/dist/video-editor/transcript.d.cts +40 -0
  54. package/dist/video-editor/transcript.d.ts +40 -0
  55. package/dist/video-editor/types.d.cts +62 -2
  56. package/dist/video-editor/types.d.ts +62 -2
  57. package/dist/video-editor/ui.d.cts +21 -1
  58. package/dist/video-editor/ui.d.ts +21 -1
  59. package/package.json +1 -1
  60. package/skills/README.md +2 -0
  61. package/skills/pixerate-bgm-alignment/SKILL.md +82 -0
  62. package/skills/pixerate-talking-head-edit/SKILL.md +80 -0
  63. package/skills/pixerate-video-editor/SKILL.md +40 -2
@@ -1,5 +1,6 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3; var _class4; var _class5; var _class6;
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3; var _class4; var _class5; var _class6; var _class7;
2
2
 
3
+ var _chunk6DM3PUQDcjs = require('../chunk-6DM3PUQD.cjs');
3
4
 
4
5
 
5
6
 
@@ -134,7 +135,20 @@
134
135
 
135
136
 
136
137
 
137
- var _chunkVKF3PWKRcjs = require('../chunk-VKF3PWKR.cjs');
138
+
139
+
140
+
141
+
142
+
143
+
144
+
145
+
146
+
147
+
148
+
149
+
150
+
151
+ var _chunk3OSXHLITcjs = require('../chunk-3OSXHLIT.cjs');
138
152
  require('../chunk-PY4GWSE2.cjs');
139
153
 
140
154
 
@@ -155,7 +169,10 @@ var _chunkFJ2MSI43cjs = require('../chunk-FJ2MSI43.cjs');
155
169
 
156
170
 
157
171
  var _chunkSUF6WRXRcjs = require('../chunk-SUF6WRXR.cjs');
158
- require('../chunk-DWJJI6RT.cjs');
172
+
173
+
174
+
175
+ var _chunkDWJJI6RTcjs = require('../chunk-DWJJI6RT.cjs');
159
176
  require('../chunk-LEUZ4FRC.cjs');
160
177
  require('../chunk-A6QWKBJP.cjs');
161
178
  require('../chunk-UKCJ43YL.cjs');
@@ -197,7 +214,7 @@ function parseVideoEase(v, path) {
197
214
  if (v[0] < 0 || v[0] > 1 || v[2] < 0 || v[2] > 1) fail(path, "must have x1 and x2 between 0 and 1");
198
215
  return [v[0], v[1], v[2], v[3]];
199
216
  }
200
- return oneOf(v, _chunkVKF3PWKRcjs.VIDEO_EASE_NAMES, path);
217
+ return oneOf(v, _chunk3OSXHLITcjs.VIDEO_EASE_NAMES, path);
201
218
  }
202
219
  function parseVideoKeyframes(v, path, { min, max } = {}) {
203
220
  if (!isObject(v)) return fail(path, "must be a number or keyframes { keys: [{ t, v, ease }] }");
@@ -235,7 +252,7 @@ function parseVideoTimeRemap(v, speed, path) {
235
252
  if (typeof remap === "number" || remap === void 0) return fail(path, "must be keyframes { keys: [{ t, v, ease }] } (v: ms of source after the in point)");
236
253
  if (speed !== void 0 && speed !== 1) fail(path, 'cannot be used with a speed; a ramp sets the speed itself (remove "speed")');
237
254
  try {
238
- _chunkVKF3PWKRcjs.checkVideoTimeRemap.call(void 0, remap, path);
255
+ _chunk3OSXHLITcjs.checkVideoTimeRemap.call(void 0, remap, path);
239
256
  } catch (e) {
240
257
  throw new Error(e.message.replace(/\.?$/, "."));
241
258
  }
@@ -245,37 +262,66 @@ function parseVideoClipAnimation(v, path) {
245
262
  if (v === void 0) return void 0;
246
263
  if (!isObject(v)) return fail(path, "must be an object { effect, duration }");
247
264
  return compact({
248
- effect: oneOf(v.effect, _chunkVKF3PWKRcjs.VIDEO_CLIP_ANIMATION_EFFECTS, `${path}.effect`, false),
265
+ effect: oneOf(v.effect, _chunk3OSXHLITcjs.VIDEO_CLIP_ANIMATION_EFFECTS, `${path}.effect`, false),
249
266
  duration: num(v.duration, `${path}.duration`, { optional: true, positive: true })
250
267
  });
251
268
  }
252
269
  function parseVideoGrade(v, path) {
253
270
  if (v === void 0) return void 0;
254
271
  if (!isObject(v)) return fail(path, "must be an object such as { exposure, contrast, saturation }");
255
- const known = /* @__PURE__ */ new Set([..._chunkVKF3PWKRcjs.VIDEO_GRADE_CONTROLS.map((c) => c.key), "lut"]);
272
+ const known = /* @__PURE__ */ new Set([..._chunk3OSXHLITcjs.VIDEO_GRADE_CONTROLS.map((c) => c.key), "lut"]);
256
273
  const unknown = Object.keys(v).find((k) => !known.has(k));
257
274
  if (unknown) fail(`${path}.${unknown}`, `is not a grade setting; use ${[...known].join(", ")}`);
258
- return compact({ ...Object.fromEntries(_chunkVKF3PWKRcjs.VIDEO_GRADE_CONTROLS.map((c) => [c.key, num(v[c.key], `${path}.${c.key}`, { optional: true, min: c.min, max: c.max })])), lut: parseVideoGradeLut(v.lut, `${path}.lut`) });
275
+ return compact({ ...Object.fromEntries(_chunk3OSXHLITcjs.VIDEO_GRADE_CONTROLS.map((c) => [c.key, num(v[c.key], `${path}.${c.key}`, { optional: true, min: c.min, max: c.max })])), lut: parseVideoGradeLut(v.lut, `${path}.lut`) });
259
276
  }
260
277
  function parseVideoGradeLut(v, path) {
261
278
  if (v === void 0) return void 0;
262
279
  if (!isObject(v)) return fail(path, "must be an object { src, strength } naming a .cube file");
263
- const unknown = Object.keys(v).find((k) => !["src", "strength", "name"].includes(k));
264
- if (unknown) fail(`${path}.${unknown}`, "is not a LUT setting; use src, strength, name");
265
- return compact({ src: str(v.src, `${path}.src`), strength: num(v.strength, `${path}.strength`, { optional: true, min: 0, max: 1 }), name: str(v.name, `${path}.name`, true) });
280
+ const unknown = Object.keys(v).find((k) => !["src", "strength", "name", "preserveSkinTones"].includes(k));
281
+ if (unknown) fail(`${path}.${unknown}`, "is not a LUT setting; use src, strength, name, preserveSkinTones");
282
+ return compact({ src: str(v.src, `${path}.src`), strength: num(v.strength, `${path}.strength`, { optional: true, min: 0, max: 1 }), name: str(v.name, `${path}.name`, true), preserveSkinTones: num(v.preserveSkinTones, `${path}.preserveSkinTones`, { optional: true, min: 0, max: 1 }) });
283
+ }
284
+ var MASK_KEYS = ["shape", "x", "y", "width", "height", "rotation", "feather", "cornerRadius", "inverted", "points"];
285
+ function parseVideoClipMask(v, path) {
286
+ if (v === void 0) return void 0;
287
+ if (!isObject(v)) return fail(path, 'must be an object such as { shape: "ellipse", x: 0.5, y: 0.5, width: 0.4, height: 0.6, feather: 20 }');
288
+ const unknown = Object.keys(v).find((k) => !MASK_KEYS.includes(k));
289
+ if (unknown) fail(`${path}.${unknown}`, `is not a mask setting; use ${MASK_KEYS.join(", ")}`);
290
+ const shape = oneOf(v.shape, _chunk3OSXHLITcjs.VIDEO_MASK_SHAPES, `${path}.shape`, false);
291
+ let points;
292
+ if (v.points !== void 0) {
293
+ if (shape !== "polygon") fail(`${path}.points`, "is only for polygon masks");
294
+ if (!Array.isArray(v.points) || v.points.length < 3) fail(`${path}.points`, "must be a list of at least 3 { x, y } points");
295
+ points = v.points.map((p, i) => {
296
+ if (!isObject(p)) return fail(`${path}.points[${i}]`, "must be an object { x, y }");
297
+ return { x: num(p.x, `${path}.points[${i}].x`), y: num(p.y, `${path}.points[${i}].y`) };
298
+ });
299
+ } else if (shape === "polygon") fail(`${path}.points`, "is required for a polygon mask: at least 3 { x, y } points as fractions of the picture");
300
+ return compact({
301
+ shape,
302
+ x: num(v.x, `${path}.x`, { optional: true }),
303
+ y: num(v.y, `${path}.y`, { optional: true }),
304
+ width: num(v.width, `${path}.width`, { optional: true, min: 0 }),
305
+ height: num(v.height, `${path}.height`, { optional: true, min: 0 }),
306
+ rotation: num(v.rotation, `${path}.rotation`, { optional: true }),
307
+ feather: num(v.feather, `${path}.feather`, { optional: true, min: 0 }),
308
+ cornerRadius: num(v.cornerRadius, `${path}.cornerRadius`, { optional: true, min: 0, max: 1 }),
309
+ inverted: bool(v.inverted, `${path}.inverted`),
310
+ points
311
+ });
266
312
  }
267
313
  function parseVideoTransition(v, path) {
268
314
  if (v === void 0) return void 0;
269
315
  if (!isObject(v)) return fail(path, "must be an object { type, duration }");
270
- const type = oneOf(v.type, _chunkVKF3PWKRcjs.VIDEO_TRANSITION_TYPES, `${path}.type`, false);
316
+ const type = oneOf(v.type, _chunk3OSXHLITcjs.VIDEO_TRANSITION_TYPES, `${path}.type`, false);
271
317
  if (type === "effect" && v.effect === void 0) fail(`${path}.effect`, 'is required for an effect transition: { effect: "<effect id>", values }');
272
318
  if (type !== "effect" && v.effect !== void 0) fail(`${path}.effect`, 'is only for transitions of type "effect"');
273
319
  return compact({
274
320
  type,
275
321
  duration: num(v.duration, `${path}.duration`, { positive: true }),
276
322
  color: str(v.color, `${path}.color`, true),
277
- direction: oneOf(v.direction, _chunkVKF3PWKRcjs.VIDEO_WIPE_DIRECTIONS, `${path}.direction`),
278
- audioCurve: oneOf(v.audioCurve, _chunkVKF3PWKRcjs.VIDEO_FADE_CURVES, `${path}.audioCurve`),
323
+ direction: oneOf(v.direction, _chunk3OSXHLITcjs.VIDEO_WIPE_DIRECTIONS, `${path}.direction`),
324
+ audioCurve: oneOf(v.audioCurve, _chunk3OSXHLITcjs.VIDEO_FADE_CURVES, `${path}.audioCurve`),
279
325
  effect: v.effect === void 0 ? void 0 : parseVideoEffectRef(v.effect, `${path}.effect`, false)
280
326
  });
281
327
  }
@@ -368,7 +414,7 @@ function parseVideoAssetProxies(v, path) {
368
414
  }
369
415
  function parseVideoClip(v, path = "clip") {
370
416
  if (!isObject(v)) return fail(path, "must be an object");
371
- const type = oneOf(_nullishCoalesce(v.type, () => ( "media")), _chunkVKF3PWKRcjs.VIDEO_CLIP_TYPES, `${path}.type`, false);
417
+ const type = oneOf(_nullishCoalesce(v.type, () => ( "media")), _chunk3OSXHLITcjs.VIDEO_CLIP_TYPES, `${path}.type`, false);
372
418
  const base = {
373
419
  id: str(v.id, `${path}.id`),
374
420
  name: str(v.name, `${path}.name`, true),
@@ -376,15 +422,16 @@ function parseVideoClip(v, path = "clip") {
376
422
  duration: num(v.duration, `${path}.duration`, { positive: true }),
377
423
  opacity: parseVideoAnimatable(v.opacity, `${path}.opacity`, { optional: true, min: 0, max: 1 }),
378
424
  transform: parseVideoTransform(v.transform, `${path}.transform`),
379
- fit: oneOf(v.fit, _chunkVKF3PWKRcjs.VIDEO_FITS, `${path}.fit`),
380
- blend: oneOf(v.blend, _chunkVKF3PWKRcjs.VIDEO_BLEND_MODES, `${path}.blend`),
425
+ fit: oneOf(v.fit, _chunk3OSXHLITcjs.VIDEO_FITS, `${path}.fit`),
426
+ blend: oneOf(v.blend, _chunk3OSXHLITcjs.VIDEO_BLEND_MODES, `${path}.blend`),
381
427
  grade: parseVideoGrade(v.grade, `${path}.grade`),
382
428
  animateIn: parseVideoClipAnimation(v.animateIn, `${path}.animateIn`),
383
429
  animateOut: parseVideoClipAnimation(v.animateOut, `${path}.animateOut`),
384
430
  fadeIn: num(v.fadeIn, `${path}.fadeIn`, { optional: true, min: 0 }),
385
431
  fadeOut: num(v.fadeOut, `${path}.fadeOut`, { optional: true, min: 0 }),
386
432
  transition: parseVideoTransition(v.transition, `${path}.transition`),
387
- effects: parseVideoClipEffects(v.effects, `${path}.effects`)
433
+ effects: parseVideoClipEffects(v.effects, `${path}.effects`),
434
+ mask: parseVideoClipMask(v.mask, `${path}.mask`)
388
435
  };
389
436
  if (type === "media")
390
437
  return compact({
@@ -396,8 +443,8 @@ function parseVideoClip(v, path = "clip") {
396
443
  volume: parseVideoAnimatable(v.volume, `${path}.volume`, { optional: true, min: 0, max: 4 }),
397
444
  muted: bool(v.muted, `${path}.muted`),
398
445
  timeRemap: parseVideoTimeRemap(v.timeRemap, v.speed, `${path}.timeRemap`),
399
- fadeInCurve: oneOf(v.fadeInCurve, _chunkVKF3PWKRcjs.VIDEO_FADE_CURVES, `${path}.fadeInCurve`),
400
- fadeOutCurve: oneOf(v.fadeOutCurve, _chunkVKF3PWKRcjs.VIDEO_FADE_CURVES, `${path}.fadeOutCurve`)
446
+ fadeInCurve: oneOf(v.fadeInCurve, _chunk3OSXHLITcjs.VIDEO_FADE_CURVES, `${path}.fadeInCurve`),
447
+ fadeOutCurve: oneOf(v.fadeOutCurve, _chunk3OSXHLITcjs.VIDEO_FADE_CURVES, `${path}.fadeOutCurve`)
401
448
  });
402
449
  if (type === "sequence") {
403
450
  for (const key of ["speed", "timeRemap"])
@@ -409,8 +456,8 @@ function parseVideoClip(v, path = "clip") {
409
456
  in: num(v.in, `${path}.in`, { optional: true, min: 0 }),
410
457
  volume: parseVideoAnimatable(v.volume, `${path}.volume`, { optional: true, min: 0, max: 4 }),
411
458
  muted: bool(v.muted, `${path}.muted`),
412
- fadeInCurve: oneOf(v.fadeInCurve, _chunkVKF3PWKRcjs.VIDEO_FADE_CURVES, `${path}.fadeInCurve`),
413
- fadeOutCurve: oneOf(v.fadeOutCurve, _chunkVKF3PWKRcjs.VIDEO_FADE_CURVES, `${path}.fadeOutCurve`)
459
+ fadeInCurve: oneOf(v.fadeInCurve, _chunk3OSXHLITcjs.VIDEO_FADE_CURVES, `${path}.fadeInCurve`),
460
+ fadeOutCurve: oneOf(v.fadeOutCurve, _chunk3OSXHLITcjs.VIDEO_FADE_CURVES, `${path}.fadeOutCurve`)
414
461
  });
415
462
  }
416
463
  if (type === "text")
@@ -424,6 +471,13 @@ function parseVideoClip(v, path = "clip") {
424
471
  fontWeight: num(v.fontWeight, `${path}.fontWeight`, { optional: true, min: 100, max: 1e3 }),
425
472
  background: str(v.background, `${path}.background`, true)
426
473
  });
474
+ if (type === "adjustment") {
475
+ const scope = oneOf(v.scope, _chunk3OSXHLITcjs.VIDEO_ADJUSTMENT_SCOPES, `${path}.scope`);
476
+ const targetTrack = str(v.targetTrack, `${path}.targetTrack`, true);
477
+ if (scope === "track" && !targetTrack) fail(`${path}.targetTrack`, 'is required when scope is "track": the id of the track whose pictures the adjustment changes');
478
+ if (targetTrack !== void 0 && scope !== "track") fail(`${path}.targetTrack`, 'is only for scope "track"');
479
+ return compact({ type, ...base, scope, targetTrack });
480
+ }
427
481
  if (type === "effect") {
428
482
  const ref = parseVideoEffectRef({ effect: v.effect, provider: v.provider, name: v.name === void 0 ? void 0 : v.name, values: v.values }, path);
429
483
  return compact({ type, ...base, ...ref, name: base.name });
@@ -521,9 +575,9 @@ function createVideoProject(options = {}) {
521
575
  // src/video-editor/check.ts
522
576
  function videoClipKeyframes(clip) {
523
577
  const out = [
524
- { property: "opacity", keys: _chunkVKF3PWKRcjs.videoKeysOf.call(void 0, clip.opacity) },
525
- { property: "volume", keys: clip.type === "media" || clip.type === "sequence" ? _chunkVKF3PWKRcjs.videoKeysOf.call(void 0, clip.volume) : [] },
526
- ..._chunkVKF3PWKRcjs.VIDEO_TRANSFORM_PROPERTIES.map((p) => ({ property: p, keys: _chunkVKF3PWKRcjs.videoKeysOf.call(void 0, _optionalChain([clip, 'access', _2 => _2.transform, 'optionalAccess', _3 => _3[p]])) }))
578
+ { property: "opacity", keys: _chunk3OSXHLITcjs.videoKeysOf.call(void 0, clip.opacity) },
579
+ { property: "volume", keys: clip.type === "media" || clip.type === "sequence" ? _chunk3OSXHLITcjs.videoKeysOf.call(void 0, clip.volume) : [] },
580
+ ..._chunk3OSXHLITcjs.VIDEO_TRANSFORM_PROPERTIES.map((p) => ({ property: p, keys: _chunk3OSXHLITcjs.videoKeysOf.call(void 0, _optionalChain([clip, 'access', _2 => _2.transform, 'optionalAccess', _3 => _3[p]])) }))
527
581
  ];
528
582
  return out.filter((o) => o.keys.length);
529
583
  }
@@ -545,13 +599,14 @@ var VIDEO_ISSUE_CODES = [
545
599
  "unused-asset",
546
600
  "unused-sequence",
547
601
  "marker-past-end",
602
+ "missing-target-track",
548
603
  "empty"
549
604
  ];
550
605
  var videoIssueKey = (i) => [i.code, _nullishCoalesce(i.sequence, () => ( "")), _nullishCoalesce(i.track, () => ( "")), _nullishCoalesce(i.clip, () => ( "")), _nullishCoalesce(i.asset, () => ( "")), _nullishCoalesce(i.marker, () => ( ""))].join("|");
551
606
  function checkVideoProject(project) {
552
607
  const issues = [];
553
- const assets = _chunkVKF3PWKRcjs.videoAssetMap.call(void 0, project);
554
- const sequences = _chunkVKF3PWKRcjs.videoSequenceMap.call(void 0, project);
608
+ const assets = _chunk3OSXHLITcjs.videoAssetMap.call(void 0, project);
609
+ const sequences = _chunk3OSXHLITcjs.videoSequenceMap.call(void 0, project);
555
610
  const used = /* @__PURE__ */ new Set();
556
611
  const usedSequences = /* @__PURE__ */ new Set();
557
612
  const cycles = /* @__PURE__ */ new Map();
@@ -559,7 +614,7 @@ function checkVideoProject(project) {
559
614
  if ((asset.kind === "video" || asset.kind === "audio") && asset.duration === void 0)
560
615
  issues.push({ code: "unknown-duration", severity: "warning", asset: asset.id, message: `Asset "${asset.id}" has no duration, so clips of it cannot be checked against its length; set "duration" in ms.` });
561
616
  const checkTrack = (track, sequence) => {
562
- const clips = _chunkVKF3PWKRcjs.sortedVideoClips.call(void 0, track);
617
+ const clips = _chunk3OSXHLITcjs.sortedVideoClips.call(void 0, track);
563
618
  const where = sequence ? ` in sequence "${sequence}"` : "";
564
619
  for (let i = 0; i < clips.length; i++) {
565
620
  const clip = clips[i];
@@ -569,13 +624,13 @@ function checkVideoProject(project) {
569
624
  const seq = sequences.get(clip.sequence);
570
625
  if (!seq) issues.push({ code: "missing-sequence", severity: "error", ...at, message: `Compound clip "${clip.id}" plays sequence "${clip.sequence}", which is not in the project; add the sequence or delete the clip.` });
571
626
  else {
572
- if (!cycles.has(seq.id)) cycles.set(seq.id, _chunkVKF3PWKRcjs.findVideoSequenceCycle.call(void 0, project, seq.id));
627
+ if (!cycles.has(seq.id)) cycles.set(seq.id, _chunk3OSXHLITcjs.findVideoSequenceCycle.call(void 0, project, seq.id));
573
628
  const cycle = cycles.get(seq.id);
574
629
  if (cycle) issues.push({ code: "sequence-cycle", severity: "error", ...at, message: `Compound clip "${clip.id}" plays sequence "${seq.id}", which contains itself (${cycle.join(" \u2192 ")}), so it is left out of the picture and sound; remove the compound clip that closes the loop.` });
575
- const length = _chunkVKF3PWKRcjs.videoSequenceDuration.call(void 0, seq);
576
- const range = _chunkVKF3PWKRcjs.videoClipSourceRange.call(void 0, clip);
577
- if (range.out > length + _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON)
578
- issues.push({ code: "source-overrun", severity: "error", ...at, message: `Compound clip "${clip.id}" plays to ${_chunkVKF3PWKRcjs.formatVideoSeconds.call(void 0, range.out)} of sequence "${seq.id}", which is only ${_chunkVKF3PWKRcjs.formatVideoSeconds.call(void 0, length)} long; shorten the clip or move its in point.` });
630
+ const length = _chunk3OSXHLITcjs.videoSequenceDuration.call(void 0, seq);
631
+ const range = _chunk3OSXHLITcjs.videoClipSourceRange.call(void 0, clip);
632
+ if (range.out > length + _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON)
633
+ issues.push({ code: "source-overrun", severity: "error", ...at, message: `Compound clip "${clip.id}" plays to ${_chunk3OSXHLITcjs.formatVideoSeconds.call(void 0, range.out)} of sequence "${seq.id}", which is only ${_chunk3OSXHLITcjs.formatVideoSeconds.call(void 0, length)} long; shorten the clip or move its in point.` });
579
634
  }
580
635
  } else if (clip.type === "media") {
581
636
  used.add(clip.asset);
@@ -584,28 +639,30 @@ function checkVideoProject(project) {
584
639
  else {
585
640
  if (track.kind === "audio" && asset.kind === "image") issues.push({ code: "wrong-track", severity: "error", ...at, message: `Clip "${clip.id}" is an image on audio track "${track.id}"; move it to a video track.` });
586
641
  if (track.kind === "video" && asset.kind === "audio") issues.push({ code: "wrong-track", severity: "error", ...at, message: `Clip "${clip.id}" is audio on video track "${track.id}"; move it to an audio track.` });
587
- const range = _chunkVKF3PWKRcjs.videoClipSourceRange.call(void 0, clip);
588
- if (asset.duration !== void 0 && range.out > asset.duration + _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON)
589
- issues.push({ code: "source-overrun", severity: "error", ...at, message: `Clip "${clip.id}" plays to ${_chunkVKF3PWKRcjs.formatVideoSeconds.call(void 0, range.out)} of "${asset.id}", which is only ${_chunkVKF3PWKRcjs.formatVideoSeconds.call(void 0, asset.duration)} long; shorten the clip or move its in point.` });
642
+ const range = _chunk3OSXHLITcjs.videoClipSourceRange.call(void 0, clip);
643
+ if (asset.duration !== void 0 && range.out > asset.duration + _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON)
644
+ issues.push({ code: "source-overrun", severity: "error", ...at, message: `Clip "${clip.id}" plays to ${_chunk3OSXHLITcjs.formatVideoSeconds.call(void 0, range.out)} of "${asset.id}", which is only ${_chunk3OSXHLITcjs.formatVideoSeconds.call(void 0, asset.duration)} long; shorten the clip or move its in point.` });
590
645
  }
591
646
  } else if (track.kind === "audio") issues.push({ code: "wrong-track", severity: "error", ...at, message: `Clip "${clip.id}" is a ${clip.type} clip on audio track "${track.id}"; move it to a video track.` });
647
+ if (clip.type === "adjustment" && (_nullishCoalesce(clip.scope, () => ( "underneath"))) === "track" && !(sequence ? _nullishCoalesce(_optionalChain([sequences, 'access', _4 => _4.get, 'call', _5 => _5(sequence), 'optionalAccess', _6 => _6.tracks]), () => ( [])) : project.tracks).some((t) => t.id === clip.targetTrack))
648
+ issues.push({ code: "missing-target-track", severity: "warning", ...at, message: `Adjustment clip "${clip.id}" changes track "${clip.targetTrack}", which is not ${sequence ? `in sequence "${sequence}"` : "in the project"}, so it changes nothing; set its targetTrack to a track beside it or change its scope.` });
592
649
  const next = clips[i + 1];
593
- if (next && next.start < _chunkVKF3PWKRcjs.videoClipEnd.call(void 0, clip) - _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON)
594
- issues.push({ code: "overlap", severity: "error", ...at, message: `Clips "${clip.id}" and "${next.id}" overlap on track "${track.id}"${where} from ${_chunkVKF3PWKRcjs.formatVideoSeconds.call(void 0, next.start)}; move one of them.` });
595
- if ((_nullishCoalesce(clip.fadeIn, () => ( 0))) + (_nullishCoalesce(clip.fadeOut, () => ( 0))) > clip.duration + _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON)
650
+ if (next && next.start < _chunk3OSXHLITcjs.videoClipEnd.call(void 0, clip) - _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON)
651
+ issues.push({ code: "overlap", severity: "error", ...at, message: `Clips "${clip.id}" and "${next.id}" overlap on track "${track.id}"${where} from ${_chunk3OSXHLITcjs.formatVideoSeconds.call(void 0, next.start)}; move one of them.` });
652
+ if ((_nullishCoalesce(clip.fadeIn, () => ( 0))) + (_nullishCoalesce(clip.fadeOut, () => ( 0))) > clip.duration + _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON)
596
653
  issues.push({ code: "fades-too-long", severity: "warning", ...at, message: `Clip "${clip.id}"'s fades (${_nullishCoalesce(clip.fadeIn, () => ( 0))} + ${_nullishCoalesce(clip.fadeOut, () => ( 0))} ms) are longer than the clip (${clip.duration} ms); shorten them.` });
597
- const anim = (clip.animateIn ? _nullishCoalesce(clip.animateIn.duration, () => ( _chunkVKF3PWKRcjs.VIDEO_CLIP_ANIMATION_DURATION)) : 0) + (clip.animateOut ? _nullishCoalesce(clip.animateOut.duration, () => ( _chunkVKF3PWKRcjs.VIDEO_CLIP_ANIMATION_DURATION)) : 0);
598
- if (anim > clip.duration + _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON)
654
+ const anim = (clip.animateIn ? _nullishCoalesce(clip.animateIn.duration, () => ( _chunk3OSXHLITcjs.VIDEO_CLIP_ANIMATION_DURATION)) : 0) + (clip.animateOut ? _nullishCoalesce(clip.animateOut.duration, () => ( _chunk3OSXHLITcjs.VIDEO_CLIP_ANIMATION_DURATION)) : 0);
655
+ if (anim > clip.duration + _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON)
599
656
  issues.push({ code: "animations-too-long", severity: "warning", ...at, message: `Clip "${clip.id}"'s entrance and exit (${anim} ms together) are longer than the clip (${clip.duration} ms), so both are shortened; give them shorter durations.` });
600
657
  for (const which of ["animateIn", "animateOut"]) {
601
658
  const a = clip[which];
602
659
  if (!a) continue;
603
- const keyed = _chunkVKF3PWKRcjs.videoClipAnimationProperties.call(void 0, a).filter((p) => _chunkVKF3PWKRcjs.videoKeysOf.call(void 0, p === "opacity" ? clip.opacity : _optionalChain([clip, 'access', _4 => _4.transform, 'optionalAccess', _5 => _5[p]])).length);
660
+ const keyed = _chunk3OSXHLITcjs.videoClipAnimationProperties.call(void 0, a).filter((p) => _chunk3OSXHLITcjs.videoKeysOf.call(void 0, p === "opacity" ? clip.opacity : _optionalChain([clip, 'access', _7 => _7.transform, 'optionalAccess', _8 => _8[p]])).length);
604
661
  if (keyed.length)
605
662
  issues.push({ code: "animation-keyed", severity: "note", ...at, message: `Clip "${clip.id}"'s ${a.effect} ${which === "animateIn" ? "entrance" : "exit"} leaves ${keyed.join(", ")} alone, because the clip keys ${keyed.length > 1 ? "them" : "it"} itself; remove those keyframes or the animation.` });
606
663
  }
607
664
  for (const { property, keys } of videoClipKeyframes(clip)) {
608
- const outside = keys.filter((k) => k.t < -_chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON || k.t > clip.duration + _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON);
665
+ const outside = keys.filter((k) => k.t < -_chunk3OSXHLITcjs.VIDEO_TIME_EPSILON || k.t > clip.duration + _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON);
609
666
  if (outside.length)
610
667
  issues.push({
611
668
  code: "keyframes-outside-clip",
@@ -616,20 +673,20 @@ function checkVideoProject(project) {
616
673
  }
617
674
  if (clip.transition) {
618
675
  const d = clip.transition.duration;
619
- const before = _chunkVKF3PWKRcjs.videoClipBefore.call(void 0, track, clip);
676
+ const before = _chunk3OSXHLITcjs.videoClipBefore.call(void 0, track, clip);
620
677
  if (!before) {
621
678
  issues.push({ code: "transition-without-cut", severity: "warning", ...at, message: `Clip "${clip.id}" has a transition but no clip ends where it starts on track "${track.id}", so it plays as nothing; butt a clip up against it or remove the transition.` });
622
679
  continue;
623
680
  }
624
- if (d > clip.duration + _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON || d > before.duration + _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON) {
681
+ if (d > clip.duration + _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON || d > before.duration + _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON) {
625
682
  issues.push({ code: "transition-too-long", severity: "error", ...at, message: `The ${d} ms transition into "${clip.id}" is longer than "${clip.id}" (${clip.duration} ms) or "${before.id}" (${before.duration} ms); make it at most ${Math.min(clip.duration, before.duration)} ms.` });
626
683
  continue;
627
684
  }
628
685
  if (clip.transition.type === "dip") continue;
629
- const outgoing = _chunkVKF3PWKRcjs.videoClipHandles.call(void 0, before, assets, sequences).after;
630
- const incoming = _chunkVKF3PWKRcjs.videoClipHandles.call(void 0, clip, assets, sequences).before;
686
+ const outgoing = _chunk3OSXHLITcjs.videoClipHandles.call(void 0, before, assets, sequences).after;
687
+ const incoming = _chunk3OSXHLITcjs.videoClipHandles.call(void 0, clip, assets, sequences).before;
631
688
  const half = d / 2;
632
- if (outgoing < half - _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON || incoming < half - _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON) {
689
+ if (outgoing < half - _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON || incoming < half - _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON) {
633
690
  const fits = 2 * Math.min(outgoing, incoming);
634
691
  issues.push({
635
692
  code: "transition-handles",
@@ -646,9 +703,9 @@ function checkVideoProject(project) {
646
703
  for (const sequence of _nullishCoalesce(project.sequences, () => ( [])))
647
704
  if (!usedSequences.has(sequence.id)) issues.push({ code: "unused-sequence", severity: "note", sequence: sequence.id, message: `Sequence "${sequence.id}" is not played by any compound clip.` });
648
705
  for (const asset of project.assets) if (!used.has(asset.id)) issues.push({ code: "unused-asset", severity: "note", asset: asset.id, message: `Asset "${asset.id}" is not used by any clip.` });
649
- const end = _chunkVKF3PWKRcjs.videoProjectDuration.call(void 0, project);
706
+ const end = _chunk3OSXHLITcjs.videoProjectDuration.call(void 0, project);
650
707
  for (const marker of _nullishCoalesce(project.markers, () => ( [])))
651
- if (marker.time > end + _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON) issues.push({ code: "marker-past-end", severity: "note", marker: marker.id, message: `Marker "${marker.id}" at ${_chunkVKF3PWKRcjs.formatVideoSeconds.call(void 0, marker.time)} is past the end (${_chunkVKF3PWKRcjs.formatVideoSeconds.call(void 0, end)}).` });
708
+ if (marker.time > end + _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON) issues.push({ code: "marker-past-end", severity: "note", marker: marker.id, message: `Marker "${marker.id}" at ${_chunk3OSXHLITcjs.formatVideoSeconds.call(void 0, marker.time)} is past the end (${_chunk3OSXHLITcjs.formatVideoSeconds.call(void 0, end)}).` });
652
709
  if (!project.tracks.some((t) => t.clips.length)) issues.push({ code: "empty", severity: "note", message: "The project has no clips yet; add assets and then clips." });
653
710
  const rank = { error: 0, warning: 1, note: 2 };
654
711
  return issues.sort((a, b) => rank[a.severity] - rank[b.severity]);
@@ -702,7 +759,7 @@ function mergeSpans(spans) {
702
759
  const out = [];
703
760
  for (const [from, to] of [...spans].sort((a, b) => a[0] - b[0])) {
704
761
  const last = out[out.length - 1];
705
- if (last && from <= last[1] + _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON) last[1] = Math.max(last[1], to);
762
+ if (last && from <= last[1] + _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON) last[1] = Math.max(last[1], to);
706
763
  else out.push([from, to]);
707
764
  }
708
765
  return out;
@@ -716,16 +773,16 @@ function clipAnimatables(clip) {
716
773
  // A speed ramp is keys in clip time too, so trims and splits keep it on the same frames of source.
717
774
  { get: () => c.timeRemap, set: (v) => c.timeRemap = v }
718
775
  ];
719
- for (const p of _chunkVKF3PWKRcjs.VIDEO_TRANSFORM_PROPERTIES) out.push({ get: () => _optionalChain([c, 'access', _6 => _6.transform, 'optionalAccess', _7 => _7[p]]), set: (v) => c.transform && (c.transform[p] = v) });
776
+ for (const p of _chunk3OSXHLITcjs.VIDEO_TRANSFORM_PROPERTIES) out.push({ get: () => _optionalChain([c, 'access', _9 => _9.transform, 'optionalAccess', _10 => _10[p]]), set: (v) => c.transform && (c.transform[p] = v) });
720
777
  return out;
721
778
  }
722
779
  function retimeClipKeys(clip, fn) {
723
780
  for (const a of clipAnimatables(clip)) {
724
781
  const v = a.get();
725
- if (_chunkVKF3PWKRcjs.isVideoKeyframes.call(void 0, v)) a.set(fn(v));
782
+ if (_chunk3OSXHLITcjs.isVideoKeyframes.call(void 0, v)) a.set(fn(v));
726
783
  }
727
784
  }
728
- var OPTIONAL_CLIP_FIELDS = ["name", "effects", "values", "provider", "opacity", "transform", "fit", "blend", "grade", "animateIn", "animateOut", "fadeIn", "fadeOut", "fadeInCurve", "fadeOutCurve", "transition", "in", "speed", "timeRemap", "volume", "muted", "color", "fontSize", "fontFamily", "fontWeight", "background"];
785
+ var OPTIONAL_CLIP_FIELDS = ["name", "effects", "mask", "scope", "targetTrack", "values", "provider", "opacity", "transform", "fit", "blend", "grade", "animateIn", "animateOut", "fadeIn", "fadeOut", "fadeInCurve", "fadeOutCurve", "transition", "in", "speed", "timeRemap", "volume", "muted", "color", "fontSize", "fontFamily", "fontWeight", "background"];
729
786
  var TIME_FIELDS = { slip_clip: ["delta"], roll_edit: ["delta"], slide_clip: ["delta"], lift_range: ["from", "to"], extract_range: ["from", "to"], split_at: ["at"] };
730
787
  var Editor = (_class = class {
731
788
  constructor(p) {;_class.prototype.__init.call(this);
@@ -739,8 +796,8 @@ var Editor = (_class = class {
739
796
  return t;
740
797
  }
741
798
  clip(id) {
742
- const at = _chunkVKF3PWKRcjs.findVideoClip.call(void 0, this.p, id);
743
- const nested = at ? null : _chunkVKF3PWKRcjs.findVideoSequenceClip.call(void 0, this.p, id);
799
+ const at = _chunk3OSXHLITcjs.findVideoClip.call(void 0, this.p, id);
800
+ const nested = at ? null : _chunk3OSXHLITcjs.findVideoSequenceClip.call(void 0, this.p, id);
744
801
  if (nested) refuse(`Clip "${id}" is inside sequence "${nested.sequence.id}", and clips inside sequences cannot be edited yet; edit the compound clip that plays the sequence instead (list_video_sequences shows which).`);
745
802
  if (!at) {
746
803
  const ids = this.p.tracks.flatMap((t) => t.clips.map((c) => c.id));
@@ -757,12 +814,18 @@ var Editor = (_class = class {
757
814
  return a;
758
815
  }
759
816
  fits(track, start, duration, ignore, what) {
760
- const hit = _chunkVKF3PWKRcjs.videoOverlap.call(void 0, track, start, start + duration, ignore);
817
+ const hit = _chunk3OSXHLITcjs.videoOverlap.call(void 0, track, start, start + duration, ignore);
761
818
  if (hit) {
762
- const free = _chunkVKF3PWKRcjs.nextFreeVideoSlot.call(void 0, track, start, duration, ignore);
763
- refuse(`${what} at ${_chunkVKF3PWKRcjs.formatVideoSeconds.call(void 0, start)}\u2013${_chunkVKF3PWKRcjs.formatVideoSeconds.call(void 0, start + duration)} would overlap clip "${hit.id}" (${_chunkVKF3PWKRcjs.formatVideoSeconds.call(void 0, hit.start)}\u2013${_chunkVKF3PWKRcjs.formatVideoSeconds.call(void 0, _chunkVKF3PWKRcjs.videoClipEnd.call(void 0, hit))}) on track "${track.id}"; the next free place is ${_chunkVKF3PWKRcjs.formatVideoSeconds.call(void 0, free)}, or use mode "insert" to push later clips along or "overwrite" to replace what is there.`);
819
+ const free = _chunk3OSXHLITcjs.nextFreeVideoSlot.call(void 0, track, start, duration, ignore);
820
+ refuse(`${what} at ${_chunk3OSXHLITcjs.formatVideoSeconds.call(void 0, start)}\u2013${_chunk3OSXHLITcjs.formatVideoSeconds.call(void 0, start + duration)} would overlap clip "${hit.id}" (${_chunk3OSXHLITcjs.formatVideoSeconds.call(void 0, hit.start)}\u2013${_chunk3OSXHLITcjs.formatVideoSeconds.call(void 0, _chunk3OSXHLITcjs.videoClipEnd.call(void 0, hit))}) on track "${track.id}"; the next free place is ${_chunk3OSXHLITcjs.formatVideoSeconds.call(void 0, free)}, or use mode "insert" to push later clips along or "overwrite" to replace what is there.`);
764
821
  }
765
822
  }
823
+ /** An adjustment clip's target track must exist. */
824
+ adjustmentTarget(clip) {
825
+ if (clip.type !== "adjustment" || clip.targetTrack === void 0) return;
826
+ const ids = _chunk3OSXHLITcjs.allVideoTracks.call(void 0, this.p).map(({ track }) => track.id);
827
+ if (!ids.includes(clip.targetTrack)) refuse(`Adjustment clip "${clip.id}" targets track ${_chunkEBDZWIOPcjs.namedWithSuggestion.call(void 0, clip.targetTrack, ids)}, which does not exist; the tracks are ${ids.map((t) => `"${t}"`).join(", ")}.`);
828
+ }
766
829
  kindFits(track, clip) {
767
830
  const asset = clip.type === "media" ? this.asset(clip.asset) : null;
768
831
  if (clip.type === "sequence") {
@@ -771,19 +834,19 @@ var Editor = (_class = class {
771
834
  return;
772
835
  }
773
836
  if (track.kind === "audio" && (clip.type !== "media" || asset.kind === "image")) refuse(`Track "${track.id}" is an audio track; ${clip.type === "media" ? "images" : `${clip.type} clips`} go on a video track.`);
774
- if (track.kind === "video" && _optionalChain([asset, 'optionalAccess', _8 => _8.kind]) === "audio") refuse(`Track "${track.id}" is a video track; audio asset "${asset.id}" goes on an audio track.`);
837
+ if (track.kind === "video" && _optionalChain([asset, 'optionalAccess', _11 => _11.kind]) === "audio") refuse(`Track "${track.id}" is a video track; audio asset "${asset.id}" goes on an audio track.`);
775
838
  }
776
839
  /** Moves every clip on `track` starting at or after `from` by `delta`. */
777
840
  shift(track, from, delta, except = /* @__PURE__ */ new Set()) {
778
841
  if (!delta) return;
779
- for (const c of track.clips) if (!except.has(c.id) && c.start >= from - _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON) c.start = Math.max(0, c.start + delta);
842
+ for (const c of track.clips) if (!except.has(c.id) && c.start >= from - _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON) c.start = Math.max(0, c.start + delta);
780
843
  track.clips.sort((a, b) => a.start - b.start);
781
844
  }
782
845
  get frame() {
783
846
  return 1e3 / this.p.fps;
784
847
  }
785
848
  ids() {
786
- return _chunkVKF3PWKRcjs.allVideoClipIds.call(void 0, this.p);
849
+ return _chunk3OSXHLITcjs.allVideoClipIds.call(void 0, this.p);
787
850
  }
788
851
  /**
789
852
  * The tracks a ripple on `sources` moves, top to bottom: the sources, and every unlocked
@@ -791,8 +854,8 @@ var Editor = (_class = class {
791
854
  */
792
855
  rippleTracks(sources) {
793
856
  const set = new Set(sources);
794
- if ([...set].some(_chunkVKF3PWKRcjs.videoTrackSyncLocked)) {
795
- for (const t of this.p.tracks) if (_chunkVKF3PWKRcjs.videoTrackSyncLocked.call(void 0, t) && !t.locked) set.add(t);
857
+ if ([...set].some(_chunk3OSXHLITcjs.videoTrackSyncLocked)) {
858
+ for (const t of this.p.tracks) if (_chunk3OSXHLITcjs.videoTrackSyncLocked.call(void 0, t) && !t.locked) set.add(t);
796
859
  }
797
860
  return this.p.tracks.filter((t) => set.has(t));
798
861
  }
@@ -809,7 +872,7 @@ var Editor = (_class = class {
809
872
  return open;
810
873
  }
811
874
  range(from, to) {
812
- if (!(to - from > _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON)) refuse(`"to" (${_chunkVKF3PWKRcjs.formatVideoSeconds.call(void 0, to)}) must be after "from" (${_chunkVKF3PWKRcjs.formatVideoSeconds.call(void 0, from)}).`);
875
+ if (!(to - from > _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON)) refuse(`"to" (${_chunk3OSXHLITcjs.formatVideoSeconds.call(void 0, to)}) must be after "from" (${_chunk3OSXHLITcjs.formatVideoSeconds.call(void 0, from)}).`);
813
876
  }
814
877
  /** Moves a media or compound clip's in point by `delta` of timeline time (stills, titles and colours have none). */
815
878
  moveIn(clip, delta) {
@@ -818,18 +881,18 @@ var Editor = (_class = class {
818
881
  if (clip.in < 1e-9) delete clip.in;
819
882
  return;
820
883
  }
821
- if (clip.type !== "media" || _optionalChain([_chunkVKF3PWKRcjs.videoAssetMap.call(void 0, this.p), 'access', _9 => _9.get, 'call', _10 => _10(clip.asset), 'optionalAccess', _11 => _11.kind]) === "image") return;
822
- clip.in = Math.max(0, (_nullishCoalesce(clip.in, () => ( 0))) + delta * _chunkVKF3PWKRcjs.videoClipSpeed.call(void 0, clip));
884
+ if (clip.type !== "media" || _optionalChain([_chunk3OSXHLITcjs.videoAssetMap.call(void 0, this.p), 'access', _12 => _12.get, 'call', _13 => _13(clip.asset), 'optionalAccess', _14 => _14.kind]) === "image") return;
885
+ clip.in = Math.max(0, (_nullishCoalesce(clip.in, () => ( 0))) + delta * _chunk3OSXHLITcjs.videoClipSpeed.call(void 0, clip));
823
886
  if (clip.in < 1e-9) delete clip.in;
824
887
  }
825
888
  /** Moves a clip's head `delta` later in time (start, in point and keys); the caller sets start and duration. */
826
889
  trimHead(clip, delta) {
827
890
  if (!isRamped(clip)) this.moveIn(clip, delta);
828
- retimeClipKeys(clip, (v) => _chunkVKF3PWKRcjs.retimeVideoAnimatable.call(void 0, v, -delta));
891
+ retimeClipKeys(clip, (v) => _chunk3OSXHLITcjs.retimeVideoAnimatable.call(void 0, v, -delta));
829
892
  }
830
893
  /** Cuts `clip` at `at` (inside it): the left part keeps the id, the right part is returned. */
831
894
  cut(track, clip, at, id) {
832
- const end = _chunkVKF3PWKRcjs.videoClipEnd.call(void 0, clip);
895
+ const end = _chunk3OSXHLITcjs.videoClipEnd.call(void 0, clip);
833
896
  const right = { ...structuredClone(clip), id: _nullishCoalesce(id, () => ( freshId(`${clip.id}-`, this.ids()))), start: at, duration: end - at };
834
897
  delete right.transition;
835
898
  delete right.fadeIn;
@@ -837,9 +900,9 @@ var Editor = (_class = class {
837
900
  delete right.animateIn;
838
901
  const leftValues = clipAnimatables(clip);
839
902
  clipAnimatables(right).forEach((r, i) => {
840
- const [l, rr] = _chunkVKF3PWKRcjs.splitVideoAnimatable.call(void 0, leftValues[i].get(), at - clip.start);
841
- if (_chunkVKF3PWKRcjs.isVideoKeyframes.call(void 0, l)) leftValues[i].set(l);
842
- if (_chunkVKF3PWKRcjs.isVideoKeyframes.call(void 0, rr)) r.set(rr);
903
+ const [l, rr] = _chunk3OSXHLITcjs.splitVideoAnimatable.call(void 0, leftValues[i].get(), at - clip.start);
904
+ if (_chunk3OSXHLITcjs.isVideoKeyframes.call(void 0, l)) leftValues[i].set(l);
905
+ if (_chunk3OSXHLITcjs.isVideoKeyframes.call(void 0, rr)) r.set(rr);
843
906
  });
844
907
  if (!isRamped(right)) this.moveIn(right, at - clip.start);
845
908
  clip.duration = at - clip.start;
@@ -853,7 +916,7 @@ var Editor = (_class = class {
853
916
  /** Cuts every clip on `tracks` that runs across `at`; returns the new right-hand parts. */
854
917
  cutAcross(tracks, at) {
855
918
  const made = [];
856
- for (const t of tracks) for (const c of [...t.clips]) if (c.start < at - _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON && _chunkVKF3PWKRcjs.videoClipEnd.call(void 0, c) > at + _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON) made.push(this.cut(t, c, at));
919
+ for (const t of tracks) for (const c of [...t.clips]) if (c.start < at - _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON && _chunk3OSXHLITcjs.videoClipEnd.call(void 0, c) > at + _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON) made.push(this.cut(t, c, at));
857
920
  return made;
858
921
  }
859
922
  /**
@@ -871,10 +934,10 @@ var Editor = (_class = class {
871
934
  */
872
935
  close(tracks, from, to) {
873
936
  for (const t of tracks) {
874
- const hit = _chunkVKF3PWKRcjs.videoOverlap.call(void 0, t, from, to);
937
+ const hit = _chunk3OSXHLITcjs.videoOverlap.call(void 0, t, from, to);
875
938
  if (hit)
876
939
  refuse(
877
- `Rippling out ${_chunkVKF3PWKRcjs.formatVideoSeconds.call(void 0, from)}\u2013${_chunkVKF3PWKRcjs.formatVideoSeconds.call(void 0, to)} would pull track "${t.id}" out of sync: clip "${hit.id}" (${_chunkVKF3PWKRcjs.formatVideoSeconds.call(void 0, hit.start)}\u2013${_chunkVKF3PWKRcjs.formatVideoSeconds.call(void 0, _chunkVKF3PWKRcjs.videoClipEnd.call(void 0, hit))}) is in that stretch. Extract the range across "${t.id}" too (extract_range with its id in "tracks"), turn its sync lock off (update_track with syncLocked false) or lock it, or lift instead of rippling.`
940
+ `Rippling out ${_chunk3OSXHLITcjs.formatVideoSeconds.call(void 0, from)}\u2013${_chunk3OSXHLITcjs.formatVideoSeconds.call(void 0, to)} would pull track "${t.id}" out of sync: clip "${hit.id}" (${_chunk3OSXHLITcjs.formatVideoSeconds.call(void 0, hit.start)}\u2013${_chunk3OSXHLITcjs.formatVideoSeconds.call(void 0, _chunk3OSXHLITcjs.videoClipEnd.call(void 0, hit))}) is in that stretch. Extract the range across "${t.id}" too (extract_range with its id in "tracks"), turn its sync lock off (update_track with syncLocked false) or lock it, or lift instead of rippling.`
878
941
  );
879
942
  }
880
943
  for (const t of tracks) this.shift(t, to, from - to);
@@ -886,10 +949,10 @@ var Editor = (_class = class {
886
949
  clear(track, from, to, report) {
887
950
  for (const c of [...track.clips]) {
888
951
  const start = c.start;
889
- const end = _chunkVKF3PWKRcjs.videoClipEnd.call(void 0, c);
890
- if (end <= from + _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON || start >= to - _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON) continue;
891
- const coversHead = start >= from - _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON;
892
- const coversTail = end <= to + _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON;
952
+ const end = _chunk3OSXHLITcjs.videoClipEnd.call(void 0, c);
953
+ if (end <= from + _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON || start >= to - _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON) continue;
954
+ const coversHead = start >= from - _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON;
955
+ const coversTail = end <= to + _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON;
893
956
  if (coversHead && coversTail) {
894
957
  track.clips = track.clips.filter((x) => x !== c);
895
958
  report.removed.push(c.id);
@@ -912,8 +975,8 @@ var Editor = (_class = class {
912
975
  /** Clamps a slip, roll or slide to `[lo, hi]`, refusing when it cannot move that way at all. */
913
976
  clamp(delta, lo, hi, why) {
914
977
  const applied = Math.min(Math.max(delta, Math.min(0, lo)), Math.max(0, hi));
915
- if (Math.abs(delta) > _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON && Math.abs(applied) <= _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON) refuse(why());
916
- return Math.abs(applied) <= _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON ? 0 : applied;
978
+ if (Math.abs(delta) > _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON && Math.abs(applied) <= _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON) refuse(why());
979
+ return Math.abs(applied) <= _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON ? 0 : applied;
917
980
  }
918
981
  /** Places a clip on a track by `mode`, returning what it cut, trimmed, removed and rippled. */
919
982
  place(track, clip, mode, outcome) {
@@ -923,14 +986,14 @@ var Editor = (_class = class {
923
986
  outcome.rippled = tracks.map((t) => t.id);
924
987
  } else if (mode === "overwrite") {
925
988
  const report = { removed: [], trimmed: [], created: [] };
926
- this.clear(track, clip.start, _chunkVKF3PWKRcjs.videoClipEnd.call(void 0, clip), report);
989
+ this.clear(track, clip.start, _chunk3OSXHLITcjs.videoClipEnd.call(void 0, clip), report);
927
990
  Object.assign(outcome, report);
928
991
  } else this.fits(track, clip.start, clip.duration, /* @__PURE__ */ new Set([clip.id]), `Clip "${clip.id}"`);
929
992
  track.clips.push(clip);
930
993
  track.clips.sort((a, b) => a.start - b.start);
931
994
  }
932
995
  placement(mode, ripple) {
933
- if (mode !== void 0 && !_chunkVKF3PWKRcjs.VIDEO_PLACEMENT_MODES.includes(mode)) refuse(`"mode" must be ${_chunkVKF3PWKRcjs.VIDEO_PLACEMENT_MODES.map((m) => `"${m}"`).join(", ")}.`);
996
+ if (mode !== void 0 && !_chunk3OSXHLITcjs.VIDEO_PLACEMENT_MODES.includes(mode)) refuse(`"mode" must be ${_chunk3OSXHLITcjs.VIDEO_PLACEMENT_MODES.map((m) => `"${m}"`).join(", ")}.`);
934
997
  if (ripple && mode !== void 0 && mode !== "insert") refuse(`"ripple" means mode "insert", so it cannot go with mode "${mode}"; pass one or the other.`);
935
998
  return _nullishCoalesce(mode, () => ( (ripple ? "insert" : "refuse")));
936
999
  }
@@ -967,7 +1030,7 @@ var Editor = (_class = class {
967
1030
  }
968
1031
  case "remove_asset": {
969
1032
  const asset = this.asset(e.asset);
970
- const users = _chunkVKF3PWKRcjs.allVideoTracks.call(void 0, this.p).flatMap(({ track: t }) => t.clips.filter((c) => c.type === "media" && c.asset === asset.id).map((c) => c.id));
1033
+ const users = _chunk3OSXHLITcjs.allVideoTracks.call(void 0, this.p).flatMap(({ track: t }) => t.clips.filter((c) => c.type === "media" && c.asset === asset.id).map((c) => c.id));
971
1034
  if (users.length) refuse(`Asset "${asset.id}" is used by clip${users.length > 1 ? "s" : ""} ${users.map((u) => `"${u}"`).join(", ")}; delete ${users.length > 1 ? "them" : "it"} first.`);
972
1035
  this.p.assets = this.p.assets.filter((a) => a !== asset);
973
1036
  return this.outcomes.push({ op: e.op, asset: asset.id });
@@ -1020,14 +1083,15 @@ var Editor = (_class = class {
1020
1083
  else draft.duration = (asset.duration - (_nullishCoalesce(draft.in, () => ( 0)))) / (_nullishCoalesce(draft.speed, () => ( 1)));
1021
1084
  if (!(draft.duration > 0)) refuse(`"in" (${draft.in} ms) is at or past the end of "${asset.id}" (${asset.duration} ms).`);
1022
1085
  } else if (draft.type === "sequence") {
1023
- const length = _chunkVKF3PWKRcjs.videoSequenceDuration.call(void 0, _chunkVKF3PWKRcjs.videoSequenceMap.call(void 0, this.p).get(draft.sequence)) - (_nullishCoalesce(draft.in, () => ( 0)));
1086
+ const length = _chunk3OSXHLITcjs.videoSequenceDuration.call(void 0, _chunk3OSXHLITcjs.videoSequenceMap.call(void 0, this.p).get(draft.sequence)) - (_nullishCoalesce(draft.in, () => ( 0)));
1024
1087
  if (!(length > 0)) refuse(`"in" (${_nullishCoalesce(draft.in, () => ( 0))} ms) is at or past the end of sequence "${draft.sequence}".`);
1025
1088
  draft.duration = length;
1026
1089
  } else draft.duration = VIDEO_DEFAULT_TITLE_DURATION;
1027
1090
  }
1028
- const end = _chunkVKF3PWKRcjs.sortedVideoClips.call(void 0, track).reduce((m, c) => Math.max(m, _chunkVKF3PWKRcjs.videoClipEnd.call(void 0, c)), 0);
1091
+ const end = _chunk3OSXHLITcjs.sortedVideoClips.call(void 0, track).reduce((m, c) => Math.max(m, _chunk3OSXHLITcjs.videoClipEnd.call(void 0, c)), 0);
1029
1092
  draft.start ??= end;
1030
1093
  const clip = parseVideoClip(draft, "clip");
1094
+ this.adjustmentTarget(clip);
1031
1095
  const outcome = { op: e.op, clip: clip.id, track: track.id };
1032
1096
  this.place(track, clip, mode, outcome);
1033
1097
  return this.outcomes.push(outcome);
@@ -1050,20 +1114,20 @@ var Editor = (_class = class {
1050
1114
  case "trim_clip": {
1051
1115
  const { track, clip } = this.clip(e.clip);
1052
1116
  this.unlocked(track);
1053
- const speed = _chunkVKF3PWKRcjs.videoClipSpeed.call(void 0, clip);
1054
- const end = _chunkVKF3PWKRcjs.videoClipEnd.call(void 0, clip);
1117
+ const speed = _chunk3OSXHLITcjs.videoClipSpeed.call(void 0, clip);
1118
+ const end = _chunk3OSXHLITcjs.videoClipEnd.call(void 0, clip);
1055
1119
  const minimum = 1e3 / this.p.fps;
1056
- const assets = _chunkVKF3PWKRcjs.videoAssetMap.call(void 0, this.p);
1120
+ const assets = _chunk3OSXHLITcjs.videoAssetMap.call(void 0, this.p);
1057
1121
  if (e.edge === "start") {
1058
1122
  const delta = e.to - clip.start;
1059
- if (clip.duration - delta < minimum - _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON) refuse(`Trimming the start to ${_chunkVKF3PWKRcjs.formatVideoSeconds.call(void 0, e.to)} would leave "${clip.id}" shorter than a frame; it ends at ${_chunkVKF3PWKRcjs.formatVideoSeconds.call(void 0, end)}.`);
1123
+ if (clip.duration - delta < minimum - _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON) refuse(`Trimming the start to ${_chunk3OSXHLITcjs.formatVideoSeconds.call(void 0, e.to)} would leave "${clip.id}" shorter than a frame; it ends at ${_chunk3OSXHLITcjs.formatVideoSeconds.call(void 0, end)}.`);
1060
1124
  if (clip.type === "media" && !isRamped(clip) || clip.type === "sequence") {
1061
1125
  const sourceIn = (_nullishCoalesce(clip.in, () => ( 0))) + delta * speed;
1062
- if (sourceIn < -_chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON) refuse(`"${clip.id}" starts ${Math.round((_nullishCoalesce(clip.in, () => ( 0))) / speed)} ms into its source, so its start can move at most that far earlier (to ${_chunkVKF3PWKRcjs.formatVideoSeconds.call(void 0, clip.start - (_nullishCoalesce(clip.in, () => ( 0))) / speed)}).`);
1126
+ if (sourceIn < -_chunk3OSXHLITcjs.VIDEO_TIME_EPSILON) refuse(`"${clip.id}" starts ${Math.round((_nullishCoalesce(clip.in, () => ( 0))) / speed)} ms into its source, so its start can move at most that far earlier (to ${_chunk3OSXHLITcjs.formatVideoSeconds.call(void 0, clip.start - (_nullishCoalesce(clip.in, () => ( 0))) / speed)}).`);
1063
1127
  clip.in = Math.max(0, sourceIn);
1064
1128
  if (clip.in === 0) delete clip.in;
1065
1129
  }
1066
- retimeClipKeys(clip, (v) => _chunkVKF3PWKRcjs.retimeVideoAnimatable.call(void 0, v, -delta));
1130
+ retimeClipKeys(clip, (v) => _chunk3OSXHLITcjs.retimeVideoAnimatable.call(void 0, v, -delta));
1067
1131
  if (e.ripple) {
1068
1132
  const tracks = this.rippleTracks([track]);
1069
1133
  if (delta > 0) {
@@ -1084,17 +1148,17 @@ var Editor = (_class = class {
1084
1148
  }
1085
1149
  } else {
1086
1150
  const duration = e.to - clip.start;
1087
- if (duration < minimum - _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON) refuse(`Trimming the end to ${_chunkVKF3PWKRcjs.formatVideoSeconds.call(void 0, e.to)} would leave "${clip.id}" shorter than a frame; it starts at ${_chunkVKF3PWKRcjs.formatVideoSeconds.call(void 0, clip.start)}.`);
1151
+ if (duration < minimum - _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON) refuse(`Trimming the end to ${_chunk3OSXHLITcjs.formatVideoSeconds.call(void 0, e.to)} would leave "${clip.id}" shorter than a frame; it starts at ${_chunk3OSXHLITcjs.formatVideoSeconds.call(void 0, clip.start)}.`);
1088
1152
  if (clip.type === "media" && !isRamped(clip)) {
1089
1153
  const asset = assets.get(clip.asset);
1090
1154
  const out = (_nullishCoalesce(clip.in, () => ( 0))) + duration * speed;
1091
- if (_optionalChain([asset, 'optionalAccess', _12 => _12.duration]) !== void 0 && out > asset.duration + _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON)
1092
- refuse(`"${clip.id}" can end at most at ${_chunkVKF3PWKRcjs.formatVideoSeconds.call(void 0, clip.start + (asset.duration - (_nullishCoalesce(clip.in, () => ( 0)))) / speed)}, where "${asset.id}" runs out.`);
1155
+ if (_optionalChain([asset, 'optionalAccess', _15 => _15.duration]) !== void 0 && out > asset.duration + _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON)
1156
+ refuse(`"${clip.id}" can end at most at ${_chunk3OSXHLITcjs.formatVideoSeconds.call(void 0, clip.start + (asset.duration - (_nullishCoalesce(clip.in, () => ( 0)))) / speed)}, where "${asset.id}" runs out.`);
1093
1157
  }
1094
1158
  if (clip.type === "sequence") {
1095
- const seq = _chunkVKF3PWKRcjs.videoSequenceMap.call(void 0, this.p).get(clip.sequence);
1096
- const length = seq ? _chunkVKF3PWKRcjs.videoSequenceDuration.call(void 0, seq) : Infinity;
1097
- if ((_nullishCoalesce(clip.in, () => ( 0))) + duration > length + _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON) refuse(`"${clip.id}" can end at most at ${_chunkVKF3PWKRcjs.formatVideoSeconds.call(void 0, clip.start + length - (_nullishCoalesce(clip.in, () => ( 0))))}, where sequence "${clip.sequence}" ends.`);
1159
+ const seq = _chunk3OSXHLITcjs.videoSequenceMap.call(void 0, this.p).get(clip.sequence);
1160
+ const length = seq ? _chunk3OSXHLITcjs.videoSequenceDuration.call(void 0, seq) : Infinity;
1161
+ if ((_nullishCoalesce(clip.in, () => ( 0))) + duration > length + _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON) refuse(`"${clip.id}" can end at most at ${_chunk3OSXHLITcjs.formatVideoSeconds.call(void 0, clip.start + length - (_nullishCoalesce(clip.in, () => ( 0))))}, where sequence "${clip.sequence}" ends.`);
1098
1162
  }
1099
1163
  if (e.ripple) {
1100
1164
  const tracks = this.rippleTracks([track]);
@@ -1116,9 +1180,9 @@ var Editor = (_class = class {
1116
1180
  case "split_clip": {
1117
1181
  const { track, clip } = this.clip(e.clip);
1118
1182
  this.unlocked(track);
1119
- const end = _chunkVKF3PWKRcjs.videoClipEnd.call(void 0, clip);
1183
+ const end = _chunk3OSXHLITcjs.videoClipEnd.call(void 0, clip);
1120
1184
  const minimum = 1e3 / this.p.fps;
1121
- if (e.at < clip.start + minimum - _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON || e.at > end - minimum + _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON) refuse(`${_chunkVKF3PWKRcjs.formatVideoSeconds.call(void 0, e.at)} is not inside "${clip.id}" (${_chunkVKF3PWKRcjs.formatVideoSeconds.call(void 0, clip.start)}\u2013${_chunkVKF3PWKRcjs.formatVideoSeconds.call(void 0, end)}), at least a frame from either end.`);
1185
+ if (e.at < clip.start + minimum - _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON || e.at > end - minimum + _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON) refuse(`${_chunk3OSXHLITcjs.formatVideoSeconds.call(void 0, e.at)} is not inside "${clip.id}" (${_chunk3OSXHLITcjs.formatVideoSeconds.call(void 0, clip.start)}\u2013${_chunk3OSXHLITcjs.formatVideoSeconds.call(void 0, end)}), at least a frame from either end.`);
1122
1186
  if (e.id !== void 0 && this.ids().includes(e.id)) refuse(`There is already a clip "${e.id}".`);
1123
1187
  const right = this.cut(track, clip, e.at, e.id);
1124
1188
  return this.outcomes.push({ op: e.op, clips: [clip.id, right.id], clip: right.id, track: track.id });
@@ -1129,12 +1193,12 @@ var Editor = (_class = class {
1129
1193
  for (const track of new Set(found.map((f) => f.track))) track.clips = track.clips.filter((c) => !found.some((f) => f.clip === c));
1130
1194
  if (!e.ripple) return this.outcomes.push({ op: e.op, clips: found.map((f) => f.clip.id) });
1131
1195
  const rippled = /* @__PURE__ */ new Set();
1132
- const groups = [...new Set(found.map((f) => f.track))].filter((t) => !_chunkVKF3PWKRcjs.videoTrackSyncLocked.call(void 0, t)).map((t) => [t]);
1133
- if (found.some((f) => _chunkVKF3PWKRcjs.videoTrackSyncLocked.call(void 0, f.track))) groups.push(found.filter((f) => _chunkVKF3PWKRcjs.videoTrackSyncLocked.call(void 0, f.track)).map((f) => f.track));
1196
+ const groups = [...new Set(found.map((f) => f.track))].filter((t) => !_chunk3OSXHLITcjs.videoTrackSyncLocked.call(void 0, t)).map((t) => [t]);
1197
+ if (found.some((f) => _chunk3OSXHLITcjs.videoTrackSyncLocked.call(void 0, f.track))) groups.push(found.filter((f) => _chunk3OSXHLITcjs.videoTrackSyncLocked.call(void 0, f.track)).map((f) => f.track));
1134
1198
  for (const sources of groups) {
1135
1199
  const tracks = this.rippleTracks(sources);
1136
1200
  tracks.forEach((t) => rippled.add(t));
1137
- const spans = mergeSpans(found.filter((f) => sources.includes(f.track)).map((f) => [f.clip.start, _chunkVKF3PWKRcjs.videoClipEnd.call(void 0, f.clip)]));
1201
+ const spans = mergeSpans(found.filter((f) => sources.includes(f.track)).map((f) => [f.clip.start, _chunk3OSXHLITcjs.videoClipEnd.call(void 0, f.clip)]));
1138
1202
  for (const [from, to] of spans.reverse()) this.close(tracks, from, to);
1139
1203
  }
1140
1204
  return this.outcomes.push({ op: e.op, clips: found.map((f) => f.clip.id), rippled: this.p.tracks.filter((t) => rippled.has(t)).map((t) => t.id) });
@@ -1150,17 +1214,18 @@ var Editor = (_class = class {
1150
1214
  if ("timeRemap" in set) refuse("Set a speed ramp with set_time_remap, which checks it against the media and moves later clips if asked.");
1151
1215
  let stretch = 1;
1152
1216
  if (at.clip.type === "media" && !("duration" in set)) {
1153
- const was = _chunkVKF3PWKRcjs.videoClipSpeed.call(void 0, at.clip);
1154
- const now = "speed" in set ? set.speed : _optionalChain([e, 'access', _13 => _13.remove, 'optionalAccess', _14 => _14.includes, 'call', _15 => _15("speed")]) ? 1 : was;
1217
+ const was = _chunk3OSXHLITcjs.videoClipSpeed.call(void 0, at.clip);
1218
+ const now = "speed" in set ? set.speed : _optionalChain([e, 'access', _16 => _16.remove, 'optionalAccess', _17 => _17.includes, 'call', _18 => _18("speed")]) ? 1 : was;
1155
1219
  if (now !== was) {
1156
1220
  set.duration = at.clip.duration * was / now;
1157
1221
  stretch = was / now;
1158
1222
  }
1159
1223
  }
1160
1224
  const next = { ...structuredClone(at.clip), ...set };
1161
- if (stretch !== 1) retimeClipKeys(next, (v) => _chunkVKF3PWKRcjs.retimeVideoAnimatable.call(void 0, v, 0, stretch));
1225
+ if (stretch !== 1) retimeClipKeys(next, (v) => _chunk3OSXHLITcjs.retimeVideoAnimatable.call(void 0, v, 0, stretch));
1162
1226
  for (const k of _nullishCoalesce(e.remove, () => ( []))) delete next[k];
1163
1227
  const clip = parseVideoClip(next, "set");
1228
+ this.adjustmentTarget(clip);
1164
1229
  if (clip.type === "media" && clip.asset !== at.clip.asset) this.kindFits(at.track, clip);
1165
1230
  if (clip.start !== at.clip.start || clip.duration !== at.clip.duration) this.fits(at.track, clip.start, clip.duration, /* @__PURE__ */ new Set([clip.id]), `Clip "${clip.id}"`);
1166
1231
  at.track.clips[at.index] = clip;
@@ -1171,17 +1236,17 @@ var Editor = (_class = class {
1171
1236
  const { track, clip } = this.clip(e.clip);
1172
1237
  this.unlocked(track);
1173
1238
  if (clip.type === "sequence") refuse(`Clip "${clip.id}" is a compound clip, which plays its sequence at normal speed; ramp the clips inside the sequence instead.`);
1174
- if (clip.type !== "media") refuse(`Clip "${clip.id}" is a ${clip.type === "text" ? "title" : clip.type === "effect" ? "effect clip" : "colour"}, so it has no source to ramp; only video and audio clips change speed.`);
1239
+ if (clip.type !== "media") refuse(`Clip "${clip.id}" is a ${_chunk3OSXHLITcjs.videoClipTypeLabel.call(void 0, clip)}, so it has no source to ramp; only video and audio clips change speed.`);
1175
1240
  const asset = this.asset(clip.asset);
1176
1241
  if (asset.kind === "image") refuse(`Clip "${clip.id}" is a still ("${asset.id}"), so it has no motion to ramp.`);
1177
1242
  const media = clip;
1178
- const end = _chunkVKF3PWKRcjs.videoClipEnd.call(void 0, media);
1243
+ const end = _chunk3OSXHLITcjs.videoClipEnd.call(void 0, media);
1179
1244
  const duration = _nullishCoalesce(e.duration, () => ( media.duration));
1180
- if (!(duration >= 1e3 / this.p.fps - _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON)) refuse(`"duration" must be at least a frame (${Math.ceil(1e3 / this.p.fps)} ms).`);
1245
+ if (!(duration >= 1e3 / this.p.fps - _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON)) refuse(`"duration" must be at least a frame (${Math.ceil(1e3 / this.p.fps)} ms).`);
1181
1246
  if (e.keys === null) {
1182
1247
  if (!media.timeRemap) refuse(`Clip "${clip.id}" has no speed ramp to remove.`);
1183
1248
  const from = (_nullishCoalesce(media.in, () => ( 0))) + media.timeRemap.keys[0].v;
1184
- if (asset.duration !== void 0 && from + duration > asset.duration + _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON)
1249
+ if (asset.duration !== void 0 && from + duration > asset.duration + _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON)
1185
1250
  refuse(`At normal speed from ${Math.round(from)} ms, "${clip.id}" would run past the end of "${asset.id}" (${asset.duration} ms); give a duration of at most ${Math.floor(asset.duration - from)} ms.`);
1186
1251
  delete media.timeRemap;
1187
1252
  media.in = from;
@@ -1189,17 +1254,17 @@ var Editor = (_class = class {
1189
1254
  } else {
1190
1255
  if (!Array.isArray(e.keys) || !e.keys.length) refuse('"keys" must be a list of at least one { t, v, ease } (or null to remove the ramp).');
1191
1256
  const remap = parseVideoTimeRemap({ keys: e.keys }, void 0, "keys");
1192
- remap.keys = _chunkVKF3PWKRcjs.sortVideoKeys.call(void 0, remap.keys);
1193
- _chunkVKF3PWKRcjs.checkVideoTimeRemap.call(void 0, remap, "keys");
1194
- const span = _chunkVKF3PWKRcjs.videoTimeRemapSpan.call(void 0, remap);
1195
- if (asset.duration !== void 0 && (_nullishCoalesce(media.in, () => ( 0))) + span.to > asset.duration + _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON)
1257
+ remap.keys = _chunk3OSXHLITcjs.sortVideoKeys.call(void 0, remap.keys);
1258
+ _chunk3OSXHLITcjs.checkVideoTimeRemap.call(void 0, remap, "keys");
1259
+ const span = _chunk3OSXHLITcjs.videoTimeRemapSpan.call(void 0, remap);
1260
+ if (asset.duration !== void 0 && (_nullishCoalesce(media.in, () => ( 0))) + span.to > asset.duration + _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON)
1196
1261
  refuse(`The ramp plays ${Math.round(span.to)} ms of source from the in point (${_nullishCoalesce(media.in, () => ( 0))} ms), past the end of "${asset.id}" (${asset.duration} ms); it can use at most ${Math.floor(asset.duration - (_nullishCoalesce(media.in, () => ( 0))))} ms.`);
1197
1262
  media.timeRemap = remap;
1198
1263
  delete media.speed;
1199
1264
  }
1200
1265
  const newEnd = media.start + duration;
1201
1266
  const outcome = { op: e.op, clip: clip.id, track: track.id };
1202
- if (Math.abs(newEnd - end) > _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON) {
1267
+ if (Math.abs(newEnd - end) > _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON) {
1203
1268
  if (e.ripple) {
1204
1269
  const tracks = this.rippleTracks([track]);
1205
1270
  if (newEnd < end) {
@@ -1228,11 +1293,11 @@ var Editor = (_class = class {
1228
1293
  case "slip_clip": {
1229
1294
  const { track, clip } = this.clip(e.clip);
1230
1295
  this.unlocked(track);
1231
- if (clip.type !== "media" && clip.type !== "sequence") refuse(`Clip "${clip.id}" is a ${clip.type === "text" ? "title" : clip.type === "effect" ? "effect clip" : "colour"}, so it has no source to slip; only video, audio and compound clips slip.`);
1296
+ if (clip.type !== "media" && clip.type !== "sequence") refuse(`Clip "${clip.id}" is a ${_chunk3OSXHLITcjs.videoClipTypeLabel.call(void 0, clip)}, so it has no source to slip; only video, audio and compound clips slip.`);
1232
1297
  const source = clip.type === "sequence" ? `sequence "${clip.sequence}"` : `"${this.asset(clip.asset).id}"`;
1233
1298
  if (clip.type === "media" && this.asset(clip.asset).kind === "image") refuse(`Clip "${clip.id}" is a still ("${clip.asset}"), so it has no source to slip; only video and audio clips slip.`);
1234
- const handles = _chunkVKF3PWKRcjs.videoClipHandles.call(void 0, clip, _chunkVKF3PWKRcjs.videoAssetMap.call(void 0, this.p), _chunkVKF3PWKRcjs.videoSequenceMap.call(void 0, this.p));
1235
- const { lead, tail } = _chunkVKF3PWKRcjs.videoClipOverhang.call(void 0, track, clip);
1299
+ const handles = _chunk3OSXHLITcjs.videoClipHandles.call(void 0, clip, _chunk3OSXHLITcjs.videoAssetMap.call(void 0, this.p), _chunk3OSXHLITcjs.videoSequenceMap.call(void 0, this.p));
1300
+ const { lead, tail } = _chunk3OSXHLITcjs.videoClipOverhang.call(void 0, track, clip);
1236
1301
  const delta = this.clamp(
1237
1302
  e.delta,
1238
1303
  lead - handles.before,
@@ -1250,13 +1315,13 @@ var Editor = (_class = class {
1250
1315
  this.unlocked(track);
1251
1316
  const L = left.clip;
1252
1317
  const R = right.clip;
1253
- if (Math.abs(_chunkVKF3PWKRcjs.videoClipEnd.call(void 0, L) - R.start) > _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON) {
1254
- if (Math.abs(_chunkVKF3PWKRcjs.videoClipEnd.call(void 0, R) - L.start) <= _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON) refuse(`"${R.id}" comes before "${L.id}"; pass left "${R.id}" and right "${L.id}".`);
1255
- refuse(`"${L.id}" ends at ${_chunkVKF3PWKRcjs.formatVideoSeconds.call(void 0, _chunkVKF3PWKRcjs.videoClipEnd.call(void 0, L))} but "${R.id}" starts at ${_chunkVKF3PWKRcjs.formatVideoSeconds.call(void 0, R.start)}; a roll needs two clips that meet at a cut (use trim_clip to move one edge).`);
1318
+ if (Math.abs(_chunk3OSXHLITcjs.videoClipEnd.call(void 0, L) - R.start) > _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON) {
1319
+ if (Math.abs(_chunk3OSXHLITcjs.videoClipEnd.call(void 0, R) - L.start) <= _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON) refuse(`"${R.id}" comes before "${L.id}"; pass left "${R.id}" and right "${L.id}".`);
1320
+ refuse(`"${L.id}" ends at ${_chunk3OSXHLITcjs.formatVideoSeconds.call(void 0, _chunk3OSXHLITcjs.videoClipEnd.call(void 0, L))} but "${R.id}" starts at ${_chunk3OSXHLITcjs.formatVideoSeconds.call(void 0, R.start)}; a roll needs two clips that meet at a cut (use trim_clip to move one edge).`);
1256
1321
  }
1257
- const assets = _chunkVKF3PWKRcjs.videoAssetMap.call(void 0, this.p);
1258
- const lo = Math.max(-(L.duration - this.frame), -(_chunkVKF3PWKRcjs.videoClipHandles.call(void 0, R, assets, _chunkVKF3PWKRcjs.videoSequenceMap.call(void 0, this.p)).before - _chunkVKF3PWKRcjs.videoClipOverhang.call(void 0, track, R).lead));
1259
- const hi = Math.min(R.duration - this.frame, _chunkVKF3PWKRcjs.videoClipHandles.call(void 0, L, assets, _chunkVKF3PWKRcjs.videoSequenceMap.call(void 0, this.p)).after - _chunkVKF3PWKRcjs.videoClipOverhang.call(void 0, track, L).tail);
1322
+ const assets = _chunk3OSXHLITcjs.videoAssetMap.call(void 0, this.p);
1323
+ const lo = Math.max(-(L.duration - this.frame), -(_chunk3OSXHLITcjs.videoClipHandles.call(void 0, R, assets, _chunk3OSXHLITcjs.videoSequenceMap.call(void 0, this.p)).before - _chunk3OSXHLITcjs.videoClipOverhang.call(void 0, track, R).lead));
1324
+ const hi = Math.min(R.duration - this.frame, _chunk3OSXHLITcjs.videoClipHandles.call(void 0, L, assets, _chunk3OSXHLITcjs.videoSequenceMap.call(void 0, this.p)).after - _chunk3OSXHLITcjs.videoClipOverhang.call(void 0, track, L).tail);
1260
1325
  const delta = this.clamp(
1261
1326
  e.delta,
1262
1327
  lo,
@@ -1272,18 +1337,18 @@ var Editor = (_class = class {
1272
1337
  case "slide_clip": {
1273
1338
  const { track, clip } = this.clip(e.clip);
1274
1339
  this.unlocked(track);
1275
- const before = _chunkVKF3PWKRcjs.videoClipBefore.call(void 0, track, clip);
1276
- const after = _chunkVKF3PWKRcjs.videoClipAfter.call(void 0, track, clip);
1340
+ const before = _chunk3OSXHLITcjs.videoClipBefore.call(void 0, track, clip);
1341
+ const after = _chunk3OSXHLITcjs.videoClipAfter.call(void 0, track, clip);
1277
1342
  if (!before && !after) refuse(`"${clip.id}" has no clip right before or after it on track "${track.id}", so there is nothing to slide against; use move_clip.`);
1278
- const assets = _chunkVKF3PWKRcjs.videoAssetMap.call(void 0, this.p);
1279
- const end = _chunkVKF3PWKRcjs.videoClipEnd.call(void 0, clip);
1343
+ const assets = _chunk3OSXHLITcjs.videoAssetMap.call(void 0, this.p);
1344
+ const end = _chunk3OSXHLITcjs.videoClipEnd.call(void 0, clip);
1280
1345
  const others = track.clips.filter((c) => c !== clip);
1281
1346
  const room = {
1282
- earlier: before ? before.duration - this.frame : clip.start - others.filter((c) => _chunkVKF3PWKRcjs.videoClipEnd.call(void 0, c) <= clip.start + _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON).reduce((m, c) => Math.max(m, _chunkVKF3PWKRcjs.videoClipEnd.call(void 0, c)), 0),
1283
- later: after ? after.duration - this.frame : others.filter((c) => c.start >= end - _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON).reduce((m, c) => Math.min(m, c.start - end), Infinity)
1347
+ earlier: before ? before.duration - this.frame : clip.start - others.filter((c) => _chunk3OSXHLITcjs.videoClipEnd.call(void 0, c) <= clip.start + _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON).reduce((m, c) => Math.max(m, _chunk3OSXHLITcjs.videoClipEnd.call(void 0, c)), 0),
1348
+ later: after ? after.duration - this.frame : others.filter((c) => c.start >= end - _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON).reduce((m, c) => Math.min(m, c.start - end), Infinity)
1284
1349
  };
1285
- const lo = -Math.min(room.earlier, after ? _chunkVKF3PWKRcjs.videoClipHandles.call(void 0, after, assets, _chunkVKF3PWKRcjs.videoSequenceMap.call(void 0, this.p)).before - _chunkVKF3PWKRcjs.videoClipOverhang.call(void 0, track, after).lead : Infinity);
1286
- const hi = Math.min(room.later, before ? _chunkVKF3PWKRcjs.videoClipHandles.call(void 0, before, assets, _chunkVKF3PWKRcjs.videoSequenceMap.call(void 0, this.p)).after - _chunkVKF3PWKRcjs.videoClipOverhang.call(void 0, track, before).tail : Infinity);
1350
+ const lo = -Math.min(room.earlier, after ? _chunk3OSXHLITcjs.videoClipHandles.call(void 0, after, assets, _chunk3OSXHLITcjs.videoSequenceMap.call(void 0, this.p)).before - _chunk3OSXHLITcjs.videoClipOverhang.call(void 0, track, after).lead : Infinity);
1351
+ const hi = Math.min(room.later, before ? _chunk3OSXHLITcjs.videoClipHandles.call(void 0, before, assets, _chunk3OSXHLITcjs.videoSequenceMap.call(void 0, this.p)).after - _chunk3OSXHLITcjs.videoClipOverhang.call(void 0, track, before).tail : Infinity);
1287
1352
  const delta = this.clamp(
1288
1353
  e.delta,
1289
1354
  lo,
@@ -1298,7 +1363,7 @@ var Editor = (_class = class {
1298
1363
  this.trimHead(after, delta);
1299
1364
  }
1300
1365
  track.clips.sort((a, b) => a.start - b.start);
1301
- return this.outcomes.push({ op: e.op, clip: clip.id, clips: [_optionalChain([before, 'optionalAccess', _16 => _16.id]), clip.id, _optionalChain([after, 'optionalAccess', _17 => _17.id])].filter((x) => !!x), track: track.id, delta });
1366
+ return this.outcomes.push({ op: e.op, clip: clip.id, clips: [_optionalChain([before, 'optionalAccess', _19 => _19.id]), clip.id, _optionalChain([after, 'optionalAccess', _20 => _20.id])].filter((x) => !!x), track: track.id, delta });
1302
1367
  }
1303
1368
  case "lift_range":
1304
1369
  case "extract_range": {
@@ -1316,29 +1381,29 @@ var Editor = (_class = class {
1316
1381
  }
1317
1382
  case "split_at": {
1318
1383
  const tracks = this.rangeTracks(e.tracks);
1319
- const lefts = tracks.flatMap((t) => t.clips.filter((c) => c.start < e.at - _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON && _chunkVKF3PWKRcjs.videoClipEnd.call(void 0, c) > e.at + _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON).map((c) => c.id));
1320
- if (!lefts.length) refuse(`No clip runs across ${_chunkVKF3PWKRcjs.formatVideoSeconds.call(void 0, e.at)} on ${e.tracks ? `track${tracks.length > 1 ? "s" : ""} ${tracks.map((t) => `"${t.id}"`).join(", ")}` : "the unlocked tracks"}, so there is nothing to split there.`);
1384
+ const lefts = tracks.flatMap((t) => t.clips.filter((c) => c.start < e.at - _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON && _chunk3OSXHLITcjs.videoClipEnd.call(void 0, c) > e.at + _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON).map((c) => c.id));
1385
+ if (!lefts.length) refuse(`No clip runs across ${_chunk3OSXHLITcjs.formatVideoSeconds.call(void 0, e.at)} on ${e.tracks ? `track${tracks.length > 1 ? "s" : ""} ${tracks.map((t) => `"${t.id}"`).join(", ")}` : "the unlocked tracks"}, so there is nothing to split there.`);
1321
1386
  const created = this.cutAcross(tracks, e.at).map((c) => c.id);
1322
1387
  return this.outcomes.push({ op: e.op, clips: lefts, created });
1323
1388
  }
1324
1389
  case "set_keyframes": {
1325
1390
  const { track, clip } = this.clip(e.clip);
1326
1391
  this.unlocked(track);
1327
- if (!_chunkVKF3PWKRcjs.VIDEO_KEYFRAME_PROPERTIES.includes(e.property)) refuse(`"${e.property}" cannot be keyframed; use one of ${_chunkVKF3PWKRcjs.VIDEO_KEYFRAME_PROPERTIES.join(", ")}.`);
1392
+ if (!_chunk3OSXHLITcjs.VIDEO_KEYFRAME_PROPERTIES.includes(e.property)) refuse(`"${e.property}" cannot be keyframed; use one of ${_chunk3OSXHLITcjs.VIDEO_KEYFRAME_PROPERTIES.join(", ")}.`);
1328
1393
  if (e.property === "volume" && clip.type !== "media" && clip.type !== "sequence") refuse(`Clip "${clip.id}" is a ${clip.type} clip and has no volume; volume keys go on media and compound clips.`);
1329
- const limits = _chunkVKF3PWKRcjs.VIDEO_KEYFRAME_LIMITS[e.property];
1330
- const transform = _chunkVKF3PWKRcjs.VIDEO_TRANSFORM_PROPERTIES.includes(e.property);
1394
+ const limits = _chunk3OSXHLITcjs.VIDEO_KEYFRAME_LIMITS[e.property];
1395
+ const transform = _chunk3OSXHLITcjs.VIDEO_TRANSFORM_PROPERTIES.includes(e.property);
1331
1396
  const c = clip;
1332
- const current = transform ? _optionalChain([c, 'access', _18 => _18.transform, 'optionalAccess', _19 => _19[e.property]]) : c[e.property];
1397
+ const current = transform ? _optionalChain([c, 'access', _21 => _21.transform, 'optionalAccess', _22 => _22[e.property]]) : c[e.property];
1333
1398
  let value;
1334
1399
  if (e.keys === null) {
1335
- const v = _chunkVKF3PWKRcjs.videoValueAt.call(void 0, current, 0, limits.default);
1336
- value = _chunkVKF3PWKRcjs.isVideoKeyframes.call(void 0, current) ? v === limits.default ? void 0 : v : current;
1400
+ const v = _chunk3OSXHLITcjs.videoValueAt.call(void 0, current, 0, limits.default);
1401
+ value = _chunk3OSXHLITcjs.isVideoKeyframes.call(void 0, current) ? v === limits.default ? void 0 : v : current;
1337
1402
  } else {
1338
1403
  if (!Array.isArray(e.keys) || !e.keys.length) refuse('"keys" must list at least one key { t, v }; pass null to remove the keyframes.');
1339
1404
  let sorted = [];
1340
1405
  try {
1341
- sorted = _chunkVKF3PWKRcjs.sortVideoKeys.call(void 0, e.keys);
1406
+ sorted = _chunk3OSXHLITcjs.sortVideoKeys.call(void 0, e.keys);
1342
1407
  } catch (error) {
1343
1408
  refuse(error.message);
1344
1409
  }
@@ -1357,12 +1422,12 @@ var Editor = (_class = class {
1357
1422
  this.unlocked(track);
1358
1423
  let at = 0;
1359
1424
  const moved = [];
1360
- for (const clip of _chunkVKF3PWKRcjs.sortedVideoClips.call(void 0, track)) {
1361
- if (clip.start >= (_nullishCoalesce(e.from, () => ( 0))) - _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON && clip.start > at + _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON) {
1425
+ for (const clip of _chunk3OSXHLITcjs.sortedVideoClips.call(void 0, track)) {
1426
+ if (clip.start >= (_nullishCoalesce(e.from, () => ( 0))) - _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON && clip.start > at + _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON) {
1362
1427
  clip.start = Math.max(at, _nullishCoalesce(e.from, () => ( 0)));
1363
1428
  moved.push(clip.id);
1364
1429
  }
1365
- at = Math.max(at, _chunkVKF3PWKRcjs.videoClipEnd.call(void 0, clip));
1430
+ at = Math.max(at, _chunk3OSXHLITcjs.videoClipEnd.call(void 0, clip));
1366
1431
  }
1367
1432
  return this.outcomes.push({ op: e.op, track: track.id, clips: moved });
1368
1433
  }
@@ -1411,7 +1476,7 @@ function applyVideoEdits(project, edits) {
1411
1476
  editor.run(edit);
1412
1477
  } catch (error) {
1413
1478
  const message = error.message;
1414
- return { ok: false, error: `${list.length > 1 ? `op ${i + 1} (${_optionalChain([edit, 'optionalAccess', _20 => _20.op])}): ` : ""}${message}${/Nothing was changed\.$/.test(message) ? "" : " Nothing was changed."}` };
1479
+ return { ok: false, error: `${list.length > 1 ? `op ${i + 1} (${_optionalChain([edit, 'optionalAccess', _23 => _23.op])}): ` : ""}${message}${/Nothing was changed\.$/.test(message) ? "" : " Nothing was changed."}` };
1415
1480
  }
1416
1481
  }
1417
1482
  let next;
@@ -1428,6 +1493,432 @@ function applyVideoEdits(project, edits) {
1428
1493
  return { ok: true, project: next, issues, outcomes: editor.outcomes };
1429
1494
  }
1430
1495
 
1496
+ // src/video-editor/transcript.ts
1497
+ var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
1498
+ function cueTime(s) {
1499
+ const m = s.trim().match(/^(?:(\d+):)?(\d{1,2}):(\d{1,2})[.,](\d{1,3})$/);
1500
+ if (!m) throw new Error(`"${s.trim()}" is not a cue time (HH:MM:SS,mmm).`);
1501
+ return ((Number(_nullishCoalesce(m[1], () => ( 0))) * 60 + Number(m[2])) * 60 + Number(m[3])) * 1e3 + Number(m[4].padEnd(3, "0"));
1502
+ }
1503
+ function cueWords(text, start, end) {
1504
+ const words = text.replace(/<[^>]+>/g, "").split(/\s+/).filter(Boolean);
1505
+ const step = (end - start) / Math.max(1, words.length);
1506
+ return words.map((word, i) => ({ word, start: Math.round(start + i * step), end: Math.round(start + (i + 1) * step) }));
1507
+ }
1508
+ function parseVideoTranscript(input) {
1509
+ let data = input;
1510
+ if (typeof input === "string") {
1511
+ const text = input.replace(/^/, "").trim();
1512
+ if (text.startsWith("{") || text.startsWith("[")) {
1513
+ try {
1514
+ data = JSON.parse(text);
1515
+ } catch (e2) {
1516
+ throw new Error("The transcript looks like JSON but is not valid JSON.");
1517
+ }
1518
+ } else {
1519
+ const words2 = [];
1520
+ for (const block of text.replace(/\r\n?/g, "\n").split(/\n{2,}/)) {
1521
+ const lines = block.split("\n");
1522
+ const at = lines.findIndex((l) => l.includes("-->"));
1523
+ if (at < 0) continue;
1524
+ const [a, b] = lines[at].split("-->");
1525
+ words2.push(...cueWords(lines.slice(at + 1).join(" "), cueTime(a), cueTime(b.trim().split(/\s+/)[0])));
1526
+ }
1527
+ if (!words2.length) throw new Error("No words found: expected Whisper-style JSON with word timings, SubRip (.srt) or WebVTT.");
1528
+ return { words: words2, approximate: true };
1529
+ }
1530
+ }
1531
+ const seconds = (v) => typeof v === "number" && Number.isFinite(v) ? Math.round(v * 1e3) : NaN;
1532
+ const fromList = (list, toMs) => list.map((w, i) => {
1533
+ if (!isRecord(w)) throw new Error(`Word ${i + 1} is not an object { word, start, end }.`);
1534
+ const word = String(_nullishCoalesce(_nullishCoalesce(w.word, () => ( w.text)), () => ( ""))).trim();
1535
+ const start = toMs(w.start);
1536
+ const end = toMs(w.end);
1537
+ if (!word || !Number.isFinite(start) || !Number.isFinite(end)) throw new Error(`Word ${i + 1} needs a word and numeric start and end.`);
1538
+ return { word, start, end: Math.max(start, end) };
1539
+ });
1540
+ let words = [];
1541
+ if (Array.isArray(data)) words = fromList(data, (v) => typeof v === "number" ? v : NaN);
1542
+ else if (isRecord(data) && Array.isArray(data.words)) words = fromList(data.words, seconds);
1543
+ else if (isRecord(data) && Array.isArray(data.segments)) {
1544
+ const segments = data.segments.filter(isRecord);
1545
+ if (segments.some((s) => Array.isArray(s.words))) words = fromList(segments.flatMap((s) => Array.isArray(s.words) ? s.words : []), seconds);
1546
+ else {
1547
+ for (const s of segments) words.push(...cueWords(String(_nullishCoalesce(s.text, () => ( ""))), seconds(s.start), seconds(s.end)));
1548
+ if (!words.length) throw new Error("No words found in the segments.");
1549
+ return { words, approximate: true };
1550
+ }
1551
+ }
1552
+ if (!words.length) throw new Error("No words found: expected Whisper-style JSON with word timings, SubRip (.srt) or WebVTT.");
1553
+ return { words: words.sort((a, b) => a.start - b.start), approximate: false };
1554
+ }
1555
+ function videoTranscriptTrack(project, track) {
1556
+ if (track !== void 0) {
1557
+ if (!project.tracks.some((t) => t.id === track)) throw new Error(`There is no track "${track}"; the tracks are ${project.tracks.map((t) => `"${t.id}"`).join(", ")}.`);
1558
+ return track;
1559
+ }
1560
+ const assets = _chunk3OSXHLITcjs.videoAssetMap.call(void 0, project);
1561
+ const transcribed = (t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access', _24 => _24.get, 'call', _25 => _25(c.asset), 'optionalAccess', _26 => _26.transcript, 'optionalAccess', _27 => _27.length]));
1562
+ return _nullishCoalesce(_optionalChain([(_nullishCoalesce(project.tracks.find((t) => t.kind === "audio" && transcribed(t)), () => ( project.tracks.find(transcribed)))), 'optionalAccess', _28 => _28.id]), () => ( null));
1563
+ }
1564
+ function videoTimelineTranscript(project, options = {}) {
1565
+ const id = videoTranscriptTrack(project, options.track);
1566
+ if (!id) return [];
1567
+ const track = project.tracks.find((t) => t.id === id);
1568
+ const assets = _chunk3OSXHLITcjs.videoAssetMap.call(void 0, project);
1569
+ const out = [];
1570
+ for (const clip of _chunk3OSXHLITcjs.sortedVideoClips.call(void 0, track)) {
1571
+ if (clip.type !== "media") continue;
1572
+ const words = _optionalChain([assets, 'access', _29 => _29.get, 'call', _30 => _30(clip.asset), 'optionalAccess', _31 => _31.transcript]);
1573
+ if (!_optionalChain([words, 'optionalAccess', _32 => _32.length])) continue;
1574
+ const speed = _chunk3OSXHLITcjs.videoClipSpeed.call(void 0, clip);
1575
+ const from = _nullishCoalesce(clip.in, () => ( 0));
1576
+ const to = from + clip.duration * speed;
1577
+ const toTimeline = (s) => Math.min(_chunk3OSXHLITcjs.videoClipEnd.call(void 0, clip), Math.max(clip.start, clip.start + (s - from) / speed));
1578
+ words.forEach((w, index) => {
1579
+ const mid = (w.start + w.end) / 2;
1580
+ if (mid < from || mid >= to) return;
1581
+ out.push({ word: w.word, start: toTimeline(w.start), end: toTimeline(w.end), clip: clip.id, track: track.id, index });
1582
+ });
1583
+ }
1584
+ return out;
1585
+ }
1586
+ var normalizeVideoWord = (word) => word.toLowerCase().replace(/^[^\p{L}\p{N}']+|[^\p{L}\p{N}']+$/gu, "");
1587
+ var rangeOf = (words, i, n) => ({ from: words[i].start, to: words[i + n - 1].end, text: words.slice(i, i + n).map((w) => w.word).join(" ") });
1588
+ function findVideoTranscriptText(words, query) {
1589
+ const q = query.split(/\s+/).map(normalizeVideoWord).filter(Boolean);
1590
+ if (!q.length) throw new Error("Search for at least one word.");
1591
+ const norm = words.map((w) => normalizeVideoWord(w.word));
1592
+ const out = [];
1593
+ for (let i = 0; i + q.length <= norm.length; i++) if (q.every((x, k) => norm[i + k] === x)) out.push(rangeOf(words, i, q.length));
1594
+ return out;
1595
+ }
1596
+ var VIDEO_DEFAULT_FILLERS = ["um", "umm", "uh", "uhh", "uhm", "erm", "er", "ah", "hmm", "mm", "you know", "i mean"];
1597
+ function findVideoFillers(words, fillers = VIDEO_DEFAULT_FILLERS) {
1598
+ const phrases = fillers.map((f) => f.split(/\s+/).map(normalizeVideoWord).filter(Boolean)).filter((p) => p.length).sort((a, b) => b.length - a.length);
1599
+ const norm = words.map((w) => normalizeVideoWord(w.word));
1600
+ const out = [];
1601
+ for (let i = 0; i < norm.length; ) {
1602
+ const hit = phrases.find((p) => i + p.length <= norm.length && p.every((x, k) => norm[i + k] === x));
1603
+ if (hit) {
1604
+ out.push(rangeOf(words, i, hit.length));
1605
+ i += hit.length;
1606
+ } else i++;
1607
+ }
1608
+ return out;
1609
+ }
1610
+ function findVideoTranscriptPauses(words, options = {}) {
1611
+ const min = _nullishCoalesce(options.min, () => ( 700));
1612
+ const keep = _nullishCoalesce(options.keep, () => ( 150));
1613
+ const out = [];
1614
+ for (let i = 1; i < words.length; i++) {
1615
+ const a = words[i - 1];
1616
+ const b = words[i];
1617
+ if (a.clip !== b.clip || b.start - a.end < min) continue;
1618
+ let from = a.end + keep;
1619
+ let to = b.start - keep;
1620
+ if (options.fps) {
1621
+ const f = 1e3 / options.fps;
1622
+ from = _chunk3OSXHLITcjs.snapToVideoFrame.call(void 0, Math.ceil(from / f - 1e-9) * f, options.fps);
1623
+ to = _chunk3OSXHLITcjs.snapToVideoFrame.call(void 0, Math.floor(to / f + 1e-9) * f, options.fps);
1624
+ }
1625
+ if (to - from > _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON) out.push({ from, to, text: `(pause after "${a.word}")` });
1626
+ }
1627
+ return out;
1628
+ }
1629
+ function mergeVideoRanges(ranges) {
1630
+ const sorted = ranges.filter((r) => r.to - r.from > _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON).map((r) => ({ from: r.from, to: r.to })).sort((a, b) => a.from - b.from);
1631
+ const out = [];
1632
+ for (const r of sorted) {
1633
+ const last = out[out.length - 1];
1634
+ if (last && r.from <= last.to + _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON) last.to = Math.max(last.to, r.to);
1635
+ else out.push(r);
1636
+ }
1637
+ return out;
1638
+ }
1639
+ function videoRemoveRangesEdits(ranges, tracks) {
1640
+ return mergeVideoRanges(ranges).reverse().map((r) => ({ op: "extract_range", from: r.from, to: r.to, ...tracks ? { tracks } : {} }));
1641
+ }
1642
+ function videoWordsRange(words, a, b) {
1643
+ const [i, j] = a <= b ? [a, b] : [b, a];
1644
+ return { from: words[i].start, to: words[j].end };
1645
+ }
1646
+ function videoTranscriptEdit(assetId, text) {
1647
+ return { op: "update_asset", asset: assetId, set: { transcript: parseVideoTranscript(text).words } };
1648
+ }
1649
+ function videoWordAt(words, time) {
1650
+ let lo = 0;
1651
+ let hi = words.length - 1;
1652
+ while (lo <= hi) {
1653
+ const mid = lo + hi >> 1;
1654
+ if (words[mid].end <= time) lo = mid + 1;
1655
+ else if (words[mid].start > time) hi = mid - 1;
1656
+ else return mid;
1657
+ }
1658
+ return -1;
1659
+ }
1660
+ function findVideoRepetitions(words, options = {}) {
1661
+ const max = Math.max(1, _nullishCoalesce(options.maxPhrase, () => ( 3)));
1662
+ const maxGap = _nullishCoalesce(options.maxGap, () => ( 1e3));
1663
+ const norm = words.map((w) => normalizeVideoWord(w.word));
1664
+ const out = [];
1665
+ for (let i = 0; i < norm.length; i++) {
1666
+ for (let n = Math.min(max, Math.floor((norm.length - i) / 2)); n >= 1; n--) {
1667
+ let same = true;
1668
+ for (let k = 0; k < n && same; k++) same = !!norm[i + k] && norm[i + k] === norm[i + n + k];
1669
+ if (!same || words[i + n].start - words[i + n - 1].end > maxGap) continue;
1670
+ out.push({ from: words[i].start, to: words[i + n].start, text: words.slice(i, i + n).map((w) => w.word).join(" ") });
1671
+ i += n - 1;
1672
+ break;
1673
+ }
1674
+ }
1675
+ return out;
1676
+ }
1677
+ var VIDEO_DISFLUENCY_KINDS = ["filler", "pause", "repetition"];
1678
+ function detectVideoDisfluencies(words, options = {}) {
1679
+ const kinds = new Set(_nullishCoalesce(options.kinds, () => ( VIDEO_DISFLUENCY_KINDS)));
1680
+ const startIndex = (time) => Math.max(0, words.findIndex((w) => w.start >= time - _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON));
1681
+ const out = [];
1682
+ if (kinds.has("filler"))
1683
+ for (const r of findVideoFillers(words, options.fillers)) {
1684
+ const first = startIndex(r.from);
1685
+ out.push({ ...r, kind: "filler", words: [first, first + r.text.split(" ").length - 1] });
1686
+ }
1687
+ if (kinds.has("repetition"))
1688
+ for (const r of findVideoRepetitions(words, options.repetitions)) {
1689
+ const first = startIndex(r.from);
1690
+ out.push({ ...r, kind: "repetition", words: [first, first + r.text.split(" ").length - 1] });
1691
+ }
1692
+ if (kinds.has("pause"))
1693
+ for (const r of findVideoTranscriptPauses(words, options.pauses)) {
1694
+ const after = words.findIndex((w) => w.start >= r.to - _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON);
1695
+ out.push({ ...r, kind: "pause", words: [Math.max(0, after - 1), Math.max(0, after)] });
1696
+ }
1697
+ return out.sort((a, b) => a.from - b.from || a.to - b.to);
1698
+ }
1699
+ function videoCutsWithMargin(ranges, keep) {
1700
+ return ranges.map((r) => ({ ...r, from: r.from + keep, to: r.to - keep })).filter((r) => r.to - r.from > _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON);
1701
+ }
1702
+
1703
+ // src/video-editor/mix.ts
1704
+ var VIDEO_LOUDNESS_HOP = 100;
1705
+ function kWeighting(sampleRate) {
1706
+ let K = Math.tan(Math.PI * 1681.974450955533 / sampleRate);
1707
+ const Vh = 10 ** (3.999843853973347 / 20);
1708
+ const Vb = Vh ** 0.4996667741545416;
1709
+ let Q = 0.7071752369554196;
1710
+ let a0 = 1 + K / Q + K * K;
1711
+ const shelf = { b: [(Vh + Vb * K / Q + K * K) / a0, 2 * (K * K - Vh) / a0, (Vh - Vb * K / Q + K * K) / a0], a: [2 * (K * K - 1) / a0, (1 - K / Q + K * K) / a0] };
1712
+ K = Math.tan(Math.PI * 38.13547087602444 / sampleRate);
1713
+ Q = 0.5003270373238773;
1714
+ a0 = 1 + K / Q + K * K;
1715
+ const highPass = { b: [1, -2, 1], a: [2 * (K * K - 1) / a0, (1 - K / Q + K * K) / a0] };
1716
+ return [shelf, highPass];
1717
+ }
1718
+ function videoLoudnessEnergy(channels, sampleRate) {
1719
+ const length = channels.reduce((n, c) => Math.max(n, c.length), 0);
1720
+ const hopSamples = Math.max(1, Math.round(sampleRate * VIDEO_LOUDNESS_HOP / 1e3));
1721
+ const hops = Math.max(1, Math.ceil(length / hopSamples));
1722
+ const energy = new Float32Array(hops);
1723
+ const filters = kWeighting(sampleRate);
1724
+ for (const channel of channels) {
1725
+ const x1 = [0, 0];
1726
+ const x2 = [0, 0];
1727
+ const y1 = [0, 0];
1728
+ const y2 = [0, 0];
1729
+ for (let i = 0; i < channel.length; i++) {
1730
+ let v = Number.isFinite(channel[i]) ? channel[i] : 0;
1731
+ for (let f = 0; f < 2; f++) {
1732
+ const { b, a } = filters[f];
1733
+ const y = b[0] * v + b[1] * x1[f] + b[2] * x2[f] - a[0] * y1[f] - a[1] * y2[f];
1734
+ x2[f] = x1[f];
1735
+ x1[f] = v;
1736
+ y2[f] = y1[f];
1737
+ y1[f] = y;
1738
+ v = y;
1739
+ }
1740
+ energy[Math.floor(i / hopSamples)] += v * v;
1741
+ }
1742
+ }
1743
+ for (let h = 0; h < hops; h++) energy[h] /= Math.min(hopSamples, length - h * hopSamples) || 1;
1744
+ return { hop: VIDEO_LOUDNESS_HOP, energy };
1745
+ }
1746
+ var lufs = (meanSquare) => -0.691 + 10 * Math.log10(meanSquare);
1747
+ function videoLoudnessOf(loudness, from = 0, to = Infinity) {
1748
+ const { hop, energy } = loudness;
1749
+ const a = Math.max(0, Math.floor(from / hop));
1750
+ const b = Math.min(energy.length, Math.ceil(to / hop));
1751
+ if (b <= a) return null;
1752
+ const blocks = [];
1753
+ if (b - a < 4) {
1754
+ let sum = 0;
1755
+ for (let h = a; h < b; h++) sum += energy[h];
1756
+ blocks.push(sum / (b - a));
1757
+ } else
1758
+ for (let h = a; h + 4 <= b; h++) blocks.push((energy[h] + energy[h + 1] + energy[h + 2] + energy[h + 3]) / 4);
1759
+ const absolute = blocks.filter((z) => z > 0 && lufs(z) > -70);
1760
+ if (!absolute.length) return null;
1761
+ const threshold = lufs(absolute.reduce((s, z) => s + z, 0) / absolute.length) - 10;
1762
+ const gated = absolute.filter((z) => lufs(z) > threshold);
1763
+ if (!gated.length) return null;
1764
+ return Math.round(lufs(gated.reduce((s, z) => s + z, 0) / gated.length) * 10) / 10;
1765
+ }
1766
+ function measureVideoLoudness(channels, sampleRate) {
1767
+ return videoLoudnessOf(videoLoudnessEnergy(channels, sampleRate));
1768
+ }
1769
+ function videoAudioLoudness(peaks, from = 0, to = Infinity) {
1770
+ if (!peaks.loudness) throw new Error("These peaks have no loudness data; build them again with buildVideoAudioPeaks (or decodeVideoAudioPeaks), which measures it.");
1771
+ return videoLoudnessOf(peaks.loudness, from, to);
1772
+ }
1773
+ function videoLoudnessGain(measured, target = -14, options = {}) {
1774
+ let db = target - measured;
1775
+ let limitedBy;
1776
+ const maxDb = 20 * Math.log10(_nullishCoalesce(options.maxGain, () => ( 4)));
1777
+ if (db > maxDb) {
1778
+ db = maxDb;
1779
+ limitedBy = "max-gain";
1780
+ }
1781
+ if (options.peakDb !== void 0 && options.peakDb > -100) {
1782
+ const room = (_nullishCoalesce(options.ceiling, () => ( -1))) - options.peakDb;
1783
+ if (db > room) {
1784
+ db = room;
1785
+ limitedBy = "peak";
1786
+ }
1787
+ }
1788
+ return { gain: Math.round(10 ** (db / 20) * 1e3) / 1e3, gainDb: Math.round(db * 10) / 10, ...limitedBy ? { limitedBy } : {} };
1789
+ }
1790
+ function videoSpeechRanges(words, mergeGap = 500) {
1791
+ const merged = [];
1792
+ for (const r of mergeVideoRanges(words.map((w) => ({ from: w.start, to: w.end })))) {
1793
+ const last = merged[merged.length - 1];
1794
+ if (last && r.from - last.to < mergeGap) last.to = Math.max(last.to, r.to);
1795
+ else merged.push({ ...r });
1796
+ }
1797
+ return merged;
1798
+ }
1799
+ function videoDuckingKeys(clip, speech, options = {}) {
1800
+ const duckDb = _nullishCoalesce(options.duckDb, () => ( -14));
1801
+ const attack = Math.max(0, _nullishCoalesce(options.attack, () => ( 200)));
1802
+ const release = Math.max(0, _nullishCoalesce(options.release, () => ( 400)));
1803
+ const gap = Math.max(_nullishCoalesce(options.mergeGap, () => ( 600)), attack + release);
1804
+ const base = _nullishCoalesce(options.base, () => ( 1));
1805
+ const low = Math.round(base * 10 ** (duckDb / 20) * 1e4) / 1e4;
1806
+ const end = _chunk3OSXHLITcjs.videoClipEnd.call(void 0, clip);
1807
+ const ranges = [];
1808
+ for (const r of mergeVideoRanges(speech)) {
1809
+ if (r.to <= clip.start - release || r.from >= end + attack) continue;
1810
+ const last = ranges[ranges.length - 1];
1811
+ if (last && r.from - last.to < gap) last.to = Math.max(last.to, r.to);
1812
+ else ranges.push({ ...r });
1813
+ }
1814
+ if (!ranges.length) return [];
1815
+ const keys = [];
1816
+ const at = (t) => Math.round(Math.min(clip.duration, Math.max(0, t - clip.start)));
1817
+ const push = (t, v, ease) => {
1818
+ const last = keys[keys.length - 1];
1819
+ if (last && t <= last.t) {
1820
+ last.v = v;
1821
+ if (ease) last.ease = ease;
1822
+ else delete last.ease;
1823
+ return;
1824
+ }
1825
+ keys.push({ t, v, ...ease ? { ease } : {} });
1826
+ };
1827
+ for (const r of ranges) {
1828
+ const down = at(r.from - attack);
1829
+ const held = at(r.from);
1830
+ if (held > down) push(down, base, "ease-in-out");
1831
+ push(held, low);
1832
+ const hold = at(r.to);
1833
+ const up = at(r.to + release);
1834
+ if (up > hold) {
1835
+ push(hold, low, "ease-in-out");
1836
+ push(up, base);
1837
+ }
1838
+ }
1839
+ return keys;
1840
+ }
1841
+ function findVideoAudioBeats(peaks, from, to, options = {}) {
1842
+ const res = Math.max(1, _nullishCoalesce(options.resolution, () => ( 10)));
1843
+ const n = Math.max(2, Math.ceil((to - from) / res));
1844
+ const spans = videoAudioPeakSpans(peaks, from, to, n);
1845
+ const env = new Float32Array(n);
1846
+ for (let i = 0; i < n; i++) env[i] = Math.max(Math.abs(spans[2 * i]), Math.abs(spans[2 * i + 1]));
1847
+ const onset = new Float32Array(n);
1848
+ for (let i = 1; i < n; i++) onset[i] = Math.max(0, env[i] - env[i - 1]);
1849
+ const win = Math.max(1, Math.round(500 / res));
1850
+ const k = _nullishCoalesce(options.sensitivity, () => ( 1.5));
1851
+ const minGap = Math.max(1, Math.round((_nullishCoalesce(options.minInterval, () => ( 250))) / res));
1852
+ const onsets = [];
1853
+ const strength = [];
1854
+ let lastAt = -Infinity;
1855
+ for (let i = 1; i < n - 1; i++) {
1856
+ if (!(onset[i] > 0 && onset[i] >= onset[i - 1] && onset[i] >= onset[i + 1])) continue;
1857
+ let sum = 0;
1858
+ let sq = 0;
1859
+ let count = 0;
1860
+ for (let j = Math.max(0, i - win); j < Math.min(n, i + win + 1); j++) {
1861
+ sum += onset[j];
1862
+ sq += onset[j] * onset[j];
1863
+ count++;
1864
+ }
1865
+ const mean = sum / count;
1866
+ const sd = Math.sqrt(Math.max(0, sq / count - mean * mean));
1867
+ if (onset[i] < mean + k * sd || onset[i] < 0.02) continue;
1868
+ if (i - lastAt < minGap) {
1869
+ if (onset[i] > strength[strength.length - 1]) {
1870
+ onsets[onsets.length - 1] = from + i * res;
1871
+ strength[strength.length - 1] = onset[i];
1872
+ lastAt = i;
1873
+ }
1874
+ continue;
1875
+ }
1876
+ onsets.push(from + i * res);
1877
+ strength.push(onset[i]);
1878
+ lastAt = i;
1879
+ }
1880
+ const minLag = Math.max(1, Math.round(6e4 / (_nullishCoalesce(options.maxBpm, () => ( 200))) / res));
1881
+ const maxLag = Math.min(n - 1, Math.round(6e4 / (_nullishCoalesce(options.minBpm, () => ( 60))) / res));
1882
+ let bestLag = 0;
1883
+ let best = 0;
1884
+ let total = 0;
1885
+ for (let i = 0; i < n; i++) total += onset[i] * onset[i];
1886
+ for (let lag = minLag; lag <= maxLag; lag++) {
1887
+ let s = 0;
1888
+ for (let i = 0; i + lag < n; i++) s += onset[i] * onset[i + lag];
1889
+ s /= n - lag;
1890
+ if (s > best * 1.0001) {
1891
+ best = s;
1892
+ bestLag = lag;
1893
+ }
1894
+ }
1895
+ const repeats = onsets.length >= 3 && bestLag > 0 && best * n > 0.2 * total;
1896
+ if (!repeats) return { onsets, bpm: null, beats: [] };
1897
+ const period = bestLag * res;
1898
+ const bpm = Math.round(6e4 / period * 10) / 10;
1899
+ let phase = onsets[0];
1900
+ let agree = -1;
1901
+ for (const o of onsets) {
1902
+ let a = 0;
1903
+ for (const p of onsets) {
1904
+ const d = Math.abs(((p - o) % period + period) % period);
1905
+ if (Math.min(d, period - d) <= res * 2) a++;
1906
+ }
1907
+ if (a > agree) {
1908
+ agree = a;
1909
+ phase = o;
1910
+ }
1911
+ }
1912
+ const beats = [];
1913
+ for (let t = phase - Math.floor((phase - from) / period) * period; t <= to + 1e-6; t += period) if (t >= from - 1e-6) beats.push(Math.round(t * 10) / 10);
1914
+ return { onsets, bpm, beats };
1915
+ }
1916
+ function nearestVideoBeat(beats, time, tolerance = Infinity) {
1917
+ let best = null;
1918
+ for (const b of beats) if (Math.abs(b - time) <= tolerance && (best === null || Math.abs(b - time) < Math.abs(best - time))) best = b;
1919
+ return best;
1920
+ }
1921
+
1431
1922
  // src/video-editor/audio.ts
1432
1923
  var VIDEO_PEAK_BLOCK = 32;
1433
1924
  function buildVideoAudioPeaks(channels, sampleRate, blockSize = VIDEO_PEAK_BLOCK) {
@@ -1467,7 +1958,7 @@ function buildVideoAudioPeaks(channels, sampleRate, blockSize = VIDEO_PEAK_BLOCK
1467
1958
  }
1468
1959
  levels.push(up);
1469
1960
  }
1470
- return { sampleRate, length, blockSize, levels };
1961
+ return { sampleRate, length, blockSize, levels, loudness: videoLoudnessEnergy(channels, sampleRate) };
1471
1962
  }
1472
1963
  var videoAudioPeaksDuration = (peaks) => peaks.length * 1e3 / peaks.sampleRate;
1473
1964
  function videoAudioPeakRange(peaks, from, to) {
@@ -1517,7 +2008,7 @@ function videoClipWaveform(clip, peaks, columns) {
1517
2008
  const step = clip.duration / Math.max(1, n);
1518
2009
  for (let c = 0; c < n; c++) {
1519
2010
  const { min, max } = videoAudioPeakRange(peaks, start + c * step * speed, start + (c + 1) * step * speed);
1520
- const gain = _chunkVKF3PWKRcjs.videoClipSoundGain.call(void 0, clip, (c + 0.5) * step);
2011
+ const gain = _chunk3OSXHLITcjs.videoClipSoundGain.call(void 0, clip, (c + 0.5) * step);
1521
2012
  out[2 * c] = min * gain;
1522
2013
  out[2 * c + 1] = max * gain;
1523
2014
  }
@@ -1574,7 +2065,7 @@ async function decodeVideoAudioPeaks(src, options = {}) {
1574
2065
  let buffer;
1575
2066
  try {
1576
2067
  buffer = await new Offline(1, 1, 44100).decodeAudioData(data);
1577
- } catch (e2) {
2068
+ } catch (e3) {
1578
2069
  throw new Error(`"${src}" has no sound this browser can decode.`);
1579
2070
  }
1580
2071
  return buildVideoAudioPeaks(Array.from({ length: buffer.numberOfChannels }, (_, i) => buffer.getChannelData(i)), buffer.sampleRate);
@@ -1645,6 +2136,50 @@ var VideoAudioPeaksCache = (_class2 = class {
1645
2136
  }
1646
2137
  }, _class2);
1647
2138
 
2139
+ // src/video-editor/plan.ts
2140
+ function clipIndex(project) {
2141
+ const out = /* @__PURE__ */ new Map();
2142
+ for (const { track } of _chunk3OSXHLITcjs.allVideoTracks.call(void 0, project)) for (const c of track.clips) out.set(c.id, c);
2143
+ return out;
2144
+ }
2145
+ function diffVideoProjects(before, after) {
2146
+ const a = clipIndex(before);
2147
+ const b = clipIndex(after);
2148
+ const clipsAdded = [...b.keys()].filter((id) => !a.has(id));
2149
+ const clipsRemoved = [...a.keys()].filter((id) => !b.has(id));
2150
+ const clipsChanged = [...b.keys()].filter((id) => a.has(id) && JSON.stringify(a.get(id)) !== JSON.stringify(b.get(id)));
2151
+ const durationBefore = _chunk3OSXHLITcjs.videoProjectDuration.call(void 0, before);
2152
+ const durationAfter = _chunk3OSXHLITcjs.videoProjectDuration.call(void 0, after);
2153
+ return { durationBefore, durationAfter, durationDelta: durationAfter - durationBefore, clipsAdded, clipsRemoved, clipsChanged };
2154
+ }
2155
+ function planVideoEdits(project, input) {
2156
+ if (!_optionalChain([input, 'access', _33 => _33.title, 'optionalAccess', _34 => _34.trim, 'call', _35 => _35()])) return { ok: false, error: 'A plan needs a title saying what it does (e.g. "Remove 12 filler words").' };
2157
+ if (!input.edits.length) return { ok: false, error: "The plan has no edits; there is nothing to propose." };
2158
+ const result = applyVideoEdits(project, input.edits);
2159
+ if (!result.ok) return { ok: false, error: result.error };
2160
+ const known = new Set(checkVideoProject(project).map(videoIssueKey));
2161
+ const summary = { ...diffVideoProjects(project, result.project), newIssues: result.issues.filter((i) => !known.has(videoIssueKey(i))), edits: input.edits.length };
2162
+ const plan = {
2163
+ id: _nullishCoalesce(input.id, () => ( "plan")),
2164
+ title: input.title.trim(),
2165
+ ...input.description ? { description: input.description } : {},
2166
+ ...input.source ? { source: input.source } : {},
2167
+ edits: structuredClone([...input.edits]),
2168
+ ..._optionalChain([input, 'access', _36 => _36.ranges, 'optionalAccess', _37 => _37.length]) ? { ranges: input.ranges.map((r) => ({ ...r })) } : {},
2169
+ summary
2170
+ };
2171
+ return { ok: true, plan, project: result.project };
2172
+ }
2173
+ function describeVideoEditPlan(plan) {
2174
+ const s = plan.summary;
2175
+ const parts = [];
2176
+ if (_optionalChain([plan, 'access', _38 => _38.ranges, 'optionalAccess', _39 => _39.length])) parts.push(`${plan.ranges.length} stretch${plan.ranges.length > 1 ? "es" : ""}`);
2177
+ const delta = s.durationDelta;
2178
+ const length = Math.abs(delta) < 0.5 ? "same length" : `${(Math.abs(delta) / 1e3).toFixed(1)} s ${delta < 0 ? "shorter" : "longer"}`;
2179
+ const clips = [s.clipsAdded.length && `${s.clipsAdded.length} added`, s.clipsChanged.length && `${s.clipsChanged.length} changed`, s.clipsRemoved.length && `${s.clipsRemoved.length} removed`].filter(Boolean).join(", ");
2180
+ return `${plan.title} (${parts.length ? `${parts.join(", ")}, ` : ""}${length})${clips ? `: clips ${clips}` : ""}`;
2181
+ }
2182
+
1648
2183
  // src/video-editor/scopes.ts
1649
2184
  var VIDEO_SCOPE_KINDS = ["histogram", "waveform", "vectorscope"];
1650
2185
  var videoLuma = (r, g, b) => 0.2126 * r + 0.7152 * g + 0.0722 * b;
@@ -2018,180 +2553,184 @@ function readVideoScopesPanel(feed, kinds = VIDEO_SCOPE_KINDS, options = {}) {
2018
2553
  return { ok: true, time: snap.time, complete: snap.complete, images, stats, notes: videoExposureNotes(stats) };
2019
2554
  }
2020
2555
 
2021
- // src/video-editor/transcript.ts
2022
- var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
2023
- function cueTime(s) {
2024
- const m = s.trim().match(/^(?:(\d+):)?(\d{1,2}):(\d{1,2})[.,](\d{1,3})$/);
2025
- if (!m) throw new Error(`"${s.trim()}" is not a cue time (HH:MM:SS,mmm).`);
2026
- return ((Number(_nullishCoalesce(m[1], () => ( 0))) * 60 + Number(m[2])) * 60 + Number(m[3])) * 1e3 + Number(m[4].padEnd(3, "0"));
2027
- }
2028
- function cueWords(text, start, end) {
2029
- const words = text.replace(/<[^>]+>/g, "").split(/\s+/).filter(Boolean);
2030
- const step = (end - start) / Math.max(1, words.length);
2031
- return words.map((word, i) => ({ word, start: Math.round(start + i * step), end: Math.round(start + (i + 1) * step) }));
2032
- }
2033
- function parseVideoTranscript(input) {
2034
- let data = input;
2035
- if (typeof input === "string") {
2036
- const text = input.replace(/^/, "").trim();
2037
- if (text.startsWith("{") || text.startsWith("[")) {
2038
- try {
2039
- data = JSON.parse(text);
2040
- } catch (e3) {
2041
- throw new Error("The transcript looks like JSON but is not valid JSON.");
2042
- }
2043
- } else {
2044
- const words2 = [];
2045
- for (const block of text.replace(/\r\n?/g, "\n").split(/\n{2,}/)) {
2046
- const lines = block.split("\n");
2047
- const at = lines.findIndex((l) => l.includes("-->"));
2048
- if (at < 0) continue;
2049
- const [a, b] = lines[at].split("-->");
2050
- words2.push(...cueWords(lines.slice(at + 1).join(" "), cueTime(a), cueTime(b.trim().split(/\s+/)[0])));
2051
- }
2052
- if (!words2.length) throw new Error("No words found: expected Whisper-style JSON with word timings, SubRip (.srt) or WebVTT.");
2053
- return { words: words2, approximate: true };
2054
- }
2556
+ // src/video-editor/transcript-controller.ts
2557
+ var TranscriptVideoController = (_class4 = class {
2558
+ constructor(controller, options = {}) {;_class4.prototype.__init9.call(this);_class4.prototype.__init10.call(this);_class4.prototype.__init11.call(this);_class4.prototype.__init12.call(this);_class4.prototype.__init13.call(this);_class4.prototype.__init14.call(this);_class4.prototype.__init15.call(this);
2559
+ this.controller = controller;
2560
+ this.options = options;
2561
+ this.track = options.track;
2562
+ this.state = this.compute();
2563
+ this.connect();
2055
2564
  }
2056
- const seconds = (v) => typeof v === "number" && Number.isFinite(v) ? Math.round(v * 1e3) : NaN;
2057
- const fromList = (list, toMs) => list.map((w, i) => {
2058
- if (!isRecord(w)) throw new Error(`Word ${i + 1} is not an object { word, start, end }.`);
2059
- const word = String(_nullishCoalesce(_nullishCoalesce(w.word, () => ( w.text)), () => ( ""))).trim();
2060
- const start = toMs(w.start);
2061
- const end = toMs(w.end);
2062
- if (!word || !Number.isFinite(start) || !Number.isFinite(end)) throw new Error(`Word ${i + 1} needs a word and numeric start and end.`);
2063
- return { word, start, end: Math.max(start, end) };
2064
- });
2065
- let words = [];
2066
- if (Array.isArray(data)) words = fromList(data, (v) => typeof v === "number" ? v : NaN);
2067
- else if (isRecord(data) && Array.isArray(data.words)) words = fromList(data.words, seconds);
2068
- else if (isRecord(data) && Array.isArray(data.segments)) {
2069
- const segments = data.segments.filter(isRecord);
2070
- if (segments.some((s) => Array.isArray(s.words))) words = fromList(segments.flatMap((s) => Array.isArray(s.words) ? s.words : []), seconds);
2071
- else {
2072
- for (const s of segments) words.push(...cueWords(String(_nullishCoalesce(s.text, () => ( ""))), seconds(s.start), seconds(s.end)));
2073
- if (!words.length) throw new Error("No words found in the segments.");
2074
- return { words, approximate: true };
2075
- }
2565
+
2566
+
2567
+ __init9() {this.listeners = /* @__PURE__ */ new Set()}
2568
+ __init10() {this.unsubscribe = null}
2569
+
2570
+ __init11() {this.selection = null}
2571
+ __init12() {this.error = null}
2572
+
2573
+ /** Words and disfluencies are worked out once per project. */
2574
+ __init13() {this.cache = null}
2575
+ __init14() {this.getState = () => this.state}
2576
+ connect() {
2577
+ if (this.unsubscribe) return;
2578
+ this.unsubscribe = this.controller.subscribe(() => this.refresh());
2579
+ this.refresh();
2076
2580
  }
2077
- if (!words.length) throw new Error("No words found: expected Whisper-style JSON with word timings, SubRip (.srt) or WebVTT.");
2078
- return { words: words.sort((a, b) => a.start - b.start), approximate: false };
2079
- }
2080
- function videoTranscriptTrack(project, track) {
2081
- if (track !== void 0) {
2082
- if (!project.tracks.some((t) => t.id === track)) throw new Error(`There is no track "${track}"; the tracks are ${project.tracks.map((t) => `"${t.id}"`).join(", ")}.`);
2083
- return track;
2581
+ /** Listens for changes. Subscribing after `destroy` follows the editor again (React StrictMode remounts). */
2582
+ __init15() {this.subscribe = (listener) => {
2583
+ this.connect();
2584
+ this.listeners.add(listener);
2585
+ return () => {
2586
+ this.listeners.delete(listener);
2587
+ };
2588
+ }}
2589
+ /** Stops following the editor. */
2590
+ destroy() {
2591
+ _optionalChain([this, 'access', _40 => _40.unsubscribe, 'optionalCall', _41 => _41()]);
2592
+ this.unsubscribe = null;
2593
+ this.listeners.clear();
2084
2594
  }
2085
- const assets = _chunkVKF3PWKRcjs.videoAssetMap.call(void 0, project);
2086
- const transcribed = (t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access', _21 => _21.get, 'call', _22 => _22(c.asset), 'optionalAccess', _23 => _23.transcript, 'optionalAccess', _24 => _24.length]));
2087
- return _nullishCoalesce(_optionalChain([(_nullishCoalesce(project.tracks.find((t) => t.kind === "audio" && transcribed(t)), () => ( project.tracks.find(transcribed)))), 'optionalAccess', _25 => _25.id]), () => ( null));
2088
- }
2089
- function videoTimelineTranscript(project, options = {}) {
2090
- const id = videoTranscriptTrack(project, options.track);
2091
- if (!id) return [];
2092
- const track = project.tracks.find((t) => t.id === id);
2093
- const assets = _chunkVKF3PWKRcjs.videoAssetMap.call(void 0, project);
2094
- const out = [];
2095
- for (const clip of _chunkVKF3PWKRcjs.sortedVideoClips.call(void 0, track)) {
2096
- if (clip.type !== "media") continue;
2097
- const words = _optionalChain([assets, 'access', _26 => _26.get, 'call', _27 => _27(clip.asset), 'optionalAccess', _28 => _28.transcript]);
2098
- if (!_optionalChain([words, 'optionalAccess', _29 => _29.length])) continue;
2099
- const speed = _chunkVKF3PWKRcjs.videoClipSpeed.call(void 0, clip);
2100
- const from = _nullishCoalesce(clip.in, () => ( 0));
2101
- const to = from + clip.duration * speed;
2102
- const toTimeline = (s) => Math.min(_chunkVKF3PWKRcjs.videoClipEnd.call(void 0, clip), Math.max(clip.start, clip.start + (s - from) / speed));
2103
- words.forEach((w, index) => {
2104
- const mid = (w.start + w.end) / 2;
2105
- if (mid < from || mid >= to) return;
2106
- out.push({ word: w.word, start: toTimeline(w.start), end: toTimeline(w.end), clip: clip.id, track: track.id, index });
2107
- });
2595
+ words(project) {
2596
+ const c = this.cache;
2597
+ if (c && c.project === project && c.track === this.track) return c;
2598
+ let words = [];
2599
+ let trackId = null;
2600
+ try {
2601
+ trackId = videoTranscriptTrack(project, this.track);
2602
+ words = videoTimelineTranscript(project, { track: this.track });
2603
+ } catch (e4) {
2604
+ }
2605
+ const disfluencies = detectVideoDisfluencies(words, { ...this.options.disfluencies, pauses: { fps: project.fps, ..._optionalChain([this, 'access', _42 => _42.options, 'access', _43 => _43.disfluencies, 'optionalAccess', _44 => _44.pauses]) } });
2606
+ const flags = {};
2607
+ for (const d of disfluencies)
2608
+ for (let i = d.words[0]; i <= d.words[1]; i++) {
2609
+ if (d.kind === "pause" && i !== d.words[0]) continue;
2610
+ const f = flags[i] ??= [];
2611
+ if (!f.includes(d.kind)) f.push(d.kind);
2612
+ }
2613
+ this.cache = { project, track: this.track, words, trackId, text: words.map((w) => w.word).join(" "), disfluencies, flags };
2614
+ return this.cache;
2615
+ }
2616
+ compute() {
2617
+ const editor = this.controller.getState();
2618
+ const c = this.words(editor.project);
2619
+ if (this.selection && (this.selection[0] >= c.words.length || this.selection[1] >= c.words.length)) this.selection = null;
2620
+ return { track: c.trackId, words: c.words, text: c.text, current: videoWordAt(c.words, editor.time), selection: this.selection, disfluencies: c.disfluencies, flags: c.flags, editable: !editor.readOnly, error: this.error };
2621
+ }
2622
+ refresh() {
2623
+ const next = this.compute();
2624
+ const prev = this.state;
2625
+ if (next.words === prev.words && next.current === prev.current && next.selection === prev.selection && next.editable === prev.editable && next.error === prev.error && next.disfluencies === prev.disfluencies) return;
2626
+ this.state = next;
2627
+ this.listeners.forEach((l) => l(next));
2628
+ }
2629
+ /** Follows another track's words (`undefined`: the default track). */
2630
+ setTrack(track) {
2631
+ this.track = track;
2632
+ this.selection = null;
2633
+ this.refresh();
2634
+ }
2635
+ /** Moves the playhead to the start of a word. */
2636
+ seekToWord(index) {
2637
+ const w = this.state.words[index];
2638
+ if (w) this.controller.seek(w.start);
2639
+ }
2640
+ /** Selects words `a` to `b` (either order), or clears the selection with `null`. */
2641
+ select(a, b = a) {
2642
+ const n = this.state.words.length;
2643
+ this.selection = a === null || b === null || a < 0 || b < 0 || a >= n || b >= n ? null : a <= b ? [a, b] : [b, a];
2644
+ this.error = null;
2645
+ this.refresh();
2646
+ }
2647
+ /** Index of the word playing at a timeline time, or -1. */
2648
+ wordAt(time) {
2649
+ return videoWordAt(this.state.words, time);
2650
+ }
2651
+ /** Where word `index` begins in `text` (characters). */
2652
+ textOffset(index) {
2653
+ let offset = 0;
2654
+ for (let i = 0; i < Math.min(index, this.state.words.length); i++) offset += this.state.words[i].word.length + 1;
2655
+ return offset;
2656
+ }
2657
+ /** The word a caret at `offset` characters into `text` is in (or just after), as an index; -1 when there are no words. */
2658
+ wordAtOffset(offset) {
2659
+ const { words } = this.state;
2660
+ let at = 0;
2661
+ for (let i = 0; i < words.length; i++) {
2662
+ at += words[i].word.length;
2663
+ if (offset <= at) return i;
2664
+ at += 1;
2665
+ }
2666
+ return words.length - 1;
2108
2667
  }
2109
- return out;
2110
- }
2111
- var normalizeVideoWord = (word) => word.toLowerCase().replace(/^[^\p{L}\p{N}']+|[^\p{L}\p{N}']+$/gu, "");
2112
- var rangeOf = (words, i, n) => ({ from: words[i].start, to: words[i + n - 1].end, text: words.slice(i, i + n).map((w) => w.word).join(" ") });
2113
- function findVideoTranscriptText(words, query) {
2114
- const q = query.split(/\s+/).map(normalizeVideoWord).filter(Boolean);
2115
- if (!q.length) throw new Error("Search for at least one word.");
2116
- const norm = words.map((w) => normalizeVideoWord(w.word));
2117
- const out = [];
2118
- for (let i = 0; i + q.length <= norm.length; i++) if (q.every((x, k) => norm[i + k] === x)) out.push(rangeOf(words, i, q.length));
2119
- return out;
2120
- }
2121
- var VIDEO_DEFAULT_FILLERS = ["um", "umm", "uh", "uhh", "uhm", "erm", "er", "ah", "hmm", "mm", "you know", "i mean"];
2122
- function findVideoFillers(words, fillers = VIDEO_DEFAULT_FILLERS) {
2123
- const phrases = fillers.map((f) => f.split(/\s+/).map(normalizeVideoWord).filter(Boolean)).filter((p) => p.length).sort((a, b) => b.length - a.length);
2124
- const norm = words.map((w) => normalizeVideoWord(w.word));
2125
- const out = [];
2126
- for (let i = 0; i < norm.length; ) {
2127
- const hit = phrases.find((p) => i + p.length <= norm.length && p.every((x, k) => norm[i + k] === x));
2128
- if (hit) {
2129
- out.push(rangeOf(words, i, hit.length));
2130
- i += hit.length;
2131
- } else i++;
2668
+ /** The timeline time a caret at `offset` characters into `text` stands for: the start of its word. */
2669
+ timeAtOffset(offset) {
2670
+ const i = this.wordAtOffset(offset);
2671
+ return i < 0 ? null : this.state.words[i].start;
2132
2672
  }
2133
- return out;
2134
- }
2135
- function findVideoTranscriptPauses(words, options = {}) {
2136
- const min = _nullishCoalesce(options.min, () => ( 700));
2137
- const keep = _nullishCoalesce(options.keep, () => ( 150));
2138
- const out = [];
2139
- for (let i = 1; i < words.length; i++) {
2140
- const a = words[i - 1];
2141
- const b = words[i];
2142
- if (a.clip !== b.clip || b.start - a.end < min) continue;
2143
- let from = a.end + keep;
2144
- let to = b.start - keep;
2145
- if (options.fps) {
2146
- const f = 1e3 / options.fps;
2147
- from = _chunkVKF3PWKRcjs.snapToVideoFrame.call(void 0, Math.ceil(from / f - 1e-9) * f, options.fps);
2148
- to = _chunkVKF3PWKRcjs.snapToVideoFrame.call(void 0, Math.floor(to / f + 1e-9) * f, options.fps);
2149
- }
2150
- if (to - from > _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON) out.push({ from, to, text: `(pause after "${a.word}")` });
2673
+ fail(message) {
2674
+ this.error = message;
2675
+ this.refresh();
2676
+ return { ok: false, error: message };
2151
2677
  }
2152
- return out;
2153
- }
2154
- function mergeVideoRanges(ranges) {
2155
- const sorted = ranges.filter((r) => r.to - r.from > _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON).map((r) => ({ from: r.from, to: r.to })).sort((a, b) => a.from - b.from);
2156
- const out = [];
2157
- for (const r of sorted) {
2158
- const last = out[out.length - 1];
2159
- if (last && r.from <= last.to + _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON) last.to = Math.max(last.to, r.to);
2160
- else out.push(r);
2678
+ /** Cuts ranges out (or proposes a plan to, with `plan`), as one undo step. */
2679
+ cut(ranges, title, plan) {
2680
+ if (!ranges.length) return this.fail("There is nothing to cut.");
2681
+ const merged = mergeVideoRanges(ranges);
2682
+ const edits = videoRemoveRangesEdits(merged);
2683
+ if (plan) {
2684
+ const r2 = this.controller.proposePlan({ title, edits, ranges, source: "transcript" });
2685
+ if (!r2.ok) return this.fail(r2.error);
2686
+ this.error = null;
2687
+ this.select(null);
2688
+ return { ok: true, plan: r2.plan };
2689
+ }
2690
+ const r = this.controller.apply(edits, title);
2691
+ if (!r.ok) return this.fail(r.error);
2692
+ this.error = null;
2693
+ this.select(null);
2694
+ return { ok: true, applied: r };
2161
2695
  }
2162
- return out;
2163
- }
2164
- function videoRemoveRangesEdits(ranges, tracks) {
2165
- return mergeVideoRanges(ranges).reverse().map((r) => ({ op: "extract_range", from: r.from, to: r.to, ...tracks ? { tracks } : {} }));
2166
- }
2167
- function videoWordsRange(words, a, b) {
2168
- const [i, j] = a <= b ? [a, b] : [b, a];
2169
- return { from: words[i].start, to: words[j].end };
2170
- }
2171
- function videoTranscriptEdit(assetId, text) {
2172
- return { op: "update_asset", asset: assetId, set: { transcript: parseVideoTranscript(text).words } };
2173
- }
2174
- function videoWordAt(words, time) {
2175
- let lo = 0;
2176
- let hi = words.length - 1;
2177
- while (lo <= hi) {
2178
- const mid = lo + hi >> 1;
2179
- if (words[mid].end <= time) lo = mid + 1;
2180
- else if (words[mid].start > time) hi = mid - 1;
2181
- else return mid;
2696
+ /** Deletes the selected words, cutting their stretch out of the timeline (or proposing it with `planFirst`). */
2697
+ deleteSelection(options = {}) {
2698
+ const sel = this.state.selection;
2699
+ if (!sel) return this.fail("Select the words to delete first.");
2700
+ const { words } = this.state;
2701
+ const range = videoWordsRange(words, sel[0], sel[1]);
2702
+ const text = words.slice(sel[0], sel[1] + 1).map((w) => w.word).join(" ");
2703
+ return this.cut([{ ...range, text, reason: "deleted" }], sel[0] === sel[1] ? `Delete "${text}"` : `Delete ${sel[1] - sel[0] + 1} words`, _nullishCoalesce(_nullishCoalesce(options.plan, () => ( this.options.planFirst)), () => ( false)));
2704
+ }
2705
+ /** Cuts one disfluency out at once (its index in `disfluencies`), as one undo step. */
2706
+ removeDisfluency(index) {
2707
+ const d = this.state.disfluencies[index];
2708
+ if (!d) return this.fail(`There is no disfluency ${index}; there are ${this.state.disfluencies.length}.`);
2709
+ return this.cut([{ from: d.from, to: d.to, text: d.text, reason: d.kind }], d.kind === "pause" ? "Tighten pause" : `Remove "${d.text}"`, false);
2710
+ }
2711
+ /** Proposes one plan that cuts every disfluency of the given kinds (default all), for review. */
2712
+ planCleanup(kinds) {
2713
+ const found = this.state.disfluencies.filter((d) => !kinds || kinds.includes(d.kind));
2714
+ if (!found.length) return this.fail(kinds ? `No ${kinds.join(", ")} found.` : "No fillers, long pauses or repetitions found.");
2715
+ const counts = ["filler", "pause", "repetition"].map((k) => [k, found.filter((d) => d.kind === k).length]).filter(([, n]) => n);
2716
+ const title = `Clean up ${counts.map(([k, n]) => `${n} ${k}${n > 1 ? "s" : ""}`).join(", ")}`;
2717
+ return this.cut(
2718
+ found.map((d) => ({ from: d.from, to: d.to, text: d.text, reason: d.kind })),
2719
+ title,
2720
+ true
2721
+ );
2182
2722
  }
2183
- return -1;
2184
- }
2723
+ }, _class4);
2185
2724
 
2186
2725
  // src/video-editor/proxy.ts
2187
2726
  var VIDEO_PROXY_SCALES = [0.5, 0.25];
2188
2727
  function videoAssetProxy(asset, scale) {
2189
- if (!_optionalChain([asset, 'optionalAccess', _30 => _30.proxies, 'optionalAccess', _31 => _31.length]) || !(scale > 0) || scale >= 1) return null;
2728
+ if (!_optionalChain([asset, 'optionalAccess', _45 => _45.proxies, 'optionalAccess', _46 => _46.length]) || !(scale > 0) || scale >= 1) return null;
2190
2729
  let best = null;
2191
2730
  for (const p of asset.proxies) if (p.scale >= scale - 1e-9 && p.scale < 1 && (!best || p.scale < best.scale)) best = p;
2192
2731
  return best;
2193
2732
  }
2194
- var videoPreviewSrc = (asset, scale, fallback = _nullishCoalesce(_optionalChain([asset, 'optionalAccess', _32 => _32.src]), () => ( ""))) => _nullishCoalesce(_optionalChain([videoAssetProxy, 'call', _33 => _33(asset, scale), 'optionalAccess', _34 => _34.src]), () => ( fallback));
2733
+ var videoPreviewSrc = (asset, scale, fallback = _nullishCoalesce(_optionalChain([asset, 'optionalAccess', _47 => _47.src]), () => ( ""))) => _nullishCoalesce(_optionalChain([videoAssetProxy, 'call', _48 => _48(asset, scale), 'optionalAccess', _49 => _49.src]), () => ( fallback));
2195
2734
  function resolveVideoPreviewScale(scale, shown) {
2196
2735
  if (scale === void 0 || scale === "auto") return Math.min(1, Math.max(0, shown));
2197
2736
  if (scale === "full") return 1;
@@ -2240,7 +2779,7 @@ function videoProxyFfmpeg(asset, options = {}) {
2240
2779
  output
2241
2780
  ];
2242
2781
  const proxy = { scale, src: output, ...asset.width && asset.height ? { width: Math.trunc(asset.width * scale / 2) * 2, height: Math.trunc(asset.height * scale / 2) * 2 } : {} };
2243
- return { asset: asset.id, args, command: ["ffmpeg", ...args].map(_chunkVKF3PWKRcjs.videoShellQuote).join(" "), proxy };
2782
+ return { asset: asset.id, args, command: ["ffmpeg", ...args].map(_chunk3OSXHLITcjs.videoShellQuote).join(" "), proxy };
2244
2783
  }
2245
2784
  function videoProxyPlan(project, options = {}) {
2246
2785
  const scale = _nullishCoalesce(options.scale, () => ( 0.5));
@@ -2250,7 +2789,7 @@ function videoProxyPlan(project, options = {}) {
2250
2789
  if (!a) throw new Error(`There is no asset "${id}"; the assets are ${project.assets.map((x) => `"${x.id}"`).join(", ") || "none"}.`);
2251
2790
  if (a.kind !== "video") throw new Error(`Asset "${id}" is ${a.kind === "image" ? "an image" : "audio"}; only video assets have proxies.`);
2252
2791
  }
2253
- return project.assets.filter((a) => a.kind === "video" && (!wanted || wanted.has(a.id)) && !_optionalChain([a, 'access', _35 => _35.proxies, 'optionalAccess', _36 => _36.some, 'call', _37 => _37((p) => Math.abs(p.scale - scale) < 1e-9)])).map((a) => videoProxyFfmpeg(a, { ...options, scale }));
2792
+ return project.assets.filter((a) => a.kind === "video" && (!wanted || wanted.has(a.id)) && !_optionalChain([a, 'access', _50 => _50.proxies, 'optionalAccess', _51 => _51.some, 'call', _52 => _52((p) => Math.abs(p.scale - scale) < 1e-9)])).map((a) => videoProxyFfmpeg(a, { ...options, scale }));
2254
2793
  }
2255
2794
  var VIDEO_PREVIEW_SCALE_OPTIONS = [
2256
2795
  { value: "auto", label: "Auto", scale: "auto" },
@@ -2258,7 +2797,7 @@ var VIDEO_PREVIEW_SCALE_OPTIONS = [
2258
2797
  ...VIDEO_PROXY_SCALES.map((s) => ({ value: String(s), label: s === 0.5 ? "\xBD" : s === 0.25 ? "\xBC" : `${s * 100}%`, scale: s }))
2259
2798
  ];
2260
2799
  var parseVideoPreviewScale = (value) => value === "full" ? "full" : Number(value) > 0 && Number(value) <= 1 ? Number(value) : "auto";
2261
- var videoProjectHasProxies = (project) => project.assets.some((a) => !!_optionalChain([a, 'access', _38 => _38.proxies, 'optionalAccess', _39 => _39.length]));
2800
+ var videoProjectHasProxies = (project) => project.assets.some((a) => !!_optionalChain([a, 'access', _53 => _53.proxies, 'optionalAccess', _54 => _54.length]));
2262
2801
 
2263
2802
  // src/video-editor/frame-cache.ts
2264
2803
  var videoSourceFrame = (sourceTime, fps) => Math.max(0, Math.floor(sourceTime * fps / 1e3 + 1e-6));
@@ -2266,7 +2805,7 @@ var videoSourceFrameTime = (frame, fps) => (frame + 0.5) * 1e3 / fps;
2266
2805
  var videoFrameCacheKey = (src, frame) => `${frame}@${src}`;
2267
2806
  function videoFrameRequestsAt(project, time, srcFor = (l) => l.src) {
2268
2807
  const out = [];
2269
- for (const layer of _chunkVKF3PWKRcjs.videoFrameLeafLayers.call(void 0, _chunkVKF3PWKRcjs.videoFrameAt.call(void 0, project, time).layers)) {
2808
+ for (const layer of _chunk3OSXHLITcjs.videoFrameLeafLayers.call(void 0, _chunk3OSXHLITcjs.videoFrameAt.call(void 0, project, time).layers)) {
2270
2809
  if (layer.type !== "media" || layer.assetKind !== "video") continue;
2271
2810
  const src = srcFor(layer);
2272
2811
  if (!src) continue;
@@ -2302,7 +2841,7 @@ function videoCacheEvictions(entries, options) {
2302
2841
  return d >= 0 ? d : -d * behind;
2303
2842
  };
2304
2843
  const out = [];
2305
- for (const e of entries.filter((x) => !_optionalChain([options, 'access', _40 => _40.protect, 'optionalAccess', _41 => _41.has, 'call', _42 => _42(x.key)])).sort((a, b) => cost(b) - cost(a))) {
2844
+ for (const e of entries.filter((x) => !_optionalChain([options, 'access', _55 => _55.protect, 'optionalAccess', _56 => _56.has, 'call', _57 => _57(x.key)])).sort((a, b) => cost(b) - cost(a))) {
2306
2845
  if (total <= options.maxBytes) break;
2307
2846
  out.push(e.key);
2308
2847
  total -= e.bytes;
@@ -2314,26 +2853,26 @@ var defaultCapture = async (source) => {
2314
2853
  const image = await createImageBitmap(source);
2315
2854
  return { image, width: image.width, height: image.height };
2316
2855
  };
2317
- var VideoFrameCache = (_class4 = class {
2318
- constructor(options = {}) {;_class4.prototype.__init9.call(this);_class4.prototype.__init10.call(this);_class4.prototype.__init11.call(this);_class4.prototype.__init12.call(this);_class4.prototype.__init13.call(this);_class4.prototype.__init14.call(this);_class4.prototype.__init15.call(this);_class4.prototype.__init16.call(this);_class4.prototype.__init17.call(this);_class4.prototype.__init18.call(this);_class4.prototype.__init19.call(this);_class4.prototype.__init20.call(this);
2856
+ var VideoFrameCache = (_class5 = class {
2857
+ constructor(options = {}) {;_class5.prototype.__init16.call(this);_class5.prototype.__init17.call(this);_class5.prototype.__init18.call(this);_class5.prototype.__init19.call(this);_class5.prototype.__init20.call(this);_class5.prototype.__init21.call(this);_class5.prototype.__init22.call(this);_class5.prototype.__init23.call(this);_class5.prototype.__init24.call(this);_class5.prototype.__init25.call(this);_class5.prototype.__init26.call(this);_class5.prototype.__init27.call(this);
2319
2858
  this.options = options;
2320
2859
  this.maxBytes = _nullishCoalesce(options.maxBytes, () => ( 512 * 1024 * 1024));
2321
2860
  this.prefetch = _nullishCoalesce(options.prefetch, () => ( 2e3));
2322
2861
  this.decode = options.decode !== void 0 ? options.decode : typeof document !== "undefined" ? createVideoElementDecoder() : null;
2323
2862
  }
2324
2863
 
2325
- __init9() {this.entries = /* @__PURE__ */ new Map()}
2326
- __init10() {this.inFlight = /* @__PURE__ */ new Set()}
2327
- __init11() {this.queue = []}
2328
- __init12() {this.protect = /* @__PURE__ */ new Set()}
2329
- __init13() {this.listeners = /* @__PURE__ */ new Set()}
2330
- __init14() {this.playhead = 0}
2331
- __init15() {this.direction = 1}
2332
- __init16() {this.hits = 0}
2333
- __init17() {this.misses = 0}
2334
- __init18() {this.bytes = 0}
2335
- __init19() {this.generation = 0}
2336
- __init20() {this.destroyed = false}
2864
+ __init16() {this.entries = /* @__PURE__ */ new Map()}
2865
+ __init17() {this.inFlight = /* @__PURE__ */ new Set()}
2866
+ __init18() {this.queue = []}
2867
+ __init19() {this.protect = /* @__PURE__ */ new Set()}
2868
+ __init20() {this.listeners = /* @__PURE__ */ new Set()}
2869
+ __init21() {this.playhead = 0}
2870
+ __init22() {this.direction = 1}
2871
+ __init23() {this.hits = 0}
2872
+ __init24() {this.misses = 0}
2873
+ __init25() {this.bytes = 0}
2874
+ __init26() {this.generation = 0}
2875
+ __init27() {this.destroyed = false}
2337
2876
 
2338
2877
 
2339
2878
 
@@ -2343,7 +2882,7 @@ var VideoFrameCache = (_class4 = class {
2343
2882
  const entry = this.entries.get(videoFrameCacheKey(src, videoSourceFrame(sourceTime, fps)));
2344
2883
  if (entry) this.hits++;
2345
2884
  else this.misses++;
2346
- return _optionalChain([entry, 'optionalAccess', _43 => _43.frame, 'access', _44 => _44.image]);
2885
+ return _optionalChain([entry, 'optionalAccess', _58 => _58.frame, 'access', _59 => _59.image]);
2347
2886
  }
2348
2887
  /** Whether a frame is cached, without counting it. */
2349
2888
  has(src, sourceTime, fps) {
@@ -2351,7 +2890,7 @@ var VideoFrameCache = (_class4 = class {
2351
2890
  }
2352
2891
  /** Keeps a decoded frame (replacing any with the same key), then evicts to the budget. */
2353
2892
  put(request, frame) {
2354
- if (this.destroyed) return void _optionalChain([frame, 'access', _45 => _45.image, 'access', _46 => _46.close, 'optionalCall', _47 => _47()]);
2893
+ if (this.destroyed) return void _optionalChain([frame, 'access', _60 => _60.image, 'access', _61 => _61.close, 'optionalCall', _62 => _62()]);
2355
2894
  const old = this.entries.get(request.key);
2356
2895
  if (old) this.drop(old);
2357
2896
  const bytes = Math.max(1, frame.width * frame.height * 4);
@@ -2450,7 +2989,7 @@ var VideoFrameCache = (_class4 = class {
2450
2989
  (frame) => {
2451
2990
  this.inFlight.delete(r.key);
2452
2991
  if (generation === this.generation || this.bytes + frame.width * frame.height * 4 <= this.maxBytes) this.put(r, frame);
2453
- else _optionalChain([frame, 'access', _48 => _48.image, 'access', _49 => _49.close, 'optionalCall', _50 => _50()]);
2992
+ else _optionalChain([frame, 'access', _63 => _63.image, 'access', _64 => _64.close, 'optionalCall', _65 => _65()]);
2454
2993
  },
2455
2994
  () => this.inFlight.delete(r.key)
2456
2995
  ).finally(() => this.pump());
@@ -2463,12 +3002,12 @@ var VideoFrameCache = (_class4 = class {
2463
3002
  drop(e) {
2464
3003
  this.entries.delete(e.key);
2465
3004
  this.bytes -= e.bytes;
2466
- _optionalChain([e, 'access', _51 => _51.frame, 'access', _52 => _52.image, 'access', _53 => _53.close, 'optionalCall', _54 => _54()]);
3005
+ _optionalChain([e, 'access', _66 => _66.frame, 'access', _67 => _67.image, 'access', _68 => _68.close, 'optionalCall', _69 => _69()]);
2467
3006
  }
2468
3007
  emit() {
2469
3008
  for (const l of [...this.listeners]) l();
2470
3009
  }
2471
- }, _class4);
3010
+ }, _class5);
2472
3011
  function createVideoElementDecoder(options = {}) {
2473
3012
  const videos = /* @__PURE__ */ new Map();
2474
3013
  const make = _nullishCoalesce(options.createVideo, () => ( (() => {
@@ -2510,12 +3049,12 @@ function createVideoElementDecoder(options = {}) {
2510
3049
  }
2511
3050
 
2512
3051
  // src/video-editor/preview.ts
2513
- var VideoPreviewEngine = (_class5 = class {
2514
- constructor(source, canvas2, options = {}) {;_class5.prototype.__init21.call(this);_class5.prototype.__init22.call(this);_class5.prototype.__init23.call(this);_class5.prototype.__init24.call(this);_class5.prototype.__init25.call(this);_class5.prototype.__init26.call(this);_class5.prototype.__init27.call(this);_class5.prototype.__init28.call(this);_class5.prototype.__init29.call(this);_class5.prototype.__init30.call(this);_class5.prototype.__init31.call(this);_class5.prototype.__init32.call(this);_class5.prototype.__init33.call(this);_class5.prototype.__init34.call(this);_class5.prototype.__init35.call(this);_class5.prototype.__init36.call(this);
3052
+ var VideoPreviewEngine = (_class6 = class {
3053
+ constructor(source, canvas2, options = {}) {;_class6.prototype.__init28.call(this);_class6.prototype.__init29.call(this);_class6.prototype.__init30.call(this);_class6.prototype.__init31.call(this);_class6.prototype.__init32.call(this);_class6.prototype.__init33.call(this);_class6.prototype.__init34.call(this);_class6.prototype.__init35.call(this);_class6.prototype.__init36.call(this);_class6.prototype.__init37.call(this);_class6.prototype.__init38.call(this);_class6.prototype.__init39.call(this);_class6.prototype.__init40.call(this);_class6.prototype.__init41.call(this);_class6.prototype.__init42.call(this);_class6.prototype.__init43.call(this);
2515
3054
  this.source = source;
2516
3055
  this.options = options;
2517
3056
  this.canvas = canvas2;
2518
- this.ctx = options.context !== void 0 ? options.context : _nullishCoalesce(_optionalChain([canvas2, 'access', _59 => _59.getContext, 'optionalCall', _60 => _60("2d")]), () => ( null));
3057
+ this.ctx = options.context !== void 0 ? options.context : _nullishCoalesce(_optionalChain([canvas2, 'access', _74 => _74.getContext, 'optionalCall', _75 => _75("2d")]), () => ( null));
2519
3058
  this.unsubscribe = source.subscribe(() => this.update());
2520
3059
  this.lutCache = options.luts !== void 0 ? options.luts : _nullishCoalesce(source.luts, () => ( null));
2521
3060
  this.unsubscribeLuts = this.lutCache ? this.lutCache.subscribe(() => this.redraw()) : () => {
@@ -2536,31 +3075,31 @@ var VideoPreviewEngine = (_class5 = class {
2536
3075
 
2537
3076
 
2538
3077
 
2539
- __init21() {this.media = /* @__PURE__ */ new Map()}
2540
- __init22() {this.images = /* @__PURE__ */ new Map()}
3078
+ __init28() {this.media = /* @__PURE__ */ new Map()}
3079
+ __init29() {this.images = /* @__PURE__ */ new Map()}
2541
3080
 
2542
- __init23() {this.scheduled = false}
2543
- __init24() {this.destroyed = false}
2544
- __init25() {this.filterHost = null}
2545
- __init26() {this.filters = /* @__PURE__ */ new Set()}
3081
+ __init30() {this.scheduled = false}
3082
+ __init31() {this.destroyed = false}
3083
+ __init32() {this.filterHost = null}
3084
+ __init33() {this.filters = /* @__PURE__ */ new Set()}
2546
3085
 
2547
3086
 
2548
- __init27() {this.drawn = null}
2549
- __init28() {this.complete = true}
3087
+ __init34() {this.drawn = null}
3088
+ __init35() {this.complete = true}
2550
3089
 
2551
3090
 
2552
3091
 
2553
- __init29() {this.srcFor = (layer) => layer.src}
2554
- __init30() {this.playing = false}
2555
- __init31() {this.fps = 30}
3092
+ __init36() {this.srcFor = (layer) => layer.src}
3093
+ __init37() {this.playing = false}
3094
+ __init38() {this.fps = 30}
2556
3095
 
2557
- __init32() {this.scratchCanvases = /* @__PURE__ */ new Map()}
3096
+ __init39() {this.scratchCanvases = /* @__PURE__ */ new Map()}
2558
3097
 
2559
3098
 
2560
3099
  /** The canvas's pixels and the frame they show, or `null` when its context cannot be read. */
2561
3100
  readPixels() {
2562
3101
  const ctx = this.ctx;
2563
- if (!_optionalChain([ctx, 'optionalAccess', _61 => _61.getImageData]) || !this.drawn) return null;
3102
+ if (!_optionalChain([ctx, 'optionalAccess', _76 => _76.getImageData]) || !this.drawn) return null;
2564
3103
  return { ...this.drawn, pixels: ctx.getImageData(0, 0, this.canvas.width, this.canvas.height) };
2565
3104
  }
2566
3105
  createMedia(kind) {
@@ -2598,8 +3137,8 @@ var VideoPreviewEngine = (_class5 = class {
2598
3137
  entry.media.pause();
2599
3138
  entry.media.removeEventListener("seeked", entry.onReady);
2600
3139
  entry.media.removeEventListener("loadeddata", entry.onReady);
2601
- _optionalChain([entry, 'access', _62 => _62.media, 'access', _63 => _63.removeAttribute, 'optionalCall', _64 => _64("src")]);
2602
- _optionalChain([entry, 'access', _65 => _65.media, 'access', _66 => _66.load, 'optionalCall', _67 => _67()]);
3140
+ _optionalChain([entry, 'access', _77 => _77.media, 'access', _78 => _78.removeAttribute, 'optionalCall', _79 => _79("src")]);
3141
+ _optionalChain([entry, 'access', _80 => _80.media, 'access', _81 => _81.load, 'optionalCall', _82 => _82()]);
2603
3142
  this.media.delete(entry.clip);
2604
3143
  }
2605
3144
  /** Brings the media elements in step with the editor and draws the frame. */
@@ -2607,13 +3146,13 @@ var VideoPreviewEngine = (_class5 = class {
2607
3146
  if (this.destroyed) return;
2608
3147
  const state = this.source.getState();
2609
3148
  const { project, time, playing } = state;
2610
- const frame = _chunkVKF3PWKRcjs.videoFrameAt.call(void 0, project, time);
3149
+ const frame = _chunk3OSXHLITcjs.videoFrameAt.call(void 0, project, time);
2611
3150
  const scale = resolveVideoPreviewScale(_nullishCoalesce(this.options.previewScale, () => ( state.previewScale)), this.resolutionFor(project.width));
2612
3151
  const assets = new Map(project.assets.map((a) => [a.id, a]));
2613
3152
  this.srcFor = (layer) => videoPreviewSrc(assets.get(_nullishCoalesce(layer.asset, () => ( ""))), scale, layer.src);
2614
3153
  this.playing = playing;
2615
3154
  this.fps = project.fps;
2616
- _optionalChain([this, 'access', _68 => _68.frameCache, 'optionalAccess', _69 => _69.update, 'call', _70 => _70(project, time, { playing, srcFor: this.srcFor })]);
3155
+ _optionalChain([this, 'access', _83 => _83.frameCache, 'optionalAccess', _84 => _84.update, 'call', _85 => _85(project, time, { playing, srcFor: this.srcFor })]);
2617
3156
  const tolerance = (_nullishCoalesce(this.options.tolerance, () => ( 200))) / 1e3;
2618
3157
  const active = /* @__PURE__ */ new Set();
2619
3158
  const sync = (clip, src, kind, sourceTime, speed, gain) => {
@@ -2637,10 +3176,10 @@ var VideoPreviewEngine = (_class5 = class {
2637
3176
  }
2638
3177
  };
2639
3178
  const sounding = new Map(frame.audio.map((a) => [a.clip, a]));
2640
- for (const layer of _chunkVKF3PWKRcjs.videoFrameLeafLayers.call(void 0, frame.layers)) {
3179
+ for (const layer of _chunk3OSXHLITcjs.videoFrameLeafLayers.call(void 0, frame.layers)) {
2641
3180
  if (layer.type !== "media" || layer.assetKind !== "video" || !layer.src) continue;
2642
3181
  const sound = sounding.get(layer.clip);
2643
- sync(layer.clip, this.srcFor(layer), "video", _nullishCoalesce(layer.sourceTime, () => ( 0)), _nullishCoalesce(_optionalChain([sound, 'optionalAccess', _71 => _71.speed]), () => ( 1)), sound ? sound.gain : null);
3182
+ sync(layer.clip, this.srcFor(layer), "video", _nullishCoalesce(layer.sourceTime, () => ( 0)), _nullishCoalesce(_optionalChain([sound, 'optionalAccess', _86 => _86.speed]), () => ( 1)), sound ? sound.gain : null);
2644
3183
  sounding.delete(layer.clip);
2645
3184
  }
2646
3185
  for (const a of sounding.values()) sync(a.clip, a.src, "audio", a.sourceTime, a.speed, a.gain);
@@ -2649,9 +3188,9 @@ var VideoPreviewEngine = (_class5 = class {
2649
3188
  }
2650
3189
  /** With effects, the frame is drawn at the start of its frame, so effect pictures are cached once per frame. */
2651
3190
  drawable(project, frame) {
2652
- if (!_optionalChain([this, 'access', _72 => _72.effects, 'optionalAccess', _73 => _73.available]) || !_chunkVKF3PWKRcjs.videoLayersUseEffects.call(void 0, frame.layers)) return frame;
2653
- const at = _chunkVKF3PWKRcjs.videoEffectFrameTime.call(void 0, frame.time, project.fps);
2654
- return at === frame.time ? frame : { ..._chunkVKF3PWKRcjs.videoFrameAt.call(void 0, project, at), time: frame.time };
3191
+ if (!_optionalChain([this, 'access', _87 => _87.effects, 'optionalAccess', _88 => _88.available]) || !_chunk3OSXHLITcjs.videoLayersUseEffects.call(void 0, frame.layers)) return frame;
3192
+ const at = _chunk3OSXHLITcjs.videoEffectFrameTime.call(void 0, frame.time, project.fps);
3193
+ return at === frame.time ? frame : { ..._chunk3OSXHLITcjs.videoFrameAt.call(void 0, project, at), time: frame.time };
2655
3194
  }
2656
3195
  redraw() {
2657
3196
  if (this.scheduled || this.destroyed) return;
@@ -2660,10 +3199,10 @@ var VideoPreviewEngine = (_class5 = class {
2660
3199
  this.scheduled = false;
2661
3200
  if (this.destroyed) return;
2662
3201
  const { project, time } = this.source.getState();
2663
- this.draw(this.drawable(project, _chunkVKF3PWKRcjs.videoFrameAt.call(void 0, project, time)));
3202
+ this.draw(this.drawable(project, _chunk3OSXHLITcjs.videoFrameAt.call(void 0, project, time)));
2664
3203
  });
2665
3204
  }
2666
- __init33() {this.sourceFor = (layer) => {
3205
+ __init40() {this.sourceFor = (layer) => {
2667
3206
  if (layer.assetKind === "image" && layer.src) {
2668
3207
  const img = this.image(layer.src);
2669
3208
  return img.complete ? img : null;
@@ -2671,7 +3210,7 @@ var VideoPreviewEngine = (_class5 = class {
2671
3210
  const entry = this.media.get(layer.clip);
2672
3211
  const ready = !!entry && entry.media.readyState >= 2;
2673
3212
  const cache = this.frameCache;
2674
- const src = _nullishCoalesce(_optionalChain([entry, 'optionalAccess', _74 => _74.src]), () => ( this.srcFor(layer)));
3213
+ const src = _nullishCoalesce(_optionalChain([entry, 'optionalAccess', _89 => _89.src]), () => ( this.srcFor(layer)));
2675
3214
  const sourceTime = _nullishCoalesce(layer.sourceTime, () => ( 0));
2676
3215
  if (cache && src && (!this.playing || !ready || entry.media.seeking)) {
2677
3216
  const hit = cache.get(src, sourceTime, this.fps);
@@ -2703,7 +3242,7 @@ var VideoPreviewEngine = (_class5 = class {
2703
3242
  if (!source) this.complete = false;
2704
3243
  return source;
2705
3244
  };
2706
- _chunkVKF3PWKRcjs.drawVideoFrame.call(void 0, this.ctx, frame, sourceFor, resolution, {
3245
+ _chunk3OSXHLITcjs.drawVideoFrame.call(void 0, this.ctx, frame, sourceFor, resolution, {
2707
3246
  defineSvgFilter: this.defineSvgFilter,
2708
3247
  lutFor: this.lutFor,
2709
3248
  scratch: this.scratch,
@@ -2713,16 +3252,16 @@ var VideoPreviewEngine = (_class5 = class {
2713
3252
  }
2714
3253
  });
2715
3254
  this.drawn = { time: frame.time, complete: this.complete };
2716
- _optionalChain([this, 'access', _75 => _75.options, 'access', _76 => _76.onFrame, 'optionalCall', _77 => _77(frame)]);
2717
- _optionalChain([this, 'access', _78 => _78.pixelsTarget, 'optionalAccess', _79 => _79.drawn, 'call', _80 => _80(frame.time, this.complete)]);
3255
+ _optionalChain([this, 'access', _90 => _90.options, 'access', _91 => _91.onFrame, 'optionalCall', _92 => _92(frame)]);
3256
+ _optionalChain([this, 'access', _93 => _93.pixelsTarget, 'optionalAccess', _94 => _94.drawn, 'call', _95 => _95(frame.time, this.complete)]);
2718
3257
  }
2719
3258
  /** A grade's LUT once loaded; while it loads (or if it fails) the frame is drawn without it and counts as incomplete. */
2720
- __init34() {this.lutFor = (src) => {
2721
- const lut = _optionalChain([this, 'access', _81 => _81.lutCache, 'optionalAccess', _82 => _82.get, 'call', _83 => _83(src)]);
3259
+ __init41() {this.lutFor = (src) => {
3260
+ const lut = _optionalChain([this, 'access', _96 => _96.lutCache, 'optionalAccess', _97 => _97.get, 'call', _98 => _98(src)]);
2722
3261
  if (!lut) this.complete = false;
2723
3262
  return lut;
2724
3263
  }}
2725
- __init35() {this.scratch = (width, height, slot) => {
3264
+ __init42() {this.scratch = (width, height, slot) => {
2726
3265
  if (!this.scratchCanvases.has(slot)) {
2727
3266
  let made = null;
2728
3267
  if (this.options.createScratch) made = this.options.createScratch(width, height, slot);
@@ -2740,7 +3279,7 @@ var VideoPreviewEngine = (_class5 = class {
2740
3279
  if (c.height !== height) c.height = height;
2741
3280
  return s;
2742
3281
  }}
2743
- __init36() {this.defineSvgFilter = (id, markup) => {
3282
+ __init43() {this.defineSvgFilter = (id, markup) => {
2744
3283
  if (this.options.defineSvgFilter === null) return;
2745
3284
  if (this.options.defineSvgFilter) return this.options.defineSvgFilter(id, markup);
2746
3285
  if (this.filters.has(id) || typeof document === "undefined") return;
@@ -2763,23 +3302,23 @@ var VideoPreviewEngine = (_class5 = class {
2763
3302
  this.unsubscribeLuts();
2764
3303
  this.unsubscribeFrames();
2765
3304
  this.unsubscribeEffects();
2766
- _optionalChain([this, 'access', _84 => _84.filterHost, 'optionalAccess', _85 => _85.remove, 'call', _86 => _86()]);
3305
+ _optionalChain([this, 'access', _99 => _99.filterHost, 'optionalAccess', _100 => _100.remove, 'call', _101 => _101()]);
2767
3306
  this.filterHost = null;
2768
3307
  this.filters.clear();
2769
3308
  this.unsubscribe();
2770
3309
  for (const entry of [...this.media.values()]) this.release(entry);
2771
3310
  this.images.clear();
2772
3311
  }
2773
- }, _class5);
3312
+ }, _class6);
2774
3313
 
2775
3314
  // src/video-editor/controller.ts
2776
- var VideoEditorController = (_class6 = class {
3315
+ var VideoEditorController = (_class7 = class _VideoEditorController {
2777
3316
 
2778
3317
 
2779
3318
  /** Sound levels per asset, loaded on demand for waveforms and agents. */
2780
3319
 
2781
3320
  /** The pixels the preview last drew, for scopes and `get_video_scopes` (a `VideoPreviewEngine` fills it). */
2782
- __init37() {this.previewPixels = new VideoPreviewPixels()}
3321
+ __init44() {this.previewPixels = new VideoPreviewPixels()}
2783
3322
  /** Grades' LUTs, loaded on demand for the preview, SVG frames and agents. */
2784
3323
 
2785
3324
  /** The host's frame cache, if it gave one (the preview fills and reads it). */
@@ -2790,30 +3329,34 @@ var VideoEditorController = (_class6 = class {
2790
3329
 
2791
3330
 
2792
3331
 
2793
- __init38() {this.selection = []}
3332
+ __init45() {this.selection = []}
2794
3333
 
2795
3334
 
2796
- __init39() {this.error = null}
2797
- __init40() {this.revision = 0}
2798
- __init41() {this.applying = false}
3335
+ __init46() {this.error = null}
3336
+ __init47() {this.revision = 0}
3337
+ __init48() {this.applying = false}
2799
3338
 
2800
- __init42() {this.listeners = /* @__PURE__ */ new Set()}
2801
- __init43() {this.unsubscribers = []}
3339
+ __init49() {this.listeners = /* @__PURE__ */ new Set()}
3340
+ __init50() {this.unsubscribers = []}
2802
3341
 
2803
- constructor(options = {}) {;_class6.prototype.__init37.call(this);_class6.prototype.__init38.call(this);_class6.prototype.__init39.call(this);_class6.prototype.__init40.call(this);_class6.prototype.__init41.call(this);_class6.prototype.__init42.call(this);_class6.prototype.__init43.call(this);_class6.prototype.__init44.call(this);_class6.prototype.__init45.call(this);
3342
+ __init51() {this.plans = []}
3343
+ /** The project each open plan was worked out against. */
3344
+ __init52() {this.planBasis = /* @__PURE__ */ new Map()}
3345
+ __init53() {this.planCount = 0}
3346
+ constructor(options = {}) {;_class7.prototype.__init44.call(this);_class7.prototype.__init45.call(this);_class7.prototype.__init46.call(this);_class7.prototype.__init47.call(this);_class7.prototype.__init48.call(this);_class7.prototype.__init49.call(this);_class7.prototype.__init50.call(this);_class7.prototype.__init51.call(this);_class7.prototype.__init52.call(this);_class7.prototype.__init53.call(this);_class7.prototype.__init54.call(this);_class7.prototype.__init55.call(this);
2804
3347
  this.history = _nullishCoalesce(options.history, () => ( new (0, _chunkFJ2MSI43cjs.HistoryManager)()));
2805
3348
  this.audioPeaks = options.audioPeaks instanceof VideoAudioPeaksCache ? options.audioPeaks : new VideoAudioPeaksCache(options.audioPeaks);
2806
- this.luts = options.luts instanceof _chunkVKF3PWKRcjs.VideoLutCache ? options.luts : new (0, _chunkVKF3PWKRcjs.VideoLutCache)(options.luts);
3349
+ this.luts = options.luts instanceof _chunk3OSXHLITcjs.VideoLutCache ? options.luts : new (0, _chunk3OSXHLITcjs.VideoLutCache)(options.luts);
2807
3350
  this.frameCache = _nullishCoalesce(options.frameCache, () => ( null));
2808
3351
  this.effects = new (0, _chunkRUZQMYXHcjs.EffectsSlot)();
2809
3352
  if (options.effects) this.effects.set(options.effects);
2810
- this.effectsEngine = new (0, _chunkVKF3PWKRcjs.VideoEffectsEngine)({ slot: this.effects });
3353
+ this.effectsEngine = new (0, _chunk3OSXHLITcjs.VideoEffectsEngine)({ slot: this.effects });
2811
3354
  this.project = options.project ? parseVideoProject(options.project) : createVideoProject();
2812
3355
  this.issues = checkVideoProject(this.project);
2813
3356
  this.readOnly = _nullishCoalesce(options.readOnly, () => ( false));
2814
3357
  this.previewScale = checkPreviewScale(_nullishCoalesce(options.previewScale, () => ( "auto")));
2815
3358
  this.onChange = options.onChange;
2816
- this.player = new (0, _chunkSUF6WRXRcjs.AnimationPlayer)({ clock: options.clock, duration: () => _chunkVKF3PWKRcjs.videoProjectDuration.call(void 0, this.project), loop: "none" });
3359
+ this.player = new (0, _chunkSUF6WRXRcjs.AnimationPlayer)({ clock: options.clock, duration: () => _chunk3OSXHLITcjs.videoProjectDuration.call(void 0, this.project), loop: "none" });
2817
3360
  this.state = this.computeState();
2818
3361
  this.connect();
2819
3362
  }
@@ -2833,8 +3376,8 @@ var VideoEditorController = (_class6 = class {
2833
3376
  this.effectsEngine.reset();
2834
3377
  this.unsubscribers.splice(0).forEach((u) => u());
2835
3378
  }
2836
- __init44() {this.getState = () => this.state}
2837
- __init45() {this.subscribe = (listener) => {
3379
+ __init54() {this.getState = () => this.state}
3380
+ __init55() {this.subscribe = (listener) => {
2838
3381
  this.connect();
2839
3382
  this.listeners.add(listener);
2840
3383
  return () => {
@@ -2850,12 +3393,12 @@ var VideoEditorController = (_class6 = class {
2850
3393
  const h = this.history.getState();
2851
3394
  return {
2852
3395
  project: this.project,
2853
- duration: _chunkVKF3PWKRcjs.videoProjectDuration.call(void 0, this.project),
3396
+ duration: _chunk3OSXHLITcjs.videoProjectDuration.call(void 0, this.project),
2854
3397
  time: p.time,
2855
3398
  playing: p.playing,
2856
3399
  loop: p.loop,
2857
3400
  selection: this.selection,
2858
- current: _chunkVKF3PWKRcjs.videoClipsAt.call(void 0, this.project, p.time).map((c) => c.clip.id),
3401
+ current: _chunk3OSXHLITcjs.videoClipsAt.call(void 0, this.project, p.time).map((c) => c.clip.id),
2859
3402
  issues: this.issues,
2860
3403
  readOnly: this.readOnly,
2861
3404
  error: this.error,
@@ -2865,6 +3408,7 @@ var VideoEditorController = (_class6 = class {
2865
3408
  redoLabel: h.redoName,
2866
3409
  previewScale: this.previewScale,
2867
3410
  effectsProvider: this.effects.provider,
3411
+ plans: this.plans,
2868
3412
  revision: this.revision
2869
3413
  };
2870
3414
  }
@@ -2887,12 +3431,12 @@ var VideoEditorController = (_class6 = class {
2887
3431
  this.issues = checkVideoProject(project);
2888
3432
  const ids = new Set(project.tracks.flatMap((t) => t.clips.map((c) => c.id)));
2889
3433
  if (this.selection.some((id) => !ids.has(id))) this.selection = this.selection.filter((id) => ids.has(id));
2890
- this.player.setDuration(() => _chunkVKF3PWKRcjs.videoProjectDuration.call(void 0, this.project));
3434
+ this.player.setDuration(() => _chunk3OSXHLITcjs.videoProjectDuration.call(void 0, this.project));
2891
3435
  } finally {
2892
3436
  this.applying = false;
2893
3437
  }
2894
3438
  this.notify();
2895
- _optionalChain([this, 'access', _87 => _87.onChange, 'optionalCall', _88 => _88(project)]);
3439
+ _optionalChain([this, 'access', _102 => _102.onChange, 'optionalCall', _103 => _103(project)]);
2896
3440
  }
2897
3441
  commit(label, next, merge) {
2898
3442
  const before = this.project;
@@ -2933,6 +3477,86 @@ var VideoEditorController = (_class6 = class {
2933
3477
  if (!options.dryRun) this.commit(label, result.project, options.merge);
2934
3478
  return result;
2935
3479
  }
3480
+ // -----------------------------------------------------------------------------------------
3481
+ // Edit plans: propose, review, apply or discard
3482
+ // -----------------------------------------------------------------------------------------
3483
+ /** Most plans kept open at once; proposing another drops the oldest. */
3484
+ static __initStatic() {this.MAX_PLANS = 20}
3485
+ /**
3486
+ * Works out a batch of edits without applying it and keeps it for review (see `VideoEditPlan`):
3487
+ * the result says what it would do. Allowed while read-only (nothing changes); applying is not.
3488
+ * A plan with the id of an open one replaces it.
3489
+ */
3490
+ proposePlan(input) {
3491
+ const ids = new Set(this.plans.map((p) => p.id));
3492
+ let id = input.id;
3493
+ let count = this.planCount;
3494
+ if (id === void 0) {
3495
+ do
3496
+ id = `plan${++count}`;
3497
+ while (ids.has(id));
3498
+ }
3499
+ const result = planVideoEdits(this.project, { ...input, id });
3500
+ if (!result.ok) return result;
3501
+ this.planCount = count;
3502
+ const kept = this.plans.filter((p) => p.id !== id);
3503
+ while (kept.length >= _VideoEditorController.MAX_PLANS) this.planBasis.delete(kept.shift().id);
3504
+ this.plans = [...kept, result.plan];
3505
+ this.planBasis.set(id, this.project);
3506
+ this.notify();
3507
+ return { ok: true, plan: result.plan };
3508
+ }
3509
+ /** An open plan, or `undefined`. */
3510
+ getPlan(id) {
3511
+ return this.plans.find((p) => p.id === id);
3512
+ }
3513
+ /** Whether the project changed since a plan was made (its summary may then be out of date). */
3514
+ isPlanStale(id) {
3515
+ const basis = this.planBasis.get(id);
3516
+ return !!basis && basis !== this.project;
3517
+ }
3518
+ /** The project as an open plan would leave it, worked out against the current project; `null` when it no longer applies. */
3519
+ planPreview(id) {
3520
+ const plan = this.getPlan(id);
3521
+ if (!plan) return null;
3522
+ const result = applyVideoEdits(this.project, plan.edits);
3523
+ return result.ok ? result.project : null;
3524
+ }
3525
+ /**
3526
+ * Applies an open plan as one undo step labelled with its title and closes it. If the project
3527
+ * changed since it was made, its edits are checked again against the project as it is now and
3528
+ * refused (the plan stays open) when they no longer apply.
3529
+ */
3530
+ applyPlan(id) {
3531
+ const plan = this.getPlan(id);
3532
+ if (!plan) return this.refused(`There is no open plan "${id}"; the open plans are ${this.plans.map((p) => `"${p.id}"`).join(", ") || "none"}.`);
3533
+ const reason = this.readOnlyReason();
3534
+ if (reason) return this.refused(reason);
3535
+ const stale = this.isPlanStale(id);
3536
+ const result = applyVideoEdits(this.project, plan.edits);
3537
+ if (!result.ok) return this.refused(stale ? `The project changed since plan "${id}" was made and it no longer applies (${result.error}); discard it and make a new plan.` : result.error);
3538
+ this.dropPlan(id);
3539
+ this.commit(plan.title, result.project);
3540
+ return { ...result, plan, stale };
3541
+ }
3542
+ /** Closes an open plan without applying it. False when there is no such plan. */
3543
+ discardPlan(id) {
3544
+ if (!this.getPlan(id)) return false;
3545
+ this.dropPlan(id);
3546
+ this.notify();
3547
+ return true;
3548
+ }
3549
+ /** Closes every open plan. */
3550
+ discardPlans() {
3551
+ if (!this.plans.length) return;
3552
+ this.plans = [];
3553
+ this.planBasis.clear();
3554
+ this.notify();
3555
+ }
3556
+ dropPlan(id) {
3557
+ this.plans = this.plans.filter((p) => p.id !== id);
3558
+ this.planBasis.delete(id);
3559
+ }
2936
3560
  /** Opens a project (JSON or object). `clearHistory` drops the undo stack (opening a file); otherwise it is one undo step. */
2937
3561
  load(project, options = {}) {
2938
3562
  let parsed;
@@ -2969,7 +3593,7 @@ var VideoEditorController = (_class6 = class {
2969
3593
  }
2970
3594
  /** Selects clips (unknown ids are dropped). `add` toggles them into the current selection. */
2971
3595
  select(clips, options = {}) {
2972
- const known = _nullishCoalesce(_optionalChain([clips, 'optionalAccess', _89 => _89.filter, 'call', _90 => _90((id) => _chunkVKF3PWKRcjs.findVideoClip.call(void 0, this.project, id))]), () => ( []));
3596
+ const known = _nullishCoalesce(_optionalChain([clips, 'optionalAccess', _104 => _104.filter, 'call', _105 => _105((id) => _chunk3OSXHLITcjs.findVideoClip.call(void 0, this.project, id))]), () => ( []));
2973
3597
  let next;
2974
3598
  if (options.add) {
2975
3599
  next = [...this.selection];
@@ -2996,7 +3620,7 @@ var VideoEditorController = (_class6 = class {
2996
3620
  step(frames) {
2997
3621
  this.player.pause();
2998
3622
  const fps = this.project.fps;
2999
- this.player.seek(Math.max(0, _chunkVKF3PWKRcjs.snapToVideoFrame.call(void 0, this.player.currentTime, fps) + frames * 1e3 / fps));
3623
+ this.player.seek(Math.max(0, _chunk3OSXHLITcjs.snapToVideoFrame.call(void 0, this.player.currentTime, fps) + frames * 1e3 / fps));
3000
3624
  }
3001
3625
  setLoop(loop) {
3002
3626
  this.player.setLoop(loop);
@@ -3013,14 +3637,14 @@ var VideoEditorController = (_class6 = class {
3013
3637
  */
3014
3638
  snap(time, options = {}) {
3015
3639
  const threshold = _nullishCoalesce(options.threshold, () => ( 8 * 1e3 / this.project.fps));
3016
- const points = _chunkVKF3PWKRcjs.videoSnapPoints.call(void 0, this.project, { exclude: options.exclude, playhead: options.playhead === false ? void 0 : this.player.currentTime });
3017
- const hit = _chunkVKF3PWKRcjs.snapVideoTime.call(void 0, time, points, threshold);
3018
- return hit.snapped !== null ? hit : { time: Math.max(0, _chunkVKF3PWKRcjs.snapToVideoFrame.call(void 0, time, this.project.fps)), snapped: null };
3640
+ const points = _chunk3OSXHLITcjs.videoSnapPoints.call(void 0, this.project, { exclude: options.exclude, playhead: options.playhead === false ? void 0 : this.player.currentTime });
3641
+ const hit = _chunk3OSXHLITcjs.snapVideoTime.call(void 0, time, points, threshold);
3642
+ return hit.snapped !== null ? hit : { time: Math.max(0, _chunk3OSXHLITcjs.snapToVideoFrame.call(void 0, time, this.project.fps)), snapped: null };
3019
3643
  }
3020
3644
  /** Splits the selected clips (or every clip) under the playhead there, as one undo step. */
3021
3645
  splitAtPlayhead() {
3022
- const t = _chunkVKF3PWKRcjs.snapToVideoFrame.call(void 0, this.player.currentTime, this.project.fps);
3023
- const under = _chunkVKF3PWKRcjs.videoClipsAt.call(void 0, this.project, t).filter(({ clip }) => t > clip.start && t < _chunkVKF3PWKRcjs.videoClipEnd.call(void 0, clip));
3646
+ const t = _chunk3OSXHLITcjs.snapToVideoFrame.call(void 0, this.player.currentTime, this.project.fps);
3647
+ const under = _chunk3OSXHLITcjs.videoClipsAt.call(void 0, this.project, t).filter(({ clip }) => t > clip.start && t < _chunk3OSXHLITcjs.videoClipEnd.call(void 0, clip));
3024
3648
  const chosen = this.selection.length ? under.filter(({ clip }) => this.selection.includes(clip.id)) : under;
3025
3649
  if (!chosen.length) return this.refuse("No clip is under the playhead to split.");
3026
3650
  return this.apply(chosen.map(({ clip }) => ({ op: "split_clip", clip: clip.id, at: t })), "Split");
@@ -3048,7 +3672,7 @@ var VideoEditorController = (_class6 = class {
3048
3672
  /** The provider's description of an effect, checked for `place` (rejects without a provider or for an unknown id). */
3049
3673
  async describeEffect(effect, place) {
3050
3674
  const d = await this.effects.require().describe(effect);
3051
- if (place) _chunkVKF3PWKRcjs.checkVideoEffectPlace.call(void 0, d, place);
3675
+ if (place) _chunk3OSXHLITcjs.checkVideoEffectPlace.call(void 0, d, place);
3052
3676
  return d;
3053
3677
  }
3054
3678
  refused(message) {
@@ -3069,7 +3693,7 @@ var VideoEditorController = (_class6 = class {
3069
3693
  return { ...result, clip: plan.clip, ...plan.effect ? { effect: plan.effect() } : {} };
3070
3694
  }
3071
3695
  clipOrThrow(id) {
3072
- const at = _chunkVKF3PWKRcjs.findVideoClip.call(void 0, this.project, id);
3696
+ const at = _chunk3OSXHLITcjs.findVideoClip.call(void 0, this.project, id);
3073
3697
  if (!at) throw new Error(`There is no clip "${id}"; list_applied_effects or get_video_project lists the clips.`);
3074
3698
  return at.clip;
3075
3699
  }
@@ -3082,7 +3706,7 @@ var VideoEditorController = (_class6 = class {
3082
3706
  return this.effectEdit(async () => {
3083
3707
  const current = this.clipOrThrow(clip);
3084
3708
  const d = await this.describeEffect(effect, "clip");
3085
- const values = _chunkVKF3PWKRcjs.checkVideoEffectValues.call(void 0, d, options.values, { keyframes: true });
3709
+ const values = _chunk3OSXHLITcjs.checkVideoEffectValues.call(void 0, d, options.values, { keyframes: true });
3086
3710
  const stack = [..._nullishCoalesce(current.effects, () => ( []))];
3087
3711
  const taken = new Set(stack.map((e) => e.id));
3088
3712
  let n = 1;
@@ -3118,7 +3742,7 @@ var VideoEditorController = (_class6 = class {
3118
3742
  if (!(k in d.controls.properties)) throw new Error(`"${k}" is not a control of "${d.id}"; its controls are ${Object.keys(d.controls.properties).join(", ")}.`);
3119
3743
  base[k] = d.defaults[k];
3120
3744
  }
3121
- next.values = _chunkVKF3PWKRcjs.checkVideoEffectValues.call(void 0, d, given, { base, keyframes: true });
3745
+ next.values = _chunk3OSXHLITcjs.checkVideoEffectValues.call(void 0, d, given, { base, keyframes: true });
3122
3746
  }
3123
3747
  if (changes.enabled !== void 0) {
3124
3748
  if (changes.enabled) delete next.enabled;
@@ -3136,7 +3760,7 @@ var VideoEditorController = (_class6 = class {
3136
3760
  removeClipEffect(clip, instance) {
3137
3761
  const reason = this.readOnlyReason();
3138
3762
  if (reason) return this.refused(reason);
3139
- const at = _chunkVKF3PWKRcjs.findVideoClip.call(void 0, this.project, clip);
3763
+ const at = _chunk3OSXHLITcjs.findVideoClip.call(void 0, this.project, clip);
3140
3764
  if (!at) return this.refused(`There is no clip "${clip}".`);
3141
3765
  const stack = _nullishCoalesce(at.clip.effects, () => ( []));
3142
3766
  const was = stack.find((e) => e.id === instance);
@@ -3153,7 +3777,7 @@ var VideoEditorController = (_class6 = class {
3153
3777
  return this.effectEdit(async () => {
3154
3778
  this.clipOrThrow(clip);
3155
3779
  const d = await this.describeEffect(effect, "transition");
3156
- const values = _chunkVKF3PWKRcjs.checkVideoEffectValues.call(void 0, d, options.values);
3780
+ const values = _chunk3OSXHLITcjs.checkVideoEffectValues.call(void 0, d, options.values);
3157
3781
  const duration = _nullishCoalesce(_nullishCoalesce(options.duration, () => ( d.duration)), () => ( 1e3));
3158
3782
  return { edit: { op: "set_transition", clip, transition: { type: "effect", duration, effect: { effect: d.id, provider: this.effects.require().id, name: d.name, values } } }, label: `Transition ${d.name}`, clip };
3159
3783
  });
@@ -3166,7 +3790,7 @@ var VideoEditorController = (_class6 = class {
3166
3790
  addEffectClip(track, effect, options = {}) {
3167
3791
  return this.effectEdit(async () => {
3168
3792
  const d = await this.describeEffect(effect, "generator");
3169
- const values = _chunkVKF3PWKRcjs.checkVideoEffectValues.call(void 0, d, options.values, { keyframes: true });
3793
+ const values = _chunk3OSXHLITcjs.checkVideoEffectValues.call(void 0, d, options.values, { keyframes: true });
3170
3794
  const duration = _nullishCoalesce(_nullishCoalesce(options.duration, () => ( d.duration)), () => ( 5e3));
3171
3795
  const label = `Add ${d.name}`;
3172
3796
  const clipId = _nullishCoalesce(options.id, () => ( ""));
@@ -3175,7 +3799,7 @@ var VideoEditorController = (_class6 = class {
3175
3799
  label,
3176
3800
  clip: clipId
3177
3801
  };
3178
- }).then((r) => r.ok && !r.clip ? { ...r, clip: _nullishCoalesce(_optionalChain([r, 'access', _91 => _91.outcomes, 'access', _92 => _92[0], 'optionalAccess', _93 => _93.clip]), () => ( "")) } : r);
3802
+ }).then((r) => r.ok && !r.clip ? { ...r, clip: _nullishCoalesce(_optionalChain([r, 'access', _106 => _106.outcomes, 'access', _107 => _107[0], 'optionalAccess', _108 => _108.clip]), () => ( "")) } : r);
3179
3803
  }
3180
3804
  /**
3181
3805
  * Changes the values of an effect clip's picture (`target` its id) or of the effect transition
@@ -3188,44 +3812,44 @@ var VideoEditorController = (_class6 = class {
3188
3812
  if (part === "generator") {
3189
3813
  if (current.type !== "effect") throw new Error(`Clip "${clip}" is not an effect clip.`);
3190
3814
  const d2 = await this.describeEffect(current.effect);
3191
- return { edit: { op: "update_clip", clip, set: { values: _chunkVKF3PWKRcjs.checkVideoEffectValues.call(void 0, d2, values, { base: _nullishCoalesce(current.values, () => ( {})), keyframes: true }) } }, label: `Change ${_nullishCoalesce(current.name, () => ( d2.name))}`, clip };
3815
+ return { edit: { op: "update_clip", clip, set: { values: _chunk3OSXHLITcjs.checkVideoEffectValues.call(void 0, d2, values, { base: _nullishCoalesce(current.values, () => ( {})), keyframes: true }) } }, label: `Change ${_nullishCoalesce(current.name, () => ( d2.name))}`, clip };
3192
3816
  }
3193
3817
  const t = current.transition;
3194
- if (_optionalChain([t, 'optionalAccess', _94 => _94.type]) !== "effect" || !t.effect) throw new Error(`Clip "${clip}" has no effect transition.`);
3818
+ if (_optionalChain([t, 'optionalAccess', _109 => _109.type]) !== "effect" || !t.effect) throw new Error(`Clip "${clip}" has no effect transition.`);
3195
3819
  const d = await this.describeEffect(t.effect.effect);
3196
- const merged = _chunkVKF3PWKRcjs.checkVideoEffectValues.call(void 0, d, values, { base: _nullishCoalesce(t.effect.values, () => ( {})) });
3820
+ const merged = _chunk3OSXHLITcjs.checkVideoEffectValues.call(void 0, d, values, { base: _nullishCoalesce(t.effect.values, () => ( {})) });
3197
3821
  return { edit: { op: "set_transition", clip, transition: { ...t, effect: { ...t.effect, values: merged } } }, label: `Change transition ${d.name}`, clip };
3198
3822
  }, options.merge);
3199
3823
  }
3200
3824
  /** Keys for one numeric control of a clip effect (clip-relative ms), or `null` to stop animating it (keeping its value at the clip's start). */
3201
3825
  setClipEffectKeyframes(clip, instance, control, keys) {
3202
- const current = _optionalChain([_chunkVKF3PWKRcjs.findVideoClip.call(void 0, this.project, clip), 'optionalAccess', _95 => _95.clip]);
3203
- const was = _optionalChain([current, 'optionalAccess', _96 => _96.effects, 'optionalAccess', _97 => _97.find, 'call', _98 => _98((e) => e.id === instance)]);
3204
- const value = _optionalChain([was, 'optionalAccess', _99 => _99.values, 'optionalAccess', _100 => _100[control]]);
3205
- const plain = keys === null ? _chunkVKF3PWKRcjs.isVideoKeyframes.call(void 0, value) ? _optionalChain([value, 'access', _101 => _101.keys, 'access', _102 => _102[0], 'optionalAccess', _103 => _103.v]) : value : { keys: [...keys].sort((a, b) => a.t - b.t) };
3826
+ const current = _optionalChain([_chunk3OSXHLITcjs.findVideoClip.call(void 0, this.project, clip), 'optionalAccess', _110 => _110.clip]);
3827
+ const was = _optionalChain([current, 'optionalAccess', _111 => _111.effects, 'optionalAccess', _112 => _112.find, 'call', _113 => _113((e) => e.id === instance)]);
3828
+ const value = _optionalChain([was, 'optionalAccess', _114 => _114.values, 'optionalAccess', _115 => _115[control]]);
3829
+ const plain = keys === null ? _chunk3OSXHLITcjs.isVideoKeyframes.call(void 0, value) ? _optionalChain([value, 'access', _116 => _116.keys, 'access', _117 => _117[0], 'optionalAccess', _118 => _118.v]) : value : { keys: [...keys].sort((a, b) => a.t - b.t) };
3206
3830
  return this.updateClipEffect(clip, instance, { values: { [control]: _nullishCoalesce(plain, () => ( null)) } });
3207
3831
  }
3208
3832
  /** What plays at `time` (default: the playhead). */
3209
3833
  frame(time = this.player.currentTime) {
3210
- return _chunkVKF3PWKRcjs.videoFrameAt.call(void 0, this.project, time);
3834
+ return _chunk3OSXHLITcjs.videoFrameAt.call(void 0, this.project, time);
3211
3835
  }
3212
3836
  /** The frame at `time` (default: the playhead) as SVG. */
3213
3837
  toSvg(time = this.player.currentTime, options = {}) {
3214
- return _chunkVKF3PWKRcjs.videoProjectSvg.call(void 0, this.project, time, { lutFor: (src) => this.luts.peek(src), ...options });
3838
+ return _chunk3OSXHLITcjs.videoProjectSvg.call(void 0, this.project, time, { lutFor: (src) => this.luts.peek(src), ...options });
3215
3839
  }
3216
3840
  /** The project as an ffmpeg command. */
3217
3841
  toFfmpeg(options = {}) {
3218
- return _chunkVKF3PWKRcjs.toVideoFfmpeg.call(void 0, this.project, options);
3842
+ return _chunk3OSXHLITcjs.toVideoFfmpeg.call(void 0, this.project, options);
3219
3843
  }
3220
3844
  /** The ffmpeg command that measures the mix's loudness (see `toVideoLoudnessMeasure`). */
3221
3845
  toLoudnessMeasure(options = {}) {
3222
- return _chunkVKF3PWKRcjs.toVideoLoudnessMeasure.call(void 0, this.project, options);
3846
+ return _chunk3OSXHLITcjs.toVideoLoudnessMeasure.call(void 0, this.project, options);
3223
3847
  }
3224
3848
  /** One track as a CMX 3600 EDL. */
3225
3849
  toEdl(options = {}) {
3226
- return _chunkVKF3PWKRcjs.toVideoEdl.call(void 0, this.project, options);
3850
+ return _chunk3OSXHLITcjs.toVideoEdl.call(void 0, this.project, options);
3227
3851
  }
3228
- }, _class6);
3852
+ }, _class7.__initStatic(), _class7);
3229
3853
  function checkPreviewScale(scale) {
3230
3854
  if (scale === "auto" || scale === "full") return scale;
3231
3855
  if (typeof scale !== "number" || !(scale > 0 && scale <= 1)) throw new Error(`A preview scale is "auto", "full" or a fraction above 0 up to 1 (0.5 is half size), not ${JSON.stringify(scale)}.`);
@@ -3257,7 +3881,7 @@ function planVideoToolPlayback(tool, args) {
3257
3881
  const clips = clipsOf(args);
3258
3882
  const track = str2(args.track) ? args.track : void 0;
3259
3883
  const newClip = args.clip && typeof args.clip === "object" ? args.clip : null;
3260
- const time = [args.start, args.at, args.time, args.to, _optionalChain([newClip, 'optionalAccess', _104 => _104.start])].find(num2);
3884
+ const time = [args.start, args.at, args.time, args.to, _optionalChain([newClip, 'optionalAccess', _119 => _119.start])].find(num2);
3261
3885
  const click = (ref) => [{ type: "target", ref }, { type: "press" }, { type: "release" }];
3262
3886
  if (clips.length === 1 && (time !== void 0 || track) && tool !== "split_video_clip") {
3263
3887
  const clip = clips[0];
@@ -3291,12 +3915,12 @@ function videoRulerTicks(duration, pxPerSecond, fps) {
3291
3915
  const ticks = [];
3292
3916
  for (let t = 0; t <= duration + 1e-6; t += step) {
3293
3917
  const isMajor = Math.abs(t / major - Math.round(t / major)) < 1e-9;
3294
- ticks.push({ time: t, x: t * pxPerMs, major: isMajor, ...isMajor ? { label: _chunkVKF3PWKRcjs.formatVideoTimecode.call(void 0, t, fps).replace(/^00:/, "") } : {} });
3918
+ ticks.push({ time: t, x: t * pxPerMs, major: isMajor, ...isMajor ? { label: _chunk3OSXHLITcjs.formatVideoTimecode.call(void 0, t, fps).replace(/^00:/, "") } : {} });
3295
3919
  }
3296
3920
  return ticks;
3297
3921
  }
3298
3922
  function videoClipShowsWaveform(clip, asset, mode = "audio") {
3299
- if (clip.type !== "media" || mode === "none" || !_chunkVKF3PWKRcjs.videoAssetHasSound.call(void 0, asset)) return false;
3923
+ if (clip.type !== "media" || mode === "none" || !_chunk3OSXHLITcjs.videoAssetHasSound.call(void 0, asset)) return false;
3300
3924
  return mode === "all" || asset.kind === "audio";
3301
3925
  }
3302
3926
  var VIDEO_CLIP_ANIMATION_LABELS = {
@@ -3345,6 +3969,7 @@ function videoClipTint(clip, asset) {
3345
3969
  if (clip.type === "sequence") return "#0d9488";
3346
3970
  if (clip.type === "color") return clip.color;
3347
3971
  if (clip.type === "effect") return "#db2777";
3972
+ if (clip.type === "adjustment") return "#64748b";
3348
3973
  if (!asset) return "#ef4444";
3349
3974
  return asset.kind === "audio" ? "#10b981" : asset.kind === "image" ? "#f59e0b" : "#3b82f6";
3350
3975
  }
@@ -3352,14 +3977,15 @@ function videoClipLabel(clip, asset, sequence) {
3352
3977
  if (clip.name) return clip.name;
3353
3978
  if (clip.type === "text") return clip.text.split("\n")[0];
3354
3979
  if (clip.type === "color") return clip.color;
3355
- if (clip.type === "sequence") return `Compound: ${_nullishCoalesce(_optionalChain([sequence, 'optionalAccess', _105 => _105.name]), () => ( clip.sequence))}`;
3980
+ if (clip.type === "sequence") return `Compound: ${_nullishCoalesce(_optionalChain([sequence, 'optionalAccess', _120 => _120.name]), () => ( clip.sequence))}`;
3356
3981
  if (clip.type === "effect") return `Effect: ${clip.effect}`;
3357
- const base = _nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _106 => _106.name]), () => ( _optionalChain([asset, 'optionalAccess', _107 => _107.src, 'access', _108 => _108.replace, 'call', _109 => _109(/^.*[\\/]/, "")]))), () => ( clip.asset));
3358
- const speed = _chunkVKF3PWKRcjs.videoClipSpeed.call(void 0, clip);
3982
+ if (clip.type === "adjustment") return `Adjustment${clip.scope === "all" ? " (all tracks)" : clip.scope === "track" ? ` (${clip.targetTrack})` : ""}`;
3983
+ const base = _nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _121 => _121.name]), () => ( _optionalChain([asset, 'optionalAccess', _122 => _122.src, 'access', _123 => _123.replace, 'call', _124 => _124(/^.*[\\/]/, "")]))), () => ( clip.asset));
3984
+ const speed = _chunk3OSXHLITcjs.videoClipSpeed.call(void 0, clip);
3359
3985
  return speed !== 1 ? `${base} (${Math.round(speed * 100)}%)` : base;
3360
3986
  }
3361
3987
  function planVideoClipDrag(project, input) {
3362
- const at = _chunkVKF3PWKRcjs.findVideoClip.call(void 0, project, input.clip);
3988
+ const at = _chunk3OSXHLITcjs.findVideoClip.call(void 0, project, input.clip);
3363
3989
  if (!at) return null;
3364
3990
  const { clip, track } = at;
3365
3991
  const snap = _nullishCoalesce(input.snap, () => ( ((t) => t)));
@@ -3376,17 +4002,17 @@ function planVideoClipDrag(project, input) {
3376
4002
  }
3377
4003
  const minimum = 1e3 / project.fps;
3378
4004
  if (input.mode === "trim-start") {
3379
- const to2 = Math.min(snap(Math.max(0, clip.start + input.delta)), _chunkVKF3PWKRcjs.videoClipEnd.call(void 0, clip) - minimum);
4005
+ const to2 = Math.min(snap(Math.max(0, clip.start + input.delta)), _chunk3OSXHLITcjs.videoClipEnd.call(void 0, clip) - minimum);
3380
4006
  const changed2 = Math.abs(to2 - clip.start) > 1e-6;
3381
- const plan = input.ripple ? { start: clip.start, duration: clip.duration - (to2 - clip.start) } : { start: to2, duration: _chunkVKF3PWKRcjs.videoClipEnd.call(void 0, clip) - to2 };
4007
+ const plan = input.ripple ? { start: clip.start, duration: clip.duration - (to2 - clip.start) } : { start: to2, duration: _chunk3OSXHLITcjs.videoClipEnd.call(void 0, clip) - to2 };
3382
4008
  return { ...plan, track: track.id, edit: changed2 ? { op: "trim_clip", clip: clip.id, edge: "start", to: to2, ...input.ripple ? { ripple: true } : {} } : null };
3383
4009
  }
3384
- const to = Math.max(snap(_chunkVKF3PWKRcjs.videoClipEnd.call(void 0, clip) + input.delta), clip.start + minimum);
3385
- const changed = Math.abs(to - _chunkVKF3PWKRcjs.videoClipEnd.call(void 0, clip)) > 1e-6;
4010
+ const to = Math.max(snap(_chunk3OSXHLITcjs.videoClipEnd.call(void 0, clip) + input.delta), clip.start + minimum);
4011
+ const changed = Math.abs(to - _chunk3OSXHLITcjs.videoClipEnd.call(void 0, clip)) > 1e-6;
3386
4012
  return { start: clip.start, duration: to - clip.start, track: track.id, edit: changed ? { op: "trim_clip", clip: clip.id, edge: "end", to, ...input.ripple ? { ripple: true } : {} } : null };
3387
4013
  }
3388
4014
  function videoDropTracks(project, clipId) {
3389
- const at = _chunkVKF3PWKRcjs.findVideoClip.call(void 0, project, clipId);
4015
+ const at = _chunk3OSXHLITcjs.findVideoClip.call(void 0, project, clipId);
3390
4016
  if (!at) return [];
3391
4017
  return project.tracks.filter((t) => t.kind === at.track.kind && !t.locked).map((t) => t.id);
3392
4018
  }
@@ -3395,10 +4021,10 @@ function videoDragLabel(edit) {
3395
4021
  return edit.op === "trim_clip" && edit.ripple ? "Ripple trim" : "Trim clip";
3396
4022
  }
3397
4023
  function videoAssetDropEdit(project, assetId, options = {}) {
3398
- const asset = _chunkVKF3PWKRcjs.videoAssetMap.call(void 0, project).get(assetId);
4024
+ const asset = _chunk3OSXHLITcjs.videoAssetMap.call(void 0, project).get(assetId);
3399
4025
  if (!asset) return null;
3400
4026
  const kind = asset.kind === "audio" ? "audio" : "video";
3401
- const track = _nullishCoalesce(options.track, () => ( _optionalChain([project, 'access', _110 => _110.tracks, 'access', _111 => _111.find, 'call', _112 => _112((t) => t.kind === kind && !t.locked), 'optionalAccess', _113 => _113.id])));
4027
+ const track = _nullishCoalesce(options.track, () => ( _optionalChain([project, 'access', _125 => _125.tracks, 'access', _126 => _126.find, 'call', _127 => _127((t) => t.kind === kind && !t.locked), 'optionalAccess', _128 => _128.id])));
3402
4028
  if (!track) return null;
3403
4029
  return { op: "add_clip", track, clip: { type: "media", asset: asset.id, ...options.start !== void 0 ? { start: options.start } : {} }, ...options.ripple ? { ripple: true } : {}, ...options.mode && options.mode !== "refuse" ? { mode: options.mode } : {} };
3404
4030
  }
@@ -3447,25 +4073,25 @@ var KEY_LABELS = {
3447
4073
  scale: { label: "Scale", step: 0.05 },
3448
4074
  rotation: { label: "Rotation", step: 1 }
3449
4075
  };
3450
- var animatableOf = (clip, property) => _chunkVKF3PWKRcjs.VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([clip, 'access', _114 => _114.transform, 'optionalAccess', _115 => _115[property]]) : property === "volume" ? clip.type === "media" || clip.type === "sequence" ? clip.volume : void 0 : clip.opacity;
4076
+ var animatableOf = (clip, property) => _chunk3OSXHLITcjs.VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([clip, 'access', _129 => _129.transform, 'optionalAccess', _130 => _130[property]]) : property === "volume" ? clip.type === "media" || clip.type === "sequence" ? clip.volume : void 0 : clip.opacity;
3451
4077
  var playheadInClip = (clip, time) => Math.round(Math.min(clip.duration, Math.max(0, time - clip.start)));
3452
4078
  function videoKeyframeRows(project, clipId, time) {
3453
- const at = _chunkVKF3PWKRcjs.findVideoClip.call(void 0, project, clipId);
4079
+ const at = _chunk3OSXHLITcjs.findVideoClip.call(void 0, project, clipId);
3454
4080
  if (!at) return [];
3455
4081
  const { clip } = at;
3456
- const asset = clip.type === "media" ? _chunkVKF3PWKRcjs.videoAssetMap.call(void 0, project).get(clip.asset) : void 0;
3457
- const visual = !(_optionalChain([asset, 'optionalAccess', _116 => _116.kind]) === "audio" || at.track.kind === "audio");
3458
- const audible = clip.type === "media" && _chunkVKF3PWKRcjs.videoAssetHasSound.call(void 0, asset) || clip.type === "sequence";
3459
- const properties = [...visual ? ["opacity"] : [], ...audible ? ["volume"] : [], ...visual ? _chunkVKF3PWKRcjs.VIDEO_TRANSFORM_PROPERTIES : []];
4082
+ const asset = clip.type === "media" ? _chunk3OSXHLITcjs.videoAssetMap.call(void 0, project).get(clip.asset) : void 0;
4083
+ const visual = !(_optionalChain([asset, 'optionalAccess', _131 => _131.kind]) === "audio" || at.track.kind === "audio");
4084
+ const audible = clip.type === "media" && _chunk3OSXHLITcjs.videoAssetHasSound.call(void 0, asset) || clip.type === "sequence";
4085
+ const properties = [...visual ? ["opacity"] : [], ...audible ? ["volume"] : [], ...visual ? _chunk3OSXHLITcjs.VIDEO_TRANSFORM_PROPERTIES : []];
3460
4086
  const clipTime = playheadInClip(clip, time);
3461
4087
  return properties.map((property) => {
3462
4088
  const prop = animatableOf(clip, property);
3463
- const keys = _chunkVKF3PWKRcjs.isVideoKeyframes.call(void 0, prop) ? prop.keys : [];
3464
- const limits = _chunkVKF3PWKRcjs.VIDEO_KEYFRAME_LIMITS[property];
4089
+ const keys = _chunk3OSXHLITcjs.isVideoKeyframes.call(void 0, prop) ? prop.keys : [];
4090
+ const limits = _chunk3OSXHLITcjs.VIDEO_KEYFRAME_LIMITS[property];
3465
4091
  return {
3466
4092
  property,
3467
4093
  ...KEY_LABELS[property],
3468
- value: Math.round(_chunkVKF3PWKRcjs.videoValueAt.call(void 0, prop, clipTime, limits.default) * 1e3) / 1e3,
4094
+ value: Math.round(_chunk3OSXHLITcjs.videoValueAt.call(void 0, prop, clipTime, limits.default) * 1e3) / 1e3,
3469
4095
  keyed: keys.length > 0,
3470
4096
  keys: keys.length,
3471
4097
  onKey: keys.some((k) => Math.abs(k.t - clipTime) < 0.5),
@@ -3476,48 +4102,92 @@ function videoKeyframeRows(project, clipId, time) {
3476
4102
  });
3477
4103
  }
3478
4104
  function plainValueEdit(clip, property, value) {
3479
- if (!_chunkVKF3PWKRcjs.VIDEO_TRANSFORM_PROPERTIES.includes(property))
4105
+ if (!_chunk3OSXHLITcjs.VIDEO_TRANSFORM_PROPERTIES.includes(property))
3480
4106
  return value === void 0 ? { op: "update_clip", clip: clip.id, remove: [property] } : { op: "update_clip", clip: clip.id, set: { [property]: value } };
3481
4107
  const transform = { ...clip.transform, [property]: value };
3482
4108
  if (value === void 0) delete transform[property];
3483
4109
  return Object.keys(transform).length ? { op: "update_clip", clip: clip.id, set: { transform } } : { op: "update_clip", clip: clip.id, remove: ["transform"] };
3484
4110
  }
3485
4111
  function videoKeyframeValueEdit(project, clipId, property, time, value) {
3486
- const at = _chunkVKF3PWKRcjs.findVideoClip.call(void 0, project, clipId);
4112
+ const at = _chunk3OSXHLITcjs.findVideoClip.call(void 0, project, clipId);
3487
4113
  if (!at) return null;
3488
4114
  const prop = animatableOf(at.clip, property);
3489
- if (!_chunkVKF3PWKRcjs.isVideoKeyframes.call(void 0, prop)) return plainValueEdit(at.clip, property, value);
4115
+ if (!_chunk3OSXHLITcjs.isVideoKeyframes.call(void 0, prop)) return plainValueEdit(at.clip, property, value);
3490
4116
  if (value === void 0) return null;
3491
- return { op: "set_keyframes", clip: clipId, property, keys: _chunkVKF3PWKRcjs.setVideoKey.call(void 0, prop, { t: playheadInClip(at.clip, time), v: value }, _chunkVKF3PWKRcjs.VIDEO_KEYFRAME_LIMITS[property].default).keys };
4117
+ return { op: "set_keyframes", clip: clipId, property, keys: _chunk3OSXHLITcjs.setVideoKey.call(void 0, prop, { t: playheadInClip(at.clip, time), v: value }, _chunk3OSXHLITcjs.VIDEO_KEYFRAME_LIMITS[property].default).keys };
3492
4118
  }
3493
4119
  function videoKeyframeToggleEdit(project, clipId, property, time) {
3494
- const at = _chunkVKF3PWKRcjs.findVideoClip.call(void 0, project, clipId);
4120
+ const at = _chunk3OSXHLITcjs.findVideoClip.call(void 0, project, clipId);
3495
4121
  if (!at) return null;
3496
4122
  const prop = animatableOf(at.clip, property);
3497
4123
  const t = playheadInClip(at.clip, time);
3498
- const fallback = _chunkVKF3PWKRcjs.VIDEO_KEYFRAME_LIMITS[property].default;
3499
- if (_chunkVKF3PWKRcjs.isVideoKeyframes.call(void 0, prop)) {
4124
+ const fallback = _chunk3OSXHLITcjs.VIDEO_KEYFRAME_LIMITS[property].default;
4125
+ if (_chunk3OSXHLITcjs.isVideoKeyframes.call(void 0, prop)) {
3500
4126
  const key = prop.keys.find((k) => Math.abs(k.t - t) < 0.5);
3501
4127
  if (key) {
3502
- const rest = _chunkVKF3PWKRcjs.removeVideoKey.call(void 0, prop, key.t);
3503
- return _chunkVKF3PWKRcjs.isVideoKeyframes.call(void 0, rest) ? { op: "set_keyframes", clip: clipId, property, keys: rest.keys } : plainValueEdit(at.clip, property, rest === fallback ? void 0 : rest);
4128
+ const rest = _chunk3OSXHLITcjs.removeVideoKey.call(void 0, prop, key.t);
4129
+ return _chunk3OSXHLITcjs.isVideoKeyframes.call(void 0, rest) ? { op: "set_keyframes", clip: clipId, property, keys: rest.keys } : plainValueEdit(at.clip, property, rest === fallback ? void 0 : rest);
3504
4130
  }
3505
4131
  }
3506
- const v = _chunkVKF3PWKRcjs.videoValueAt.call(void 0, prop, t, fallback);
3507
- return { op: "set_keyframes", clip: clipId, property, keys: _chunkVKF3PWKRcjs.isVideoKeyframes.call(void 0, prop) ? _chunkVKF3PWKRcjs.setVideoKey.call(void 0, prop, { t, v }, fallback).keys : [{ t, v }] };
4132
+ const v = _chunk3OSXHLITcjs.videoValueAt.call(void 0, prop, t, fallback);
4133
+ return { op: "set_keyframes", clip: clipId, property, keys: _chunk3OSXHLITcjs.isVideoKeyframes.call(void 0, prop) ? _chunk3OSXHLITcjs.setVideoKey.call(void 0, prop, { t, v }, fallback).keys : [{ t, v }] };
3508
4134
  }
3509
4135
  function videoLutEdits(project, clips, lut) {
3510
4136
  const edits = [];
3511
4137
  for (const id of clips) {
3512
- const at = _chunkVKF3PWKRcjs.findVideoClip.call(void 0, project, id);
4138
+ const at = _chunk3OSXHLITcjs.findVideoClip.call(void 0, project, id);
3513
4139
  if (!at) continue;
3514
4140
  const { lut: current, ...rest } = _nullishCoalesce(at.clip.grade, () => ( {}));
3515
4141
  if (!lut && !current) continue;
3516
- const grade = _chunkVKF3PWKRcjs.normalizeVideoGrade.call(void 0, lut ? { ...rest, lut } : rest);
4142
+ const grade = _chunk3OSXHLITcjs.normalizeVideoGrade.call(void 0, lut ? { ...rest, lut } : rest);
3517
4143
  edits.push(grade ? { op: "update_clip", clip: id, set: { grade } } : { op: "update_clip", clip: id, remove: ["grade"] });
3518
4144
  }
3519
4145
  return edits;
3520
4146
  }
4147
+ var VIDEO_ADJUSTMENT_SCOPE_LABELS = {
4148
+ underneath: "Tracks underneath",
4149
+ all: "Whole frame",
4150
+ track: "One track"
4151
+ };
4152
+ var VIDEO_MASK_SHAPE_LABELS = {
4153
+ rectangle: "Rectangle",
4154
+ ellipse: "Ellipse",
4155
+ linear: "Split",
4156
+ polygon: "Polygon"
4157
+ };
4158
+ var videoClipHeading = (clip) => ({ media: "Media clip", text: "Title", sequence: "Compound clip", color: "Colour clip", effect: "Effect clip", adjustment: "Adjustment clip" })[clip.type];
4159
+ function videoAddAdjustmentEdits(project, time, options = {}) {
4160
+ const duration = _nullishCoalesce(options.duration, () => ( 3e3));
4161
+ const start = Math.max(0, time);
4162
+ const top = project.tracks[0];
4163
+ const free = top && top.kind === "video" && !top.locked && !top.clips.some((c) => c.start < start + duration && _chunk3OSXHLITcjs.videoClipEnd.call(void 0, c) > start);
4164
+ const edits = [];
4165
+ let track = _optionalChain([top, 'optionalAccess', _132 => _132.id]);
4166
+ if (!free) {
4167
+ const ids = new Set(project.tracks.map((t) => t.id));
4168
+ let n = 1;
4169
+ while (ids.has(n === 1 ? "adj" : `adj${n}`)) n++;
4170
+ track = n === 1 ? "adj" : `adj${n}`;
4171
+ edits.push({ op: "add_track", track: { id: track, kind: "video", name: "Adjustments" }, at: 0 });
4172
+ }
4173
+ edits.push({ op: "add_clip", track, clip: { type: "adjustment", ...options.id ? { id: options.id } : {}, start, duration, ...options.grade ? { grade: options.grade } : {} } });
4174
+ return edits;
4175
+ }
4176
+ function videoMaskEdit(clip, mask) {
4177
+ return mask ? { op: "update_clip", clip, set: { mask } } : { op: "update_clip", clip, remove: ["mask"] };
4178
+ }
4179
+ function videoSkinProtectionEdits(project, clips, amount) {
4180
+ const edits = [];
4181
+ for (const id of clips) {
4182
+ const clip = _optionalChain([_chunk3OSXHLITcjs.findVideoClip.call(void 0, project, id), 'optionalAccess', _133 => _133.clip]);
4183
+ const lut = _optionalChain([clip, 'optionalAccess', _134 => _134.grade, 'optionalAccess', _135 => _135.lut]);
4184
+ if (!clip || !lut) continue;
4185
+ const { preserveSkinTones: _p, ...rest } = lut;
4186
+ const k = Math.min(1, Math.max(0, amount));
4187
+ edits.push({ op: "update_clip", clip: id, set: { grade: { ...clip.grade, lut: k > 0 ? { ...rest, preserveSkinTones: k } : rest } } });
4188
+ }
4189
+ return edits;
4190
+ }
3521
4191
 
3522
4192
  // src/video-editor/tools.ts
3523
4193
  var round = (ms) => Math.round(ms * 10) / 10;
@@ -3543,7 +4213,7 @@ var RIPPLE = {
3543
4213
  };
3544
4214
  var MODE = {
3545
4215
  type: "string",
3546
- enum: [..._chunkVKF3PWKRcjs.VIDEO_PLACEMENT_MODES],
4216
+ enum: [..._chunk3OSXHLITcjs.VIDEO_PLACEMENT_MODES],
3547
4217
  description: 'What to do with clips already where it lands: "refuse" (default) fails with the next free place; "overwrite" cuts, trims or removes what is underneath on that track; "insert" cuts everything across the start and pushes later material along by its length on this track and every sync-locked track.'
3548
4218
  };
3549
4219
  var DELTA = (what) => ({ type: "number", description: `${what}, in milliseconds of timeline time: positive is later, negative earlier. It is clamped to what the media and neighbours allow; the result says how far it went.` });
@@ -3560,28 +4230,29 @@ var TRANSFORM = object(
3560
4230
  );
3561
4231
  var TRANSITION = object(
3562
4232
  {
3563
- type: { type: "string", enum: _chunkVKF3PWKRcjs.VIDEO_TRANSITION_TYPES.filter((t) => t !== "effect"), description: "crossfade (this clip fades up over the one before), dip (down into a colour and up out of it), wipe (an edge travels across revealing this clip), push (this clip slides in and pushes the one before out), slide (this clip slides in over the one before) or iris (a circle opens from the centre). For a transition effect from the effects provider use set_video_effect_transition." },
4233
+ type: { type: "string", enum: _chunk3OSXHLITcjs.VIDEO_TRANSITION_TYPES.filter((t) => t !== "effect"), description: "crossfade (this clip fades up over the one before), dip (down into a colour and up out of it), wipe (an edge travels across revealing this clip), push (this clip slides in and pushes the one before out), slide (this clip slides in over the one before) or iris (a circle opens from the centre). For a transition effect from the effects provider use set_video_effect_transition." },
3564
4234
  duration: { type: "number", exclusiveMinimum: 0, description: "Length in ms, centred on the cut. Every type but dip needs half of it in media past the cut on both sides (handles); a dip needs none." },
3565
4235
  color: COLOR("For dip: the colour passed through (default black)"),
3566
- direction: { type: "string", enum: [..._chunkVKF3PWKRcjs.VIDEO_WIPE_DIRECTIONS], description: "For wipe, push and slide: the way the edge or picture travels (default right, coming in from the left)." },
3567
- audioCurve: { type: "string", enum: [..._chunkVKF3PWKRcjs.VIDEO_FADE_CURVES], description: "Shape of the sound's fades through it: default equal-power for every type but dip (steady loudness while the two sounds overlap), linear for dip; or s-curve, exponential, logarithmic." }
4236
+ direction: { type: "string", enum: [..._chunk3OSXHLITcjs.VIDEO_WIPE_DIRECTIONS], description: "For wipe, push and slide: the way the edge or picture travels (default right, coming in from the left)." },
4237
+ audioCurve: { type: "string", enum: [..._chunk3OSXHLITcjs.VIDEO_FADE_CURVES], description: "Shape of the sound's fades through it: default equal-power for every type but dip (steady loudness while the two sounds overlap), linear for dip; or s-curve, exponential, logarithmic." }
3568
4238
  },
3569
4239
  ["type", "duration"],
3570
4240
  "A transition from the clip that ends where this one starts on the same track."
3571
4241
  );
3572
4242
  var LUT_SRC = { type: "string", minLength: 1, description: 'A .cube 3D LUT file the host can load and ffmpeg can read: a URL or path, often from the user (e.g. "luts/teal-orange.cube").' };
3573
4243
  var LUT_STRENGTH = { type: "number", minimum: 0, maximum: 1, description: "How much of the LUT's change to apply, 0 (none) to 1 (full, the default); 0.3-0.7 for a subtler look." };
4244
+ var SKIN = { type: "number", minimum: 0, maximum: 1, description: "How much of the LUT's change to hold back on skin tones, 0 (none, the default) to 1 (skin keeps its colour); 0.5-0.8 keeps faces natural under a heavy look. Applied in the preview and frame renders; ffmpeg's lut3d cannot, so the export notes it." };
3574
4245
  var GRADE = object(
3575
4246
  {
3576
- ...Object.fromEntries(_chunkVKF3PWKRcjs.VIDEO_GRADE_CONTROLS.map((c) => [c.key, { type: "number", minimum: c.min, maximum: c.max, description: c.description }])),
3577
- lut: object({ src: LUT_SRC, strength: LUT_STRENGTH, name: { type: "string", minLength: 1, description: "A label for the look." } }, ["src"], "A 3D LUT applied after the other settings (set_video_lut checks the file first).")
4247
+ ...Object.fromEntries(_chunk3OSXHLITcjs.VIDEO_GRADE_CONTROLS.map((c) => [c.key, { type: "number", minimum: c.min, maximum: c.max, description: c.description }])),
4248
+ lut: object({ src: LUT_SRC, strength: LUT_STRENGTH, name: { type: "string", minLength: 1, description: "A label for the look." }, preserveSkinTones: SKIN }, ["src"], "A 3D LUT applied after the other settings (set_video_lut checks the file first).")
3578
4249
  },
3579
4250
  [],
3580
4251
  "Colour correction of the picture, applied in order: exposure, contrast, saturation, hue, white balance (temperature, tint), then a LUT. Leave a setting out for no change; the object replaces the clip's whole grade."
3581
4252
  );
3582
4253
  var ANIMATION = (when) => object(
3583
4254
  {
3584
- effect: { type: "string", enum: [..._chunkVKF3PWKRcjs.VIDEO_CLIP_ANIMATION_EFFECTS], description: "fade; rise (up into place) or drop (down into place); slide-left / slide-right (moving that way); zoom (grows into place); pop (grows past its size and settles). Each also fades; an exit plays the movement backwards." },
4255
+ effect: { type: "string", enum: [..._chunk3OSXHLITcjs.VIDEO_CLIP_ANIMATION_EFFECTS], description: "fade; rise (up into place) or drop (down into place); slide-left / slide-right (moving that way); zoom (grows into place); pop (grows past its size and settles). Each also fades; an exit plays the movement backwards." },
3585
4256
  duration: { type: "number", exclusiveMinimum: 0, description: "Length in ms (default 500)." }
3586
4257
  },
3587
4258
  ["effect"],
@@ -3591,11 +4262,11 @@ var CLIP_COMMON = {
3591
4262
  name: { type: "string", description: "A label for the clip." },
3592
4263
  opacity: { type: "number", minimum: 0, maximum: 1, description: "Opacity 0\u20131 (default 1). A number replaces any keyframes; set_video_keyframes animates it." },
3593
4264
  transform: TRANSFORM,
3594
- fit: { type: "string", enum: [..._chunkVKF3PWKRcjs.VIDEO_FITS], description: "How the picture fits the frame before the transform, as CSS object-fit (default contain for media, fill for colours)." },
4265
+ fit: { type: "string", enum: [..._chunk3OSXHLITcjs.VIDEO_FITS], description: "How the picture fits the frame before the transform, as CSS object-fit (default contain for media, fill for colours)." },
3595
4266
  grade: GRADE,
3596
4267
  animateIn: ANIMATION("How the picture enters, from the clip's start (on top of any fade in). Titles usually rise, pop or fade in."),
3597
4268
  animateOut: ANIMATION("How the picture leaves, ending at the clip's end."),
3598
- blend: { type: "string", enum: [..._chunkVKF3PWKRcjs.VIDEO_BLEND_MODES], description: "How the picture mixes with what is beneath it, as CSS mix-blend-mode (default normal): multiply darkens (shadows, textures), screen and add lighten (light leaks, flares, glows), overlay and soft-light add contrast, difference and exclusion invert." },
4269
+ blend: { type: "string", enum: [..._chunk3OSXHLITcjs.VIDEO_BLEND_MODES], description: "How the picture mixes with what is beneath it, as CSS mix-blend-mode (default normal): multiply darkens (shadows, textures), screen and add lighten (light leaks, flares, glows), overlay and soft-light add contrast, difference and exclusion invert." },
3599
4270
  fadeIn: MS("Fade up from nothing (picture and sound) over this long"),
3600
4271
  fadeOut: MS("Fade down to nothing over this long")
3601
4272
  };
@@ -3604,8 +4275,8 @@ var MEDIA_FIELDS = {
3604
4275
  speed: { type: "number", minimum: 0.05, maximum: 20, description: "Playback speed (default 1). Changing it keeps the stretch of source, so the clip gets shorter or longer." },
3605
4276
  volume: { type: "number", minimum: 0, maximum: 4, description: "Gain (1 = unchanged, default 1). A number replaces any keyframes; set_video_keyframes animates it." },
3606
4277
  muted: { type: "boolean", description: "Silence this clip (default false)." },
3607
- fadeInCurve: { type: "string", enum: [..._chunkVKF3PWKRcjs.VIDEO_FADE_CURVES], description: "Shape of the sound's fade in (default linear; the picture always fades linearly): linear, equal-power (steady loudness, best for crossfades), s-curve (gentle at both ends), exponential (slow start, natural from silence) or logarithmic (fast start)." },
3608
- fadeOutCurve: { type: "string", enum: [..._chunkVKF3PWKRcjs.VIDEO_FADE_CURVES], description: "Shape of the sound's fade out (default linear), the same choices as fadeInCurve; exponential sounds natural fading to silence." }
4278
+ fadeInCurve: { type: "string", enum: [..._chunk3OSXHLITcjs.VIDEO_FADE_CURVES], description: "Shape of the sound's fade in (default linear; the picture always fades linearly): linear, equal-power (steady loudness, best for crossfades), s-curve (gentle at both ends), exponential (slow start, natural from silence) or logarithmic (fast start)." },
4279
+ fadeOutCurve: { type: "string", enum: [..._chunk3OSXHLITcjs.VIDEO_FADE_CURVES], description: "Shape of the sound's fade out (default linear), the same choices as fadeInCurve; exponential sounds natural fading to silence." }
3609
4280
  };
3610
4281
  var TEXT_FIELDS = {
3611
4282
  text: { type: "string", minLength: 1, description: `The title's text ("\\n" breaks lines).` },
@@ -3654,7 +4325,7 @@ var ASSET_FIELDS = {
3654
4325
  };
3655
4326
  var KEY_PROPERTY = {
3656
4327
  type: "string",
3657
- enum: [..._chunkVKF3PWKRcjs.VIDEO_KEYFRAME_PROPERTIES],
4328
+ enum: [..._chunk3OSXHLITcjs.VIDEO_KEYFRAME_PROPERTIES],
3658
4329
  description: "Which property: opacity (0\u20131), volume (media only, 0\u20134 gain), x / y (pixels from the frame's centre, right / down), scale (1 = fitted size) or rotation (degrees clockwise)."
3659
4330
  };
3660
4331
  var KEYS = {
@@ -3669,7 +4340,7 @@ var KEYS = {
3669
4340
  ease: {
3670
4341
  description: "How the value moves from this key to the next (default linear): linear, hold (stays, then jumps at the next key), ease-in, ease-out, ease-in-out, or CSS cubic-bezier points [x1, y1, x2, y2] with x1 and x2 in 0\u20131.",
3671
4342
  anyOf: [
3672
- { type: "string", enum: [..._chunkVKF3PWKRcjs.VIDEO_EASE_NAMES] },
4343
+ { type: "string", enum: [..._chunk3OSXHLITcjs.VIDEO_EASE_NAMES] },
3673
4344
  { type: "array", items: { type: "number" }, minItems: 4, maxItems: 4 }
3674
4345
  ]
3675
4346
  }
@@ -3698,19 +4369,19 @@ var describeVideoTracks = (tracks) => tracks.map((t, index) => ({
3698
4369
  ...t.locked ? { locked: true } : {},
3699
4370
  ...t.syncLocked === false ? { syncLocked: false } : {},
3700
4371
  ...t.volume !== void 0 ? { volume: t.volume } : {},
3701
- clips: _chunkVKF3PWKRcjs.sortedVideoClips.call(void 0, t).map((c) => ({ ...c, end: _chunkVKF3PWKRcjs.videoClipEnd.call(void 0, c), ...c.type === "media" || c.type === "sequence" ? { source: _chunkVKF3PWKRcjs.videoClipSourceRange.call(void 0, c) } : {} }))
4372
+ clips: _chunk3OSXHLITcjs.sortedVideoClips.call(void 0, t).map((c) => ({ ...c, end: _chunk3OSXHLITcjs.videoClipEnd.call(void 0, c), ...c.type === "media" || c.type === "sequence" ? { source: _chunk3OSXHLITcjs.videoClipSourceRange.call(void 0, c) } : {} }))
3702
4373
  }));
3703
- var videoSequenceUsers = (project, id) => _chunkVKF3PWKRcjs.allVideoTracks.call(void 0, project).flatMap(
3704
- ({ sequence, track }) => track.clips.flatMap((c) => c.type === "sequence" && c.sequence === id ? [{ clip: c.id, ...sequence ? { inSequence: sequence } : {}, track: track.id, start: c.start, end: _chunkVKF3PWKRcjs.videoClipEnd.call(void 0, c), source: _chunkVKF3PWKRcjs.videoClipSourceRange.call(void 0, c) }] : [])
4374
+ var videoSequenceUsers = (project, id) => _chunk3OSXHLITcjs.allVideoTracks.call(void 0, project).flatMap(
4375
+ ({ sequence, track }) => track.clips.flatMap((c) => c.type === "sequence" && c.sequence === id ? [{ clip: c.id, ...sequence ? { inSequence: sequence } : {}, track: track.id, start: c.start, end: _chunk3OSXHLITcjs.videoClipEnd.call(void 0, c), source: _chunk3OSXHLITcjs.videoClipSourceRange.call(void 0, c) }] : [])
3705
4376
  );
3706
4377
  function describeVideoSequence(project, sequence, tracks = true) {
3707
- const cycle = _chunkVKF3PWKRcjs.findVideoSequenceCycle.call(void 0, project, sequence.id);
4378
+ const cycle = _chunk3OSXHLITcjs.findVideoSequenceCycle.call(void 0, project, sequence.id);
3708
4379
  return {
3709
4380
  id: sequence.id,
3710
4381
  ...sequence.name ? { name: sequence.name } : {},
3711
- duration: _chunkVKF3PWKRcjs.videoSequenceDuration.call(void 0, sequence),
4382
+ duration: _chunk3OSXHLITcjs.videoSequenceDuration.call(void 0, sequence),
3712
4383
  clipCount: sequence.tracks.reduce((n, t) => n + t.clips.length, 0),
3713
- nests: _chunkVKF3PWKRcjs.videoSequenceChildren.call(void 0, sequence),
4384
+ nests: _chunk3OSXHLITcjs.videoSequenceChildren.call(void 0, sequence),
3714
4385
  usedBy: videoSequenceUsers(project, sequence.id),
3715
4386
  ...cycle ? { cycle } : {},
3716
4387
  ...tracks ? { tracks: describeVideoTracks(sequence.tracks) } : { trackCount: sequence.tracks.length }
@@ -3722,11 +4393,11 @@ function describeVideoProject(project) {
3722
4393
  width: project.width,
3723
4394
  height: project.height,
3724
4395
  fps: project.fps,
3725
- duration: _chunkVKF3PWKRcjs.videoProjectDuration.call(void 0, project),
4396
+ duration: _chunk3OSXHLITcjs.videoProjectDuration.call(void 0, project),
3726
4397
  ...project.background ? { background: project.background } : {},
3727
4398
  assets: project.assets.map(videoAssetOut),
3728
4399
  tracks: describeVideoTracks(project.tracks),
3729
- ..._optionalChain([project, 'access', _117 => _117.sequences, 'optionalAccess', _118 => _118.length]) ? { sequences: project.sequences.map((q) => describeVideoSequence(project, q, false)) } : {},
4400
+ ..._optionalChain([project, 'access', _136 => _136.sequences, 'optionalAccess', _137 => _137.length]) ? { sequences: project.sequences.map((q) => describeVideoSequence(project, q, false)) } : {},
3730
4401
  markers: _nullishCoalesce(project.markers, () => ( []))
3731
4402
  };
3732
4403
  }
@@ -3739,7 +4410,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
3739
4410
  const project = () => controller.getProject();
3740
4411
  const outcome = (result, extra = {}) => {
3741
4412
  if (!result.ok) throw new Error(result.error);
3742
- return { ...extra, duration: _chunkVKF3PWKRcjs.videoProjectDuration.call(void 0, result.project), issues: issueSummary(result.issues) };
4413
+ return { ...extra, duration: _chunk3OSXHLITcjs.videoProjectDuration.call(void 0, result.project), issues: issueSummary(result.issues) };
3743
4414
  };
3744
4415
  const apply = (edits, label, extra) => {
3745
4416
  const result = controller.apply(edits, label);
@@ -3748,19 +4419,19 @@ function createVideoEditorToolProvider(controller, options = {}) {
3748
4419
  const clipOut = (p, id) => {
3749
4420
  for (const t of p.tracks) {
3750
4421
  const c = t.clips.find((x) => x.id === id);
3751
- if (c) return { track: t.id, ...c, end: _chunkVKF3PWKRcjs.videoClipEnd.call(void 0, c), ...c.type === "media" || c.type === "sequence" ? { source: _chunkVKF3PWKRcjs.videoClipSourceRange.call(void 0, c) } : {} };
4422
+ if (c) return { track: t.id, ...c, end: _chunk3OSXHLITcjs.videoClipEnd.call(void 0, c), ...c.type === "media" || c.type === "sequence" ? { source: _chunk3OSXHLITcjs.videoClipSourceRange.call(void 0, c) } : {} };
3752
4423
  }
3753
4424
  return null;
3754
4425
  };
3755
4426
  const placed = (o) => ({
3756
- ..._optionalChain([o, 'access', _119 => _119.created, 'optionalAccess', _120 => _120.length]) ? { created: o.created } : {},
3757
- ..._optionalChain([o, 'access', _121 => _121.trimmed, 'optionalAccess', _122 => _122.length]) ? { trimmed: o.trimmed } : {},
3758
- ..._optionalChain([o, 'access', _123 => _123.removed, 'optionalAccess', _124 => _124.length]) ? { removed: o.removed } : {},
4427
+ ..._optionalChain([o, 'access', _138 => _138.created, 'optionalAccess', _139 => _139.length]) ? { created: o.created } : {},
4428
+ ..._optionalChain([o, 'access', _140 => _140.trimmed, 'optionalAccess', _141 => _141.length]) ? { trimmed: o.trimmed } : {},
4429
+ ..._optionalChain([o, 'access', _142 => _142.removed, 'optionalAccess', _143 => _143.length]) ? { removed: o.removed } : {},
3759
4430
  ...o.rippled ? { rippled: o.rippled } : {}
3760
4431
  });
3761
4432
  const keysOut = (p, clip, property) => {
3762
4433
  const c = clipOut(p, clip);
3763
- const value = _chunkVKF3PWKRcjs.VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([c, 'optionalAccess', _125 => _125.transform, 'optionalAccess', _126 => _126[property]]) : _optionalChain([c, 'optionalAccess', _127 => _127[property]]);
4434
+ const value = _chunk3OSXHLITcjs.VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([c, 'optionalAccess', _144 => _144.transform, 'optionalAccess', _145 => _145[property]]) : _optionalChain([c, 'optionalAccess', _146 => _146[property]]);
3764
4435
  return typeof value === "object" ? { keys: value.keys } : { keys: [], value: _nullishCoalesce(value, () => ( null)) };
3765
4436
  };
3766
4437
  const getProject = {
@@ -3782,8 +4453,8 @@ function createVideoEditorToolProvider(controller, options = {}) {
3782
4453
  inputSchema: object({ time: MS("Time from the start (default: the playhead)") }),
3783
4454
  annotations: { title: "What plays at a moment", ...READ },
3784
4455
  handler: ({ time }) => {
3785
- const frame = _chunkVKF3PWKRcjs.videoFrameAt.call(void 0, project(), _nullishCoalesce(time, () => ( controller.getState().time)));
3786
- return { ...frame, timecode: _chunkVKF3PWKRcjs.formatVideoTimecode.call(void 0, frame.time, project().fps) };
4456
+ const frame = _chunk3OSXHLITcjs.videoFrameAt.call(void 0, project(), _nullishCoalesce(time, () => ( controller.getState().time)));
4457
+ return { ...frame, timecode: _chunk3OSXHLITcjs.formatVideoTimecode.call(void 0, frame.time, project().fps) };
3787
4458
  }
3788
4459
  };
3789
4460
  const listSequences = {
@@ -3811,7 +4482,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
3811
4482
  annotations: { title: "Find gaps", ...READ },
3812
4483
  handler: ({ track }) => {
3813
4484
  if (track !== void 0 && !project().tracks.some((t) => t.id === track)) throw new Error(`There is no track "${track}"; the tracks are ${project().tracks.map((t) => `"${t.id}"`).join(", ")}.`);
3814
- return { gaps: _chunkVKF3PWKRcjs.findVideoGaps.call(void 0, project(), track) };
4485
+ return { gaps: _chunk3OSXHLITcjs.findVideoGaps.call(void 0, project(), track) };
3815
4486
  }
3816
4487
  };
3817
4488
  const checkProject = {
@@ -3838,10 +4509,10 @@ function createVideoEditorToolProvider(controller, options = {}) {
3838
4509
  annotations: { title: "Render a frame", ...READ },
3839
4510
  handler: async ({ time, times, columns, format = "svg" }) => {
3840
4511
  const p = project();
3841
- const lutSrcs = [...new Set(p.tracks.flatMap((t) => t.clips.map((c) => _optionalChain([c, 'access', _128 => _128.grade, 'optionalAccess', _129 => _129.lut, 'optionalAccess', _130 => _130.src])).filter((x) => !!x)))];
4512
+ const lutSrcs = [...new Set(p.tracks.flatMap((t) => t.clips.map((c) => _optionalChain([c, 'access', _147 => _147.grade, 'optionalAccess', _148 => _148.lut, 'optionalAccess', _149 => _149.src])).filter((x) => !!x)))];
3842
4513
  const missing = (await Promise.all(lutSrcs.map((src) => controller.luts.load(src).then(() => null, (e) => e.message)))).filter((m) => !!m);
3843
4514
  const svgOptions = { lutFor: (src) => controller.luts.peek(src), ...columns ? { columns } : {} };
3844
- const svg = times ? _chunkVKF3PWKRcjs.videoContactSheetSvg.call(void 0, p, times, svgOptions) : _chunkVKF3PWKRcjs.videoProjectSvg.call(void 0, p, _nullishCoalesce(time, () => ( controller.getState().time)), svgOptions);
4515
+ const svg = times ? _chunk3OSXHLITcjs.videoContactSheetSvg.call(void 0, p, times, svgOptions) : _chunk3OSXHLITcjs.videoProjectSvg.call(void 0, p, _nullishCoalesce(time, () => ( controller.getState().time)), svgOptions);
3845
4516
  const notes = [...missing.map((m) => `${m}; that LUT is left out of the picture.`), ...lutSrcs.length > missing.length ? ["LUTs are drawn approximately in SVG (curves and a colour matrix); the preview and export apply them exactly."] : []];
3846
4517
  if (format === "png") {
3847
4518
  if (!options.rasterize) throw new Error('This host cannot make PNG images; call render_video_frame with format "svg".');
@@ -3931,7 +4602,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
3931
4602
  if (play === true) controller.play();
3932
4603
  if (play === false) controller.pause();
3933
4604
  const state = controller.getState();
3934
- return { time: state.time, timecode: _chunkVKF3PWKRcjs.formatVideoTimecode.call(void 0, state.time, p.fps), playing: state.playing, clips: state.current };
4605
+ return { time: state.time, timecode: _chunk3OSXHLITcjs.formatVideoTimecode.call(void 0, state.time, p.fps), playing: state.playing, clips: state.current };
3935
4606
  }
3936
4607
  };
3937
4608
  const setSettings = {
@@ -4053,12 +4724,12 @@ function createVideoEditorToolProvider(controller, options = {}) {
4053
4724
  const updateClip = {
4054
4725
  name: "update_video_clip",
4055
4726
  title: "Change a clip",
4056
- description: `Changes a clip's properties (one undo step): opacity, transform, fit, blend mode, colour grade, entrance and exit animations, fades, name; for media volume, mute, the sound's fade curves, speed (keeps the stretch of source, so the length changes) or asset; for compound clips in point, volume, mute and fade curves (no speed); for titles text and style; for colours the colour. Fields in "remove" go back to their defaults. Use move / trim for timing. Returns the clip.`,
4727
+ description: `Changes a clip's properties (one undo step): opacity, transform, fit, blend mode, colour grade, entrance and exit animations, fades, name; for media volume, mute, the sound's fade curves, speed (keeps the stretch of source, so the length changes) or asset; for compound clips in point, volume, mute and fade curves (no speed); for titles text and style; for colours the colour; for adjustment clips the scope and target track (masks: set_video_clip_mask). Fields in "remove" go back to their defaults. Use move / trim for timing. Returns the clip.`,
4057
4728
  inputSchema: object(
4058
4729
  {
4059
4730
  clip: CLIP_ID,
4060
- set: object({ ...CLIP_COMMON, ...MEDIA_FIELDS, asset: { ...ASSET_ID, description: "For media: play another asset (same in point)." }, ...TEXT_FIELDS, color: COLOR("Text colour, or a colour clip's fill") }, [], "Fields to replace (only those that fit the clip's type)."),
4061
- remove: { type: "array", items: { type: "string", enum: ["name", "opacity", "transform", "fit", "blend", "grade", "animateIn", "animateOut", "fadeIn", "fadeOut", "fadeInCurve", "fadeOutCurve", "in", "speed", "volume", "muted", "fontSize", "fontFamily", "fontWeight", "background"] }, description: "Fields to reset to their defaults." }
4731
+ set: object({ ...CLIP_COMMON, ...MEDIA_FIELDS, asset: { ...ASSET_ID, description: "For media: play another asset (same in point)." }, ...TEXT_FIELDS, color: COLOR("Text colour, or a colour clip's fill"), scope: { type: "string", enum: [..._chunk3OSXHLITcjs.VIDEO_ADJUSTMENT_SCOPES], description: "For adjustment clips: which pictures it changes, underneath (default), all or track." }, targetTrack: { ...TRACK_ID, description: 'For adjustment clips with scope "track": the track whose pictures it changes.' } }, [], "Fields to replace (only those that fit the clip's type)."),
4732
+ remove: { type: "array", items: { type: "string", enum: ["name", "opacity", "transform", "fit", "blend", "grade", "animateIn", "animateOut", "fadeIn", "fadeOut", "fadeInCurve", "fadeOutCurve", "in", "speed", "volume", "muted", "fontSize", "fontFamily", "fontWeight", "background", "scope", "targetTrack"] }, description: 'Fields to reset to their defaults (an adjustment clip going from scope "track" to another scope must also remove targetTrack).' }
4062
4733
  },
4063
4734
  ["clip"]
4064
4735
  ),
@@ -4076,10 +4747,10 @@ function createVideoEditorToolProvider(controller, options = {}) {
4076
4747
  const gradeTargets = (ids) => {
4077
4748
  const p = project();
4078
4749
  for (const id of ids) {
4079
- const at = _chunkVKF3PWKRcjs.findVideoClip.call(void 0, p, id);
4750
+ const at = _chunk3OSXHLITcjs.findVideoClip.call(void 0, p, id);
4080
4751
  if (!at) throw new Error(`There is no clip "${id}"; get_video_project lists the clips.`);
4081
4752
  const { clip } = at;
4082
- if (at.track.kind !== "video" || clip.type === "media" && _optionalChain([p, 'access', _131 => _131.assets, 'access', _132 => _132.find, 'call', _133 => _133((a) => a.id === clip.asset), 'optionalAccess', _134 => _134.kind]) === "audio")
4753
+ if (at.track.kind !== "video" || clip.type === "media" && _optionalChain([p, 'access', _150 => _150.assets, 'access', _151 => _151.find, 'call', _152 => _152((a) => a.id === clip.asset), 'optionalAccess', _153 => _153.kind]) === "audio")
4083
4754
  throw new Error(`Clip "${id}" is sound only; grades change pictures. Pass clips on video tracks.`);
4084
4755
  }
4085
4756
  };
@@ -4090,7 +4761,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
4090
4761
  inputSchema: object(
4091
4762
  {
4092
4763
  clips: { type: "array", items: CLIP_ID, minItems: 1, description: "Clips on video tracks to grade." },
4093
- preset: { type: "string", enum: [..._chunkVKF3PWKRcjs.VIDEO_GRADE_PRESET_NAMES], description: "The look." },
4764
+ preset: { type: "string", enum: [..._chunk3OSXHLITcjs.VIDEO_GRADE_PRESET_NAMES], description: "The look." },
4094
4765
  amount: { type: "number", minimum: 0, maximum: 1, description: "Strength, 0 (none) to 1 (full, the default); 0.5 is a subtler version." }
4095
4766
  },
4096
4767
  ["clips", "preset"]
@@ -4099,10 +4770,10 @@ function createVideoEditorToolProvider(controller, options = {}) {
4099
4770
  handler: ({ clips, preset, amount }) => {
4100
4771
  gradeTargets(clips);
4101
4772
  const edits = clips.map((clip) => {
4102
- const grade = _chunkVKF3PWKRcjs.videoGradeLook.call(void 0, _chunkVKF3PWKRcjs.findVideoClip.call(void 0, project(), clip).clip.grade, preset, _nullishCoalesce(amount, () => ( 1)));
4773
+ const grade = _chunk3OSXHLITcjs.videoGradeLook.call(void 0, _chunk3OSXHLITcjs.findVideoClip.call(void 0, project(), clip).clip.grade, preset, _nullishCoalesce(amount, () => ( 1)));
4103
4774
  return grade ? { op: "update_clip", clip, set: { grade } } : { op: "update_clip", clip, remove: ["grade"] };
4104
4775
  });
4105
- return apply(edits, `Apply ${preset} look`, (r) => ({ clips: clips.map((id) => ({ id, grade: _nullishCoalesce(_optionalChain([_chunkVKF3PWKRcjs.findVideoClip.call(void 0, r.project, id), 'optionalAccess', _135 => _135.clip, 'access', _136 => _136.grade]), () => ( null)) })) }));
4776
+ return apply(edits, `Apply ${preset} look`, (r) => ({ clips: clips.map((id) => ({ id, grade: _nullishCoalesce(_optionalChain([_chunk3OSXHLITcjs.findVideoClip.call(void 0, r.project, id), 'optionalAccess', _154 => _154.clip, 'access', _155 => _155.grade]), () => ( null)) })) }));
4106
4777
  }
4107
4778
  };
4108
4779
  const copyGrade = {
@@ -4113,7 +4784,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
4113
4784
  annotations: { title: "Copy a colour grade", ...WRITE_IDEMPOTENT },
4114
4785
  handler: ({ from, to }) => {
4115
4786
  gradeTargets([from, ...to]);
4116
- const grade = _chunkVKF3PWKRcjs.findVideoClip.call(void 0, project(), from).clip.grade;
4787
+ const grade = _chunk3OSXHLITcjs.findVideoClip.call(void 0, project(), from).clip.grade;
4117
4788
  const edits = to.map((clip) => grade ? { op: "update_clip", clip, set: { grade: { ...grade } } } : { op: "update_clip", clip, remove: ["grade"] });
4118
4789
  return apply(edits, "Copy grade", () => ({ grade: _nullishCoalesce(grade, () => ( null)), clips: to }));
4119
4790
  }
@@ -4127,18 +4798,19 @@ function createVideoEditorToolProvider(controller, options = {}) {
4127
4798
  clips: { type: "array", items: CLIP_ID, minItems: 1, uniqueItems: true, description: "Clips on video tracks to grade." },
4128
4799
  src: LUT_SRC,
4129
4800
  strength: LUT_STRENGTH,
4130
- name: { type: "string", minLength: 1, description: "A label for the look (default the file's TITLE)." }
4801
+ name: { type: "string", minLength: 1, description: "A label for the look (default the file's TITLE)." },
4802
+ preserve_skin_tones: SKIN
4131
4803
  },
4132
4804
  ["clips", "src"]
4133
4805
  ),
4134
4806
  annotations: { title: "Apply a LUT", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
4135
- handler: async ({ clips, src, strength, name }) => {
4807
+ handler: async ({ clips, src, strength, name, preserve_skin_tones }) => {
4136
4808
  if (strength === 0) throw new Error("A strength of 0 changes nothing; pass a strength above 0, or remove the LUT with remove_video_lut.");
4137
4809
  gradeTargets(clips);
4138
4810
  const lut = await controller.luts.load(src);
4139
4811
  const label = _nullishCoalesce(name, () => ( lut.title));
4140
- const setting = { src, ...strength !== void 0 && strength !== 1 ? { strength } : {}, ...label ? { name: label } : {} };
4141
- return apply(videoLutEdits(project(), clips, setting), "Apply LUT", (r) => ({ lut: { src, ...lut.title ? { title: lut.title } : {}, size: lut.size }, clips: clips.map((id) => ({ id, grade: _nullishCoalesce(_optionalChain([_chunkVKF3PWKRcjs.findVideoClip.call(void 0, r.project, id), 'optionalAccess', _137 => _137.clip, 'access', _138 => _138.grade]), () => ( null)) })) }));
4812
+ const setting = { src, ...strength !== void 0 && strength !== 1 ? { strength } : {}, ...label ? { name: label } : {}, ...preserve_skin_tones ? { preserveSkinTones: preserve_skin_tones } : {} };
4813
+ return apply(videoLutEdits(project(), clips, setting), "Apply LUT", (r) => ({ lut: { src, ...lut.title ? { title: lut.title } : {}, size: lut.size }, clips: clips.map((id) => ({ id, grade: _nullishCoalesce(_optionalChain([_chunk3OSXHLITcjs.findVideoClip.call(void 0, r.project, id), 'optionalAccess', _156 => _156.clip, 'access', _157 => _157.grade]), () => ( null)) })) }));
4142
4814
  }
4143
4815
  };
4144
4816
  const removeLut = {
@@ -4150,9 +4822,9 @@ function createVideoEditorToolProvider(controller, options = {}) {
4150
4822
  handler: ({ clips }) => {
4151
4823
  gradeTargets(clips);
4152
4824
  const p = project();
4153
- const withLut = clips.filter((id) => _optionalChain([_chunkVKF3PWKRcjs.findVideoClip.call(void 0, p, id), 'access', _139 => _139.clip, 'access', _140 => _140.grade, 'optionalAccess', _141 => _141.lut]));
4825
+ const withLut = clips.filter((id) => _optionalChain([_chunk3OSXHLITcjs.findVideoClip.call(void 0, p, id), 'access', _158 => _158.clip, 'access', _159 => _159.grade, 'optionalAccess', _160 => _160.lut]));
4154
4826
  if (!withLut.length) throw new Error(`None of ${clips.map((c) => `"${c}"`).join(", ")} has a LUT; get_video_project shows each clip's grade.`);
4155
- return apply(videoLutEdits(p, withLut, null), "Remove LUT", (r) => ({ clips: clips.map((id) => ({ id, grade: _nullishCoalesce(_optionalChain([_chunkVKF3PWKRcjs.findVideoClip.call(void 0, r.project, id), 'optionalAccess', _142 => _142.clip, 'access', _143 => _143.grade]), () => ( null)) })) }));
4827
+ return apply(videoLutEdits(p, withLut, null), "Remove LUT", (r) => ({ clips: clips.map((id) => ({ id, grade: _nullishCoalesce(_optionalChain([_chunk3OSXHLITcjs.findVideoClip.call(void 0, r.project, id), 'optionalAccess', _161 => _161.clip, 'access', _162 => _162.grade]), () => ( null)) })) }));
4156
4828
  }
4157
4829
  };
4158
4830
  const PROXY_SCALE = { type: "number", exclusiveMinimum: 0, exclusiveMaximum: 1, description: "Proxy size as a fraction of the original: 0.5 (half width and height, the default) or 0.25 for very large footage." };
@@ -4210,7 +4882,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
4210
4882
  controller.setPreviewScale(scale);
4211
4883
  const s = controller.getState().previewScale;
4212
4884
  const fixed = s === "full" ? 1 : typeof s === "number" ? s : null;
4213
- const plays = fixed === null ? void 0 : project().assets.filter((a) => a.kind === "video").map((a) => ({ asset: a.id, src: _nullishCoalesce(_optionalChain([videoAssetProxy, 'call', _144 => _144(a, fixed), 'optionalAccess', _145 => _145.src]), () => ( a.src)), proxy: !!videoAssetProxy(a, fixed) }));
4885
+ const plays = fixed === null ? void 0 : project().assets.filter((a) => a.kind === "video").map((a) => ({ asset: a.id, src: _nullishCoalesce(_optionalChain([videoAssetProxy, 'call', _163 => _163(a, fixed), 'optionalAccess', _164 => _164.src]), () => ( a.src)), proxy: !!videoAssetProxy(a, fixed) }));
4214
4886
  return { previewScale: s, ...plays ? { plays } : { note: "auto picks the smallest proxy at least as large as the preview canvas shows, so it depends on the preview's size." } };
4215
4887
  }
4216
4888
  };
@@ -4261,7 +4933,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
4261
4933
  approximate: parsed.approximate,
4262
4934
  first: list[0],
4263
4935
  last: list[list.length - 1],
4264
- ...a.duration !== void 0 && list[list.length - 1].end > a.duration + _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON ? { warning: `The transcript runs to ${list[list.length - 1].end} ms, past the asset's ${a.duration} ms: is it for this asset?` } : {}
4936
+ ...a.duration !== void 0 && list[list.length - 1].end > a.duration + _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON ? { warning: `The transcript runs to ${list[list.length - 1].end} ms, past the asset's ${a.duration} ms: is it for this asset?` } : {}
4265
4937
  }));
4266
4938
  }
4267
4939
  };
@@ -4279,7 +4951,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
4279
4951
  handler: ({ track, from, to, limit }) => {
4280
4952
  const all = timelineWords(track).filter((w) => w.end > (_nullishCoalesce(from, () => ( 0))) && w.start < (_nullishCoalesce(to, () => ( Infinity))));
4281
4953
  const words = all.slice(0, _nullishCoalesce(limit, () => ( 1e3)));
4282
- return { track: _nullishCoalesce(_optionalChain([words, 'access', _146 => _146[0], 'optionalAccess', _147 => _147.track]), () => ( videoTranscriptTrack(project(), track))), text: words.map((w) => w.word).join(" "), words: words.map((w) => ({ word: w.word, start: round(w.start), end: round(w.end), clip: w.clip })), more: all.length > words.length };
4954
+ return { track: _nullishCoalesce(_optionalChain([words, 'access', _165 => _165[0], 'optionalAccess', _166 => _166.track]), () => ( videoTranscriptTrack(project(), track))), text: words.map((w) => w.word).join(" "), words: words.map((w) => ({ word: w.word, start: round(w.start), end: round(w.end), clip: w.clip })), more: all.length > words.length };
4283
4955
  }
4284
4956
  };
4285
4957
  const findText = {
@@ -4332,22 +5004,22 @@ function createVideoEditorToolProvider(controller, options = {}) {
4332
5004
  const bad = ranges.findIndex((r) => !(r.to > r.from));
4333
5005
  if (bad >= 0) throw new Error(`Range ${bad + 1} is empty or backwards (${ranges[bad].from}\u2013${ranges[bad].to} ms); "to" must be after "from".`);
4334
5006
  const merged = mergeVideoRanges(ranges);
4335
- const before = _chunkVKF3PWKRcjs.videoProjectDuration.call(void 0, project());
4336
- return apply(videoRemoveRangesEdits(merged, tracks), merged.length > 1 ? `Cut ${merged.length} stretches` : "Cut stretch", (r) => ({ removed: merged.length, time: round(before - _chunkVKF3PWKRcjs.videoProjectDuration.call(void 0, r.project)), duration: _chunkVKF3PWKRcjs.videoProjectDuration.call(void 0, r.project) }));
5007
+ const before = _chunk3OSXHLITcjs.videoProjectDuration.call(void 0, project());
5008
+ return apply(videoRemoveRangesEdits(merged, tracks), merged.length > 1 ? `Cut ${merged.length} stretches` : "Cut stretch", (r) => ({ removed: merged.length, time: round(before - _chunk3OSXHLITcjs.videoProjectDuration.call(void 0, r.project)), duration: _chunk3OSXHLITcjs.videoProjectDuration.call(void 0, r.project) }));
4337
5009
  }
4338
5010
  };
4339
5011
  const rampTarget = (id) => {
4340
- const at = _chunkVKF3PWKRcjs.findVideoClip.call(void 0, project(), id);
5012
+ const at = _chunk3OSXHLITcjs.findVideoClip.call(void 0, project(), id);
4341
5013
  if (!at) throw new Error(`There is no clip "${id}"; get_video_project lists the clips.`);
4342
5014
  const { clip } = at;
4343
5015
  if (clip.type === "sequence") throw new Error(`Clip "${id}" is a compound clip, which plays its sequence at normal speed; change the speed of the clips inside the sequence instead.`);
4344
- if (clip.type !== "media") throw new Error(`Clip "${id}" is a ${clip.type === "text" ? "title" : clip.type === "effect" ? "effect clip" : "colour"}, so it has no source to change the speed of; use a video or audio clip.`);
4345
- if (_optionalChain([project, 'call', _148 => _148(), 'access', _149 => _149.assets, 'access', _150 => _150.find, 'call', _151 => _151((a) => a.id === clip.asset), 'optionalAccess', _152 => _152.kind]) === "image") throw new Error(`Clip "${id}" is a still, so it has no motion to change the speed of.`);
5016
+ if (clip.type !== "media") throw new Error(`Clip "${id}" is a ${_chunk3OSXHLITcjs.videoClipTypeLabel.call(void 0, clip)}, so it has no source to change the speed of; use a video or audio clip.`);
5017
+ if (_optionalChain([project, 'call', _167 => _167(), 'access', _168 => _168.assets, 'access', _169 => _169.find, 'call', _170 => _170((a) => a.id === clip.asset), 'optionalAccess', _171 => _171.kind]) === "image") throw new Error(`Clip "${id}" is a still, so it has no motion to change the speed of.`);
4346
5018
  return clip;
4347
5019
  };
4348
5020
  const rampOut = (r, id) => {
4349
5021
  const outcome2 = r.outcomes[r.outcomes.length - 1];
4350
- return { clip: clipOut(r.project, id), ...outcome2.rippled ? { rippled: outcome2.rippled } : {}, duration: _chunkVKF3PWKRcjs.videoProjectDuration.call(void 0, r.project) };
5022
+ return { clip: clipOut(r.project, id), ...outcome2.rippled ? { rippled: outcome2.rippled } : {}, duration: _chunk3OSXHLITcjs.videoProjectDuration.call(void 0, r.project) };
4351
5023
  };
4352
5024
  const RIPPLE_RAMP = { type: "boolean", description: "Move later material on every sync-locked track by the change in length (default true); false refuses if the clip would overlap the next one." };
4353
5025
  const setSpeedRamp = {
@@ -4378,8 +5050,8 @@ function createVideoEditorToolProvider(controller, options = {}) {
4378
5050
  ),
4379
5051
  annotations: { title: "Ramp the speed", ...WRITE_IDEMPOTENT },
4380
5052
  handler: ({ clip, segments, ripple }) => {
4381
- const edit = _chunkVKF3PWKRcjs.videoSpeedRampEdit.call(void 0, rampTarget(clip), segments, _nullishCoalesce(ripple, () => ( true)));
4382
- return apply(edit, "Speed ramp", (r) => ({ ...rampOut(r, clip), source: Math.round(_chunkVKF3PWKRcjs.videoSpeedRampKeys.call(void 0, segments).source) }));
5053
+ const edit = _chunk3OSXHLITcjs.videoSpeedRampEdit.call(void 0, rampTarget(clip), segments, _nullishCoalesce(ripple, () => ( true)));
5054
+ return apply(edit, "Speed ramp", (r) => ({ ...rampOut(r, clip), source: Math.round(_chunk3OSXHLITcjs.videoSpeedRampKeys.call(void 0, segments).source) }));
4383
5055
  }
4384
5056
  };
4385
5057
  const addFreeze = {
@@ -4399,8 +5071,8 @@ function createVideoEditorToolProvider(controller, options = {}) {
4399
5071
  handler: ({ clip, at, duration, ripple }) => {
4400
5072
  const c = rampTarget(clip);
4401
5073
  const t = at - c.start;
4402
- if (t < 0 || t > c.duration) throw new Error(`${_chunkVKF3PWKRcjs.formatVideoSeconds.call(void 0, at)} is outside clip "${clip}" (${_chunkVKF3PWKRcjs.formatVideoSeconds.call(void 0, c.start)}\u2013${_chunkVKF3PWKRcjs.formatVideoSeconds.call(void 0, _chunkVKF3PWKRcjs.videoClipEnd.call(void 0, c))}); pick a time inside it.`);
4403
- return apply(_chunkVKF3PWKRcjs.videoFreezeFrameEdit.call(void 0, c, t, duration, _nullishCoalesce(ripple, () => ( true))), "Freeze frame", (r) => rampOut(r, clip));
5074
+ if (t < 0 || t > c.duration) throw new Error(`${_chunk3OSXHLITcjs.formatVideoSeconds.call(void 0, at)} is outside clip "${clip}" (${_chunk3OSXHLITcjs.formatVideoSeconds.call(void 0, c.start)}\u2013${_chunk3OSXHLITcjs.formatVideoSeconds.call(void 0, _chunk3OSXHLITcjs.videoClipEnd.call(void 0, c))}); pick a time inside it.`);
5075
+ return apply(_chunk3OSXHLITcjs.videoFreezeFrameEdit.call(void 0, c, t, duration, _nullishCoalesce(ripple, () => ( true))), "Freeze frame", (r) => rampOut(r, clip));
4404
5076
  }
4405
5077
  };
4406
5078
  const removeRamp = {
@@ -4573,7 +5245,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
4573
5245
  let from;
4574
5246
  let to;
4575
5247
  if (args.clip !== void 0) {
4576
- const found = _chunkVKF3PWKRcjs.findVideoClip.call(void 0, p, args.clip);
5248
+ const found = _chunk3OSXHLITcjs.findVideoClip.call(void 0, p, args.clip);
4577
5249
  if (!found) throw new Error(`There is no clip "${args.clip}"; get_video_project lists the clips.`);
4578
5250
  const c = found.clip;
4579
5251
  if (c.type === "sequence") throw new Error(`Clip "${c.id}" is a compound clip; its sound is the mix of its sequence "${c.sequence}", so read the levels of the media in it (list_video_sequences lists its clips and assets).`);
@@ -4583,7 +5255,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
4583
5255
  toSource = (t) => (_nullishCoalesce(c.in, () => ( 0))) + (t - c.start) * speed;
4584
5256
  toOut = (s) => c.start + (s - (_nullishCoalesce(c.in, () => ( 0)))) / speed;
4585
5257
  from = Math.max(c.start, _nullishCoalesce(args.from, () => ( c.start)));
4586
- to = Math.min(_chunkVKF3PWKRcjs.videoClipEnd.call(void 0, c), _nullishCoalesce(args.to, () => ( _chunkVKF3PWKRcjs.videoClipEnd.call(void 0, c))));
5258
+ to = Math.min(_chunk3OSXHLITcjs.videoClipEnd.call(void 0, c), _nullishCoalesce(args.to, () => ( _chunk3OSXHLITcjs.videoClipEnd.call(void 0, c))));
4587
5259
  } else {
4588
5260
  asset = p.assets.find((a) => a.id === args.asset);
4589
5261
  if (!asset) throw new Error(`There is no asset "${args.asset}"; the assets are ${p.assets.map((a) => `"${a.id}"`).join(", ") || "none"}.`);
@@ -4595,7 +5267,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
4595
5267
  if (!(to > from)) throw new Error(`The stretch to read is empty (${from}\u2013${to} ms); "to" must be after "from"${args.clip !== void 0 ? " and both inside the clip" : asset.duration === void 0 ? ', and the asset has no duration, so pass "to"' : ""}.`);
4596
5268
  return { asset, from, to, toSource, toOut };
4597
5269
  };
4598
- const SOUND_TARGET = {
5270
+ const SOUND_TARGET2 = {
4599
5271
  clip: { ...CLIP_ID, description: "A media clip: read the stretch of source it plays, with times on the timeline. Pass this or asset." },
4600
5272
  asset: { ...ASSET_ID, description: "A video or audio asset: read its source, with times in the source. Pass this or clip." },
4601
5273
  from: MS("Start of the stretch (default: the clip's start, or 0 for an asset)"),
@@ -4606,7 +5278,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
4606
5278
  name: "get_video_audio_levels",
4607
5279
  title: "Read sound levels",
4608
5280
  description: 'Measures how loud the source sound is over a clip or an asset, without changing anything: the peak level in dBFS (0 = full scale, -100 = silence) for each of "buckets" equal stretches, plus the loudest peak. Levels are the recording as it is, before clip volume and fades. Use it to find where speech or a beat starts, or to judge whether a clip is too loud. The first call for an asset decodes its file, which can take a moment.',
4609
- inputSchema: object({ ...SOUND_TARGET, buckets: { type: "integer", minimum: 1, maximum: 500, description: "How many equal stretches to measure (default 50)." } }),
5281
+ inputSchema: object({ ...SOUND_TARGET2, buckets: { type: "integer", minimum: 1, maximum: 500, description: "How many equal stretches to measure (default 50)." } }),
4610
5282
  annotations: { title: "Read sound levels", ...SOUND_ANNOTATIONS },
4611
5283
  handler: async (args) => {
4612
5284
  const r = soundRange(args);
@@ -4620,7 +5292,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
4620
5292
  title: "Find silences",
4621
5293
  description: "Lists the stretches of a clip or an asset whose source sound stays below a level for at least a minimum length, as { start, end } ms (timeline times for a clip, so they can go straight to extract_video_range or split_video_at; source times for an asset). Use it to cut pauses, dead air or the gaps between takes. The first call for an asset decodes its file.",
4622
5294
  inputSchema: object({
4623
- ...SOUND_TARGET,
5295
+ ...SOUND_TARGET2,
4624
5296
  thresholdDb: { type: "number", minimum: -100, maximum: 0, description: "Quieter than this peak level counts as silence, in dBFS (default -40; -50 for a quiet room, -30 over background noise)." },
4625
5297
  minDuration: { type: "number", exclusiveMinimum: 0, description: "Shortest silence to report, in ms (default 300)." }
4626
5298
  }),
@@ -4664,15 +5336,174 @@ function createVideoEditorToolProvider(controller, options = {}) {
4664
5336
  annotations: { title: "Measure a frame with scopes", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
4665
5337
  handler: async ({ time, bins, columns }) => {
4666
5338
  const p = project();
4667
- const end = _chunkVKF3PWKRcjs.videoProjectDuration.call(void 0, p);
5339
+ const end = _chunk3OSXHLITcjs.videoProjectDuration.call(void 0, p);
4668
5340
  const t = _nullishCoalesce(time, () => ( controller.getState().time));
4669
5341
  if (time !== void 0 && time > end) throw new Error(`Time ${time} ms is past the end of the project (${end} ms).`);
4670
5342
  const pixels = await pixelsAt(t);
4671
- return { time: t, timecode: _chunkVKF3PWKRcjs.formatVideoTimecode.call(void 0, t, p.fps), width: pixels.width, height: pixels.height, ...summarizeVideoScopes(pixels, { bins, columns }) };
5343
+ return { time: t, timecode: _chunk3OSXHLITcjs.formatVideoTimecode.call(void 0, t, p.fps), width: pixels.width, height: pixels.height, ...summarizeVideoScopes(pixels, { bins, columns }) };
5344
+ }
5345
+ };
5346
+ const MASK_FIELDS = {
5347
+ shape: { type: "string", enum: [..._chunk3OSXHLITcjs.VIDEO_MASK_SHAPES], description: "rectangle (with corner_radius), ellipse, linear (a straight split: the side above the line at rotation 0 shows) or polygon (points)." },
5348
+ x: { type: "number", description: "Centre across the clip's picture, 0 (left edge) to 1 (right edge); default 0.5." },
5349
+ y: { type: "number", description: "Centre down the clip's picture, 0 (top) to 1 (bottom); default 0.5." },
5350
+ width: { type: "number", minimum: 0, description: "Width as a fraction of the picture's width (default 0.5). Unused by linear." },
5351
+ height: { type: "number", minimum: 0, description: "Height as a fraction of the picture's height (default 0.5). Unused by linear." },
5352
+ rotation: { type: "number", description: "Degrees clockwise (default 0)." },
5353
+ feather: { type: "number", minimum: 0, description: "Soft edge width in frame pixels (default 0 = hard edge)." },
5354
+ corner_radius: { type: "number", minimum: 0, maximum: 1, description: "For rectangle: corner rounding as a fraction of half the shorter side (default 0)." },
5355
+ inverted: { type: "boolean", description: "Show the outside of the shape instead of the inside (default false)." },
5356
+ points: { type: "array", minItems: 3, maxItems: 200, items: object({ x: { type: "number", description: "Across the picture, 0\u20131." }, y: { type: "number", description: "Down the picture, 0\u20131." } }, ["x", "y"], "A corner of the polygon."), description: "For polygon (required): its corners in order, as fractions of the clip's picture." }
5357
+ };
5358
+ const addAdjustment = {
5359
+ name: "add_video_adjustment_clip",
5360
+ title: "Add an adjustment clip",
5361
+ description: 'Adds an adjustment clip to a video track (one undo step): it has no picture, but while it plays its grade (and any effects added later with apply_effect, and a mask from set_video_clip_mask) change the composite of the pictures in its scope: "underneath" (every video track below its own, the default), "all" (the whole frame) or "track" (only target_track). Put it on a track above what it should change, usually a new top track. Give the look as "grade" and/or "preset" (a named look at "amount"). Returns the clip (id, start, duration, end, scope).',
5362
+ inputSchema: object(
5363
+ {
5364
+ track: { ...TRACK_ID, description: 'A video track to put it on; tracks below it are "underneath".' },
5365
+ start: MS("Where it starts on the timeline (default: the end of the track)"),
5366
+ duration: { type: "number", exclusiveMinimum: 0, description: "How long it lasts, in ms (default 3000)." },
5367
+ scope: { type: "string", enum: [..._chunk3OSXHLITcjs.VIDEO_ADJUSTMENT_SCOPES], description: "Which pictures it changes: underneath (default), all or track." },
5368
+ target_track: { ...TRACK_ID, description: 'For scope "track" (required then): the track whose pictures it changes.' },
5369
+ grade: GRADE,
5370
+ preset: { type: "string", enum: [..._chunk3OSXHLITcjs.VIDEO_GRADE_PRESET_NAMES], description: "A named look to start from (grade settings given override it)." },
5371
+ amount: { type: "number", minimum: 0, maximum: 1, description: "Strength of the preset, 0\u20131 (default 1)." },
5372
+ name: { type: "string", minLength: 1, description: 'A label for the clip (e.g. "Night look").' },
5373
+ id: { type: "string", minLength: 1, description: 'Your own id for the clip (default: a fresh one such as "c7").' },
5374
+ mode: MODE
5375
+ },
5376
+ ["track"]
5377
+ ),
5378
+ annotations: { title: "Add an adjustment clip", ...WRITE },
5379
+ handler: ({ track, start, duration, scope, target_track, grade, preset, amount, name, id, mode }) => {
5380
+ const t = project().tracks.find((x) => x.id === track);
5381
+ if (t && t.kind !== "video") throw new Error(`Track "${track}" is an audio track; adjustment clips change pictures, so put it on a video track.`);
5382
+ if (scope === "track" && !target_track) throw new Error('Scope "track" needs target_track: the id of the track whose pictures it changes.');
5383
+ if (target_track !== void 0 && scope !== "track") throw new Error('target_track is only for scope "track"; leave it out or set scope to "track".');
5384
+ const look = { ...preset ? _chunk3OSXHLITcjs.videoGradePreset.call(void 0, preset, _nullishCoalesce(amount, () => ( 1))) : {}, ..._nullishCoalesce(grade, () => ( {})) };
5385
+ const clip = { type: "adjustment", ...id ? { id } : {}, ...name ? { name } : {}, ...start !== void 0 ? { start } : {}, duration: _nullishCoalesce(duration, () => ( 3e3)), ...scope ? { scope } : {}, ...target_track ? { targetTrack: target_track } : {}, ...Object.keys(look).length ? { grade: look } : {} };
5386
+ return apply({ op: "add_clip", track, clip, mode }, "Add adjustment clip", (r) => ({ clip: clipOut(r.project, r.outcomes[0].clip), ...placed(r.outcomes[0]), ...Object.keys(look).length ? {} : { hint: "It changes nothing yet: give it a grade (update_video_clip), effects (apply_effect) or a mask." } }));
5387
+ }
5388
+ };
5389
+ const maskTarget = (id) => {
5390
+ const at = _chunk3OSXHLITcjs.findVideoClip.call(void 0, project(), id);
5391
+ if (!at) throw new Error(`There is no clip "${id}"; get_video_project lists the clips.`);
5392
+ if (at.track.kind !== "video") throw new Error(`Clip "${id}" is on audio track "${at.track.id}"; masks cut pictures, so pass a clip on a video track.`);
5393
+ return at;
5394
+ };
5395
+ const setMask = {
5396
+ name: "set_video_clip_mask",
5397
+ title: "Mask a clip",
5398
+ description: `Cuts a clip's picture to a shape (one undo step): only the inside shows, or the outside when inverted, with a soft edge of "feather" pixels. Positions are fractions of the clip's placed picture, so the mask follows the clip's transform. On an adjustment clip it limits the adjustment to the shape (e.g. brighten a face). Replaces any mask the clip had. Returns the clip with its mask. ffmpeg exports need rendered frames for masks.`,
5399
+ inputSchema: object({ clip: CLIP_ID, ...MASK_FIELDS }, ["clip", "shape"]),
5400
+ annotations: { title: "Mask a clip", ...WRITE_IDEMPOTENT },
5401
+ handler: ({ clip, corner_radius, ...fields }) => {
5402
+ maskTarget(clip);
5403
+ if (fields.shape === "polygon" && !fields.points) throw new Error('A polygon mask needs "points": at least 3 { x, y } corners as fractions of the picture.');
5404
+ if (fields.shape !== "polygon" && fields.points) throw new Error('"points" is only for shape "polygon".');
5405
+ if (corner_radius !== void 0 && fields.shape !== "rectangle") throw new Error('"corner_radius" is only for shape "rectangle".');
5406
+ const mask = { ...fields, ...corner_radius !== void 0 ? { cornerRadius: corner_radius } : {} };
5407
+ return apply({ op: "update_clip", clip, set: { mask } }, "Mask clip", (r) => ({ clip: clipOut(r.project, clip) }));
5408
+ }
5409
+ };
5410
+ const removeMask = {
5411
+ name: "remove_video_clip_mask",
5412
+ title: "Remove a clip's mask",
5413
+ description: "Takes a clip's mask off, so its whole picture shows again (one undo step). Fails if it has none. Returns the clip.",
5414
+ inputSchema: object({ clip: CLIP_ID }, ["clip"]),
5415
+ annotations: { title: "Remove a clip's mask", ...DESTRUCTIVE },
5416
+ handler: ({ clip }) => {
5417
+ const at = maskTarget(clip);
5418
+ if (!at.clip.mask) throw new Error(`Clip "${clip}" has no mask.`);
5419
+ return apply({ op: "update_clip", clip, remove: ["mask"] }, "Remove mask", (r) => ({ clip: clipOut(r.project, clip) }));
5420
+ }
5421
+ };
5422
+ const setSkin = {
5423
+ name: "set_video_skin_protection",
5424
+ title: "Protect skin tones from a LUT",
5425
+ description: `Holds a LUT back on skin tones for clips that already have one (one undo step): at "amount" 1 skin keeps its colour while the rest of the picture takes the full look; 0 turns the protection off. Skin is found by colour, so warm orange or beige objects are protected too. Fails for clips without a LUT (add one with set_video_lut first). Returns each clip's LUT setting.`,
5426
+ inputSchema: object({ clips: { type: "array", items: CLIP_ID, minItems: 1, uniqueItems: true, description: "Clips with a LUT." }, amount: { ...SKIN, description: "How much of the LUT to hold back on skin, 0 (off) to 1 (all of it)." } }, ["clips", "amount"]),
5427
+ annotations: { title: "Protect skin tones from a LUT", ...WRITE_IDEMPOTENT },
5428
+ handler: ({ clips, amount }) => {
5429
+ gradeTargets(clips);
5430
+ const edits = [];
5431
+ for (const id of clips) {
5432
+ const clip = _chunk3OSXHLITcjs.findVideoClip.call(void 0, project(), id).clip;
5433
+ const lut = _optionalChain([clip, 'access', _172 => _172.grade, 'optionalAccess', _173 => _173.lut]);
5434
+ if (!lut) throw new Error(`Clip "${id}" has no LUT to protect skin from; add one with set_video_lut first.`);
5435
+ const { preserveSkinTones: _p, ...rest } = lut;
5436
+ edits.push({ op: "update_clip", clip: id, set: { grade: { ...clip.grade, lut: amount > 0 ? { ...rest, preserveSkinTones: amount } : rest } } });
5437
+ }
5438
+ return apply(edits, amount > 0 ? "Protect skin tones" : "Stop protecting skin tones", (r) => ({ clips: clips.map((id) => ({ id, lut: _nullishCoalesce(_optionalChain([_chunk3OSXHLITcjs.findVideoClip.call(void 0, r.project, id), 'optionalAccess', _174 => _174.clip, 'access', _175 => _175.grade, 'optionalAccess', _176 => _176.lut]), () => ( null)) })) }));
5439
+ }
5440
+ };
5441
+ const planOut = (plan) => ({
5442
+ id: plan.id,
5443
+ title: plan.title,
5444
+ ...plan.description ? { description: plan.description } : {},
5445
+ ...plan.source ? { source: plan.source } : {},
5446
+ summary: { ...plan.summary, newIssues: issueSummary(plan.summary.newIssues) },
5447
+ ...plan.ranges ? { ranges: plan.ranges.map((r) => ({ ...r, from: round(r.from), to: round(r.to) })) } : {},
5448
+ edits: plan.edits.length,
5449
+ stale: controller.isPlanStale(plan.id),
5450
+ text: describeVideoEditPlan(plan)
5451
+ });
5452
+ const PLAN_ID = { type: "string", minLength: 1, description: 'A plan id from plan_video_edits, a plan_* tool or list_video_edit_plans (e.g. "plan1").' };
5453
+ const planEdits = {
5454
+ name: "plan_video_edits",
5455
+ title: "Propose edits for review",
5456
+ description: "Works out a batch of edits without applying them and keeps it as a plan for review: the project does not change. The plan says what it would do (length before and after, clips added, removed and changed, problems it would add) and shows in the editor for the user to accept or discard. Use it for anything the user should see before it happens (a rough cut, a cleanup, a re-time). Fails, keeping nothing, if the edits do not apply. Apply it with apply_video_edit_plan. Returns the plan.",
5457
+ inputSchema: object(
5458
+ {
5459
+ title: { type: "string", minLength: 1, maxLength: 120, description: 'What the plan does, shown to the user and used as the undo label (e.g. "Tighten the intro").' },
5460
+ description: { type: "string", maxLength: 2e3, description: "Why, and anything the reviewer should check." },
5461
+ ops: { type: "array", items: EDIT_OP, minItems: 1, maxItems: 500, description: "The edits, in order, as for edit_video." },
5462
+ ranges: { type: "array", maxItems: 500, items: object({ from: MS("Start, on the timeline"), to: MS("End, on the timeline"), text: { type: "string", description: "What is said there." }, reason: { type: "string", description: 'Why it is in the plan (e.g. "filler").' } }, ["from", "to"], "A stretch the plan cuts or changes."), description: "The stretches it cuts or changes, for the review view (optional)." }
5463
+ },
5464
+ ["title", "ops"]
5465
+ ),
5466
+ annotations: { title: "Propose edits for review", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
5467
+ handler: ({ title, description, ops, ranges }) => {
5468
+ if (ops.some((o) => o.op === "replace_project")) throw new Error("A plan cannot replace the whole project; use the individual ops.");
5469
+ const r = controller.proposePlan({ title, description, edits: ops, ranges, source: "agent" });
5470
+ if (!r.ok) throw new Error(r.error);
5471
+ return { plan: planOut(r.plan), hint: `Nothing changed yet. Ask the user to review it, then call apply_video_edit_plan with plan "${r.plan.id}" (or discard_video_edit_plan).` };
5472
+ }
5473
+ };
5474
+ const listPlans = {
5475
+ name: "list_video_edit_plans",
5476
+ title: "List proposed edit plans",
5477
+ description: "Lists the plans waiting for review, oldest first: id, title, summary (length change, clips added, removed and changed, new problems), the stretches they cut, and whether the project changed since each was made (stale: it is checked again when applied).",
5478
+ inputSchema: object({}),
5479
+ annotations: { title: "List proposed edit plans", ...READ },
5480
+ handler: () => ({ plans: controller.getState().plans.map(planOut) })
5481
+ };
5482
+ const applyPlan = {
5483
+ name: "apply_video_edit_plan",
5484
+ title: "Apply an edit plan",
5485
+ description: "Applies a proposed plan as one undo step (labelled with its title) and closes it. A plan made before the project changed is checked again first and refused, changing nothing, if its edits no longer apply. Returns what it did (each op's outcome), the new length and issues, and whether it was stale.",
5486
+ inputSchema: object({ plan: PLAN_ID }, ["plan"]),
5487
+ annotations: { title: "Apply an edit plan", ...DESTRUCTIVE },
5488
+ handler: ({ plan }) => {
5489
+ const r = controller.applyPlan(plan);
5490
+ if (!r.ok) throw new Error(r.error);
5491
+ return { plan: r.plan.id, title: r.plan.title, stale: r.stale, outcomes: r.outcomes, duration: _chunk3OSXHLITcjs.videoProjectDuration.call(void 0, r.project), issues: issueSummary(r.issues) };
5492
+ }
5493
+ };
5494
+ const discardPlan = {
5495
+ name: "discard_video_edit_plan",
5496
+ title: "Discard an edit plan",
5497
+ description: "Closes a proposed plan without applying it; the project is unchanged. Returns the plans still open.",
5498
+ inputSchema: object({ plan: PLAN_ID }, ["plan"]),
5499
+ annotations: { title: "Discard an edit plan", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
5500
+ handler: ({ plan }) => {
5501
+ if (!controller.discardPlan(plan)) throw new Error(`There is no open plan "${plan}"; the open plans are ${controller.getState().plans.map((p) => `"${p.id}"`).join(", ") || "none"}.`);
5502
+ return { discarded: plan, open: controller.getState().plans.map((p) => p.id) };
4672
5503
  }
4673
5504
  };
4674
- const reads = [getProject, listSequences, getFrame, getTranscript, findText, findFillers, findPauses, findGaps, checkProject, renderFrame, getScopes, previewCache, planProxies, exportFfmpeg, exportLoudnessMeasure, exportEdl, audioLevels, findSilences, seek, setPreviewScale];
4675
- const writes = [setSettings, addAsset, updateAsset, removeAsset, setProxies, addTrack, updateTrack, removeTrack, moveTrack, addClip, moveClip, trimClip, splitClip, deleteClips, updateClip, setTransition, setSpeedRamp, addFreeze, removeRamp, setTranscript, removeRanges, applyGradePreset, copyGrade, setLut, removeLut, setKeyframes, removeKeyframes, slipClip, rollEdit, slideClip, liftRange, extractRange, splitAt, closeGaps, addMarker, removeMarker, editVideo, undo, redo];
5505
+ const reads = [getProject, listPlans, listSequences, getFrame, getTranscript, findText, findFillers, findPauses, findGaps, checkProject, renderFrame, getScopes, previewCache, planProxies, exportFfmpeg, exportLoudnessMeasure, exportEdl, audioLevels, findSilences, seek, setPreviewScale];
5506
+ const writes = [setSettings, addAsset, updateAsset, removeAsset, setProxies, addTrack, updateTrack, removeTrack, moveTrack, addClip, moveClip, trimClip, splitClip, deleteClips, updateClip, setTransition, setSpeedRamp, addFreeze, removeRamp, setTranscript, removeRanges, applyGradePreset, copyGrade, setLut, removeLut, setSkin, addAdjustment, setMask, removeMask, planEdits, applyPlan, discardPlan, setKeyframes, removeKeyframes, slipClip, rollEdit, slideClip, liftRange, extractRange, splitAt, closeGaps, addMarker, removeMarker, editVideo, undo, redo];
4676
5507
  return withVideoPlayback({
4677
5508
  id: _nullishCoalesce(options.id, () => ( "video")),
4678
5509
  namespace: options.namespace,
@@ -4698,7 +5529,7 @@ function videoClipAppliedEffects(clip) {
4698
5529
  const out = [];
4699
5530
  const ref = (r) => ({ effect: r.effect, ...r.name ? { name: r.name } : {}, ...r.provider ? { provider: r.provider } : {} });
4700
5531
  if (clip.type === "effect") out.push({ id: "generator", ...ref(clip), role: "effect clip", enabled: true, values: _nullishCoalesce(clip.values, () => ( {})) });
4701
- if (_optionalChain([clip, 'access', _153 => _153.transition, 'optionalAccess', _154 => _154.type]) === "effect" && clip.transition.effect) out.push({ id: "transition", ...ref(clip.transition.effect), role: "transition", enabled: true, values: { ..._nullishCoalesce(clip.transition.effect.values, () => ( {})) }, duration: clip.transition.duration });
5532
+ if (_optionalChain([clip, 'access', _177 => _177.transition, 'optionalAccess', _178 => _178.type]) === "effect" && clip.transition.effect) out.push({ id: "transition", ...ref(clip.transition.effect), role: "transition", enabled: true, values: { ..._nullishCoalesce(clip.transition.effect.values, () => ( {})) }, duration: clip.transition.duration });
4702
5533
  (_nullishCoalesce(clip.effects, () => ( []))).forEach((e, index) => out.push({ id: e.id, ...ref(e), role: "clip effect", enabled: e.enabled !== false, ...e.opacity !== void 0 ? { opacity: e.opacity } : {}, values: _nullishCoalesce(e.values, () => ( {})), index }));
4703
5534
  return out;
4704
5535
  }
@@ -4711,7 +5542,7 @@ var KEYS2 = {
4711
5542
  properties: {
4712
5543
  t: { type: "number", description: "Clip-relative time in ms (0 = the clip's start)." },
4713
5544
  v: { type: "number", description: "The control's value at t, within its range from describe_effect." },
4714
- ease: { anyOf: [{ type: "string", enum: [..._chunkVKF3PWKRcjs.VIDEO_EASE_NAMES] }, { type: "array", items: { type: "number" }, minItems: 4, maxItems: 4 }], description: "How the value moves to the next key: linear (default), hold, ease-in, ease-out, ease-in-out, or CSS cubic-bezier [x1, y1, x2, y2]." }
5545
+ ease: { anyOf: [{ type: "string", enum: [..._chunk3OSXHLITcjs.VIDEO_EASE_NAMES] }, { type: "array", items: { type: "number" }, minItems: 4, maxItems: 4 }], description: "How the value moves to the next key: linear (default), hold, ease-in, ease-out, ease-in-out, or CSS cubic-bezier [x1, y1, x2, y2]." }
4715
5546
  },
4716
5547
  required: ["t", "v"],
4717
5548
  additionalProperties: false
@@ -4725,7 +5556,7 @@ function createVideoEffectsToolProvider(controller, options = {}) {
4725
5556
  return controller.readOnlyReason();
4726
5557
  };
4727
5558
  const clipOf = (id) => {
4728
- const at = _chunkVKF3PWKRcjs.findVideoClip.call(void 0, controller.getProject(), id);
5559
+ const at = _chunk3OSXHLITcjs.findVideoClip.call(void 0, controller.getProject(), id);
4729
5560
  if (!at) {
4730
5561
  const ids = controller.getProject().tracks.flatMap((t) => t.clips.map((c) => c.id));
4731
5562
  throw new Error(`There is no clip "${id}"; the clips are ${ids.map((c) => `"${c}"`).join(", ") || "none"} (get_video_project lists them).`);
@@ -4735,7 +5566,7 @@ function createVideoEffectsToolProvider(controller, options = {}) {
4735
5566
  const result = (r, target, instance, extra = {}) => {
4736
5567
  if (!r.ok) throw new Error(r.error);
4737
5568
  const effects = videoClipAppliedEffects(clipOf(target));
4738
- const effect = _nullishCoalesce(effects.find((e) => e.id === (_nullishCoalesce(instance, () => ( _optionalChain([r, 'access', _155 => _155.effect, 'optionalAccess', _156 => _156.id]))))), () => ( (r.effect ? { id: r.effect.id, effect: r.effect.effect, role: "clip effect", enabled: r.effect.enabled !== false, values: _nullishCoalesce(r.effect.values, () => ( {})) } : effects[0])));
5569
+ const effect = _nullishCoalesce(effects.find((e) => e.id === (_nullishCoalesce(instance, () => ( _optionalChain([r, 'access', _179 => _179.effect, 'optionalAccess', _180 => _180.id]))))), () => ( (r.effect ? { id: r.effect.id, effect: r.effect.effect, role: "clip effect", enabled: r.effect.enabled !== false, values: _nullishCoalesce(r.effect.values, () => ( {})) } : effects[0])));
4739
5570
  return { target, effect, effects, ...extra };
4740
5571
  };
4741
5572
  return withVideoPlayback(_chunkRUZQMYXHcjs.createEffectToolProvider.call(void 0, {
@@ -4796,7 +5627,7 @@ function createVideoEffectsToolProvider(controller, options = {}) {
4796
5627
  duration: { type: "number", exclusiveMinimum: 0, description: "Length in ms (default: the effect's own length, else 5000)." },
4797
5628
  values: ctx.valuesSchema("Control values over the effect's defaults", true),
4798
5629
  name: { type: "string", minLength: 1, description: "A label for the clip (default: the effect's name)." },
4799
- mode: { type: "string", enum: [..._chunkVKF3PWKRcjs.VIDEO_PLACEMENT_MODES], description: "What to do with clips already there: refuse (default), overwrite or insert." }
5630
+ mode: { type: "string", enum: [..._chunk3OSXHLITcjs.VIDEO_PLACEMENT_MODES], description: "What to do with clips already there: refuse (default), overwrite or insert." }
4800
5631
  },
4801
5632
  required: ["track", "effect"],
4802
5633
  additionalProperties: false
@@ -4855,8 +5686,8 @@ function createVideoEffectsToolProvider(controller, options = {}) {
4855
5686
  if (args.effect === "generator") {
4856
5687
  const clip = clipOf(args.clip);
4857
5688
  if (clip.type !== "effect") throw new Error(`Clip "${args.clip}" is not an effect clip; give the id of one of its effects (list_applied_effects).`);
4858
- const was = _optionalChain([clip, 'access', _157 => _157.values, 'optionalAccess', _158 => _158[args.control]]);
4859
- const plain = keys === null ? _chunkVKF3PWKRcjs.isVideoKeyframes.call(void 0, was) ? _optionalChain([was, 'access', _159 => _159.keys, 'access', _160 => _160[0], 'optionalAccess', _161 => _161.v]) : was : { keys: [...keys].sort((a, b) => a.t - b.t) };
5689
+ const was = _optionalChain([clip, 'access', _181 => _181.values, 'optionalAccess', _182 => _182[args.control]]);
5690
+ const plain = keys === null ? _chunk3OSXHLITcjs.isVideoKeyframes.call(void 0, was) ? _optionalChain([was, 'access', _183 => _183.keys, 'access', _184 => _184[0], 'optionalAccess', _185 => _185.v]) : was : { keys: [...keys].sort((a, b) => a.t - b.t) };
4860
5691
  if (plain === void 0) throw new Error(`"${args.control}" has no value to keep; give keys instead.`);
4861
5692
  r = await controller.updateEffectValues(args.clip, "generator", { [args.control]: plain });
4862
5693
  } else r = await controller.setClipEffectKeyframes(args.clip, args.effect, args.control, keys);
@@ -4868,6 +5699,456 @@ function createVideoEffectsToolProvider(controller, options = {}) {
4868
5699
  }), options.playbackSurface);
4869
5700
  }
4870
5701
 
5702
+ // src/video-editor/dialogue-tools.ts
5703
+ var round2 = (ms) => Math.round(ms * 10) / 10;
5704
+ var object2 = (properties, required = []) => ({ type: "object", properties, ...required.length ? { required } : {}, additionalProperties: false });
5705
+ var FIND_FIELDS = {
5706
+ track: { type: "string", minLength: 1, description: "Track whose words to read (default: the first audio track with a transcribed clip, else the first video track with one)." },
5707
+ kinds: { type: "array", minItems: 1, uniqueItems: true, items: { type: "string", enum: [...VIDEO_DISFLUENCY_KINDS] }, description: 'What to look for: filler (um, uh, you know\u2026), pause (long gaps between words), repetition (stutters such as "I I think"). Default all three.' },
5708
+ fillers: { type: "array", minItems: 1, items: { type: "string", minLength: 1 }, description: `Words or phrases counted as fillers instead of the defaults (${VIDEO_DEFAULT_FILLERS.join(", ")}).` },
5709
+ min_pause: { type: "number", exclusiveMinimum: 0, description: "Shortest gap between words that counts as a pause, in ms (default 700; 600 for brisk talking heads)." },
5710
+ keep: { type: "number", minimum: 0, maximum: 1e3, description: "Breath kept either side of a pause cut, in ms (default 150; 150\u2013250 sounds natural)." },
5711
+ max_phrase: { type: "integer", minimum: 1, maximum: 6, description: "Longest phrase checked for a repeat, in words (default 3)." }
5712
+ };
5713
+ function createVideoDialogueToolProvider(controller, options = {}) {
5714
+ const readOnlyReason = () => {
5715
+ const r = typeof options.readOnly === "function" ? options.readOnly() : options.readOnly;
5716
+ if (r) return typeof r === "string" ? r : "This video project is read-only.";
5717
+ return controller.readOnlyReason();
5718
+ };
5719
+ const find = (args) => {
5720
+ const project = controller.getProject();
5721
+ const words = videoTimelineTranscript(project, { track: args.track });
5722
+ if (!words.length) throw new Error(args.track ? `No clip on track "${args.track}" plays a transcribed asset; attach one with set_video_transcript.` : "No clip plays a transcribed asset; attach a transcript to the media with set_video_transcript first.");
5723
+ return detectVideoDisfluencies(words, {
5724
+ kinds: args.kinds,
5725
+ fillers: args.fillers,
5726
+ pauses: { fps: project.fps, ...args.min_pause !== void 0 ? { min: args.min_pause } : {}, ...args.keep !== void 0 ? { keep: args.keep } : {} },
5727
+ repetitions: args.max_phrase !== void 0 ? { maxPhrase: args.max_phrase } : void 0
5728
+ });
5729
+ };
5730
+ const out = (d) => ({ kind: d.kind, from: round2(d.from), to: round2(d.to), text: d.text, words: d.words });
5731
+ const findDisfluencies = {
5732
+ name: "find_video_disfluencies",
5733
+ title: "Find fillers, pauses and stutters",
5734
+ description: `Reads the timeline transcript and lists, in time order, every filler word, long pause and repeated word or phrase as { kind, from, to, text, words } (timeline ms; "words" are the first and last word indexes in get_video_transcript's list), with a count and total per kind. Changes nothing. Review the list (some repeats and pauses are meant), then call plan_video_dialogue_cleanup or remove_video_ranges with the ones to cut.`,
5735
+ inputSchema: object2(FIND_FIELDS),
5736
+ annotations: { title: "Find fillers, pauses and stutters", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
5737
+ handler: (args) => {
5738
+ const found = find(args);
5739
+ const totals = Object.fromEntries(VIDEO_DISFLUENCY_KINDS.map((k) => [k, { count: found.filter((d) => d.kind === k).length, time: round2(found.filter((d) => d.kind === k).reduce((n, d) => n + d.to - d.from, 0)) }]));
5740
+ const merged = mergeVideoRanges(found);
5741
+ return { disfluencies: found.map(out), totals, removable: round2(merged.reduce((n, r) => n + r.to - r.from, 0)) };
5742
+ }
5743
+ };
5744
+ const planCleanup = {
5745
+ name: "plan_video_dialogue_cleanup",
5746
+ title: "Propose a dialogue cleanup",
5747
+ description: 'Finds fillers, long pauses and stutters (as find_video_disfluencies) and proposes cutting them as one plan for review: nothing changes until apply_video_edit_plan. Pass "exclude" to keep finds the user wants (by their from time), and "margin" to keep a little breath around filler and stutter cuts. Every sync-locked track is cut with the dialogue. Returns the plan (id, title, length change, the stretches it cuts) for the user to review.',
5748
+ inputSchema: object2(
5749
+ {
5750
+ ...FIND_FIELDS,
5751
+ exclude: { type: "array", maxItems: 500, items: { type: "number", minimum: 0 }, description: 'The "from" times (ms) of finds to keep, from find_video_disfluencies.' },
5752
+ margin: { type: "number", minimum: 0, maximum: 200, description: "Ms left uncut at each end of a filler or stutter cut, so the words around it are not clipped (default 0)." },
5753
+ title: { type: "string", minLength: 1, maxLength: 120, description: `The plan's title (default: what it cleans up, e.g. "Clean up 12 fillers, 3 pauses").` }
5754
+ }
5755
+ ),
5756
+ annotations: { title: "Propose a dialogue cleanup", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
5757
+ handler: (args) => {
5758
+ const skip = new Set((_nullishCoalesce(args.exclude, () => ( []))).map((t) => Math.round(t)));
5759
+ const found = find(args).filter((d) => !skip.has(Math.round(d.from)));
5760
+ if (!found.length) throw new Error("Nothing to clean up: no fillers, long pauses or repetitions were found (or all were excluded).");
5761
+ const margin = _nullishCoalesce(args.margin, () => ( 0));
5762
+ const cuts = found.flatMap((d) => d.kind === "pause" || !margin ? [d] : videoCutsWithMargin([d], margin));
5763
+ const counts = VIDEO_DISFLUENCY_KINDS.map((k) => [k, found.filter((d) => d.kind === k).length]).filter(([, n]) => n);
5764
+ const title = _nullishCoalesce(args.title, () => ( `Clean up ${counts.map(([k, n]) => `${n} ${k}${n > 1 ? "s" : ""}`).join(", ")}`));
5765
+ const r = controller.proposePlan({ title, description: "Cuts fillers, long pauses and stutters found in the transcript, closing up every sync-locked track.", edits: videoRemoveRangesEdits(cuts), ranges: cuts.map((d) => ({ from: d.from, to: d.to, text: d.text, reason: d.kind })), source: "plan_video_dialogue_cleanup" });
5766
+ if (!r.ok) throw new Error(r.error);
5767
+ const p = r.plan;
5768
+ return { plan: { id: p.id, title: p.title, summary: p.summary, text: describeVideoEditPlan(p), cuts: cuts.length }, durationNow: _chunk3OSXHLITcjs.videoProjectDuration.call(void 0, controller.getProject()), hint: `Nothing changed yet. Show the user the plan, then call apply_video_edit_plan with plan "${p.id}" (or discard_video_edit_plan).` };
5769
+ }
5770
+ };
5771
+ return withVideoPlayback(
5772
+ {
5773
+ id: _nullishCoalesce(options.id, () => ( "video-dialogue")),
5774
+ namespace: options.namespace,
5775
+ title: "Video dialogue cleanup",
5776
+ description: "Talking-head cleanup by transcript: find fillers, long pauses and stutters, and propose cutting them as one plan for review.",
5777
+ skill: "pixerate-talking-head-edit",
5778
+ tools: () => readOnlyReason() ? [findDisfluencies] : [findDisfluencies, planCleanup],
5779
+ subscribe: (notify) => {
5780
+ let last = readOnlyReason();
5781
+ return controller.subscribe(() => {
5782
+ const now = readOnlyReason();
5783
+ if (now !== last) {
5784
+ last = now;
5785
+ notify();
5786
+ }
5787
+ });
5788
+ }
5789
+ },
5790
+ options.playbackSurface
5791
+ );
5792
+ }
5793
+
5794
+ // src/video-editor/music-tools.ts
5795
+ var round4 = (ms) => Math.round(ms * 10) / 10;
5796
+ var object3 = (properties, required = [], description) => ({ type: "object", ...description ? { description } : {}, properties, ...required.length ? { required } : {}, additionalProperties: false });
5797
+ var MS3 = (what) => ({ type: "number", minimum: 0, description: `${what}, in milliseconds.` });
5798
+ var CLIP_ID2 = { type: "string", minLength: 1, description: 'A clip id from get_video_project (e.g. "c1").' };
5799
+ var TRACK_ID2 = { type: "string", minLength: 1, description: 'A track id from get_video_project (e.g. "v1" or "a1").' };
5800
+ var SOUND_TARGET = {
5801
+ clip: { ...CLIP_ID2, description: "A media clip with sound: read the stretch of source it plays, times on the timeline. Pass this or asset." },
5802
+ asset: { type: "string", minLength: 1, description: "A video or audio asset: read its source, times in the source. Pass this or clip." },
5803
+ from: MS3("Start of the stretch (default: the clip's start, or 0 for an asset)"),
5804
+ to: MS3("End of the stretch (default: the clip's end, or the asset's duration)")
5805
+ };
5806
+ var SOUND = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true };
5807
+ function createVideoMusicToolProvider(controller, options = {}) {
5808
+ const readOnlyReason = () => {
5809
+ const r = typeof options.readOnly === "function" ? options.readOnly() : options.readOnly;
5810
+ if (r) return typeof r === "string" ? r : "This video project is read-only.";
5811
+ return controller.readOnlyReason();
5812
+ };
5813
+ const project = () => controller.getProject();
5814
+ const soundClip = (p, id) => {
5815
+ const at = _chunk3OSXHLITcjs.findVideoClip.call(void 0, p, id);
5816
+ if (!at) throw new Error(`There is no clip "${id}"; get_video_project lists the clips.`);
5817
+ const c = at.clip;
5818
+ if (c.type !== "media") throw new Error(`Clip "${id}" is a ${c.type} clip and has no sound of its own; pass a video or audio clip.`);
5819
+ const asset = p.assets.find((a) => a.id === c.asset);
5820
+ if (!asset) throw new Error(`Clip "${id}" plays a missing asset "${c.asset}".`);
5821
+ if (asset.kind === "image" || asset.hasAudio === false) throw new Error(`Clip "${id}" has no sound (asset "${asset.id}").`);
5822
+ return { clip: c, asset, track: at.track.id };
5823
+ };
5824
+ const stretch = (args) => {
5825
+ const p = project();
5826
+ if (args.clip === void 0 === (args.asset === void 0)) throw new Error('Pass either "clip" (a media clip, times on the timeline) or "asset" (times in the source), not both.');
5827
+ if (args.clip !== void 0) {
5828
+ const { clip, asset: asset2 } = soundClip(p, args.clip);
5829
+ const speed = _nullishCoalesce(clip.speed, () => ( 1));
5830
+ const from2 = Math.max(clip.start, _nullishCoalesce(args.from, () => ( clip.start)));
5831
+ const to2 = Math.min(_chunk3OSXHLITcjs.videoClipEnd.call(void 0, clip), _nullishCoalesce(args.to, () => ( _chunk3OSXHLITcjs.videoClipEnd.call(void 0, clip))));
5832
+ if (!(to2 > from2)) throw new Error(`The stretch is empty (${from2}\u2013${to2} ms); "from" and "to" must be inside the clip (${clip.start}\u2013${_chunk3OSXHLITcjs.videoClipEnd.call(void 0, clip)} ms) with "to" after "from".`);
5833
+ return { asset: asset2, from: (_nullishCoalesce(clip.in, () => ( 0))) + (from2 - clip.start) * speed, to: (_nullishCoalesce(clip.in, () => ( 0))) + (to2 - clip.start) * speed, out: (s) => clip.start + (s - (_nullishCoalesce(clip.in, () => ( 0)))) / speed };
5834
+ }
5835
+ const asset = p.assets.find((a) => a.id === args.asset);
5836
+ if (!asset) throw new Error(`There is no asset "${args.asset}"; the assets are ${p.assets.map((a) => `"${a.id}"`).join(", ") || "none"}.`);
5837
+ if (asset.kind === "image" || asset.hasAudio === false) throw new Error(`Asset "${asset.id}" has no sound.`);
5838
+ const from = _nullishCoalesce(args.from, () => ( 0));
5839
+ const to = _nullishCoalesce(_nullishCoalesce(args.to, () => ( asset.duration)), () => ( 0));
5840
+ if (!(to > from)) throw new Error(`The stretch is empty (${from}\u2013${to} ms)${asset.duration === void 0 ? '; the asset has no duration, so pass "to"' : ""}.`);
5841
+ return { asset, from, to, out: (s) => s };
5842
+ };
5843
+ const measureLoudness = {
5844
+ name: "measure_video_loudness",
5845
+ title: "Measure loudness",
5846
+ description: "Measures how loud the source sound of a clip or an asset is, as streaming services and broadcasters do (ITU-R BS.1770, gated): integrated loudness in LUFS, the sample peak in dBFS, and the gain that would bring it to -14 LUFS (streaming) without the peak passing -1 dBFS. Before clip volume and fades. Changes nothing. The first call for an asset decodes its file. Use normalize_video_loudness to apply the gain.",
5847
+ inputSchema: object3(SOUND_TARGET),
5848
+ annotations: { title: "Measure loudness", ...SOUND },
5849
+ handler: async (args) => {
5850
+ const r = stretch(args);
5851
+ const peaks = await controller.audioPeaks.load(r.asset);
5852
+ const lufs2 = videoAudioLoudness(peaks, r.from, r.to);
5853
+ const peakDb = Math.max(...videoAudioLevels(peaks, r.from, r.to, 50).map((l) => l.peakDb));
5854
+ return { asset: r.asset.id, lufs: lufs2, peakDb, ...lufs2 === null ? { note: "The stretch is silent (everything is below the -70 LUFS gate)." } : { toStreaming: videoLoudnessGain(lufs2, -14, { peakDb }) } };
5855
+ }
5856
+ };
5857
+ const normalizeLoudness = {
5858
+ name: "normalize_video_loudness",
5859
+ title: "Normalise loudness",
5860
+ description: "Sets each clip's volume so its sound plays at a target loudness (default -14 LUFS, streaming; -16 podcasts, -23 EBU R128 broadcast), keeping its peak under the ceiling and the gain at most 4\xD7 (one undo step). Measures each clip's own stretch of source first; silent clips are skipped. Refuses clips with volume keyframes (a constant gain would replace them) unless replace is true. Returns each clip's measured loudness, the gain set (linear and dB) and whether a cap held it short of the target.",
5861
+ inputSchema: object3(
5862
+ {
5863
+ clips: { type: "array", items: CLIP_ID2, minItems: 1, maxItems: 200, uniqueItems: true, description: "Media clips with sound." },
5864
+ target: { type: "number", minimum: -40, maximum: -5, description: "Loudness to reach, in LUFS (default -14)." },
5865
+ ceiling: { type: "number", minimum: -20, maximum: 0, description: "Highest sample peak allowed after the gain, in dBFS (default -1)." },
5866
+ replace: { type: "boolean", description: "Replace volume keyframes with the constant gain (default false: refuse)." }
5867
+ },
5868
+ ["clips"]
5869
+ ),
5870
+ annotations: { title: "Normalise loudness", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
5871
+ handler: async ({ clips, target, ceiling, replace }) => {
5872
+ const p = project();
5873
+ const targets = clips.map((id) => soundClip(p, id));
5874
+ for (const t of targets) if (_chunk3OSXHLITcjs.isVideoKeyframes.call(void 0, t.clip.volume) && !replace) throw new Error(`Clip "${t.clip.id}" has volume keyframes, which a constant gain would replace; pass replace: true, or remove them first.`);
5875
+ const results = [];
5876
+ const edits = [];
5877
+ for (const { clip, asset } of targets) {
5878
+ const peaks = await controller.audioPeaks.load(asset);
5879
+ const speed = _nullishCoalesce(clip.speed, () => ( 1));
5880
+ const from = _nullishCoalesce(clip.in, () => ( 0));
5881
+ const to = from + clip.duration * speed;
5882
+ const lufs2 = videoAudioLoudness(peaks, from, to);
5883
+ if (lufs2 === null) {
5884
+ results.push({ clip: clip.id, skipped: "silent" });
5885
+ continue;
5886
+ }
5887
+ const peakDb = Math.max(...videoAudioLevels(peaks, from, to, 50).map((l) => l.peakDb));
5888
+ const g = videoLoudnessGain(lufs2, _nullishCoalesce(target, () => ( -14)), { peakDb, ceiling });
5889
+ edits.push({ op: "update_clip", clip: clip.id, set: { volume: g.gain } });
5890
+ results.push({ clip: clip.id, lufs: lufs2, gain: g.gain, gainDb: g.gainDb, ...g.limitedBy ? { limitedBy: g.limitedBy } : {} });
5891
+ }
5892
+ if (!edits.length) throw new Error("Every clip is silent, so there is nothing to normalise.");
5893
+ const r = controller.apply(edits, "Normalise loudness");
5894
+ if (!r.ok) throw new Error(r.error);
5895
+ return { target: _nullishCoalesce(target, () => ( -14)), clips: results };
5896
+ }
5897
+ };
5898
+ const duckMusic = {
5899
+ name: "duck_video_music",
5900
+ title: "Duck music under speech",
5901
+ description: `Dips music clips under speech with volume keyframes (one undo step): before each stretch of speech the level eases down by duck_db over "attack" ms, holds, and eases back over "release" ms after it; gaps shorter than merge_gap stay ducked. Speech comes from the transcript of the dialogue track ("transcript", the default when it has one) or from where its sound is above -40 dBFS ("audio", which decodes it). Refuses music clips that already have volume keyframes unless replace is true. Returns each music clip's keys and the speech stretches used.`,
5902
+ inputSchema: object3(
5903
+ {
5904
+ music: { type: "array", items: CLIP_ID2, minItems: 1, maxItems: 100, uniqueItems: true, description: "The music clips to duck (media clips with sound)." },
5905
+ dialogue_track: { ...TRACK_ID2, description: 'Track the speech is on (default: the transcript track for "transcript"; required for "audio").' },
5906
+ speech: { type: "string", enum: ["transcript", "audio"], description: "Where speech is found: transcript words (default when transcribed) or the dialogue track's sound levels." },
5907
+ duck_db: { type: "number", minimum: -40, maximum: -1, description: "How far to dip, in dB (default -14; -12 to -18 is usual)." },
5908
+ attack: { type: "number", minimum: 0, maximum: 2e3, description: "Ms the dip takes to come in before speech (default 200)." },
5909
+ release: { type: "number", minimum: 0, maximum: 4e3, description: "Ms the music takes to come back after speech (default 400)." },
5910
+ merge_gap: { type: "number", minimum: 0, maximum: 1e4, description: "Speech gaps shorter than this stay ducked, in ms (default 600)." },
5911
+ replace: { type: "boolean", description: "Replace existing volume keyframes (default false: refuse)." }
5912
+ },
5913
+ ["music"]
5914
+ ),
5915
+ annotations: { title: "Duck music under speech", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
5916
+ handler: async (args) => {
5917
+ const p = project();
5918
+ const music = args.music.map((id) => soundClip(p, id));
5919
+ for (const m of music) if (_chunk3OSXHLITcjs.isVideoKeyframes.call(void 0, m.clip.volume) && !args.replace) throw new Error(`Music clip "${m.clip.id}" already has volume keyframes; pass replace: true to replace them, or remove them first.`);
5920
+ const transcriptTrack = videoTranscriptTrack(p, args.dialogue_track);
5921
+ const mode = _nullishCoalesce(args.speech, () => ( (transcriptTrack && videoTimelineTranscript(p, { track: transcriptTrack }).length ? "transcript" : "audio")));
5922
+ let ranges = [];
5923
+ if (mode === "transcript") {
5924
+ const words = videoTimelineTranscript(p, { track: args.dialogue_track });
5925
+ if (!words.length) throw new Error(`No transcribed speech${args.dialogue_track ? ` on track "${args.dialogue_track}"` : ""}; attach a transcript with set_video_transcript, or pass speech: "audio" with dialogue_track.`);
5926
+ ranges = videoSpeechRanges(words, 250);
5927
+ } else {
5928
+ if (!args.dialogue_track) throw new Error('speech "audio" needs dialogue_track: the track whose sound is the speech.');
5929
+ const track = p.tracks.find((t) => t.id === args.dialogue_track);
5930
+ if (!track) throw new Error(`There is no track "${args.dialogue_track}".`);
5931
+ const musicIds = new Set(music.map((m) => m.clip.id));
5932
+ for (const c of _chunk3OSXHLITcjs.sortedVideoClips.call(void 0, track)) {
5933
+ if (c.type !== "media" || musicIds.has(c.id)) continue;
5934
+ const asset = p.assets.find((a) => a.id === c.asset);
5935
+ if (!asset || asset.kind === "image" || asset.hasAudio === false) continue;
5936
+ const peaks = await controller.audioPeaks.load(asset);
5937
+ const speed = _nullishCoalesce(c.speed, () => ( 1));
5938
+ const src0 = _nullishCoalesce(c.in, () => ( 0));
5939
+ const silences = findVideoAudioSilences(peaks, src0, src0 + c.duration * speed, { thresholdDb: -40, minDuration: 250 });
5940
+ let at = c.start;
5941
+ for (const s of silences) {
5942
+ const a = c.start + (s.start - src0) / speed;
5943
+ if (a - at > _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON) ranges.push({ from: at, to: a });
5944
+ at = c.start + (s.end - src0) / speed;
5945
+ }
5946
+ if (_chunk3OSXHLITcjs.videoClipEnd.call(void 0, c) - at > _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON) ranges.push({ from: at, to: _chunk3OSXHLITcjs.videoClipEnd.call(void 0, c) });
5947
+ }
5948
+ if (!ranges.length) throw new Error(`Track "${args.dialogue_track}" has no sound above -40 dBFS, so there is no speech to duck under.`);
5949
+ }
5950
+ const edits = [];
5951
+ const out = [];
5952
+ for (const { clip } of music) {
5953
+ const base = typeof clip.volume === "number" ? clip.volume : 1;
5954
+ const keys = videoDuckingKeys(clip, ranges, { duckDb: args.duck_db, attack: args.attack, release: args.release, mergeGap: args.merge_gap, base });
5955
+ out.push({ clip: clip.id, keys: keys.length, ...keys.length ? { keyframes: keys } : { note: "No speech overlaps it." } });
5956
+ if (keys.length) edits.push({ op: "set_keyframes", clip: clip.id, property: "volume", keys });
5957
+ }
5958
+ if (!edits.length) throw new Error("No speech overlaps the music clips, so there is nothing to duck.");
5959
+ const r = controller.apply(edits, "Duck music");
5960
+ if (!r.ok) throw new Error(r.error);
5961
+ return { speech: mode, speechRanges: ranges.map((x) => ({ from: round4(x.from), to: round4(x.to) })), music: out };
5962
+ }
5963
+ };
5964
+ const BEAT_FIELDS = {
5965
+ sensitivity: { type: "number", minimum: 0.5, maximum: 4, description: "How far above the local average a hit must rise, in standard deviations (default 1.5; lower finds more)." },
5966
+ min_bpm: { type: "number", minimum: 30, maximum: 300, description: "Slowest tempo to consider (default 60)." },
5967
+ max_bpm: { type: "number", minimum: 30, maximum: 300, description: "Fastest tempo to consider (default 200)." }
5968
+ };
5969
+ const beatsOf = async (args) => {
5970
+ const r = stretch(args);
5971
+ const peaks = await controller.audioPeaks.load(r.asset);
5972
+ const found = findVideoAudioBeats(peaks, r.from, r.to, { sensitivity: args.sensitivity, minBpm: args.min_bpm, maxBpm: args.max_bpm });
5973
+ return { asset: r.asset.id, bpm: found.bpm, beats: found.beats.map((t) => round4(r.out(t))), onsets: found.onsets.map((t) => round4(r.out(t))) };
5974
+ };
5975
+ const findBeats = {
5976
+ name: "find_video_beats",
5977
+ title: "Find the beats",
5978
+ description: "Finds the beat of a music clip or asset: the tempo (BPM, or null when the hits do not repeat), a steady beat grid at that tempo, and the onsets (each hit) \u2014 timeline ms for a clip, source ms for an asset. Changes nothing. Use the beats to cut on the music (plan_video_beat_snap), place markers (add_video_marker) or time titles. Works best on percussive music. The first call for an asset decodes its file.",
5979
+ inputSchema: object3({ ...SOUND_TARGET, ...BEAT_FIELDS }),
5980
+ annotations: { title: "Find the beats", ...SOUND },
5981
+ handler: (args) => beatsOf(args)
5982
+ };
5983
+ const planBeatSnap = {
5984
+ name: "plan_video_beat_snap",
5985
+ title: "Propose cuts on the beat",
5986
+ description: 'Proposes, as a plan for review, moving each cut on a track to the nearest beat within "tolerance" ms with roll edits (the outgoing clip gets longer as the incoming one gets shorter, so nothing else moves). Beats come from a music clip (found as find_video_beats does) or are given. Cuts already on a beat, with none in reach, or without enough media to roll are left (listed in "skipped"). Nothing changes until apply_video_edit_plan. Returns the plan and what was skipped.',
5987
+ inputSchema: object3(
5988
+ {
5989
+ track: { ...TRACK_ID2, description: "The track whose cuts to move (default the first video track)." },
5990
+ music: { ...CLIP_ID2, description: "A music clip to take the beats from. Pass this or beats." },
5991
+ beats: { type: "array", minItems: 1, maxItems: 5e3, items: { type: "number", minimum: 0 }, description: "Beat times on the timeline, in ms. Pass this or music." },
5992
+ tolerance: { type: "number", exclusiveMinimum: 0, maximum: 2e3, description: "Furthest a cut may move, in ms (default 150)." },
5993
+ ...BEAT_FIELDS
5994
+ }
5995
+ ),
5996
+ annotations: { title: "Propose cuts on the beat", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
5997
+ handler: async (args) => {
5998
+ const p = project();
5999
+ if (args.music === void 0 === (args.beats === void 0)) throw new Error('Pass either "music" (a music clip to find the beats in) or "beats" (times in ms), not both.');
6000
+ const track = args.track !== void 0 ? p.tracks.find((t) => t.id === args.track) : p.tracks.find((t) => t.kind === "video");
6001
+ if (!track) throw new Error(args.track !== void 0 ? `There is no track "${args.track}".` : "The project has no video track.");
6002
+ let beats;
6003
+ if (args.music !== void 0) {
6004
+ const found = await beatsOf({ clip: args.music, sensitivity: args.sensitivity, min_bpm: args.min_bpm, max_bpm: args.max_bpm });
6005
+ beats = found.beats.length ? found.beats : found.onsets;
6006
+ if (!beats.length) throw new Error(`No beats were found in "${args.music}"; try a lower sensitivity, or pass beats.`);
6007
+ } else beats = [...args.beats].sort((a, b) => a - b);
6008
+ const tolerance = _nullishCoalesce(args.tolerance, () => ( 150));
6009
+ const clips = _chunk3OSXHLITcjs.sortedVideoClips.call(void 0, track);
6010
+ const edits = [];
6011
+ const ranges = [];
6012
+ const skipped = [];
6013
+ let working = p;
6014
+ for (let i = 0; i + 1 < clips.length; i++) {
6015
+ const left = clips[i];
6016
+ const right = clips[i + 1];
6017
+ const cut = _chunk3OSXHLITcjs.videoClipEnd.call(void 0, left);
6018
+ if (Math.abs(right.start - cut) > _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON) continue;
6019
+ const beat = nearestVideoBeat(beats, cut, tolerance);
6020
+ if (beat === null) {
6021
+ skipped.push({ cut: round4(cut), reason: `no beat within ${tolerance} ms` });
6022
+ continue;
6023
+ }
6024
+ const delta = Math.round((beat - cut) * 10) / 10;
6025
+ if (Math.abs(delta) < 1) {
6026
+ skipped.push({ cut: round4(cut), reason: "already on the beat" });
6027
+ continue;
6028
+ }
6029
+ const edit = { op: "roll_edit", left: left.id, right: right.id, delta };
6030
+ const tried = applyVideoEdits(working, edit);
6031
+ if (!tried.ok) {
6032
+ skipped.push({ cut: round4(cut), reason: tried.error });
6033
+ continue;
6034
+ }
6035
+ working = tried.project;
6036
+ edits.push(edit);
6037
+ ranges.push({ from: Math.min(cut, beat), to: Math.max(cut, beat), reason: "beat" });
6038
+ }
6039
+ if (!edits.length) throw new Error(`No cut on track "${track.id}" can move to a beat${skipped.length ? `: ${skipped.slice(0, 5).map((s) => `${s.cut} ms (${s.reason})`).join("; ")}` : " (it has no cuts between touching clips)"}.`);
6040
+ const r = controller.proposePlan({ title: `Snap ${edits.length} cut${edits.length > 1 ? "s" : ""} to the beat`, description: `Rolls cuts on track "${track.id}" to the nearest beat within ${tolerance} ms.`, edits, ranges, source: "plan_video_beat_snap" });
6041
+ if (!r.ok) throw new Error(r.error);
6042
+ return { plan: { id: r.plan.id, title: r.plan.title, summary: r.plan.summary, text: describeVideoEditPlan(r.plan) }, moved: edits.length, skipped, hint: `Nothing changed yet. Call apply_video_edit_plan with plan "${r.plan.id}" once the user agrees.` };
6043
+ }
6044
+ };
6045
+ const reads = [measureLoudness, findBeats];
6046
+ const writes = [normalizeLoudness, duckMusic, planBeatSnap];
6047
+ return withVideoPlayback(
6048
+ {
6049
+ id: _nullishCoalesce(options.id, () => ( "video-music")),
6050
+ namespace: options.namespace,
6051
+ title: "Video music and mix",
6052
+ description: "Music and the mix: beats and cuts on the beat, ducking music under speech, and loudness in LUFS.",
6053
+ skill: "pixerate-bgm-alignment",
6054
+ tools: () => readOnlyReason() ? reads : [...reads, ...writes],
6055
+ subscribe: (notify) => {
6056
+ let last = readOnlyReason();
6057
+ return controller.subscribe(() => {
6058
+ const now = readOnlyReason();
6059
+ if (now !== last) {
6060
+ last = now;
6061
+ notify();
6062
+ }
6063
+ });
6064
+ }
6065
+ },
6066
+ options.playbackSurface
6067
+ );
6068
+ }
6069
+
6070
+ // src/video-editor/effect-authoring.ts
6071
+ function videoEaseToAnimationEasing(ease) {
6072
+ if (ease === void 0 || ease === "linear") return "linear";
6073
+ if (Array.isArray(ease)) return { type: "cubic-bezier", x1: ease[0], y1: ease[1], x2: ease[2], y2: ease[3] };
6074
+ return ease;
6075
+ }
6076
+ function animationEasingToVideoEase(easing) {
6077
+ if (easing === void 0 || easing === "linear") return "linear";
6078
+ if (easing === "hold" || easing === "ease-in" || easing === "ease-out" || easing === "ease-in-out") return easing;
6079
+ if (typeof easing === "string") {
6080
+ const p = _chunkDWJJI6RTcjs.CUBIC_BEZIER_PRESETS[easing];
6081
+ return p ? [p[0], p[1], p[2], p[3]] : null;
6082
+ }
6083
+ if (easing.type === "cubic-bezier") return [easing.x1, easing.y1, easing.x2, easing.y2];
6084
+ return null;
6085
+ }
6086
+ var SAMPLES = 12;
6087
+ function videoClipEffectDocument(clip, instance, providerId) {
6088
+ const effect = (_nullishCoalesce(clip.effects, () => ( []))).find((e) => e.id === instance);
6089
+ if (!effect) throw new Error(`Clip "${clip.id}" has no effect "${instance}"; its effects are ${(_nullishCoalesce(clip.effects, () => ( []))).map((e) => `"${e.id}" (${_nullishCoalesce(e.name, () => ( e.effect))})`).join(", ") || "none"}.`);
6090
+ const values = {};
6091
+ const tracks = {};
6092
+ for (const [control, v] of Object.entries(_nullishCoalesce(effect.values, () => ( {})))) {
6093
+ if (_chunk3OSXHLITcjs.isVideoKeyframes.call(void 0, v)) {
6094
+ values[control] = v.keys[0].v;
6095
+ tracks[control] = v.keys.map((k) => ({ time: k.t, value: k.v, ...k.ease && k.ease !== "linear" ? { easing: videoEaseToAnimationEasing(k.ease) } : {} }));
6096
+ } else values[control] = v;
6097
+ }
6098
+ return { version: 1, providerId, effectId: effect.effect, name: _nullishCoalesce(effect.name, () => ( effect.effect)), values, tracks, presets: [], duration: clip.duration, loop: false };
6099
+ }
6100
+ function videoEffectValuesFromDocument(doc) {
6101
+ const values = { ...doc.values };
6102
+ const notes = [];
6103
+ for (const [control, track] of Object.entries(_nullishCoalesce(doc.tracks, () => ( {})))) {
6104
+ const kfs = [...track].sort((a, b) => a.time - b.time);
6105
+ if (!kfs.length) continue;
6106
+ if (!kfs.every((k) => typeof k.value === "number")) {
6107
+ values[control] = kfs[0].value;
6108
+ notes.push(`"${control}" is keyframed with non-numeric values, which clip effects cannot animate; it keeps its first value.`);
6109
+ continue;
6110
+ }
6111
+ const keys = [];
6112
+ let sampled = false;
6113
+ kfs.forEach((k, i) => {
6114
+ const next = kfs[i + 1];
6115
+ const ease = animationEasingToVideoEase(k.easing);
6116
+ if (!next || ease) {
6117
+ keys.push({ t: k.time, v: k.value, ...next && ease && ease !== "linear" ? { ease } : {} });
6118
+ return;
6119
+ }
6120
+ sampled = true;
6121
+ const a = k.value;
6122
+ const b = next.value;
6123
+ const span = next.time - k.time;
6124
+ for (let s = 0; s < SAMPLES; s++) keys.push({ t: Math.round((k.time + span * s / SAMPLES) * 1e3) / 1e3, v: a + (b - a) * _chunkDWJJI6RTcjs.evaluateEasing.call(void 0, k.easing, s / SAMPLES, span) });
6125
+ });
6126
+ values[control] = { keys: keys.filter((k, i) => i === 0 || k.t > keys[i - 1].t) };
6127
+ if (sampled) notes.push(`"${control}" uses a spring or elastic easing, sampled into ${SAMPLES} linear steps per segment.`);
6128
+ }
6129
+ return { values, notes };
6130
+ }
6131
+ async function openVideoClipEffectAuthoring(controller, clip, instance, options = {}) {
6132
+ const provider = controller.effectsProvider;
6133
+ if (!provider) throw new Error("No effects provider is registered, so the effect cannot be opened for authoring; register one with setEffectsProvider.");
6134
+ const at = _chunk3OSXHLITcjs.findVideoClip.call(void 0, controller.getProject(), clip);
6135
+ if (!at) throw new Error(`There is no clip "${clip}".`);
6136
+ const doc = videoClipEffectDocument(at.clip, instance, provider.id);
6137
+ const session = await _chunk6DM3PUQDcjs.createEffectAuthoringSession.call(void 0, { document: doc }, { ...options, provider });
6138
+ return {
6139
+ clip,
6140
+ instance,
6141
+ session,
6142
+ async commit() {
6143
+ const { values, notes } = videoEffectValuesFromDocument(session.exportDocument());
6144
+ const result = await controller.updateClipEffect(clip, instance, { values });
6145
+ return result.ok && notes.length ? { ...result, notes } : result;
6146
+ },
6147
+ dispose: () => session.dispose()
6148
+ };
6149
+ }
6150
+ var videoAuthorableEffects = (clip) => _nullishCoalesce(clip.effects, () => ( []));
6151
+
4871
6152
  // src/video-editor/editorDocument.ts
4872
6153
  function createVideoEditorDocument(controller) {
4873
6154
  return _chunkM4MIZPQIcjs.defineEditorDocument.call(void 0, {
@@ -5150,4 +6431,48 @@ function createVideoEditorDocument(controller) {
5150
6431
 
5151
6432
 
5152
6433
 
5153
- exports.VIDEO_BLEND_MODES = _chunkVKF3PWKRcjs.VIDEO_BLEND_MODES; exports.VIDEO_BLEND_MODE_LABELS = VIDEO_BLEND_MODE_LABELS; exports.VIDEO_CLIP_ANIMATION_DURATION = _chunkVKF3PWKRcjs.VIDEO_CLIP_ANIMATION_DURATION; exports.VIDEO_CLIP_ANIMATION_EFFECTS = _chunkVKF3PWKRcjs.VIDEO_CLIP_ANIMATION_EFFECTS; exports.VIDEO_CLIP_ANIMATION_LABELS = VIDEO_CLIP_ANIMATION_LABELS; exports.VIDEO_CLIP_TYPES = _chunkVKF3PWKRcjs.VIDEO_CLIP_TYPES; exports.VIDEO_DEFAULT_FILLERS = VIDEO_DEFAULT_FILLERS; exports.VIDEO_DEFAULT_STILL_DURATION = VIDEO_DEFAULT_STILL_DURATION; exports.VIDEO_DEFAULT_TITLE_DURATION = VIDEO_DEFAULT_TITLE_DURATION; exports.VIDEO_EASE_NAMES = _chunkVKF3PWKRcjs.VIDEO_EASE_NAMES; exports.VIDEO_EDIT_OPS = VIDEO_EDIT_OPS; exports.VIDEO_EFFECT_PLACES = _chunkVKF3PWKRcjs.VIDEO_EFFECT_PLACES; exports.VIDEO_FADE_CURVES = _chunkVKF3PWKRcjs.VIDEO_FADE_CURVES; exports.VIDEO_FADE_CURVE_LABELS = VIDEO_FADE_CURVE_LABELS; exports.VIDEO_FITS = _chunkVKF3PWKRcjs.VIDEO_FITS; exports.VIDEO_GRADE_CONTROLS = _chunkVKF3PWKRcjs.VIDEO_GRADE_CONTROLS; exports.VIDEO_GRADE_PRESETS = _chunkVKF3PWKRcjs.VIDEO_GRADE_PRESETS; exports.VIDEO_GRADE_PRESET_NAMES = _chunkVKF3PWKRcjs.VIDEO_GRADE_PRESET_NAMES; exports.VIDEO_ISSUE_CODES = VIDEO_ISSUE_CODES; exports.VIDEO_KEYFRAME_LIMITS = _chunkVKF3PWKRcjs.VIDEO_KEYFRAME_LIMITS; exports.VIDEO_KEYFRAME_PROPERTIES = _chunkVKF3PWKRcjs.VIDEO_KEYFRAME_PROPERTIES; exports.VIDEO_LUT_MAX_SIZE = _chunkVKF3PWKRcjs.VIDEO_LUT_MAX_SIZE; exports.VIDEO_PEAK_BLOCK = VIDEO_PEAK_BLOCK; exports.VIDEO_PLACEMENT_MODES = _chunkVKF3PWKRcjs.VIDEO_PLACEMENT_MODES; exports.VIDEO_PREVIEW_SCALE_OPTIONS = VIDEO_PREVIEW_SCALE_OPTIONS; exports.VIDEO_PROXY_SCALES = VIDEO_PROXY_SCALES; exports.VIDEO_SCOPE_KINDS = VIDEO_SCOPE_KINDS; exports.VIDEO_TIME_EPSILON = _chunkVKF3PWKRcjs.VIDEO_TIME_EPSILON; exports.VIDEO_TRANSFORM_PROPERTIES = _chunkVKF3PWKRcjs.VIDEO_TRANSFORM_PROPERTIES; exports.VIDEO_TRANSITION_LABELS = VIDEO_TRANSITION_LABELS; exports.VIDEO_TRANSITION_TYPES = _chunkVKF3PWKRcjs.VIDEO_TRANSITION_TYPES; exports.VIDEO_VECTORSCOPE_TARGETS = VIDEO_VECTORSCOPE_TARGETS; exports.VIDEO_WIPE_DIRECTIONS = _chunkVKF3PWKRcjs.VIDEO_WIPE_DIRECTIONS; exports.VideoAudioPeaksCache = VideoAudioPeaksCache; exports.VideoEditorController = VideoEditorController; exports.VideoEffectsEngine = _chunkVKF3PWKRcjs.VideoEffectsEngine; exports.VideoFrameCache = VideoFrameCache; exports.VideoLutCache = _chunkVKF3PWKRcjs.VideoLutCache; exports.VideoPreviewEngine = VideoPreviewEngine; exports.VideoPreviewPixels = VideoPreviewPixels; exports.allVideoClipIds = _chunkVKF3PWKRcjs.allVideoClipIds; exports.allVideoTracks = _chunkVKF3PWKRcjs.allVideoTracks; exports.applyVideoEdits = applyVideoEdits; exports.applyVideoGrade = _chunkVKF3PWKRcjs.applyVideoGrade; exports.applyVideoLut = _chunkVKF3PWKRcjs.applyVideoLut; exports.applyVideoLutToPixels = _chunkVKF3PWKRcjs.applyVideoLutToPixels; exports.buildVideoAudioPeaks = buildVideoAudioPeaks; exports.checkVideoEffectPlace = _chunkVKF3PWKRcjs.checkVideoEffectPlace; exports.checkVideoEffectValues = _chunkVKF3PWKRcjs.checkVideoEffectValues; exports.checkVideoProject = checkVideoProject; exports.checkVideoTimeRemap = _chunkVKF3PWKRcjs.checkVideoTimeRemap; exports.createVideoEditorDocument = createVideoEditorDocument; exports.createVideoEditorToolProvider = createVideoEditorToolProvider; exports.createVideoEffectsToolProvider = createVideoEffectsToolProvider; exports.createVideoElementDecoder = createVideoElementDecoder; exports.createVideoProject = createVideoProject; exports.decodeVideoAudioPeaks = decodeVideoAudioPeaks; exports.describeVideoProject = describeVideoProject; exports.describeVideoSequence = describeVideoSequence; exports.drawVideoFrame = _chunkVKF3PWKRcjs.drawVideoFrame; exports.ffmpegFilterValue = _chunkVKF3PWKRcjs.ffmpegFilterValue; exports.findVideoAudioSilences = findVideoAudioSilences; exports.findVideoClip = _chunkVKF3PWKRcjs.findVideoClip; exports.findVideoFillers = findVideoFillers; exports.findVideoGaps = _chunkVKF3PWKRcjs.findVideoGaps; exports.findVideoSequenceClip = _chunkVKF3PWKRcjs.findVideoSequenceClip; exports.findVideoSequenceCycle = _chunkVKF3PWKRcjs.findVideoSequenceCycle; exports.findVideoTranscriptPauses = findVideoTranscriptPauses; exports.findVideoTranscriptText = findVideoTranscriptText; exports.formatVideoSeconds = _chunkVKF3PWKRcjs.formatVideoSeconds; exports.formatVideoTimecode = _chunkVKF3PWKRcjs.formatVideoTimecode; exports.formatVideoTimecodeFrames = _chunkVKF3PWKRcjs.formatVideoTimecodeFrames; exports.isVideoDropFrameRate = _chunkVKF3PWKRcjs.isVideoDropFrameRate; exports.isVideoKeyframes = _chunkVKF3PWKRcjs.isVideoKeyframes; exports.mergeVideoRanges = mergeVideoRanges; exports.mixEffectPixels = _chunkVKF3PWKRcjs.mixEffectPixels; exports.nextFreeVideoSlot = _chunkVKF3PWKRcjs.nextFreeVideoSlot; exports.normalizeVideoGrade = _chunkVKF3PWKRcjs.normalizeVideoGrade; exports.normalizeVideoGradeLut = _chunkVKF3PWKRcjs.normalizeVideoGradeLut; exports.normalizeVideoWord = normalizeVideoWord; exports.overEffectPixels = _chunkVKF3PWKRcjs.overEffectPixels; exports.parseVideoAnimatable = parseVideoAnimatable; exports.parseVideoAsset = parseVideoAsset; exports.parseVideoAssetProxies = parseVideoAssetProxies; exports.parseVideoClip = parseVideoClip; exports.parseVideoClipAnimation = parseVideoClipAnimation; exports.parseVideoClipEffects = parseVideoClipEffects; exports.parseVideoCubeLut = _chunkVKF3PWKRcjs.parseVideoCubeLut; exports.parseVideoEffectRef = parseVideoEffectRef; exports.parseVideoEffectValues = parseVideoEffectValues; exports.parseVideoGrade = parseVideoGrade; exports.parseVideoGradeLut = parseVideoGradeLut; exports.parseVideoKeyframes = parseVideoKeyframes; exports.parseVideoLoudnessMeasurement = _chunkVKF3PWKRcjs.parseVideoLoudnessMeasurement; exports.parseVideoMarker = parseVideoMarker; exports.parseVideoPreviewScale = parseVideoPreviewScale; exports.parseVideoProject = parseVideoProject; exports.parseVideoSequence = parseVideoSequence; exports.parseVideoTimeRef = parseVideoTimeRef; exports.parseVideoTimeRemap = parseVideoTimeRemap; exports.parseVideoTrack = parseVideoTrack; exports.parseVideoTranscript = parseVideoTranscript; exports.parseVideoTranscriptWords = parseVideoTranscriptWords; exports.parseVideoTransform = parseVideoTransform; exports.parseVideoTransition = parseVideoTransition; exports.placeVideoRect = _chunkVKF3PWKRcjs.placeVideoRect; exports.planVideoClipDrag = planVideoClipDrag; exports.planVideoPrefetch = planVideoPrefetch; exports.planVideoToolPlayback = planVideoToolPlayback; exports.probeVideoMedia = probeVideoMedia; exports.readVideoScopesPanel = readVideoScopesPanel; exports.removeVideoKey = _chunkVKF3PWKRcjs.removeVideoKey; exports.renderVideoFrameExact = _chunkVKF3PWKRcjs.renderVideoFrameExact; exports.resolveVideoPreviewScale = resolveVideoPreviewScale; exports.retimeVideoAnimatable = _chunkVKF3PWKRcjs.retimeVideoAnimatable; exports.setVideoKey = _chunkVKF3PWKRcjs.setVideoKey; exports.snapToVideoFrame = _chunkVKF3PWKRcjs.snapToVideoFrame; exports.snapVideoTime = _chunkVKF3PWKRcjs.snapVideoTime; exports.sortVideoKeys = _chunkVKF3PWKRcjs.sortVideoKeys; exports.sortedVideoClips = _chunkVKF3PWKRcjs.sortedVideoClips; exports.splitVideoAnimatable = _chunkVKF3PWKRcjs.splitVideoAnimatable; exports.summarizeVideoScopes = summarizeVideoScopes; exports.toVideoEdl = _chunkVKF3PWKRcjs.toVideoEdl; exports.toVideoFfmpeg = _chunkVKF3PWKRcjs.toVideoFfmpeg; exports.toVideoLoudnessMeasure = _chunkVKF3PWKRcjs.toVideoLoudnessMeasure; exports.videoAssetDropEdit = videoAssetDropEdit; exports.videoAssetHasSound = _chunkVKF3PWKRcjs.videoAssetHasSound; exports.videoAssetKindOf = videoAssetKindOf; exports.videoAssetMap = _chunkVKF3PWKRcjs.videoAssetMap; exports.videoAssetProxy = videoAssetProxy; exports.videoAudioLevels = videoAudioLevels; exports.videoAudioPeakRange = videoAudioPeakRange; exports.videoAudioPeakSpans = videoAudioPeakSpans; exports.videoAudioPeaksDuration = videoAudioPeaksDuration; exports.videoCacheEvictions = videoCacheEvictions; exports.videoChroma = videoChroma; exports.videoClipAfter = _chunkVKF3PWKRcjs.videoClipAfter; exports.videoClipAnimated = _chunkVKF3PWKRcjs.videoClipAnimated; exports.videoClipAnimationProperties = _chunkVKF3PWKRcjs.videoClipAnimationProperties; exports.videoClipAnimationSpans = _chunkVKF3PWKRcjs.videoClipAnimationSpans; exports.videoClipAppliedEffects = videoClipAppliedEffects; exports.videoClipBefore = _chunkVKF3PWKRcjs.videoClipBefore; exports.videoClipEffectUses = _chunkVKF3PWKRcjs.videoClipEffectUses; exports.videoClipEnd = _chunkVKF3PWKRcjs.videoClipEnd; exports.videoClipHandles = _chunkVKF3PWKRcjs.videoClipHandles; exports.videoClipKeyframes = videoClipKeyframes; exports.videoClipLabel = videoClipLabel; exports.videoClipOverhang = _chunkVKF3PWKRcjs.videoClipOverhang; exports.videoClipShowsWaveform = videoClipShowsWaveform; exports.videoClipSoundGain = _chunkVKF3PWKRcjs.videoClipSoundGain; exports.videoClipSourceRange = _chunkVKF3PWKRcjs.videoClipSourceRange; exports.videoClipSourceTime = _chunkVKF3PWKRcjs.videoClipSourceTime; exports.videoClipSpeed = _chunkVKF3PWKRcjs.videoClipSpeed; exports.videoClipSpeedAt = _chunkVKF3PWKRcjs.videoClipSpeedAt; exports.videoClipTint = videoClipTint; exports.videoClipWaveform = videoClipWaveform; exports.videoClipWaveformPath = videoClipWaveformPath; exports.videoClipsAt = _chunkVKF3PWKRcjs.videoClipsAt; exports.videoContactSheetSvg = _chunkVKF3PWKRcjs.videoContactSheetSvg; exports.videoDragLabel = videoDragLabel; exports.videoDropTracks = videoDropTracks; exports.videoEaseBezier = _chunkVKF3PWKRcjs.videoEaseBezier; exports.videoEaseProgress = _chunkVKF3PWKRcjs.videoEaseProgress; exports.videoEffectFrameTime = _chunkVKF3PWKRcjs.videoEffectFrameTime; exports.videoEffectLayerKey = _chunkVKF3PWKRcjs.videoEffectLayerKey; exports.videoEffectValuesAt = _chunkVKF3PWKRcjs.videoEffectValuesAt; exports.videoExposureNotes = videoExposureNotes; exports.videoExposureStats = videoExposureStats; exports.videoFadeCurveGain = _chunkVKF3PWKRcjs.videoFadeCurveGain; exports.videoFfmpegUnsupportedEffects = _chunkVKF3PWKRcjs.videoFfmpegUnsupportedEffects; exports.videoFrameAt = _chunkVKF3PWKRcjs.videoFrameAt; exports.videoFrameCacheKey = videoFrameCacheKey; exports.videoFrameDuration = _chunkVKF3PWKRcjs.videoFrameDuration; exports.videoFrameLeafLayers = _chunkVKF3PWKRcjs.videoFrameLeafLayers; exports.videoFrameRequestsAt = videoFrameRequestsAt; exports.videoFrameToSvg = _chunkVKF3PWKRcjs.videoFrameToSvg; exports.videoFrames = _chunkVKF3PWKRcjs.videoFrames; exports.videoFreezeFrameEdit = _chunkVKF3PWKRcjs.videoFreezeFrameEdit; exports.videoGradeCssFilter = _chunkVKF3PWKRcjs.videoGradeCssFilter; exports.videoGradeFfmpegFilters = _chunkVKF3PWKRcjs.videoGradeFfmpegFilters; exports.videoGradeGainsFilterId = _chunkVKF3PWKRcjs.videoGradeGainsFilterId; exports.videoGradeLook = _chunkVKF3PWKRcjs.videoGradeLook; exports.videoGradePreset = _chunkVKF3PWKRcjs.videoGradePreset; exports.videoGradeStepMatrix = _chunkVKF3PWKRcjs.videoGradeStepMatrix; exports.videoGradeStepSvgValues = _chunkVKF3PWKRcjs.videoGradeStepSvgValues; exports.videoGradeSteps = _chunkVKF3PWKRcjs.videoGradeSteps; exports.videoGradeSvgDefs = _chunkVKF3PWKRcjs.videoGradeSvgDefs; exports.videoGradeSvgFilter = _chunkVKF3PWKRcjs.videoGradeSvgFilter; exports.videoHistogram = videoHistogram; exports.videoHistogramImage = videoHistogramImage; exports.videoHourFrames = _chunkVKF3PWKRcjs.videoHourFrames; exports.videoIrisRadius = _chunkVKF3PWKRcjs.videoIrisRadius; exports.videoIssueKey = videoIssueKey; exports.videoKeyframeBreakpoints = _chunkVKF3PWKRcjs.videoKeyframeBreakpoints; exports.videoKeyframeFfmpegExpr = _chunkVKF3PWKRcjs.videoKeyframeFfmpegExpr; exports.videoKeyframeRows = videoKeyframeRows; exports.videoKeyframeToggleEdit = videoKeyframeToggleEdit; exports.videoKeyframeValueEdit = videoKeyframeValueEdit; exports.videoKeysOf = _chunkVKF3PWKRcjs.videoKeysOf; exports.videoLayersUseEffects = _chunkVKF3PWKRcjs.videoLayersUseEffects; exports.videoLayersWithoutTransitionEffects = _chunkVKF3PWKRcjs.videoLayersWithoutTransitionEffects; exports.videoLuma = videoLuma; exports.videoLumaWaveform = videoLumaWaveform; exports.videoLutEdits = videoLutEdits; exports.videoLutFromFunction = _chunkVKF3PWKRcjs.videoLutFromFunction; exports.videoLutSvgApproximation = _chunkVKF3PWKRcjs.videoLutSvgApproximation; exports.videoLutSvgPrimitives = _chunkVKF3PWKRcjs.videoLutSvgPrimitives; exports.videoLutToCube = _chunkVKF3PWKRcjs.videoLutToCube; exports.videoOverlap = _chunkVKF3PWKRcjs.videoOverlap; exports.videoPeakDb = videoPeakDb; exports.videoPreviewSrc = videoPreviewSrc; exports.videoProjectDuration = _chunkVKF3PWKRcjs.videoProjectDuration; exports.videoProjectEffects = _chunkVKF3PWKRcjs.videoProjectEffects; exports.videoProjectHasProxies = videoProjectHasProxies; exports.videoProjectSvg = _chunkVKF3PWKRcjs.videoProjectSvg; exports.videoProxyFfmpeg = videoProxyFfmpeg; exports.videoProxyName = videoProxyName; exports.videoProxyPlan = videoProxyPlan; exports.videoRemoveRangesEdits = videoRemoveRangesEdits; exports.videoRulerTicks = videoRulerTicks; exports.videoScopeImage = videoScopeImage; exports.videoSequenceChildren = _chunkVKF3PWKRcjs.videoSequenceChildren; exports.videoSequenceDuration = _chunkVKF3PWKRcjs.videoSequenceDuration; exports.videoSequenceMap = _chunkVKF3PWKRcjs.videoSequenceMap; exports.videoShellQuote = _chunkVKF3PWKRcjs.videoShellQuote; exports.videoSlowMotionRamp = _chunkVKF3PWKRcjs.videoSlowMotionRamp; exports.videoSnapPoints = _chunkVKF3PWKRcjs.videoSnapPoints; exports.videoSourceFrame = videoSourceFrame; exports.videoSourceFrameTime = videoSourceFrameTime; exports.videoSpeedRampEdit = _chunkVKF3PWKRcjs.videoSpeedRampEdit; exports.videoSpeedRampKeys = _chunkVKF3PWKRcjs.videoSpeedRampKeys; exports.videoTextStyle = _chunkVKF3PWKRcjs.videoTextStyle; exports.videoTimeRef = videoTimeRef; exports.videoTimeRemapBreakpoints = _chunkVKF3PWKRcjs.videoTimeRemapBreakpoints; exports.videoTimeRemapSpan = _chunkVKF3PWKRcjs.videoTimeRemapSpan; exports.videoTimecodeFrames = _chunkVKF3PWKRcjs.videoTimecodeFrames; exports.videoTimelineTranscript = videoTimelineTranscript; exports.videoTitleFrame = _chunkVKF3PWKRcjs.videoTitleFrame; exports.videoTrackSyncLocked = _chunkVKF3PWKRcjs.videoTrackSyncLocked; exports.videoTranscriptEdit = videoTranscriptEdit; exports.videoTranscriptTrack = videoTranscriptTrack; exports.videoTransformAt = _chunkVKF3PWKRcjs.videoTransformAt; exports.videoTransitionAudioCurve = _chunkVKF3PWKRcjs.videoTransitionAudioCurve; exports.videoTransitionEase = _chunkVKF3PWKRcjs.videoTransitionEase; exports.videoTransitionHasDirection = videoTransitionHasDirection; exports.videoValueAt = _chunkVKF3PWKRcjs.videoValueAt; exports.videoVectorscope = videoVectorscope; exports.videoVectorscopeAngle = videoVectorscopeAngle; exports.videoVectorscopeImage = videoVectorscopeImage; exports.videoVectorscopePoint = videoVectorscopePoint; exports.videoWaveformImage = videoWaveformImage; exports.videoWaveformPath = videoWaveformPath; exports.videoWipeRect = _chunkVKF3PWKRcjs.videoWipeRect; exports.videoWordAt = videoWordAt; exports.videoWordsRange = videoWordsRange; exports.withVideoPlayback = withVideoPlayback;
6434
+
6435
+
6436
+
6437
+
6438
+
6439
+
6440
+
6441
+
6442
+
6443
+
6444
+
6445
+
6446
+
6447
+
6448
+
6449
+
6450
+
6451
+
6452
+
6453
+
6454
+
6455
+
6456
+
6457
+
6458
+
6459
+
6460
+
6461
+
6462
+
6463
+
6464
+
6465
+
6466
+
6467
+
6468
+
6469
+
6470
+
6471
+
6472
+
6473
+
6474
+
6475
+
6476
+
6477
+
6478
+ exports.TranscriptVideoController = TranscriptVideoController; exports.VIDEO_ADJUSTMENT_SCOPES = _chunk3OSXHLITcjs.VIDEO_ADJUSTMENT_SCOPES; exports.VIDEO_ADJUSTMENT_SCOPE_LABELS = VIDEO_ADJUSTMENT_SCOPE_LABELS; exports.VIDEO_BLEND_MODES = _chunk3OSXHLITcjs.VIDEO_BLEND_MODES; exports.VIDEO_BLEND_MODE_LABELS = VIDEO_BLEND_MODE_LABELS; exports.VIDEO_CLIP_ANIMATION_DURATION = _chunk3OSXHLITcjs.VIDEO_CLIP_ANIMATION_DURATION; exports.VIDEO_CLIP_ANIMATION_EFFECTS = _chunk3OSXHLITcjs.VIDEO_CLIP_ANIMATION_EFFECTS; exports.VIDEO_CLIP_ANIMATION_LABELS = VIDEO_CLIP_ANIMATION_LABELS; exports.VIDEO_CLIP_TYPES = _chunk3OSXHLITcjs.VIDEO_CLIP_TYPES; exports.VIDEO_DEFAULT_FILLERS = VIDEO_DEFAULT_FILLERS; exports.VIDEO_DEFAULT_STILL_DURATION = VIDEO_DEFAULT_STILL_DURATION; exports.VIDEO_DEFAULT_TITLE_DURATION = VIDEO_DEFAULT_TITLE_DURATION; exports.VIDEO_DISFLUENCY_KINDS = VIDEO_DISFLUENCY_KINDS; exports.VIDEO_EASE_NAMES = _chunk3OSXHLITcjs.VIDEO_EASE_NAMES; exports.VIDEO_EDIT_OPS = VIDEO_EDIT_OPS; exports.VIDEO_EFFECT_PLACES = _chunk3OSXHLITcjs.VIDEO_EFFECT_PLACES; exports.VIDEO_FADE_CURVES = _chunk3OSXHLITcjs.VIDEO_FADE_CURVES; exports.VIDEO_FADE_CURVE_LABELS = VIDEO_FADE_CURVE_LABELS; exports.VIDEO_FITS = _chunk3OSXHLITcjs.VIDEO_FITS; exports.VIDEO_GRADE_CONTROLS = _chunk3OSXHLITcjs.VIDEO_GRADE_CONTROLS; exports.VIDEO_GRADE_PRESETS = _chunk3OSXHLITcjs.VIDEO_GRADE_PRESETS; exports.VIDEO_GRADE_PRESET_NAMES = _chunk3OSXHLITcjs.VIDEO_GRADE_PRESET_NAMES; exports.VIDEO_ISSUE_CODES = VIDEO_ISSUE_CODES; exports.VIDEO_KEYFRAME_LIMITS = _chunk3OSXHLITcjs.VIDEO_KEYFRAME_LIMITS; exports.VIDEO_KEYFRAME_PROPERTIES = _chunk3OSXHLITcjs.VIDEO_KEYFRAME_PROPERTIES; exports.VIDEO_LOUDNESS_HOP = VIDEO_LOUDNESS_HOP; exports.VIDEO_LUT_MAX_SIZE = _chunk3OSXHLITcjs.VIDEO_LUT_MAX_SIZE; exports.VIDEO_MASK_DEFAULTS = _chunk3OSXHLITcjs.VIDEO_MASK_DEFAULTS; exports.VIDEO_MASK_SHAPES = _chunk3OSXHLITcjs.VIDEO_MASK_SHAPES; exports.VIDEO_MASK_SHAPE_LABELS = VIDEO_MASK_SHAPE_LABELS; exports.VIDEO_PEAK_BLOCK = VIDEO_PEAK_BLOCK; exports.VIDEO_PLACEMENT_MODES = _chunk3OSXHLITcjs.VIDEO_PLACEMENT_MODES; exports.VIDEO_PREVIEW_SCALE_OPTIONS = VIDEO_PREVIEW_SCALE_OPTIONS; exports.VIDEO_PROXY_SCALES = VIDEO_PROXY_SCALES; exports.VIDEO_SCOPE_KINDS = VIDEO_SCOPE_KINDS; exports.VIDEO_TIME_EPSILON = _chunk3OSXHLITcjs.VIDEO_TIME_EPSILON; exports.VIDEO_TRANSFORM_PROPERTIES = _chunk3OSXHLITcjs.VIDEO_TRANSFORM_PROPERTIES; exports.VIDEO_TRANSITION_LABELS = VIDEO_TRANSITION_LABELS; exports.VIDEO_TRANSITION_TYPES = _chunk3OSXHLITcjs.VIDEO_TRANSITION_TYPES; exports.VIDEO_VECTORSCOPE_TARGETS = VIDEO_VECTORSCOPE_TARGETS; exports.VIDEO_WIPE_DIRECTIONS = _chunk3OSXHLITcjs.VIDEO_WIPE_DIRECTIONS; exports.VideoAudioPeaksCache = VideoAudioPeaksCache; exports.VideoEditorController = VideoEditorController; exports.VideoEffectsEngine = _chunk3OSXHLITcjs.VideoEffectsEngine; exports.VideoFrameCache = VideoFrameCache; exports.VideoLutCache = _chunk3OSXHLITcjs.VideoLutCache; exports.VideoPreviewEngine = VideoPreviewEngine; exports.VideoPreviewPixels = VideoPreviewPixels; exports.allVideoClipIds = _chunk3OSXHLITcjs.allVideoClipIds; exports.allVideoTracks = _chunk3OSXHLITcjs.allVideoTracks; exports.animationEasingToVideoEase = animationEasingToVideoEase; exports.applyVideoEdits = applyVideoEdits; exports.applyVideoGrade = _chunk3OSXHLITcjs.applyVideoGrade; exports.applyVideoLut = _chunk3OSXHLITcjs.applyVideoLut; exports.applyVideoLutToPixels = _chunk3OSXHLITcjs.applyVideoLutToPixels; exports.buildVideoAudioPeaks = buildVideoAudioPeaks; exports.checkVideoEffectPlace = _chunk3OSXHLITcjs.checkVideoEffectPlace; exports.checkVideoEffectValues = _chunk3OSXHLITcjs.checkVideoEffectValues; exports.checkVideoProject = checkVideoProject; exports.checkVideoTimeRemap = _chunk3OSXHLITcjs.checkVideoTimeRemap; exports.createVideoDialogueToolProvider = createVideoDialogueToolProvider; exports.createVideoEditorDocument = createVideoEditorDocument; exports.createVideoEditorToolProvider = createVideoEditorToolProvider; exports.createVideoEffectsToolProvider = createVideoEffectsToolProvider; exports.createVideoElementDecoder = createVideoElementDecoder; exports.createVideoMusicToolProvider = createVideoMusicToolProvider; exports.createVideoProject = createVideoProject; exports.decodeVideoAudioPeaks = decodeVideoAudioPeaks; exports.describeVideoEditPlan = describeVideoEditPlan; exports.describeVideoProject = describeVideoProject; exports.describeVideoSequence = describeVideoSequence; exports.detectVideoDisfluencies = detectVideoDisfluencies; exports.diffVideoProjects = diffVideoProjects; exports.drawVideoFrame = _chunk3OSXHLITcjs.drawVideoFrame; exports.ffmpegFilterValue = _chunk3OSXHLITcjs.ffmpegFilterValue; exports.findVideoAudioBeats = findVideoAudioBeats; exports.findVideoAudioSilences = findVideoAudioSilences; exports.findVideoClip = _chunk3OSXHLITcjs.findVideoClip; exports.findVideoFillers = findVideoFillers; exports.findVideoGaps = _chunk3OSXHLITcjs.findVideoGaps; exports.findVideoRepetitions = findVideoRepetitions; exports.findVideoSequenceClip = _chunk3OSXHLITcjs.findVideoSequenceClip; exports.findVideoSequenceCycle = _chunk3OSXHLITcjs.findVideoSequenceCycle; exports.findVideoTranscriptPauses = findVideoTranscriptPauses; exports.findVideoTranscriptText = findVideoTranscriptText; exports.formatVideoSeconds = _chunk3OSXHLITcjs.formatVideoSeconds; exports.formatVideoTimecode = _chunk3OSXHLITcjs.formatVideoTimecode; exports.formatVideoTimecodeFrames = _chunk3OSXHLITcjs.formatVideoTimecodeFrames; exports.isVideoDropFrameRate = _chunk3OSXHLITcjs.isVideoDropFrameRate; exports.isVideoKeyframes = _chunk3OSXHLITcjs.isVideoKeyframes; exports.measureVideoLoudness = measureVideoLoudness; exports.mergeVideoRanges = mergeVideoRanges; exports.mixEffectPixels = _chunk3OSXHLITcjs.mixEffectPixels; exports.nearestVideoBeat = nearestVideoBeat; exports.nextFreeVideoSlot = _chunk3OSXHLITcjs.nextFreeVideoSlot; exports.normalizeVideoClipMask = _chunk3OSXHLITcjs.normalizeVideoClipMask; exports.normalizeVideoGrade = _chunk3OSXHLITcjs.normalizeVideoGrade; exports.normalizeVideoGradeLut = _chunk3OSXHLITcjs.normalizeVideoGradeLut; exports.normalizeVideoWord = normalizeVideoWord; exports.openVideoClipEffectAuthoring = openVideoClipEffectAuthoring; exports.overEffectPixels = _chunk3OSXHLITcjs.overEffectPixels; exports.parseVideoAnimatable = parseVideoAnimatable; exports.parseVideoAsset = parseVideoAsset; exports.parseVideoAssetProxies = parseVideoAssetProxies; exports.parseVideoClip = parseVideoClip; exports.parseVideoClipAnimation = parseVideoClipAnimation; exports.parseVideoClipEffects = parseVideoClipEffects; exports.parseVideoClipMask = parseVideoClipMask; exports.parseVideoCubeLut = _chunk3OSXHLITcjs.parseVideoCubeLut; exports.parseVideoEffectRef = parseVideoEffectRef; exports.parseVideoEffectValues = parseVideoEffectValues; exports.parseVideoGrade = parseVideoGrade; exports.parseVideoGradeLut = parseVideoGradeLut; exports.parseVideoKeyframes = parseVideoKeyframes; exports.parseVideoLoudnessMeasurement = _chunk3OSXHLITcjs.parseVideoLoudnessMeasurement; exports.parseVideoMarker = parseVideoMarker; exports.parseVideoPreviewScale = parseVideoPreviewScale; exports.parseVideoProject = parseVideoProject; exports.parseVideoSequence = parseVideoSequence; exports.parseVideoTimeRef = parseVideoTimeRef; exports.parseVideoTimeRemap = parseVideoTimeRemap; exports.parseVideoTrack = parseVideoTrack; exports.parseVideoTranscript = parseVideoTranscript; exports.parseVideoTranscriptWords = parseVideoTranscriptWords; exports.parseVideoTransform = parseVideoTransform; exports.parseVideoTransition = parseVideoTransition; exports.placeVideoRect = _chunk3OSXHLITcjs.placeVideoRect; exports.planVideoClipDrag = planVideoClipDrag; exports.planVideoEdits = planVideoEdits; exports.planVideoPrefetch = planVideoPrefetch; exports.planVideoToolPlayback = planVideoToolPlayback; exports.probeVideoMedia = probeVideoMedia; exports.readVideoScopesPanel = readVideoScopesPanel; exports.removeVideoKey = _chunk3OSXHLITcjs.removeVideoKey; exports.renderVideoFrameExact = _chunk3OSXHLITcjs.renderVideoFrameExact; exports.resolveVideoPreviewScale = resolveVideoPreviewScale; exports.retimeVideoAnimatable = _chunk3OSXHLITcjs.retimeVideoAnimatable; exports.setVideoKey = _chunk3OSXHLITcjs.setVideoKey; exports.snapToVideoFrame = _chunk3OSXHLITcjs.snapToVideoFrame; exports.snapVideoTime = _chunk3OSXHLITcjs.snapVideoTime; exports.sortVideoKeys = _chunk3OSXHLITcjs.sortVideoKeys; exports.sortedVideoClips = _chunk3OSXHLITcjs.sortedVideoClips; exports.splitVideoAnimatable = _chunk3OSXHLITcjs.splitVideoAnimatable; exports.summarizeVideoScopes = summarizeVideoScopes; exports.toVideoEdl = _chunk3OSXHLITcjs.toVideoEdl; exports.toVideoFfmpeg = _chunk3OSXHLITcjs.toVideoFfmpeg; exports.toVideoLoudnessMeasure = _chunk3OSXHLITcjs.toVideoLoudnessMeasure; exports.videoAddAdjustmentEdits = videoAddAdjustmentEdits; exports.videoAssetDropEdit = videoAssetDropEdit; exports.videoAssetHasSound = _chunk3OSXHLITcjs.videoAssetHasSound; exports.videoAssetKindOf = videoAssetKindOf; exports.videoAssetMap = _chunk3OSXHLITcjs.videoAssetMap; exports.videoAssetProxy = videoAssetProxy; exports.videoAudioLevels = videoAudioLevels; exports.videoAudioLoudness = videoAudioLoudness; exports.videoAudioPeakRange = videoAudioPeakRange; exports.videoAudioPeakSpans = videoAudioPeakSpans; exports.videoAudioPeaksDuration = videoAudioPeaksDuration; exports.videoAuthorableEffects = videoAuthorableEffects; exports.videoCacheEvictions = videoCacheEvictions; exports.videoChroma = videoChroma; exports.videoClipAfter = _chunk3OSXHLITcjs.videoClipAfter; exports.videoClipAnimated = _chunk3OSXHLITcjs.videoClipAnimated; exports.videoClipAnimationProperties = _chunk3OSXHLITcjs.videoClipAnimationProperties; exports.videoClipAnimationSpans = _chunk3OSXHLITcjs.videoClipAnimationSpans; exports.videoClipAppliedEffects = videoClipAppliedEffects; exports.videoClipBefore = _chunk3OSXHLITcjs.videoClipBefore; exports.videoClipEffectDocument = videoClipEffectDocument; exports.videoClipEffectUses = _chunk3OSXHLITcjs.videoClipEffectUses; exports.videoClipEnd = _chunk3OSXHLITcjs.videoClipEnd; exports.videoClipHandles = _chunk3OSXHLITcjs.videoClipHandles; exports.videoClipHeading = videoClipHeading; exports.videoClipKeyframes = videoClipKeyframes; exports.videoClipLabel = videoClipLabel; exports.videoClipOverhang = _chunk3OSXHLITcjs.videoClipOverhang; exports.videoClipShowsWaveform = videoClipShowsWaveform; exports.videoClipSoundGain = _chunk3OSXHLITcjs.videoClipSoundGain; exports.videoClipSourceRange = _chunk3OSXHLITcjs.videoClipSourceRange; exports.videoClipSourceTime = _chunk3OSXHLITcjs.videoClipSourceTime; exports.videoClipSpeed = _chunk3OSXHLITcjs.videoClipSpeed; exports.videoClipSpeedAt = _chunk3OSXHLITcjs.videoClipSpeedAt; exports.videoClipTint = videoClipTint; exports.videoClipTypeLabel = _chunk3OSXHLITcjs.videoClipTypeLabel; exports.videoClipWaveform = videoClipWaveform; exports.videoClipWaveformPath = videoClipWaveformPath; exports.videoClipsAt = _chunk3OSXHLITcjs.videoClipsAt; exports.videoContactSheetSvg = _chunk3OSXHLITcjs.videoContactSheetSvg; exports.videoCutsWithMargin = videoCutsWithMargin; exports.videoDragLabel = videoDragLabel; exports.videoDropTracks = videoDropTracks; exports.videoDuckingKeys = videoDuckingKeys; exports.videoEaseBezier = _chunk3OSXHLITcjs.videoEaseBezier; exports.videoEaseProgress = _chunk3OSXHLITcjs.videoEaseProgress; exports.videoEaseToAnimationEasing = videoEaseToAnimationEasing; exports.videoEffectFrameTime = _chunk3OSXHLITcjs.videoEffectFrameTime; exports.videoEffectLayerKey = _chunk3OSXHLITcjs.videoEffectLayerKey; exports.videoEffectValuesAt = _chunk3OSXHLITcjs.videoEffectValuesAt; exports.videoEffectValuesFromDocument = videoEffectValuesFromDocument; exports.videoExposureNotes = videoExposureNotes; exports.videoExposureStats = videoExposureStats; exports.videoFadeCurveGain = _chunk3OSXHLITcjs.videoFadeCurveGain; exports.videoFfmpegUnsupportedEffects = _chunk3OSXHLITcjs.videoFfmpegUnsupportedEffects; exports.videoFfmpegUnsupportedFeatures = _chunk3OSXHLITcjs.videoFfmpegUnsupportedFeatures; exports.videoFrameAt = _chunk3OSXHLITcjs.videoFrameAt; exports.videoFrameCacheKey = videoFrameCacheKey; exports.videoFrameDuration = _chunk3OSXHLITcjs.videoFrameDuration; exports.videoFrameLeafLayers = _chunk3OSXHLITcjs.videoFrameLeafLayers; exports.videoFrameRequestsAt = videoFrameRequestsAt; exports.videoFrameToSvg = _chunk3OSXHLITcjs.videoFrameToSvg; exports.videoFrames = _chunk3OSXHLITcjs.videoFrames; exports.videoFreezeFrameEdit = _chunk3OSXHLITcjs.videoFreezeFrameEdit; exports.videoGradeCssFilter = _chunk3OSXHLITcjs.videoGradeCssFilter; exports.videoGradeFfmpegFilters = _chunk3OSXHLITcjs.videoGradeFfmpegFilters; exports.videoGradeGainsFilterId = _chunk3OSXHLITcjs.videoGradeGainsFilterId; exports.videoGradeLook = _chunk3OSXHLITcjs.videoGradeLook; exports.videoGradePreset = _chunk3OSXHLITcjs.videoGradePreset; exports.videoGradeStepMatrix = _chunk3OSXHLITcjs.videoGradeStepMatrix; exports.videoGradeStepSvgValues = _chunk3OSXHLITcjs.videoGradeStepSvgValues; exports.videoGradeSteps = _chunk3OSXHLITcjs.videoGradeSteps; exports.videoGradeSvgDefs = _chunk3OSXHLITcjs.videoGradeSvgDefs; exports.videoGradeSvgFilter = _chunk3OSXHLITcjs.videoGradeSvgFilter; exports.videoHistogram = videoHistogram; exports.videoHistogramImage = videoHistogramImage; exports.videoHourFrames = _chunk3OSXHLITcjs.videoHourFrames; exports.videoIrisRadius = _chunk3OSXHLITcjs.videoIrisRadius; exports.videoIssueKey = videoIssueKey; exports.videoKeyframeBreakpoints = _chunk3OSXHLITcjs.videoKeyframeBreakpoints; exports.videoKeyframeFfmpegExpr = _chunk3OSXHLITcjs.videoKeyframeFfmpegExpr; exports.videoKeyframeRows = videoKeyframeRows; exports.videoKeyframeToggleEdit = videoKeyframeToggleEdit; exports.videoKeyframeValueEdit = videoKeyframeValueEdit; exports.videoKeysOf = _chunk3OSXHLITcjs.videoKeysOf; exports.videoLayerMask = _chunk3OSXHLITcjs.videoLayerMask; exports.videoLayersUseEffects = _chunk3OSXHLITcjs.videoLayersUseEffects; exports.videoLayersWithoutTransitionEffects = _chunk3OSXHLITcjs.videoLayersWithoutTransitionEffects; exports.videoLoudnessEnergy = videoLoudnessEnergy; exports.videoLoudnessGain = videoLoudnessGain; exports.videoLoudnessOf = videoLoudnessOf; exports.videoLuma = videoLuma; exports.videoLumaWaveform = videoLumaWaveform; exports.videoLutEdits = videoLutEdits; exports.videoLutFromFunction = _chunk3OSXHLITcjs.videoLutFromFunction; exports.videoLutSvgApproximation = _chunk3OSXHLITcjs.videoLutSvgApproximation; exports.videoLutSvgPrimitives = _chunk3OSXHLITcjs.videoLutSvgPrimitives; exports.videoLutToCube = _chunk3OSXHLITcjs.videoLutToCube; exports.videoMaskContains = _chunk3OSXHLITcjs.videoMaskContains; exports.videoMaskEdit = videoMaskEdit; exports.videoMaskPolygon = _chunk3OSXHLITcjs.videoMaskPolygon; exports.videoMaskSvgPath = _chunk3OSXHLITcjs.videoMaskSvgPath; exports.videoOverlap = _chunk3OSXHLITcjs.videoOverlap; exports.videoPeakDb = videoPeakDb; exports.videoPreviewSrc = videoPreviewSrc; exports.videoProjectDuration = _chunk3OSXHLITcjs.videoProjectDuration; exports.videoProjectEffects = _chunk3OSXHLITcjs.videoProjectEffects; exports.videoProjectHasProxies = videoProjectHasProxies; exports.videoProjectSvg = _chunk3OSXHLITcjs.videoProjectSvg; exports.videoProxyFfmpeg = videoProxyFfmpeg; exports.videoProxyName = videoProxyName; exports.videoProxyPlan = videoProxyPlan; exports.videoRemoveRangesEdits = videoRemoveRangesEdits; exports.videoRulerTicks = videoRulerTicks; exports.videoScopeImage = videoScopeImage; exports.videoSequenceChildren = _chunk3OSXHLITcjs.videoSequenceChildren; exports.videoSequenceDuration = _chunk3OSXHLITcjs.videoSequenceDuration; exports.videoSequenceMap = _chunk3OSXHLITcjs.videoSequenceMap; exports.videoShellQuote = _chunk3OSXHLITcjs.videoShellQuote; exports.videoSkinProtectionEdits = videoSkinProtectionEdits; exports.videoSkinToneWeight = _chunk3OSXHLITcjs.videoSkinToneWeight; exports.videoSlowMotionRamp = _chunk3OSXHLITcjs.videoSlowMotionRamp; exports.videoSnapPoints = _chunk3OSXHLITcjs.videoSnapPoints; exports.videoSourceFrame = videoSourceFrame; exports.videoSourceFrameTime = videoSourceFrameTime; exports.videoSpeechRanges = videoSpeechRanges; exports.videoSpeedRampEdit = _chunk3OSXHLITcjs.videoSpeedRampEdit; exports.videoSpeedRampKeys = _chunk3OSXHLITcjs.videoSpeedRampKeys; exports.videoTextStyle = _chunk3OSXHLITcjs.videoTextStyle; exports.videoTimeRef = videoTimeRef; exports.videoTimeRemapBreakpoints = _chunk3OSXHLITcjs.videoTimeRemapBreakpoints; exports.videoTimeRemapSpan = _chunk3OSXHLITcjs.videoTimeRemapSpan; exports.videoTimecodeFrames = _chunk3OSXHLITcjs.videoTimecodeFrames; exports.videoTimelineTranscript = videoTimelineTranscript; exports.videoTitleFrame = _chunk3OSXHLITcjs.videoTitleFrame; exports.videoTrackSyncLocked = _chunk3OSXHLITcjs.videoTrackSyncLocked; exports.videoTranscriptEdit = videoTranscriptEdit; exports.videoTranscriptTrack = videoTranscriptTrack; exports.videoTransformAt = _chunk3OSXHLITcjs.videoTransformAt; exports.videoTransitionAudioCurve = _chunk3OSXHLITcjs.videoTransitionAudioCurve; exports.videoTransitionEase = _chunk3OSXHLITcjs.videoTransitionEase; exports.videoTransitionHasDirection = videoTransitionHasDirection; exports.videoValueAt = _chunk3OSXHLITcjs.videoValueAt; exports.videoVectorscope = videoVectorscope; exports.videoVectorscopeAngle = videoVectorscopeAngle; exports.videoVectorscopeImage = videoVectorscopeImage; exports.videoVectorscopePoint = videoVectorscopePoint; exports.videoWaveformImage = videoWaveformImage; exports.videoWaveformPath = videoWaveformPath; exports.videoWipeRect = _chunk3OSXHLITcjs.videoWipeRect; exports.videoWordAt = videoWordAt; exports.videoWordsRange = videoWordsRange; exports.withVideoPlayback = withVideoPlayback;