@vosjs/studio-core 0.41.1 → 0.42.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
@@ -1523,27 +1523,30 @@ function placeFreezes(rated, freezes) {
1523
1523
  acc += Math.max(0, p.out - p.in) / (p.rate ?? 1);
1524
1524
  };
1525
1525
  for (let i = 0; i < rated.length; i++) {
1526
- let piece = rated[i];
1526
+ const piece = rated[i];
1527
1527
  const next = rated[i + 1];
1528
1528
  const continues = !!next && sameMedia(next.media, piece.media) && Math.abs(next.in - piece.out) < 1e-9 && next.out > piece.out;
1529
1529
  const mine = freezes.filter(
1530
1530
  (f) => f.seconds > 0 && !placed.has(f.id) && sameMedia(f.media, piece.media) && f.at >= piece.in - 1e-9 && (continues ? f.at < piece.out - 1e-9 : f.at <= piece.out + 1e-9)
1531
1531
  ).sort((a, b) => a.at - b.at);
1532
+ const media = piece.media;
1533
+ let rest = piece;
1532
1534
  for (const f of mine) {
1533
- if (f.at <= piece.in + 1e-9) {
1534
- freezePiece(f, piece.media);
1535
- } else if (f.at < piece.out - 1e-9) {
1536
- push2({ ...piece, out: f.at });
1537
- freezePiece(f, piece.media);
1538
- piece = { ...piece, in: f.at };
1535
+ if (!rest) {
1536
+ freezePiece(f, media);
1537
+ } else if (f.at <= rest.in + 1e-9) {
1538
+ freezePiece(f, media);
1539
+ } else if (f.at < rest.out - 1e-9) {
1540
+ push2({ ...rest, out: f.at });
1541
+ freezePiece(f, media);
1542
+ rest = { ...rest, in: f.at };
1539
1543
  } else {
1540
- const media = piece.media;
1541
- push2(piece);
1542
- piece = null;
1544
+ push2(rest);
1545
+ rest = null;
1543
1546
  freezePiece(f, media);
1544
1547
  }
1545
1548
  }
1546
- if (piece) push2(piece);
1549
+ if (rest) push2(rest);
1547
1550
  }
1548
1551
  return { pieces, marks };
1549
1552
  }
@@ -3400,6 +3403,13 @@ var TYPING_CLICK_ABSORB = 1.5;
3400
3403
  var TYPING_REFOCUS_FRAC = 0.08;
3401
3404
  function groupTrack(track, opts) {
3402
3405
  const { width, height, clusterGap, typingGap, typingZoom, targetFill } = opts;
3406
+ const stepOf = (t) => opts.steps?.find(
3407
+ (st) => st.do !== "wait" && !st.skipped && st.tStart <= t && t <= st.tEnd
3408
+ ) ?? null;
3409
+ const sameStep = (a, b) => {
3410
+ const sa = stepOf(a.t);
3411
+ return sa !== null && sa === stepOf(b.t);
3412
+ };
3403
3413
  const minLevel = opts.minLevel ?? resolveZoomStyle().minLevel;
3404
3414
  const clicks = dedupePresses(
3405
3415
  track.filter((e) => e.type === "down")
@@ -3433,7 +3443,7 @@ function groupTrack(track, opts) {
3433
3443
  for (const c of list) {
3434
3444
  const last = clusters.at(-1);
3435
3445
  const prev = last?.at(-1);
3436
- const joins = last && prev && c.t - prev.t <= clusterGap && (!spatial || unionFitLevel(unionRect([...last, c]), width, height) >= minLevel);
3446
+ const joins = last && prev && (c.t - prev.t <= clusterGap || sameStep(prev, c)) && (!spatial || unionFitLevel(unionRect([...last, c]), width, height) >= minLevel);
3437
3447
  if (joins) last.push(c);
3438
3448
  else clusters.push([c]);
3439
3449
  }
@@ -3512,7 +3522,8 @@ function planAutoZoom(track, options) {
3512
3522
  typingGap,
3513
3523
  typingZoom,
3514
3524
  targetFill,
3515
- minLevel
3525
+ minLevel,
3526
+ steps: options.steps
3516
3527
  });
3517
3528
  const { sessions } = grouped;
3518
3529
  const drags = dragsFromTrack(track, width, height);
@@ -3627,10 +3638,11 @@ function planAutoZoom(track, options) {
3627
3638
  const spans = within([...zSpans, ...kSpans, ...dragSpans]).sort(
3628
3639
  (a, b) => a.in - b.in
3629
3640
  );
3641
+ const firstStep = options.steps?.find((st) => !st.skipped)?.tStart;
3630
3642
  const dwells = dwellSpans(track, width, height, maxLevel, [
3631
3643
  ...spans,
3632
3644
  ...dragReserved
3633
- ]);
3645
+ ]).filter((d) => firstStep === void 0 || d.out > firstStep);
3634
3646
  return within([...spans, ...dwells]).sort((a, b) => a.in - b.in);
3635
3647
  }
