@vosjs/studio-core 0.42.5 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3531,16 +3531,17 @@ function planAutoZoom(track, options) {
3531
3531
  const withoutDrags = (list) => list.map((cl) => cl.filter((c) => !dragPress.has(round2(c.t)))).filter((cl) => cl.length > 0);
3532
3532
  const clusters = withoutDrags(grouped.clusters);
3533
3533
  const surfaces = withoutDrags(grouped.surfaces);
3534
- const dragSpans = drags.map((d, i) => ({
3535
- id: `g${i}`,
3536
- in: round2(Math.max(0, d.t0 - lead)),
3537
- out: round2(d.t1 + hold),
3538
- level: clampZoomLevel(Math.min(Math.max(dragLevel, 1.1), maxLevel)),
3539
- cx: round2(d.nx),
3540
- cy: round2(d.ny),
3541
- focusMode: "auto",
3542
- source: "auto"
3534
+ const dragWork = drags.map((d) => ({
3535
+ in: Math.max(0, d.t0 - lead),
3536
+ out: d.t1 + hold,
3537
+ cx: d.nx,
3538
+ cy: d.ny,
3539
+ level: Math.min(Math.max(dragLevel, 1.1), maxLevel)
3543
3540
  }));
3541
+ for (let i = 0; i + 1 < dragWork.length; i++) {
3542
+ if (dragWork[i].out > dragWork[i + 1].in)
3543
+ dragWork[i].out = dragWork[i + 1].in;
3544
+ }
3544
3545
  const eligible = clusters.filter((c) => c.length >= minClusterClicks);
3545
3546
  const clickSpans = [];
3546
3547
  const dragReserved = surfaces.map((cluster, i) => {
@@ -3612,6 +3613,19 @@ function planAutoZoom(track, options) {
3612
3613
  if (typing[i].out > typing[i + 1].in) typing[i].out = typing[i + 1].in;
3613
3614
  }
3614
3615
  typing = typing.filter((t) => t.out - t.in >= 0.3);
3616
+ for (const w of [...clickSpans, ...typing]) cedeTo(w, dragWork);
3617
+ typing = typing.filter((t) => !t.dead && t.out - t.in >= 0.3);
3618
+ for (const z of clickSpans) if (z.out - z.in < 0.3) z.dead = true;
3619
+ const dragSpans = dragWork.filter((d) => d.out - d.in >= 0.3).map((d, i) => ({
3620
+ id: `g${i}`,
3621
+ in: round2(d.in),
3622
+ out: round2(d.out),
3623
+ level: clampZoomLevel(d.level),
3624
+ cx: round2(d.cx),
3625
+ cy: round2(d.cy),
3626
+ focusMode: "auto",
3627
+ source: "auto"
3628
+ }));
3615
3629
  const zSpans = clickSpans.filter((s) => !s.dead).sort((a, b) => a.in - b.in).map((s, i) => ({
3616
3630
  id: `z${i}`,
3617
3631
  in: round2(s.in),
@@ -3645,6 +3659,17 @@ function planAutoZoom(track, options) {
3645
3659
  ]).filter((d) => firstStep === void 0 || d.out > firstStep);
3646
3660
  return within([...spans, ...dwells]).sort((a, b) => a.in - b.in);
3647
3661
  }
3662
+ function cedeTo(w, keep) {
3663
+ for (const k of keep) {
3664
+ if (w.dead || w.in >= k.out || w.out <= k.in) continue;
3665
+ if (k.in <= w.in && k.out >= w.out) w.dead = true;
3666
+ else if (k.in > w.in && k.out < w.out) {
3667
+ if (k.in - w.in >= w.out - k.out) w.out = k.in;
3668
+ else w.in = k.out;
3669
+ } else if (k.in > w.in) w.out = k.in;
3670
+ else w.in = k.out;
3671
+ }
3672
+ }
3648
3673
  function typingSessions(track, width, height, typingGap) {
3649
3674
  const pings = track.filter((e) => e.type === "key").map((e) => ({ t: e.t / 1e3, rect: e.rect, x: e.x, y: e.y }));
3650
3675
  const all = [];
@@ -3791,7 +3816,7 @@ import {
3791
3816
  EASINGS,
3792
3817
  lerpArray as lerpArray2,
3793
3818
  sample as sample2,
3794
- segmentRate as segmentRate2,
3819
+ segmentRate as segmentRate3,
3795
3820
  sortKeyframes as sortKeyframes2,
3796
3821
  sourceToTimeline,
3797
3822
  splitBySpeed as splitBySpeed3,
@@ -4245,6 +4270,51 @@ function sourceAt(rated, t) {
4245
4270
  return null;
4246
4271
  }
4247
4272
 
4273
+ // src/lower/loudness.ts
4274
+ var NORMALIZE_MIN_LUFS = -30;
4275
+ var NORMALIZE_MAX_LUFS = -6;
4276
+ var NORMALIZE_PEAK_CEILING_DB = -1;
4277
+ var NORMALIZE_MAX_LIFT_DB = 24;
4278
+ var finite = (v) => typeof v === "number" && Number.isFinite(v);
4279
+ var clamp014 = (v) => Math.max(0, Math.min(1, v));
4280
+ function loudnessMatches(clip) {
4281
+ const l = clip.loudness;
4282
+ return !!l && typeof l === "object" && finite(l.lufs) && typeof l.src === "string" && l.src === clip.key;
4283
+ }
4284
+ function clipGain(clip) {
4285
+ const trim = finite(clip.gain) ? Math.max(0, clip.gain) : 1;
4286
+ if (!finite(clip.normalize)) return { gain: clamp014(trim), basis: "gain" };
4287
+ if (!clip.loudness || typeof clip.loudness !== "object")
4288
+ return { gain: clamp014(trim), basis: "unmeasured" };
4289
+ if (!loudnessMatches(clip)) return { gain: clamp014(trim), basis: "stale" };
4290
+ const l = clip.loudness;
4291
+ const target = Math.max(
4292
+ NORMALIZE_MIN_LUFS,
4293
+ Math.min(NORMALIZE_MAX_LUFS, clip.normalize)
4294
+ );
4295
+ const asked = Math.pow(10, (target - l.lufs) / 20) * trim;
4296
+ let gain = asked;
4297
+ let capped;
4298
+ const lift = Math.pow(10, NORMALIZE_MAX_LIFT_DB / 20);
4299
+ if (gain > lift) {
4300
+ gain = lift;
4301
+ capped = "lift";
4302
+ }
4303
+ if (finite(l.peakDb)) {
4304
+ const peak = Math.pow(10, (NORMALIZE_PEAK_CEILING_DB - l.peakDb) / 20);
4305
+ if (gain > peak) {
4306
+ gain = peak;
4307
+ capped = "peak";
4308
+ }
4309
+ }
4310
+ return {
4311
+ gain: Math.max(0, gain),
4312
+ basis: "normalize",
4313
+ asked,
4314
+ ...capped ? { capped } : {}
4315
+ };
4316
+ }
4317
+
4248
4318
  // src/lower/audioEnvelope.ts
4249
4319
  function clipEnvelope(clip) {
4250
4320
  const span = clipLength(clip);
@@ -4256,7 +4326,7 @@ function clipEnvelope(clip) {
4256
4326
  fi *= scale;
4257
4327
  fo *= scale;
4258
4328
  }
4259
- const g = Math.max(0, Math.min(1, clip.gain));
4329
+ const g = clipGain(clip).gain;
4260
4330
  const pts = [];
4261
4331
  pts.push({ t: clip.start, g: fi > 0 ? 0 : g });
4262
4332
  if (fi > 0) pts.push({ t: clip.start + fi, g });
@@ -4293,8 +4363,8 @@ function followFocusEvents(span, cursor, space, layout, options = {}) {
4293
4363
  }
4294
4364
  const pts = cursor.filter((e) => e.type === "move" || e.type === "down" || e.type === "up").map((e) => ({
4295
4365
  t: e.t / 1e3,
4296
- nx: clamp014(e.x / space.w),
4297
- ny: clamp014(e.y / space.h)
4366
+ nx: clamp015(e.x / space.w),
4367
+ ny: clamp015(e.y / space.h)
4298
4368
  }));
4299
4369
  if (!pts.length) return { entry: null, events: [] };
4300
4370
  const entry = clampFocus(span.cx, span.cy, level, layout, camera);
@@ -4372,7 +4442,7 @@ function sampleAt2(pts, t) {
4372
4442
  }
4373
4443
  return pts[pts.length - 1];
4374
4444
  }
4375
- function clamp014(v) {
4445
+ function clamp015(v) {
4376
4446
  return Math.max(0, Math.min(1, v));
4377
4447
  }
4378
4448
  function round4(v) {
@@ -4709,6 +4779,114 @@ function round42(v) {
4709
4779
  return Math.round(v * 1e5) / 1e5;
4710
4780
  }
4711
4781
 
4782
+ // src/lower/anchor.ts
4783
+ import { segmentRate } from "@vosjs/timeline";
4784
+ var ELAPSED_PLACEHOLDER = "{elapsed}";
4785
+ var EPS = 1e-9;
4786
+ function sourceMomentToOutput(rated, t, media) {
4787
+ if (!Number.isFinite(t)) return null;
4788
+ let acc = 0;
4789
+ let edge = null;
4790
+ for (const p of rated) {
4791
+ const rate = segmentRate(p);
4792
+ const len = Math.max(0, p.out - p.in) / rate;
4793
+ if (sameMedia(p.media, media)) {
4794
+ if (t >= p.in - EPS && t < p.out - EPS)
4795
+ return acc + Math.max(0, t - p.in) / rate;
4796
+ if (edge === null && Math.abs(t - p.out) <= 1e-6) edge = acc + len;
4797
+ }
4798
+ acc += len;
4799
+ }
4800
+ return edge;
4801
+ }
4802
+ var isNum = (v) => typeof v === "number" && Number.isFinite(v);
4803
+ function stepElapsedMs(meta, step) {
4804
+ const skips = Array.isArray(meta.skips) ? meta.skips : [];
4805
+ for (const raw of skips) {
4806
+ if (!raw || typeof raw !== "object") continue;
4807
+ const s = raw;
4808
+ if (!isNum(s.ms) || s.ms < 0) continue;
4809
+ const named = step.id !== void 0 && (s.id === step.id || s.step === step.id) || s.step === step.step;
4810
+ if (named) return s.ms;
4811
+ }
4812
+ const own = step.offCameraMs;
4813
+ return isNum(own) && own >= 0 ? own : null;
4814
+ }
4815
+ function elapsedFor(meta, step) {
4816
+ const own = stepElapsedMs(meta, step);
4817
+ if (own !== null) return { ms: own };
4818
+ const earlier = (meta.steps ?? []).filter((s) => s !== step && s.step < step.step).sort((a, b) => b.step - a.step);
4819
+ for (const s of earlier) {
4820
+ const ms = stepElapsedMs(meta, s);
4821
+ if (ms !== null) return { ms, from: s };
4822
+ }
4823
+ return null;
4824
+ }
4825
+ function resolveAnchor(doc, anchor, rated) {
4826
+ const source = mediaSource(doc, anchor.media);
4827
+ if (!source) return { start: null, miss: "media" };
4828
+ const meta = source.meta;
4829
+ const step = findStep(meta.steps ?? [], anchor.step);
4830
+ if (!step) return { start: null, miss: "step" };
4831
+ const elapsed = elapsedFor(meta, step);
4832
+ const extra = elapsed ? {
4833
+ elapsedMs: elapsed.ms,
4834
+ ...elapsed.from ? { elapsedFrom: elapsed.from } : {}
4835
+ } : {};
4836
+ if (step.skipped) return { start: null, step, miss: "skipped", ...extra };
4837
+ const moment = anchor.at === "end" ? step.tEnd : step.tStart;
4838
+ const out = sourceMomentToOutput(rated, moment, anchor.media);
4839
+ if (out === null) return { start: null, step, miss: "cut", ...extra };
4840
+ const offset = isNum(anchor.offset) ? anchor.offset : 0;
4841
+ return { start: round33(Math.max(0, out + offset)), step, ...extra };
4842
+ }
4843
+ var round33 = (v) => Math.round(v * 1e3) / 1e3;
4844
+ function formatElapsed(ms) {
4845
+ const s = Math.max(0, ms) / 1e3;
4846
+ const plural = (n, unit) => `${n} ${unit}${n === 1 ? "" : "s"}`;
4847
+ if (s < 9.5) return plural(Math.max(1, Math.round(s)), "second");
4848
+ if (s < 57.5) return plural(Math.round(s / 5) * 5, "second");
4849
+ const m = s / 60;
4850
+ if (m < 59.5) return plural(Math.max(1, Math.round(m)), "minute");
4851
+ const h = m / 60;
4852
+ if (h < 47.5) return plural(Math.max(1, Math.round(h)), "hour");
4853
+ return plural(Math.round(h / 24), "day");
4854
+ }
4855
+ function hasElapsed(text) {
4856
+ if (typeof text === "string") return text.includes(ELAPSED_PLACEHOLDER);
4857
+ return Array.isArray(text) && text.some(
4858
+ (r) => typeof r?.text === "string" && r.text.includes(ELAPSED_PLACEHOLDER)
4859
+ );
4860
+ }
4861
+ function fillElapsed(text, words) {
4862
+ const fill = (s) => s.split(ELAPSED_PLACEHOLDER).join(words);
4863
+ if (typeof text === "string") return fill(text);
4864
+ return text.map(
4865
+ (r) => typeof r.text === "string" && r.text.includes(ELAPSED_PLACEHOLDER) ? { ...r, text: fill(r.text) } : r
4866
+ );
4867
+ }
4868
+ function anchoredClip(clip, doc, rated) {
4869
+ if (!clip.anchor || typeof clip.anchor !== "object") return clip;
4870
+ const r = resolveAnchor(doc, clip.anchor, rated);
4871
+ let next = clip;
4872
+ if (r.start !== null && r.start !== clip.start)
4873
+ next = { ...next, start: r.start };
4874
+ if (next.kind === "text" && r.elapsedMs !== void 0 && hasElapsed(next.text)) {
4875
+ next = { ...next, text: fillElapsed(next.text, formatElapsed(r.elapsedMs)) };
4876
+ }
4877
+ return next;
4878
+ }
4879
+ function resolveOverlayAnchors(doc, rated) {
4880
+ const overlays = Array.isArray(doc.overlays) ? doc.overlays : [];
4881
+ if (!overlays.some((o) => o && o.anchor)) return doc;
4882
+ return {
4883
+ ...doc,
4884
+ overlays: overlays.map(
4885
+ (o) => o && o.anchor ? anchoredClip(o, doc, rated) : o
4886
+ )
4887
+ };
4888
+ }
4889
+
4712
4890
  // src/lower/cursorIdle.ts
4713
4891
  var CURSOR_IDLE_HOLD = 1;
4714
4892
  var CURSOR_IDLE_FADE_OUT = 0.35;
@@ -6264,7 +6442,7 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
6264
6442
  }`;
6265
6443
 
6266
6444
  // src/lower/extractClicks.ts
6267
- import { segmentRate } from "@vosjs/timeline";
6445
+ import { segmentRate as segmentRate2 } from "@vosjs/timeline";
6268
6446
  var CLICK_FX_PRE = 0.06;
6269
6447
  var CLICK_RIPPLE_DUR = 0.45;
6270
6448
  var CLICK_PULSE_DUR = 0.35;
@@ -6277,7 +6455,7 @@ function outputAt(segments, sourceT, media) {
6277
6455
  if (!segments.length) return sourceT;
6278
6456
  let acc = 0;
6279
6457
  for (const s of segments) {
6280
- const rate = segmentRate(s);
6458
+ const rate = segmentRate2(s);
6281
6459
  if (sameMedia(s.media, media) && sourceT >= s.in && sourceT <= s.out)
6282
6460
  return acc + (sourceT - s.in) / rate;
6283
6461
  acc += Math.max(0, s.out - s.in) / rate;
@@ -6288,7 +6466,7 @@ function keptOutputEnd(segments, sIn, sOut, media) {
6288
6466
  let acc = 0;
6289
6467
  let end = null;
6290
6468
  for (const p of segments) {
6291
- const rate = segmentRate(p);
6469
+ const rate = segmentRate2(p);
6292
6470
  if (sameMedia(p.media, media)) {
6293
6471
  const ovIn = Math.max(sIn, p.in);
6294
6472
  const ovOut = Math.min(sOut, p.out);
@@ -6401,7 +6579,7 @@ function spanOutputExtent(segments, sIn, sOut, media) {
6401
6579
  let start = null;
6402
6580
  let end = null;
6403
6581
  for (const p of segments) {
6404
- const rate = segmentRate2(p);
6582
+ const rate = segmentRate3(p);
6405
6583
  const len = Math.max(0, p.out - p.in) / rate;
6406
6584
  if (!sameMedia(p.media, media)) {
6407
6585
  acc += len;
@@ -8623,17 +8801,22 @@ function studioLayerData(input, duration, pins) {
8623
8801
  // preview scheduler and the export's offline mix). `duckEnv` (the mic-derived
8624
8802
  // duck multiplier curve) is merged in asynchronously by useComposition — it
8625
8803
  // needs a decoded recording, which a sync lowering can't produce.
8626
- audio: (layers.audio ?? []).map((c) => ({
8627
- key: c.key,
8628
- start: round8(c.start),
8629
- in: round8(c.in),
8630
- out: round8(c.out),
8631
- gain: round8(c.gain),
8632
- loop: !!c.loop,
8633
- len: round8(clipLength(c)),
8634
- duck: !!c.duck,
8635
- env: clipEnvelope(c).map((p) => ({ t: round8(p.t), g: round8(p.g) }))
8636
- })),
8804
+ // A measured `normalize` sets the gain (it may exceed 1); a clip with
8805
+ // no target keeps its own, byte for byte.
8806
+ audio: (layers.audio ?? []).map((c) => {
8807
+ const g = clipGain(c);
8808
+ return {
8809
+ key: c.key,
8810
+ start: round8(c.start),
8811
+ in: round8(c.in),
8812
+ out: round8(c.out),
8813
+ gain: round8(g.basis === "normalize" ? g.gain : c.gain),
8814
+ loop: !!c.loop,
8815
+ len: round8(clipLength(c)),
8816
+ duck: !!c.duck,
8817
+ env: clipEnvelope(c).map((p) => ({ t: round8(p.t), g: round8(p.g) }))
8818
+ };
8819
+ }),
8637
8820
  // Object clips: numbers resolved HERE; shapes are the drafted engine spec.
8638
8821
  ...layers.objects && layers.objects.length ? {
8639
8822
  objects: layers.objects.map((o) => ({
@@ -8851,8 +9034,9 @@ function studioLayerData(input, duration, pins) {
8851
9034
  };
8852
9035
  }
8853
9036
  function lowerToComposition(input) {
8854
- const doc = migrateText(migrateMotion(input));
8855
- const rated = ratedSegments(doc);
9037
+ const read = migrateText(migrateMotion(input));
9038
+ const rated = ratedSegments(read);
9039
+ const doc = resolveOverlayAnchors(read, rated);
8856
9040
  const transitions = docTransitions(doc);
8857
9041
  const footageEnd = durationSec(doc, rated);
8858
9042
  const duration = outputEnd(doc, footageEnd);
@@ -9066,8 +9250,9 @@ function loweredZoomSpans(doc, rated, transitions, layout, camera, zoomStyle) {
9066
9250
  });
9067
9251
  }
9068
9252
  function docZoomTrack(input) {
9069
- const doc = migrateText(migrateMotion(input));
9070
- const rated = ratedSegments(doc);
9253
+ const read = migrateText(migrateMotion(input));
9254
+ const rated = ratedSegments(read);
9255
+ const doc = resolveOverlayAnchors(read, rated);
9071
9256
  const transitions = docTransitions(doc);
9072
9257
  const layout = docCardLayout(doc);
9073
9258
  const camera = cameraModel(doc.frame);
@@ -9795,14 +9980,16 @@ import { totalDuration as totalDuration5 } from "@vosjs/timeline";
9795
9980
 
9796
9981
  // src/audioBeds.ts
9797
9982
  import { totalDuration as totalDuration4 } from "@vosjs/timeline";
9798
- var round33 = (v) => Math.round(v * 1e3) / 1e3;
9983
+ var round34 = (v) => Math.round(v * 1e3) / 1e3;
9799
9984
  function docOutputDuration(doc) {
9800
- const footage = totalDuration4(ratedSegments(doc));
9801
- return isRecordingDoc(doc) ? outputEnd(migrateMotion(doc), footage) : footage;
9985
+ const rated = ratedSegments(doc);
9986
+ const footage = totalDuration4(rated);
9987
+ if (!isRecordingDoc(doc)) return footage;
9988
+ return outputEnd(resolveOverlayAnchors(migrateMotion(doc), rated), footage);
9802
9989
  }
9803
9990
  function musicBedClip(input) {
9804
- const track = round33(input.trackDuration);
9805
- const output = round33(input.outputDuration);
9991
+ const track = round34(input.trackDuration);
9992
+ const output = round34(input.outputDuration);
9806
9993
  const clip = {
9807
9994
  id: input.id,
9808
9995
  key: input.key,
@@ -9817,7 +10004,7 @@ function musicBedClip(input) {
9817
10004
  fadeIn: 0,
9818
10005
  // A bed that just stops reads as a glitch; clamp so a very short cut
9819
10006
  // still spends most of its time at full level.
9820
- fadeOut: output > 0 ? Math.min(1.5, round33(output / 4)) : 1.5,
10007
+ fadeOut: output > 0 ? Math.min(1.5, round34(output / 4)) : 1.5,
9821
10008
  duck: input.hasMic || void 0
9822
10009
  };
9823
10010
  if (output <= 0) return clip;
@@ -9836,28 +10023,28 @@ function isMusicBed(clip, outputDuration) {
9836
10023
  return Math.abs(placed - outputDuration) <= 1;
9837
10024
  }
9838
10025
  function refillAudioBeds(doc, prevDuration, nextDuration) {
9839
- const EPS2 = 0.05;
9840
- if (Math.abs(nextDuration - prevDuration) <= EPS2 || nextDuration <= 0) {
10026
+ const EPS3 = 0.05;
10027
+ if (Math.abs(nextDuration - prevDuration) <= EPS3 || nextDuration <= 0) {
9841
10028
  return false;
9842
10029
  }
9843
10030
  let changed = false;
9844
10031
  for (const clip of doc.audio) {
9845
10032
  if (clip.start >= nextDuration) continue;
9846
10033
  const placed = clip.loop ? Math.max(clip.out - clip.in, clip.loopLen ?? clip.out - clip.in) : clip.out - clip.in;
9847
- if (Math.abs(clip.start + placed - prevDuration) > EPS2) continue;
10034
+ if (Math.abs(clip.start + placed - prevDuration) > EPS3) continue;
9848
10035
  const span = clip.out - clip.in;
9849
- const nextLen = round33(nextDuration - clip.start);
10036
+ const nextLen = round34(nextDuration - clip.start);
9850
10037
  if (clip.loop) {
9851
10038
  if (nextLen >= span) {
9852
10039
  clip.loopLen = nextLen;
9853
10040
  } else {
9854
10041
  clip.loop = void 0;
9855
10042
  clip.loopLen = void 0;
9856
- clip.out = round33(clip.in + nextLen);
10043
+ clip.out = round34(clip.in + nextLen);
9857
10044
  }
9858
10045
  } else {
9859
10046
  if (clip.in + nextLen <= clip.duration) {
9860
- clip.out = round33(clip.in + nextLen);
10047
+ clip.out = round34(clip.in + nextLen);
9861
10048
  } else {
9862
10049
  clip.out = clip.duration;
9863
10050
  clip.loop = true;
@@ -10102,7 +10289,7 @@ function applyTemplate(template, take, opts) {
10102
10289
  const skipped = [];
10103
10290
  for (const clip of template.overlays ?? []) {
10104
10291
  const next = structuredClone(clip);
10105
- next.start = Math.max(0, round34(clip.start + shift));
10292
+ next.start = Math.max(0, round35(clip.start + shift));
10106
10293
  next.from = from;
10107
10294
  const mine = own.get(clip.id);
10108
10295
  if (next.kind === "text") {
@@ -10131,7 +10318,7 @@ function applyTemplate(template, take, opts) {
10131
10318
  for (const clip of template.objects ?? []) {
10132
10319
  const next = structuredClone(clip);
10133
10320
  if (next.span)
10134
- next.span.start = Math.max(0, round34(next.span.start + shift));
10321
+ next.span.start = Math.max(0, round35(next.span.start + shift));
10135
10322
  next.from = from;
10136
10323
  objects.push(next);
10137
10324
  }
@@ -10141,7 +10328,7 @@ function applyTemplate(template, take, opts) {
10141
10328
  );
10142
10329
  for (const clip of template.audio ?? []) {
10143
10330
  const next = structuredClone(clip);
10144
- next.start = Math.max(0, round34(clip.start + shift));
10331
+ next.start = Math.max(0, round35(clip.start + shift));
10145
10332
  next.from = from;
10146
10333
  audio.push(next);
10147
10334
  }
@@ -10162,7 +10349,7 @@ function applyTemplate(template, take, opts) {
10162
10349
  }
10163
10350
  return { doc, notes };
10164
10351
  }
10165
- var round34 = (v) => Math.round(v * 1e3) / 1e3;
10352
+ var round35 = (v) => Math.round(v * 1e3) / 1e3;
10166
10353
 
10167
10354
  // src/digest/geometry.ts
10168
10355
  var CROP_MIN_FRAC = 0.25;
@@ -10345,15 +10532,15 @@ function outputRangeToSource(doc, t0, t1) {
10345
10532
  srcOut: mapTime2(rated, Math.max(0, t1))
10346
10533
  };
10347
10534
  }
10348
- var EPS = 1e-6;
10535
+ var EPS2 = 1e-6;
10349
10536
  var MIN_REMAINDER = 0.25;
10350
10537
  var MIN_SEGMENT = 0.05;
10351
10538
  function setSpeedInRange(spans, srcIn, srcOut, rate) {
10352
- if (srcOut - srcIn < EPS) return [...spans];
10539
+ if (srcOut - srcIn < EPS2) return [...spans];
10353
10540
  const kept = [];
10354
10541
  const used = new Set(spans.map((s) => s.id));
10355
10542
  for (const s of spans) {
10356
- if (s.out <= srcIn + EPS || s.in >= srcOut - EPS) {
10543
+ if (s.out <= srcIn + EPS2 || s.in >= srcOut - EPS2) {
10357
10544
  kept.push(s);
10358
10545
  continue;
10359
10546
  }
@@ -10378,12 +10565,12 @@ function setSpeedInRange(spans, srcIn, srcOut, rate) {
10378
10565
  return kept.sort((a, b) => a.in - b.in);
10379
10566
  }
10380
10567
  function removeSpeedInRange(spans, srcIn, srcOut) {
10381
- return spans.filter((s) => s.out <= srcIn + EPS || s.in >= srcOut - EPS);
10568
+ return spans.filter((s) => s.out <= srcIn + EPS2 || s.in >= srcOut - EPS2);
10382
10569
  }
10383
10570
  function removeSourceRange(segments, srcIn, srcOut) {
10384
10571
  const next = [];
10385
10572
  for (const seg of segments) {
10386
- if (seg.out <= srcIn + EPS || seg.in >= srcOut - EPS) {
10573
+ if (seg.out <= srcIn + EPS2 || seg.in >= srcOut - EPS2) {
10387
10574
  next.push(seg);
10388
10575
  continue;
10389
10576
  }
@@ -10395,11 +10582,11 @@ function removeSourceRange(segments, srcIn, srcOut) {
10395
10582
  }
10396
10583
  function zoomSpanForRange(zoom, srcIn, srcOut) {
10397
10584
  let start = srcIn;
10398
- const covering = zoom.find((z) => z.in <= start + EPS && z.out > start + EPS);
10585
+ const covering = zoom.find((z) => z.in <= start + EPS2 && z.out > start + EPS2);
10399
10586
  if (covering) start = covering.out;
10400
10587
  let end = srcOut;
10401
10588
  for (const z of zoom) {
10402
- if (z.in >= start - EPS && z.in < end) end = z.in;
10589
+ if (z.in >= start - EPS2 && z.in < end) end = z.in;
10403
10590
  }
10404
10591
  if (end - start < ZOOM_SPAN_MIN) return null;
10405
10592
  const used = new Set(zoom.map((z) => z.id));
@@ -10808,7 +10995,7 @@ function applyLook(frame, look, size, video, placement) {
10808
10995
  import {
10809
10996
  mapTime as mapTime3,
10810
10997
  removeSegment,
10811
- segmentRate as segmentRate3,
10998
+ segmentRate as segmentRate4,
10812
10999
  sourceToTimeline as sourceToTimeline2,
10813
11000
  splitBySpeed as splitBySpeed4,
10814
11001
  totalDuration as totalDuration7,
@@ -11214,10 +11401,10 @@ function cursorFocusAt(doc, srcT) {
11214
11401
  let best = track[0];
11215
11402
  for (const e of track)
11216
11403
  if (Math.abs(e.t - ms) < Math.abs(best.t - ms)) best = e;
11217
- const clamp015 = (v) => Math.max(0, Math.min(1, v));
11404
+ const clamp016 = (v) => Math.max(0, Math.min(1, v));
11218
11405
  return {
11219
- cx: round13(clamp015(best.x / width)),
11220
- cy: round13(clamp015(best.y / height))
11406
+ cx: round13(clamp016(best.x / width)),
11407
+ cy: round13(clamp016(best.y / height))
11221
11408
  };
11222
11409
  }
11223
11410
  var tiltLane = {
@@ -11549,7 +11736,7 @@ var micLane = {
11549
11736
  var spanDefaultLen = (sourceDuration) => Math.max(1, sourceDuration * 0.05);
11550
11737
  function rateAt(rated, srcT) {
11551
11738
  for (const p of rated)
11552
- if (srcT >= p.in - 1e-9 && srcT < p.out + 1e-9) return segmentRate3(p);
11739
+ if (srcT >= p.in - 1e-9 && srcT < p.out + 1e-9) return segmentRate4(p);
11553
11740
  return 1;
11554
11741
  }
11555
11742
  function clampToKept(kept, newIn, len) {
@@ -11579,7 +11766,7 @@ var speedLane = {
11579
11766
  let start = null;
11580
11767
  let end = 0;
11581
11768
  for (const p of rated) {
11582
- const len = Math.max(0, p.out - p.in) / segmentRate3(p);
11769
+ const len = Math.max(0, p.out - p.in) / segmentRate4(p);
11583
11770
  if (p.rate === sp.rate && p.in >= sp.in - 1e-9 && p.out <= sp.out + 1e-9) {
11584
11771
  if (start === null) start = acc;
11585
11772
  end = acc + len;
@@ -12085,6 +12272,133 @@ function duckCurve(rms, segments, durationSec2, opts = DEFAULT_DUCK) {
12085
12272
  return points;
12086
12273
  }
12087
12274
 
12275
+ // src/docFeatures.ts
12276
+ var overlaysOf = (doc) => Array.isArray(doc.overlays) ? doc.overlays : [];
12277
+ var audioOf = (doc) => Array.isArray(doc.audio) ? doc.audio : [];
12278
+ var anchored = (o) => !!o && !!o.anchor && typeof o.anchor === "object";
12279
+ function bakeAnchors(doc) {
12280
+ const layers = overlaysOf(doc);
12281
+ if (!layers.some(anchored)) return { doc, notes: [] };
12282
+ const notes = [];
12283
+ let resolved = doc;
12284
+ if (isRecordingDoc(doc)) {
12285
+ const rated = ratedSegments(migrateMotion(doc));
12286
+ resolved = resolveOverlayAnchors(doc, rated);
12287
+ for (const o of layers) {
12288
+ if (!anchored(o) || !o.anchor) continue;
12289
+ const r = resolveAnchor(doc, o.anchor, rated);
12290
+ if (r.start === null)
12291
+ notes.push(
12292
+ `overlay ${o.id}: its step ${r.miss === "cut" ? "is cut out of the film" : r.miss === "skipped" ? "was skipped by the recorder" : "is not in the take"}, so it plays at its fallback start (${o.start.toFixed(2)}s)`
12293
+ );
12294
+ }
12295
+ }
12296
+ const overlays = overlaysOf(resolved).map((o) => {
12297
+ if (!anchored(o)) return o;
12298
+ const { anchor: _anchor, ...rest } = o;
12299
+ return rest;
12300
+ });
12301
+ return { doc: { ...resolved, overlays }, notes };
12302
+ }
12303
+ var DOC_FEATURES = [
12304
+ {
12305
+ id: "text-runs",
12306
+ since: "0.38.0",
12307
+ name: "styled text runs",
12308
+ before: "would draw those layers without their words",
12309
+ uses: (doc) => overlaysOf(doc).filter((o) => o?.kind === "text" && Array.isArray(o.text)).length
12310
+ },
12311
+ {
12312
+ id: "overlay-anchor",
12313
+ since: "0.43.0",
12314
+ name: "caption anchors",
12315
+ before: "would show them at their fallback start",
12316
+ uses: (doc) => overlaysOf(doc).filter(anchored).length,
12317
+ bake: bakeAnchors
12318
+ },
12319
+ {
12320
+ id: "text-elapsed",
12321
+ since: "0.43.0",
12322
+ name: "{elapsed} in text",
12323
+ before: "would show the placeholder as it is written",
12324
+ // Filled only on an anchored layer: anywhere else every release shows
12325
+ // the placeholder as written.
12326
+ uses: (doc) => overlaysOf(doc).filter(
12327
+ (o) => anchored(o) && o.kind === "text" && hasElapsed(o.text)
12328
+ ).length,
12329
+ // The anchors' bake fills it.
12330
+ bake: bakeAnchors
12331
+ },
12332
+ {
12333
+ id: "audio-normalize",
12334
+ since: "0.44.0",
12335
+ name: "loudness targets on sound",
12336
+ before: "would play those clips at their gain alone",
12337
+ // Only a target that applies: one with no measurement for its key is
12338
+ // ignored by every release alike.
12339
+ uses: (doc) => audioOf(doc).filter((a) => a && clipGain(a).basis === "normalize").length,
12340
+ bake: (doc) => {
12341
+ const notes = [];
12342
+ const audio = audioOf(doc).map((a) => {
12343
+ if (!a || a.normalize === void 0) return a;
12344
+ const { normalize: _normalize, ...rest } = a;
12345
+ const g = clipGain(a);
12346
+ if (g.basis !== "normalize") return rest;
12347
+ const gain = Math.round(Math.min(1, g.gain) * 1e3) / 1e3;
12348
+ if (g.gain > 1)
12349
+ notes.push(
12350
+ `audio ${a.id}: its target asks for gain ${g.gain.toFixed(2)}, which an older renderer cannot play; baked at 1, so it plays ${(20 * Math.log10(g.gain)).toFixed(1)} dB quieter than asked`
12351
+ );
12352
+ return { ...rest, gain };
12353
+ });
12354
+ return { doc: { ...doc, audio }, notes };
12355
+ }
12356
+ }
12357
+ ];
12358
+ function compareVersions(a, b) {
12359
+ const parts = (v) => v.trim().replace(/^v/, "").split(/[-+]/)[0].split(".").map((n) => parseInt(n, 10) || 0);
12360
+ const x = parts(a);
12361
+ const y = parts(b);
12362
+ for (let i = 0; i < Math.max(x.length, y.length, 3); i++) {
12363
+ const d = (x[i] ?? 0) - (y[i] ?? 0);
12364
+ if (d) return d < 0 ? -1 : 1;
12365
+ }
12366
+ return 0;
12367
+ }
12368
+ function docFeatures(doc) {
12369
+ const out = [];
12370
+ for (const feature of DOC_FEATURES) {
12371
+ const count = feature.uses(doc);
12372
+ if (count > 0) out.push({ feature, count });
12373
+ }
12374
+ return out;
12375
+ }
12376
+ function featuresNewerThan(doc, version) {
12377
+ return docFeatures(doc).filter(
12378
+ (u) => compareVersions(u.feature.since, version) > 0
12379
+ );
12380
+ }
12381
+ function bakeDocFeatures(doc, version) {
12382
+ let next = structuredClone(doc);
12383
+ const baked = [];
12384
+ const unbakeable = [];
12385
+ const notes = [];
12386
+ const done = /* @__PURE__ */ new Set();
12387
+ for (const { feature } of featuresNewerThan(doc, version)) {
12388
+ if (!feature.bake) {
12389
+ unbakeable.push(feature);
12390
+ continue;
12391
+ }
12392
+ baked.push(feature);
12393
+ if (done.has(feature.bake)) continue;
12394
+ done.add(feature.bake);
12395
+ const r = feature.bake(next);
12396
+ next = r.doc;
12397
+ notes.push(...r.notes);
12398
+ }
12399
+ return { doc: next, baked, unbakeable, notes };
12400
+ }
12401
+
12088
12402
  // src/backdrop.ts
12089
12403
  function backdropMedia(backdrop) {
12090
12404
  return {
@@ -12205,10 +12519,12 @@ export {
12205
12519
  DIGEST_CROP_MAX,
12206
12520
  DIGEST_FULL_MAX,
12207
12521
  DIGEST_VERSION,
12522
+ DOC_FEATURES,
12208
12523
  DOC_SCHEMA_VERSION,
12209
12524
  DRAG_FIT_LEVEL,
12210
12525
  DRAG_MIN_S,
12211
12526
  DRAG_MIN_TRAVEL_FRAC,
12527
+ ELAPSED_PLACEHOLDER,
12212
12528
  END_CARD_FROM,
12213
12529
  END_CARD_RECEDE,
12214
12530
  END_CARD_SECONDS,
@@ -12245,6 +12561,10 @@ export {
12245
12561
  MIXED,
12246
12562
  MOTION_DELTA,
12247
12563
  MOTION_EASE,
12564
+ NORMALIZE_MAX_LIFT_DB,
12565
+ NORMALIZE_MAX_LUFS,
12566
+ NORMALIZE_MIN_LUFS,
12567
+ NORMALIZE_PEAK_CEILING_DB,
12248
12568
  OBJECT_DEFAULT_SCALE,
12249
12569
  OVERLAY_FONT_FACES,
12250
12570
  OVERLAY_LINE_HEIGHT,
@@ -12319,12 +12639,14 @@ export {
12319
12639
  ambientDrift,
12320
12640
  anchorKindOf,
12321
12641
  anchorSourceDuration,
12642
+ anchoredClip,
12322
12643
  animStep,
12323
12644
  applyLook,
12324
12645
  applyTemplate,
12325
12646
  aspectRatioValue,
12326
12647
  audioLane,
12327
12648
  backdropMedia,
12649
+ bakeDocFeatures,
12328
12650
  boldWeightFor,
12329
12651
  buildDigest,
12330
12652
  calloutBox,
@@ -12359,9 +12681,11 @@ export {
12359
12681
  clampZoomLevel,
12360
12682
  clearStyle,
12361
12683
  clipEnvelope,
12684
+ clipGain,
12362
12685
  clipLength,
12363
12686
  clipsFrom,
12364
12687
  commonStyle,
12688
+ compareVersions,
12365
12689
  computeCardLayout,
12366
12690
  computeMicRms,
12367
12691
  computePeaks,
@@ -12382,6 +12706,7 @@ export {
12382
12706
  destinationsForChannel,
12383
12707
  diffInput,
12384
12708
  docCardLayout,
12709
+ docFeatures,
12385
12710
  docFreezes,
12386
12711
  docMediaList,
12387
12712
  docOutputDuration,
@@ -12395,6 +12720,7 @@ export {
12395
12720
  dropTemplate,
12396
12721
  duckCurve,
12397
12722
  effectiveSegments,
12723
+ elapsedFor,
12398
12724
  endCardClips,
12399
12725
  enterKey,
12400
12726
  enterOf,
@@ -12410,13 +12736,16 @@ export {
12410
12736
  expectedFrameSize,
12411
12737
  exportSizeFor,
12412
12738
  extractClicks,
12739
+ featuresNewerThan,
12413
12740
  findStep,
12414
12741
  focusBounds,
12415
12742
  focusForViewportCentre,
12416
12743
  followFocusEvents,
12744
+ formatElapsed,
12417
12745
  frameGeometry,
12418
12746
  freezeLane,
12419
12747
  freezeOutputExtents,
12748
+ hasElapsed,
12420
12749
  hexToRgb,
12421
12750
  hexToRgbTriplet,
12422
12751
  houseLook,
@@ -12455,6 +12784,7 @@ export {
12455
12784
  liveZoomStyleName,
12456
12785
  lookFromBrand,
12457
12786
  lookKindForGround,
12787
+ loudnessMatches,
12458
12788
  lowerProgramDoc,
12459
12789
  lowerStudioDoc,
12460
12790
  lowerToComposition,
@@ -12532,7 +12862,9 @@ export {
12532
12862
  removeSourceRange,
12533
12863
  removeSpeedInRange,
12534
12864
  replaceRange,
12865
+ resolveAnchor,
12535
12866
  resolveExportSize,
12867
+ resolveOverlayAnchors,
12536
12868
  resolveOverlayBox,
12537
12869
  resolveOverlayStyle,
12538
12870
  resolvePins,
@@ -12556,8 +12888,10 @@ export {
12556
12888
  smoothCursor,
12557
12889
  snapOffset,
12558
12890
  snapRunWeight,
12891
+ sourceMomentToOutput,
12559
12892
  spanOutputExtent,
12560
12893
  speedLane,
12894
+ stepElapsedMs,
12561
12895
  studioAudioPlan,
12562
12896
  studioEntry,
12563
12897
  studioLayerData,