3636
3648
  function typingSessions(track, width, height, typingGap) {
@@ -9235,13 +9247,15 @@ var DEFAULT_SPEED_PARAMS = {
9235
9247
  typingMin: 3,
9236
9248
  typingRate: 3,
9237
9249
  scrollMin: 2.5,
9238
- scrollRate: 2
9250
+ scrollRate: 2,
9251
+ stillRate: 2
9239
9252
  };
9240
9253
  var TYPING_GAP = 1.5;
9241
9254
  var SCROLL_GAP = 0.8;
9242
9255
  var IDLE_PAD = 0.6;
9243
9256
  var MIN_SPAN = 1;
9244
9257
  var PLAYBACK_ACTIVITY = 0.1;
9258
+ var STILL_ACTIVITY = 0.02;
9245
9259
  function planAutoSpeed(track, opts) {
9246
9260
  const p = { ...DEFAULT_SPEED_PARAMS, ...opts.params };
9247
9261
  const durS = opts.durationMs / 1e3;
@@ -9260,6 +9274,9 @@ function planAutoSpeed(track, opts) {
9260
9274
  p.scrollMin,
9261
9275
  (start, last) => cands.push({ in: start, out: last, rate: p.scrollRate })
9262
9276
  );
9277
+ for (const [a, b] of stillSteps(opts.steps, opts.activity)) {
9278
+ if (b - a >= MIN_SPAN) cands.push({ in: a, out: b, rate: p.stillRate });
9279
+ }
9263
9280
  for (const [a, b] of idleGaps(evs, durS, p.idleMin)) {
9264
9281
  if (isPlayback(opts.activity, a, b)) continue;
9265
9282
  const start = a + IDLE_PAD;
@@ -9313,6 +9330,29 @@ function idleGaps(track, durationS, idleMin = DEFAULT_SPEED_PARAMS.idleMin) {
9313
9330
  if (durationS - prev >= idleMin) gaps.push([prev, durationS]);
9314
9331
  return gaps;
9315
9332
  }
9333
+ function stillSteps(steps, activity) {
9334
+ if (!steps?.length || !activity?.length) return [];
9335
+ const live = steps.filter((st) => !st.skipped);
9336
+ const doing = live.map((st) => st.do !== "wait");
9337
+ const first = doing.indexOf(true);
9338
+ const last = doing.lastIndexOf(true);
9339
+ const out = [];
9340
+ for (const [i, st] of live.entries()) {
9341
+ if (st.do !== "wait" && st.do !== "hover") continue;
9342
+ if (st.do === "wait" && (first < 0 || i < first || i > last)) continue;
9343
+ if (meanActivity(activity, st.tStart, st.tEnd) >= STILL_ACTIVITY) continue;
9344
+ out.push([st.tStart, st.tEnd]);
9345
+ }
9346
+ return out;
9347
+ }
9348
+ function meanActivity(activity, a, b) {
9349
+ const lo = Math.max(0, Math.floor(a));
9350
+ const hi = Math.min(activity.length, Math.ceil(b));
9351
+ if (hi <= lo) return 0;
9352
+ let sum = 0;
9353
+ for (let i = lo; i < hi; i++) sum += activity[i];
9354
+ return sum / (hi - lo);
9355
+ }
9316
9356
  function isPlayback(activity, a, b) {
9317
9357
  if (!activity?.length) return false;
9318
9358
  const lo = Math.max(0, Math.floor(a));
@@ -9351,12 +9391,14 @@ function planForDigest(doc, activity) {
9351
9391
  width: meta.width,
9352
9392
  height: meta.height,
9353
9393
  style: doc.zoomStyle,
9354
- params: doc.zoomParams
9394
+ params: doc.zoomParams,
9395
+ steps: meta.steps
9355
9396
  });
9356
9397
  const speed = planAutoSpeed(cursor, {
9357
9398
  durationMs: meta.durationMs,
9358
9399
  params: doc.speedParams,
9359
- activity
9400
+ activity,
9401
+ steps: meta.steps
9360
9402
  });
9361
9403
  const style = resolveZoomStyle(doc.zoomStyle, doc.zoomParams);
9362
9404
  const intensity = doc.tiltStyle ?? style.tilt.intensity;
@@ -9396,7 +9438,8 @@ function momentsFromDoc(doc, plan, opts = {}) {
9396
9438
  typingGap: style.typingGap,
9397
9439
  typingZoom: style.typingZoom,
9398
9440
  targetFill: style.targetFill,
9399
- minLevel: style.minLevel
9441
+ minLevel: style.minLevel,
9442
+ steps: meta.steps
9400
9443
  });
9401
9444
  const pressed = (c, surface) => {
9402
9445
  const f = clusterFocus(c, width, height);
@@ -9492,15 +9535,30 @@ function momentsFromDoc(doc, plan, opts = {}) {
9492
9535
  rect: null
9493
9536
  });
9494
9537
  }
9538
+ for (const c of opts.changes ?? []) {
9539
+ if (!(c.at >= 0 && c.at < dur)) continue;
9540
+ const peak = Math.min(dur, Math.max(c.at, c.peakAt));
9541
+ drafts.push({
9542
+ kind: "changed",
9543
+ in: c.at,
9544
+ out: peak,
9545
+ at: peak,
9546
+ focus: { cx: c.rect.x + c.rect.w / 2, cy: c.rect.y + c.rect.h / 2 },
9547
+ rect: c.rect,
9548
+ seedAt: c.at,
9549
+ fraction: c.fraction
9550
+ });
9551
+ }
9495
9552
  const order = {
9496
9553
  head: 0,
9497
9554
  click: 1,
9498
9555
  typing: 2,
9499
- scroll: 3,
9500
- dwell: 4,
9501
- scene: 5,
9502
- idle: 6,
9503
- tail: 7
9556
+ changed: 3,
9557
+ scroll: 4,
9558
+ dwell: 5,
9559
+ scene: 6,
9560
+ idle: 7,
9561
+ tail: 8
9504
9562
  };
9505
9563
  drafts.sort((a, b) => a.in - b.in || order[a.kind] - order[b.kind]);
9506
9564
  const rated = ratedSegments(doc);
@@ -9509,8 +9567,17 @@ function momentsFromDoc(doc, plan, opts = {}) {
9509
9567
  return spanOutputExtent(rated, lo, Math.max(b, lo + 1e-3));
9510
9568
  };
9511
9569
  const covers = (s, d) => d.in === d.out ? s.in <= d.in && d.in < s.out : s.in < d.out && s.out > d.in;
9570
+ const ids = drafts.map((_, i) => `m${String(i + 1).padStart(2, "0")}`);
9571
+ const seedOf = (d) => {
9572
+ if (d.kind !== "changed" || d.seedAt === void 0) return void 0;
9573
+ const j = drafts.findIndex(
9574
+ (x) => x.kind === "click" && x.at === d.seedAt || x.kind === "typing" && x.out === d.seedAt
9575
+ );
9576
+ return j >= 0 ? ids[j] : void 0;
9577
+ };
9512
9578
  return drafts.map((d, i) => {
9513
9579
  const output = outputOf(d.in, d.out);
9580
+ const seed = seedOf(d);
9514
9581
  const outAt = d.at === null ? null : outputOf(d.at, d.at);
9515
9582
  const proposed = {};
9516
9583
  const z = plan.zoom.find((s) => covers(s, d));
@@ -9520,7 +9587,7 @@ function momentsFromDoc(doc, plan, opts = {}) {
9520
9587
  const tl = plan.tilt.find((s) => covers(s, d));
9521
9588
  if (tl) proposed.tilt = tl.id;
9522
9589
  return {
9523
- id: `m${String(i + 1).padStart(2, "0")}`,
9590
+ id: ids[i],
9524
9591
  kind: d.kind,
9525
9592
  source: { in: round10(d.in), out: round10(d.out) },
9526
9593
  output: output ? { in: round10(output.start), out: round10(output.end) } : null,
@@ -9536,9 +9603,10 @@ function momentsFromDoc(doc, plan, opts = {}) {
9536
9603
  ...d.clicks !== void 0 ? { clicks: d.clicks } : {},
9537
9604
  ...d.surface ? { surface: true } : {},
9538
9605
  ...d.pings !== void 0 ? { pings: d.pings } : {},
9539
- activity: activityOf(opts.bins, d),
9606
+ activity: d.kind === "changed" && d.fraction !== void 0 ? round10(d.fraction) : activityOf(opts.bins, d),
9540
9607
  proposed,
9541
- said: saidOver(opts.transcript, d)
9608
+ said: saidOver(opts.transcript, d),
9609
+ ...seed ? { seed } : {}
9542
9610
  };
9543
9611
  });
9544
9612
  }
@@ -9565,6 +9633,105 @@ function round10(v) {
9565
9633
  return Math.round(v * 1e3) / 1e3;
9566
9634
  }
9567
9635
 
9636
+ // src/digest/changes.ts
9637
+ var CHANGE_GRID = { w: 128, h: 72 };
9638
+ var BLOCK_FILL = 0.5;
9639
+ var CHANGE_FLOOR = 0.02;
9640
+ var CHANGE_OFFSETS = [0.3, 0.8, 1.5, 2.5];
9641
+ var CHANGE_LEAD = 0.5;
9642
+ function changedRegion(before, after, delta, minBlocks = 1, ambient = null, block = Math.max(1, Math.round(before.w / 16))) {
9643
+ const { w, h } = before;
9644
+ if (after.w !== w || after.h !== h || w <= 0 || h <= 0) return null;
9645
+ const cols = Math.ceil(w / block);
9646
+ const rows = Math.ceil(h / block);
9647
+ const moved = new Uint16Array(cols * rows);
9648
+ const cells = new Uint16Array(cols * rows);
9649
+ for (let y = 0; y < h; y++) {
9650
+ const by = Math.floor(y / block);
9651
+ for (let x = 0; x < w; x++) {
9652
+ const i = y * w + x;
9653
+ const b = by * cols + Math.floor(x / block);
9654
+ cells[b]++;
9655
+ const drift = ambient ? ambient.rate[i] * ambient.dt : 0;
9656
+ if (Math.abs(after.luma[i] - before.luma[i]) - drift > delta) moved[b]++;
9657
+ }
9658
+ }
9659
+ const filled = new Uint8Array(cols * rows);
9660
+ for (let b = 0; b < filled.length; b++)
9661
+ if (moved[b] >= cells[b] * BLOCK_FILL) filled[b] = 1;
9662
+ const seen = new Uint8Array(cols * rows);
9663
+ let best = null;
9664
+ for (let start = 0; start < filled.length; start++) {
9665
+ if (!filled[start] || seen[start]) continue;
9666
+ const stack = [start];
9667
+ seen[start] = 1;
9668
+ let size = 0;
9669
+ let minX = cols;
9670
+ let minY = rows;
9671
+ let maxX = -1;
9672
+ let maxY = -1;
9673
+ while (stack.length) {
9674
+ const b = stack.pop();
9675
+ size++;
9676
+ const bx = b % cols;
9677
+ const by = Math.floor(b / cols);
9678
+ if (bx < minX) minX = bx;
9679
+ if (bx > maxX) maxX = bx;
9680
+ if (by < minY) minY = by;
9681
+ if (by > maxY) maxY = by;
9682
+ for (const [dx, dy] of [
9683
+ [1, 0],
9684
+ [-1, 0],
9685
+ [0, 1],
9686
+ [0, -1]
9687
+ ]) {
9688
+ const nx = bx + dx;
9689
+ const ny = by + dy;
9690
+ if (nx < 0 || ny < 0 || nx >= cols || ny >= rows) continue;
9691
+ const n = ny * cols + nx;
9692
+ if (filled[n] && !seen[n]) {
9693
+ seen[n] = 1;
9694
+ stack.push(n);
9695
+ }
9696
+ }
9697
+ }
9698
+ if (!best || size > best.size) best = { size, minX, minY, maxX, maxY };
9699
+ }
9700
+ if (!best || best.size < minBlocks) return null;
9701
+ return {
9702
+ fraction: best.size / (cols * rows),
9703
+ rect: {
9704
+ x: best.minX * block / w,
9705
+ y: best.minY * block / h,
9706
+ w: Math.min(w, (best.maxX + 1) * block) / w - best.minX * block / w,
9707
+ h: Math.min(h, (best.maxY + 1) * block) / h - best.minY * block / h
9708
+ }
9709
+ };
9710
+ }
9711
+ function ambientDrift(earlier, before, lead) {
9712
+ const { w, h } = before;
9713
+ if (earlier.w !== w || earlier.h !== h || !(lead > 0)) return null;
9714
+ const rate = new Array(w * h);
9715
+ for (let i = 0; i < rate.length; i++)
9716
+ rate[i] = Math.abs(before.luma[i] - earlier.luma[i]) / lead;
9717
+ return rate;
9718
+ }
9719
+ function pickChange(seed, samples, floor = CHANGE_FLOOR) {
9720
+ let best = null;
9721
+ for (const s of samples) {
9722
+ if (!s.region || s.region.fraction < floor) continue;
9723
+ if (!best || s.region.fraction > (best.region?.fraction ?? 0)) best = s;
9724
+ }
9725
+ if (!best?.region) return null;
9726
+ return {
9727
+ seed: seed.id,
9728
+ at: seed.at,
9729
+ peakAt: seed.at + best.dt,
9730
+ fraction: best.region.fraction,
9731
+ rect: best.region.rect
9732
+ };
9733
+ }
9734
+
9568
9735
  // src/digest/scenes.ts
9569
9736
  var SCENE_MOTION = 0.25;
9570
9737
  var SCENE_QUIET = 0.1;
@@ -10096,6 +10263,7 @@ function buildDigest(input) {
10096
10263
  activity: "fraction of pixels that changed, per SOURCE second"
10097
10264
  },
10098
10265
  moments: withFiles,
10266
+ steps: digestSteps(doc),
10099
10267
  activity: input.bins,
10100
10268
  plan: input.plan,
10101
10269
  doc: {
@@ -10119,6 +10287,26 @@ function buildDigest(input) {
10119
10287
  }
10120
10288
  };
10121
10289
  }
10290
+ function digestSteps(doc) {
10291
+ const steps = doc.source.meta.steps ?? [];
10292
+ if (!steps.length) return [];
10293
+ const rated = ratedSegments(doc);
10294
+ return steps.map((s) => {
10295
+ const out = spanOutputExtent(rated, s.tStart, s.tEnd);
10296
+ return {
10297
+ step: s.step,
10298
+ id: s.id ?? null,
10299
+ do: s.do,
10300
+ selector: s.selector ?? null,
10301
+ source: { in: round11(s.tStart), out: round11(s.tEnd) },
10302
+ output: out ? { in: round11(out.start), out: round11(out.end) } : null,
10303
+ skipped: s.skipped === true,
10304
+ navigated: s.navigated === true,
10305
+ url: s.url ?? null,
10306
+ text: s.text ?? null
10307
+ };
10308
+ });
10309
+ }
10122
10310
  function outputDurationOf(doc) {
10123
10311
  return docOutputDuration(doc);
10124
10312
  }
@@ -11953,6 +12141,10 @@ export {
11953
12141
  CARD_FADE_SECONDS,
11954
12142
  CARD_FOV,
11955
12143
  CARD_Z,
12144
+ CHANGE_FLOOR,
12145
+ CHANGE_GRID,
12146
+ CHANGE_LEAD,
12147
+ CHANGE_OFFSETS,
11956
12148
  CHANNEL_SPECS_HASH,
11957
12149
  CHANNEL_SPECS_VERIFIED,
11958
12150
  CLICK_FX_INTENSITY,
@@ -12102,6 +12294,7 @@ export {
12102
12294
  ZOOM_SPAN_MIN,
12103
12295
  ZOOM_STYLES,
12104
12296
  ZOOM_STYLE_OPTIONS,
12297
+ ambientDrift,
12105
12298
  anchorKindOf,
12106
12299
  anchorSourceDuration,
12107
12300
  animStep,
@@ -12134,6 +12327,7 @@ export {
12134
12327
  cardPoseTrack,
12135
12328
  caretAt,
12136
12329
  caretStops,
12330
+ changedRegion,
12137
12331
  clampCamFrac,
12138
12332
  clampCamSize,
12139
12333
  clampFocus,
@@ -12286,6 +12480,7 @@ export {
12286
12480
  pageDisplayUrl,
12287
12481
  parseCssColour,
12288
12482
  parsePoseId,
12483
+ pickChange,
12289
12484
  pickStyle,
12290
12485
  pinBox,
12291
12486
  pinCandidates,