@cueframe/kernel 0.2.52 → 0.2.53

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 (67) hide show
  1. package/dist/_members/composition/fonts/bundledFaces.d.ts +9 -15
  2. package/dist/_members/composition/fonts/captionFaces.d.ts +2 -3
  3. package/dist/_members/composition/ops/captionOps.d.ts +11 -0
  4. package/dist/_members/composition/ops/compositionOps.d.ts +20 -19
  5. package/dist/_members/composition/validators/composition.d.ts +644 -0
  6. package/dist/_members/composition/validators/registry.d.ts +1 -1
  7. package/dist/_members/composition/validators/render.d.ts +292 -0
  8. package/dist/_members/composition-ops/index.d.ts +74 -0
  9. package/dist/_members/core/render/compositionAudioAndDepth.d.ts +1 -1
  10. package/dist/_members/core/render/index.d.ts +1 -1
  11. package/dist/_members/core/render/types.d.ts +3 -1
  12. package/dist/_members/plane/contract.d.ts +1 -1
  13. package/dist/_members/primitives/captions/Captions.d.ts +18 -1
  14. package/dist/_members/primitives/captions/index.d.ts +1 -1
  15. package/dist/animate.js +2 -2
  16. package/dist/authoring/build-tools.js +6 -6
  17. package/dist/authoring/context.js +5 -5
  18. package/dist/authoring/tools.js +5 -5
  19. package/dist/authoring.js +9 -9
  20. package/dist/{chunk-MYBK6DKF.js → chunk-2BVKNYRB.js} +1 -1
  21. package/dist/{chunk-2M65KQNG.js → chunk-46DGDPXR.js} +296 -47
  22. package/dist/{chunk-KMDZDH5E.js → chunk-4DM63RTI.js} +4 -4
  23. package/dist/{chunk-QVBUYWW6.js → chunk-5E4YP44R.js} +1 -1
  24. package/dist/{chunk-3OG2674K.js → chunk-7AUHC2X7.js} +1 -1
  25. package/dist/{chunk-AVTEPAWI.js → chunk-7RGDRDAR.js} +17 -13
  26. package/dist/{chunk-7XZW26U2.js → chunk-AONWKKXK.js} +1 -1
  27. package/dist/{chunk-4B4V3HYW.js → chunk-C6ONUJIO.js} +1 -1
  28. package/dist/{chunk-MBSQKRDF.js → chunk-CJMUTCJW.js} +2 -2
  29. package/dist/{chunk-CZOVEX2D.js → chunk-CR3G2JQP.js} +1 -1
  30. package/dist/{chunk-2PU2TSUG.js → chunk-D5E5XFHK.js} +1 -1
  31. package/dist/{chunk-6WQHVYJD.js → chunk-G7ZSFSQG.js} +1 -1
  32. package/dist/{chunk-5ZQLMPDY.js → chunk-GAACUQAB.js} +1 -1
  33. package/dist/{chunk-3I43W7DP.js → chunk-LPUQTBKU.js} +41 -20
  34. package/dist/{chunk-WZ22ZYXD.js → chunk-P3DYA5NA.js} +4 -4
  35. package/dist/{chunk-Z6NY6MCL.js → chunk-RMLGMYEJ.js} +1 -1
  36. package/dist/{chunk-5JCLZPMV.js → chunk-TPKPU4PE.js} +56 -38
  37. package/dist/{chunk-CV3QQUMK.js → chunk-UFNIB3JL.js} +117 -4
  38. package/dist/{chunk-ACURZCPN.js → chunk-UKKY7HWN.js} +1 -1
  39. package/dist/{chunk-IHXMBXXL.js → chunk-VTM7NEJC.js} +30 -4
  40. package/dist/{chunk-JWW54N24.js → chunk-ZFW35DLG.js} +1 -1
  41. package/dist/{chunk-FSEB4LSU.js → chunk-ZIPGDGBR.js} +1 -1
  42. package/dist/component-runtime/render.js +2 -2
  43. package/dist/component-runtime.js +3 -3
  44. package/dist/compose.js +5 -5
  45. package/dist/composition/fonts.js +1 -1
  46. package/dist/composition/graphics-plan.js +3 -3
  47. package/dist/composition/ops.js +4 -4
  48. package/dist/composition/projection.js +3 -3
  49. package/dist/composition/validators.js +13 -3
  50. package/dist/composition-ops.js +8 -8
  51. package/dist/composition.js +15 -5
  52. package/dist/{context-2MBCDTCZ.js → context-UAS5WJRB.js} +5 -5
  53. package/dist/core/render.js +4 -4
  54. package/dist/core.js +4 -4
  55. package/dist/mcp/adapter.js +2 -2
  56. package/dist/mcp/core.js +1 -1
  57. package/dist/mcp/scene-shot.js +2 -2
  58. package/dist/mcp/server.js +12 -12
  59. package/dist/mcp.js +12 -12
  60. package/dist/primitives/captions.js +195 -32
  61. package/dist/primitives/effects.js +3 -3
  62. package/dist/primitives/layouts.js +3 -3
  63. package/dist/primitives/scenes.js +3 -3
  64. package/dist/primitives.js +5 -5
  65. package/dist/render-harness/cardFonts.js +2 -2
  66. package/dist/render-harness.js +7 -7
  67. package/package.json +14 -14
@@ -75,7 +75,7 @@ var BUNDLED_FONT_FACES = Object.freeze({
75
75
  },
76
76
  "Instrument Serif": {
77
77
  slug: "instrument-serif",
78
- faces: [{ weight: "400", file: "400" }],
78
+ faces: [{ weight: "400", file: "400" }, { weight: "400", file: "400-italic", style: "italic" }],
79
79
  captionDefaults: { base: 400, emphasis: 400 }
80
80
  },
81
81
  "National Park": {
@@ -102,19 +102,19 @@ function bundledFaceCoversWeight(face, weight) {
102
102
  return weight >= lo && weight <= hi;
103
103
  }
104
104
  function bundledFontFaceExists(family, weight, style) {
105
- if (style !== BUNDLED_FONT_STYLE) return false;
106
105
  const bundled = bundledFontFamilyFaces(family);
107
106
  if (!bundled) return false;
108
- return bundled.faces.some((face) => bundledFaceCoversWeight(face, weight));
107
+ return bundled.faces.some((face) => (face.style ?? BUNDLED_FONT_STYLE) === style && bundledFaceCoversWeight(face, weight));
109
108
  }
110
109
  function describeBundledFontWeights(family) {
111
110
  const bundled = bundledFontFamilyFaces(family);
112
111
  if (!bundled) return "";
113
- return bundled.faces.map((face) => face.weight.split(/\s+/).join("-")).join(", ");
112
+ return [...new Set(bundled.faces.map((face) => face.weight.split(/\s+/).join("-")))].join(", ");
114
113
  }
115
114
  function unrenderableFaceField(family, weight, style, sealed) {
116
- if (bundledFontFamilyFaces(family)) {
117
- return style === BUNDLED_FONT_STYLE ? "fontWeight" : "fontStyle";
115
+ const bundled = bundledFontFamilyFaces(family);
116
+ if (bundled) {
117
+ return bundled.faces.some((face) => (face.style ?? BUNDLED_FONT_STYLE) === style) ? "fontWeight" : "fontStyle";
118
118
  }
119
119
  if (sealed.hasFamily(family)) {
120
120
  return sealed.hasWeight(family, weight) ? "fontStyle" : "fontWeight";
@@ -165,7 +165,7 @@ function admissibleFamilyForStack(stack) {
165
165
  return resolved.kind === "bundled" ? resolved.family : stack;
166
166
  }
167
167
  function weightRanges(faces) {
168
- return faces.map((face) => {
168
+ return [...new Map(faces.map((face) => [face.weight, face])).values()].map((face) => {
169
169
  const parts = face.weight.split(/\s+/).map(Number);
170
170
  const min = parts[0];
171
171
  const max = parts.length > 1 ? parts[1] : min;
@@ -188,7 +188,7 @@ function vendoredFontCatalog() {
188
188
  return {
189
189
  family,
190
190
  weights: weightRanges(entry.faces),
191
- styles: [BUNDLED_FONT_STYLE],
191
+ styles: [...new Set(entry.faces.map((face) => face.style ?? BUNDLED_FONT_STYLE))],
192
192
  captionDefaults: entry.captionDefaults,
193
193
  origin: aliased.has(family) ? "aliased" : "bundled"
194
194
  };
@@ -261,12 +261,12 @@ function faceIssue(use) {
261
261
  return {
262
262
  field: use.field,
263
263
  face: { family: use.stack, weight: use.weight, style: use.style },
264
- message: `font_not_reproducible \u2014 ${because(use)}: "${use.stack}" names no font file the render bundle carries, and it ships no italic binary at all, so the browser would slant whatever upright face it matched and the render refuses it before capture. Emphasise with emphasisFontWeight, emphasisColor or emphasisScale, or name an uploaded brand font that is sealed in italic.`
264
+ message: `font_not_reproducible \u2014 ${because(use)}: "${use.stack}" names no font file the render bundle carries, so this stack ships no italic binary to select and the browser would slant whatever upright face it matched and the render refuses it before capture. Emphasise with emphasisFontWeight, emphasisColor or emphasisScale, or name an uploaded brand font that is sealed in italic.`
265
265
  };
266
266
  }
267
267
  if (use.resolved.kind !== "bundled") return null;
268
268
  const face = { family: use.resolved.family, weight: use.weight, style: use.style };
269
- if (use.style === "italic") {
269
+ if (use.style === "italic" && !bundledFontFamilyFaces(use.resolved.family).faces.some((face2) => face2.style === "italic")) {
270
270
  return {
271
271
  field: use.field,
272
272
  face,
@@ -274,10 +274,11 @@ function faceIssue(use) {
274
274
  };
275
275
  }
276
276
  if (bundledFontFaceExists(use.resolved.family, use.weight, use.style)) return null;
277
+ const weightField = use.role === "emphasis" ? "emphasisFontWeight" : "fontWeight";
277
278
  return {
278
- field: use.field,
279
+ field: weightField,
279
280
  face,
280
- message: `font_not_reproducible \u2014 ${because(use)}: ${use.resolved.family} ships ${describeBundledFontWeights(use.resolved.family)}. The render refuses a weight outside that range before capture rather than let the browser substitute a nearby file, so set ${use.field} to a weight the family covers, or pick a family whose files do.`
281
+ message: `font_not_reproducible \u2014 ${weightField} ${use.weight} (${use.style}) has no file behind it: ${use.resolved.family} ships ${describeBundledFontWeights(use.resolved.family)}. The render refuses a weight outside that range before capture rather than let the browser substitute a nearby file, so set ${weightField} to a weight the family covers, or pick a family whose files do.`
281
282
  };
282
283
  }
283
284
  function captionFaceIssues(style) {
@@ -377,7 +378,10 @@ function captionWeightChoices(stack) {
377
378
  );
378
379
  }
379
380
  function captionStyleChoices(stack) {
380
- return captionStackFamily(stack).kind === "unknown" ? ["normal", "italic"] : ["normal"];
381
+ const resolved = captionStackFamily(stack);
382
+ if (resolved.kind === "unknown") return ["normal", "italic"];
383
+ if (resolved.kind !== "bundled") return ["normal"];
384
+ return [...new Set(bundledFontFamilyFaces(resolved.family).faces.map((face) => face.style ?? BUNDLED_FONT_STYLE))];
381
385
  }
382
386
 
383
387
  // ../composition/src/fonts.ts
@@ -31,7 +31,7 @@ import {
31
31
  } from "./chunk-CL5Y6V6K.js";
32
32
  import {
33
33
  BUNDLED_FONT_FACES
34
- } from "./chunk-AVTEPAWI.js";
34
+ } from "./chunk-7RGDRDAR.js";
35
35
 
36
36
  // ../execution/component-runtime/src/sceneAdmission.ts
37
37
  import { ZodError } from "zod";
@@ -43,7 +43,7 @@ import {
43
43
  makeDefaultScreenTexture,
44
44
  resolveCamera,
45
45
  useImageTexture
46
- } from "./chunk-2PU2TSUG.js";
46
+ } from "./chunk-D5E5XFHK.js";
47
47
  import {
48
48
  DEVICE_GLASS_OPACITY_ENCODED,
49
49
  DeviceMesh,
@@ -4,10 +4,10 @@ import {
4
4
  Format,
5
5
  GraphicRole,
6
6
  OpaqueId
7
- } from "./chunk-CV3QQUMK.js";
7
+ } from "./chunk-UFNIB3JL.js";
8
8
  import {
9
9
  brandKitCaptionFaceIssues
10
- } from "./chunk-AVTEPAWI.js";
10
+ } from "./chunk-7RGDRDAR.js";
11
11
 
12
12
  // ../composition/src/validators/composePlan.ts
13
13
  import { z } from "zod";
@@ -24,7 +24,7 @@ import {
24
24
  } from "./chunk-W2245F6G.js";
25
25
  import {
26
26
  FlexLayout
27
- } from "./chunk-2PU2TSUG.js";
27
+ } from "./chunk-D5E5XFHK.js";
28
28
 
29
29
  // ../execution/primitives/src/layouts/chat-to-preview-layout/index.tsx
30
30
  import { Easing, interpolate, useCurrentFrame, useVideoConfig } from "remotion";
@@ -34,7 +34,7 @@ import {
34
34
  } from "./chunk-CL5Y6V6K.js";
35
35
  import {
36
36
  resolveCaptionFaces
37
- } from "./chunk-AVTEPAWI.js";
37
+ } from "./chunk-7RGDRDAR.js";
38
38
 
39
39
  // ../execution/animate/src/easing/smooth.ts
40
40
  var sigmoid = (x) => 1 / (1 + Math.exp(-x));
@@ -2,7 +2,7 @@ import {
2
2
  activeFaceKeepOutSeries,
3
3
  boundingBox,
4
4
  resolveBrandRefsDeep
5
- } from "./chunk-Z6NY6MCL.js";
5
+ } from "./chunk-RMLGMYEJ.js";
6
6
  import {
7
7
  CueframeError,
8
8
  encodeError
@@ -26,7 +26,7 @@ import {
26
26
  perspectiveFor,
27
27
  resolveCamera,
28
28
  useStage
29
- } from "./chunk-2PU2TSUG.js";
29
+ } from "./chunk-D5E5XFHK.js";
30
30
 
31
31
  // ../execution/primitives/src/effects/blur/index.tsx
32
32
  import React2 from "react";
@@ -31,7 +31,7 @@ import {
31
31
  } from "./chunk-VZ7TYX6Z.js";
32
32
  import {
33
33
  resolveBrandRef
34
- } from "./chunk-Z6NY6MCL.js";
34
+ } from "./chunk-RMLGMYEJ.js";
35
35
  import {
36
36
  SHOT_SCALE_TARGET,
37
37
  shotScaleZoom
@@ -64,7 +64,7 @@ import {
64
64
  bundledFontFaceExists,
65
65
  mergedCaptionFaceIssues,
66
66
  unrenderableFaceField
67
- } from "./chunk-AVTEPAWI.js";
67
+ } from "./chunk-7RGDRDAR.js";
68
68
 
69
69
  // ../execution/core/src/render/sceneCaptureIdentity.ts
70
70
  var INTENT_FIELDS = [
@@ -1226,22 +1226,33 @@ function bindBehindSubjectHosts(overlays, clips) {
1226
1226
  );
1227
1227
  }
1228
1228
  }
1229
- function assertBehindSubjectCaptionCoverage(captionSegments, clips, fps, output, effects = []) {
1230
- for (const segment of captionSegments ?? []) {
1231
- for (const word of segment) {
1232
- const startFrame = Math.round(word.startMs / 1e3 * fps);
1233
- const endFrame = Math.max(startFrame + 1, Math.round(word.endMs / 1e3 * fps));
1234
- for (const window of visiblePackedHostWindows(clips, startFrame, endFrame, output, effects)) {
1235
- if (!window.captionVisible) continue;
1236
- if (window.hostClipId) continue;
1237
- const code = window.candidateClipIds.length > 1 ? "behind_subject_caption_host_ambiguous" : "behind_subject_caption_host_missing";
1238
- throw renderInputError(
1239
- code,
1240
- `Behind-subject caption word '${word.text}' is not fully covered by a unique visible packed host during frames [${window.startFrame}, ${window.endFrame}); an exact matte is required for the visible picture.`,
1241
- { candidateClipIds: window.candidateClipIds, startFrame: window.startFrame, endFrame: window.endFrame }
1242
- );
1243
- }
1229
+ function assertBehindSubjectCaptionCoverage(captionSegments, clips, fps, output, effects = [], captionLayouts = []) {
1230
+ const requireCoverage = (label, startMs, endMs) => {
1231
+ const startFrame = Math.round(startMs / 1e3 * fps);
1232
+ const endFrame = Math.max(startFrame + 1, Math.round(endMs / 1e3 * fps));
1233
+ for (const window of visiblePackedHostWindows(clips, startFrame, endFrame, output, effects)) {
1234
+ if (!window.captionVisible || window.hostClipId) continue;
1235
+ const code = window.candidateClipIds.length > 1 ? "behind_subject_caption_host_ambiguous" : "behind_subject_caption_host_missing";
1236
+ throw renderInputError(
1237
+ code,
1238
+ `Behind-subject caption ${label} is not fully covered by a unique visible packed host during frames [${window.startFrame}, ${window.endFrame}); an exact matte is required for the visible picture.`,
1239
+ { candidateClipIds: window.candidateClipIds, startFrame: window.startFrame, endFrame: window.endFrame }
1240
+ );
1244
1241
  }
1242
+ };
1243
+ for (const segment of captionSegments ?? []) {
1244
+ for (const word of segment) requireCoverage(`word '${word.text}'`, word.startMs, word.endMs);
1245
+ }
1246
+ for (const layout of captionLayouts ?? []) {
1247
+ const segment = captionSegments?.[layout.segmentIndex];
1248
+ const first = segment?.[layout.wordStartIndex];
1249
+ const last = segment?.[layout.holdThroughWordIndex ?? layout.wordEndIndex - 1];
1250
+ if (!first || !last) throw new Error("Behind-subject caption layout references missing saved transcript words");
1251
+ requireCoverage(
1252
+ `layout segment ${layout.segmentIndex} words ${layout.wordStartIndex}..${layout.wordEndIndex - 1}`,
1253
+ first.startMs,
1254
+ last.endMs
1255
+ );
1245
1256
  }
1246
1257
  }
1247
1258
  var FULL_RECT = { left: 0, top: 0, right: 1, bottom: 1 };
@@ -1976,6 +1987,11 @@ function assertMergedCaptionFacesDrawable(composition) {
1976
1987
  if (!captions?.segments.some((segment) => segment.words.length > 0)) return;
1977
1988
  const brandCaption = composition.brandKit?.caption;
1978
1989
  const issues = mergedCaptionFaceIssues(captions.style, brandCaption);
1990
+ for (const [index, layout] of (captions.layouts ?? []).entries()) {
1991
+ for (const issue of mergedCaptionFaceIssues({ ...captions.style, ...layout.style }, brandCaption)) {
1992
+ issues.push({ ...issue, path: `captions.layouts[${index}].style.${issue.path.split(".").at(-1)}` });
1993
+ }
1994
+ }
1979
1995
  if (issues.length === 0) return;
1980
1996
  throw renderInputError(
1981
1997
  "font_not_reproducible",
@@ -2078,9 +2094,9 @@ function compositionToRenderInput(composition, srcUrls, trajectoryUrls, coViewUr
2078
2094
  if (end > maxBackgroundEndFrames) maxBackgroundEndFrames = end;
2079
2095
  }
2080
2096
  const bodyDurationInFrames = maxBackgroundEndFrames > 0 ? Math.min(maxEndFrames, maxBackgroundEndFrames) : maxEndFrames;
2081
- const captionSegments = composition.captions?.segments.map((seg) => seg.words).filter((words) => words.length > 0);
2097
+ const captionSegments = composition.captions?.segments.map((seg) => seg.words);
2082
2098
  if (composition.captions?.zPlane === "behind-subject") {
2083
- assertBehindSubjectCaptionCoverage(captionSegments, clips, fps, { width, height }, effects);
2099
+ assertBehindSubjectCaptionCoverage(captionSegments, clips, fps, { width, height }, effects, composition.captions.layouts);
2084
2100
  }
2085
2101
  const brandKit = composition.brandKit;
2086
2102
  const { totalFrames: durationInFrames } = resolveBookendTimeline(
@@ -2102,7 +2118,7 @@ function compositionToRenderInput(composition, srcUrls, trajectoryUrls, coViewUr
2102
2118
  ...mot.exitMs != null ? { exitMs: mot.exitMs } : {}
2103
2119
  } : void 0;
2104
2120
  const baseStyle = composition.captions?.style;
2105
- const hasCaptions = captionSegments != null && captionSegments.length > 0;
2121
+ const hasCaptions = captionSegments?.some((words) => words.length > 0) ?? false;
2106
2122
  const captionStyle = hasCaptions && (cap || baseStyle || captionMotion) ? {
2107
2123
  ...baseStyle ?? {},
2108
2124
  ...cap?.fontFamily ? { fontFamily: cap.fontFamily } : {},
@@ -2127,6 +2143,10 @@ function compositionToRenderInput(composition, srcUrls, trajectoryUrls, coViewUr
2127
2143
  ...cap?.exitMs != null ? { exitMs: cap.exitMs } : {}
2128
2144
  } : void 0;
2129
2145
  const resolvedCaptionStyle = captionStyle ? resolveCaptionColorRefs(captionStyle, brandKit) : void 0;
2146
+ const captionLayouts = composition.captions?.layouts?.map((layout) => ({
2147
+ ...layout,
2148
+ ...layout.style ? { style: resolveCaptionColorRefs(layout.style, brandKit) } : {}
2149
+ }));
2130
2150
  return {
2131
2151
  clips,
2132
2152
  overlays,
@@ -2138,6 +2158,7 @@ function compositionToRenderInput(composition, srcUrls, trajectoryUrls, coViewUr
2138
2158
  ...audioTracks.length > 0 ? { audioTracks } : {},
2139
2159
  ...effects.length > 0 ? { effects } : {},
2140
2160
  ...hasCaptions ? { captionSegments } : {},
2161
+ ...hasCaptions && captionLayouts?.length ? { captionLayouts } : {},
2141
2162
  ...hasCaptions && composition.captions?.zPlane === "behind-subject" ? { captionsBehind: true } : {},
2142
2163
  ...resolvedCaptionStyle ? { captionStyle: resolvedCaptionStyle } : {},
2143
2164
  ...brandKit ? { brandKit } : {}
@@ -2,12 +2,12 @@ import {
2
2
  SCENE_SHOT_RESOURCE_DESCRIPTION,
3
3
  SCENE_SHOT_RESOURCE_URI,
4
4
  sceneShotContract
5
- } from "./chunk-3OG2674K.js";
5
+ } from "./chunk-7AUHC2X7.js";
6
6
  import {
7
7
  querySchemaShape,
8
8
  serializeQuery,
9
9
  waitForJob
10
- } from "./chunk-JWW54N24.js";
10
+ } from "./chunk-ZFW35DLG.js";
11
11
  import {
12
12
  CURATED_TOOLS
13
13
  } from "./chunk-4WBSXHQA.js";
@@ -28,10 +28,10 @@ import {
28
28
  text,
29
29
  textWithData,
30
30
  withMedia
31
- } from "./chunk-2M65KQNG.js";
31
+ } from "./chunk-46DGDPXR.js";
32
32
  import {
33
33
  CompositionOp
34
- } from "./chunk-IHXMBXXL.js";
34
+ } from "./chunk-VTM7NEJC.js";
35
35
 
36
36
  // ../mcp/src/prompts.ts
37
37
  var skillListCache = null;
@@ -10,7 +10,7 @@ import {
10
10
  import {
11
11
  captionFontSource,
12
12
  effectiveCaptionFamily
13
- } from "./chunk-AVTEPAWI.js";
13
+ } from "./chunk-7RGDRDAR.js";
14
14
 
15
15
  // ../composition/src/projection/brandKit.ts
16
16
  var LOGO_DEFAULT_PLACEMENT = "br";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  Composition
3
- } from "./chunk-CV3QQUMK.js";
3
+ } from "./chunk-UFNIB3JL.js";
4
4
  import {
5
5
  expandToLinkGroup
6
6
  } from "./chunk-KYRDKK2D.js";
@@ -132,6 +132,56 @@ function pruneSelectionInDraft(draft, removedClipIds) {
132
132
  draft.selectedOverlayId = patch.selectedOverlayId;
133
133
  }
134
134
 
135
+ // ../composition/src/ops/captionOps.ts
136
+ function createCaptionOps(set) {
137
+ return {
138
+ setCaptionStyle: (style) => {
139
+ set((draft) => {
140
+ const clean = Object.fromEntries(
141
+ Object.entries(style).filter(([, v]) => v !== void 0)
142
+ );
143
+ if (Object.keys(clean).length === 0) return;
144
+ const existing = draft.composition.captions;
145
+ if (existing) {
146
+ existing.style = { ...existing.style ?? {}, ...clean };
147
+ } else {
148
+ draft.composition.captions = { source: "authored", segments: [], style: clean };
149
+ }
150
+ });
151
+ },
152
+ setCaptionLayouts: (layouts) => {
153
+ set((draft) => {
154
+ if (!draft.composition.captions) throw new Error("setCaptionLayouts requires saved transcript captions");
155
+ if (layouts.length === 0) delete draft.composition.captions.layouts;
156
+ else draft.composition.captions.layouts = layouts;
157
+ });
158
+ },
159
+ setCaptionDepth: (zPlane) => {
160
+ set((draft) => {
161
+ const existing = draft.composition.captions;
162
+ if (!existing) return;
163
+ if (zPlane === "front") {
164
+ delete existing.zPlane;
165
+ } else {
166
+ existing.zPlane = zPlane;
167
+ }
168
+ });
169
+ },
170
+ setCaptions: (segments, style, derivedFrom) => {
171
+ set((draft) => {
172
+ const existing = draft.composition.captions;
173
+ const mergedStyle = style ? {
174
+ ...existing?.style ?? {},
175
+ ...Object.fromEntries(Object.entries(style).filter(([, v]) => v !== void 0))
176
+ } : existing?.style;
177
+ const styleField = mergedStyle && Object.keys(mergedStyle).length > 0 ? { style: mergedStyle } : {};
178
+ const depthField = existing?.zPlane ? { zPlane: existing.zPlane } : {};
179
+ draft.composition.captions = derivedFrom ? { source: "derived", from: derivedFrom, segments, ...styleField, ...depthField } : { source: "authored", segments, ...styleField, ...depthField };
180
+ });
181
+ }
182
+ };
183
+ }
184
+
135
185
  // ../composition/src/ops/compositionOps.ts
136
186
  function createCompositionOps(get, set) {
137
187
  return {
@@ -447,6 +497,7 @@ function createCompositionOps(get, set) {
447
497
  if (!Number.isFinite(start) || !Number.isFinite(end) || start >= end) return;
448
498
  const span = end - start;
449
499
  set((draft) => {
500
+ if (draft.composition.captions) delete draft.composition.captions.layouts;
450
501
  for (const track of draft.composition.tracks) {
451
502
  const next = [];
452
503
  for (const item of track.contents) {
@@ -581,6 +632,7 @@ function createCompositionOps(get, set) {
581
632
  });
582
633
  }
583
634
  if (draft.composition.captions) {
635
+ delete draft.composition.captions.layouts;
584
636
  for (const seg of draft.composition.captions.segments) {
585
637
  seg.words = seg.words.flatMap((w) => {
586
638
  const r = cutTimedRange(w.startMs, w.endMs, tStart * 1e3, tEnd * 1e3);
@@ -631,6 +683,7 @@ function createCompositionOps(get, set) {
631
683
  }
632
684
  }
633
685
  if (draft.composition.captions) {
686
+ delete draft.composition.captions.layouts;
634
687
  for (const segment of draft.composition.captions.segments) {
635
688
  for (const word of segment.words) {
636
689
  const shifted = shiftTimedRange(word.startMs, word.endMs, insertAt * 1e3, restored * 1e3);
@@ -667,45 +720,10 @@ function createCompositionOps(get, set) {
667
720
  }
668
721
  });
669
722
  },
670
- setCaptionStyle: (style) => {
671
- set((draft) => {
672
- const clean = Object.fromEntries(
673
- Object.entries(style).filter(([, v]) => v !== void 0)
674
- );
675
- if (Object.keys(clean).length === 0) return;
676
- const existing = draft.composition.captions;
677
- if (existing) {
678
- existing.style = { ...existing.style ?? {}, ...clean };
679
- } else {
680
- draft.composition.captions = { source: "authored", segments: [], style: clean };
681
- }
682
- });
683
- },
684
- setCaptionDepth: (zPlane) => {
685
- set((draft) => {
686
- const existing = draft.composition.captions;
687
- if (!existing) return;
688
- if (zPlane === "front") {
689
- delete existing.zPlane;
690
- } else {
691
- existing.zPlane = zPlane;
692
- }
693
- });
694
- },
695
- setCaptions: (segments, style, derivedFrom) => {
696
- set((draft) => {
697
- const existing = draft.composition.captions;
698
- const mergedStyle = style ? {
699
- ...existing?.style ?? {},
700
- ...Object.fromEntries(Object.entries(style).filter(([, v]) => v !== void 0))
701
- } : existing?.style;
702
- const styleField = mergedStyle && Object.keys(mergedStyle).length > 0 ? { style: mergedStyle } : {};
703
- const depthField = existing?.zPlane ? { zPlane: existing.zPlane } : {};
704
- draft.composition.captions = derivedFrom ? { source: "derived", from: derivedFrom, segments, ...styleField, ...depthField } : { source: "authored", segments, ...styleField, ...depthField };
705
- });
706
- },
723
+ ...createCaptionOps(set),
707
724
  ripple: (time, amount) => {
708
725
  set((draft) => {
726
+ if (draft.composition.captions) delete draft.composition.captions.layouts;
709
727
  for (const track of draft.composition.tracks) {
710
728
  for (const item of track.contents) {
711
729
  if (!("source" in item)) continue;
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-B7J5UUUV.js";
4
4
  import {
5
5
  captionFaceIssues
6
- } from "./chunk-AVTEPAWI.js";
6
+ } from "./chunk-7RGDRDAR.js";
7
7
 
8
8
  // ../composition/src/validators/opaqueId.ts
9
9
  import { z } from "zod";
@@ -11,6 +11,8 @@ var OpaqueId = z.string();
11
11
 
12
12
  // ../composition/src/validators/composition.ts
13
13
  import { z as z2 } from "zod";
14
+ import { sha256 } from "@noble/hashes/sha256";
15
+ import { bytesToHex } from "@noble/hashes/utils";
14
16
  var Format = z2.object({
15
17
  aspectRatio: z2.enum(["16:9", "9:16", "1:1", "4:5"]),
16
18
  fps: z2.union([z2.literal(24), z2.literal(25), z2.literal(30), z2.literal(60)]).optional(),
@@ -288,6 +290,101 @@ var CaptionStyle = z2.object({
288
290
  annotationColor: z2.string().optional(),
289
291
  maxWidthPercent: z2.number().positive().max(100, "captions style.maxWidthPercent is % of composition width (max 100)").optional()
290
292
  });
293
+ var CaptionLayoutStyle = CaptionStyle.pick({
294
+ fontFamily: true,
295
+ fontSize: true,
296
+ fontWeight: true,
297
+ color: true,
298
+ highlightColor: true,
299
+ backgroundColor: true,
300
+ plate: true,
301
+ textTransform: true,
302
+ emphasisColor: true,
303
+ emphasisScale: true,
304
+ emphasisFontFamily: true,
305
+ emphasisFontWeight: true,
306
+ emphasisTextTransform: true,
307
+ emphasisLetterSpacing: true,
308
+ emphasisFontStyle: true,
309
+ emphasisPlate: true,
310
+ emphasisPlateColor: true,
311
+ emphasisPlateTextColor: true,
312
+ emphasisPlateAltColor: true,
313
+ emphasisPlateAltTextColor: true,
314
+ emphasisPlateRadius: true,
315
+ emphasisPlatePadX: true,
316
+ emphasisPlatePadY: true,
317
+ annotationColor: true,
318
+ entrance: true,
319
+ entranceMs: true,
320
+ entranceStaggerMs: true,
321
+ exitMs: true
322
+ }).strict();
323
+ var CaptionLayoutInput = z2.object({
324
+ segmentIndex: z2.number().int().nonnegative(),
325
+ wordStartIndex: z2.number().int().nonnegative(),
326
+ wordEndIndex: z2.number().int().positive().describe("Exclusive word index in the saved caption segment."),
327
+ region: CaptionRegion,
328
+ scale: z2.number().min(0.25).max(4).optional(),
329
+ rotation: z2.number().min(-45).max(45).optional(),
330
+ style: CaptionLayoutStyle.optional(),
331
+ holdThroughWordIndex: z2.number().int().nonnegative().optional().describe(
332
+ "Inclusive saved-word index in this segment. The layout stays visible until that word ends."
333
+ ),
334
+ wordMarks: z2.array(z2.object({
335
+ wordIndex: z2.number().int().nonnegative(),
336
+ emphasis: z2.boolean().optional(),
337
+ annotate: z2.enum(["circle", "underline"]).nullable().optional()
338
+ }).strict()).optional()
339
+ }).strict();
340
+ var CaptionLayout = CaptionLayoutInput.extend({
341
+ wordSignature: z2.string().regex(/^[a-f0-9]{64}$/)
342
+ });
343
+ function captionSegmentSignature(words) {
344
+ const encoded = JSON.stringify(words.map((word) => ({
345
+ text: word.text,
346
+ startMs: word.startMs,
347
+ endMs: word.endMs,
348
+ emphasis: word.emphasis ?? null,
349
+ annotate: word.annotate ?? null
350
+ })));
351
+ return bytesToHex(sha256(new TextEncoder().encode(encoded)));
352
+ }
353
+ function captionLayoutIssues(segments, layouts, checkSignature = false) {
354
+ const issues = [];
355
+ const occupied = /* @__PURE__ */ new Set();
356
+ layouts.forEach((layout, index) => {
357
+ const words = segments[layout.segmentIndex]?.words;
358
+ if (!words) {
359
+ issues.push({ index, field: "segmentIndex", message: "caption layout segment does not exist" });
360
+ return;
361
+ }
362
+ if (layout.wordStartIndex >= layout.wordEndIndex || layout.wordEndIndex > words.length) {
363
+ issues.push({ index, field: "wordEndIndex", message: "caption layout word range is outside its segment" });
364
+ return;
365
+ }
366
+ const hold = layout.holdThroughWordIndex ?? layout.wordEndIndex - 1;
367
+ if (hold < layout.wordEndIndex - 1 || hold >= words.length) {
368
+ issues.push({ index, field: "holdThroughWordIndex", message: "caption layout hold must reference this or a later word in the same segment" });
369
+ }
370
+ const marked = /* @__PURE__ */ new Set();
371
+ for (const mark of layout.wordMarks ?? []) {
372
+ if (mark.wordIndex < layout.wordStartIndex || mark.wordIndex >= layout.wordEndIndex || marked.has(mark.wordIndex)) {
373
+ issues.push({ index, field: "wordMarks", message: "caption layout word marks must be unique words inside the layout range" });
374
+ }
375
+ marked.add(mark.wordIndex);
376
+ }
377
+ for (let word = layout.wordStartIndex; word < layout.wordEndIndex; word++) {
378
+ const key = `${layout.segmentIndex}:${word}`;
379
+ if (occupied.has(key)) issues.push({ index, field: "wordStartIndex", message: "caption layout word ranges must not overlap" });
380
+ occupied.add(key);
381
+ }
382
+ if (checkSignature && "wordSignature" in layout && layout.wordSignature !== captionSegmentSignature(words)) {
383
+ issues.push({ index, field: "wordSignature", message: "caption layout is stale for these saved transcript words; reapply setCaptionLayouts" });
384
+ }
385
+ });
386
+ return issues;
387
+ }
291
388
  var CaptionSegment = z2.object({
292
389
  words: z2.array(CaptionWord)
293
390
  });
@@ -298,18 +395,29 @@ var CompositionCaptions = z2.discriminatedUnion("source", [
298
395
  from: z2.object({ clipId: z2.string() }),
299
396
  segments: z2.array(CaptionSegment),
300
397
  style: CaptionStyle.optional(),
398
+ layouts: z2.array(CaptionLayout).optional(),
301
399
  zPlane: CaptionDepth
302
400
  }),
303
401
  z2.object({
304
402
  source: z2.literal("authored"),
305
403
  segments: z2.array(CaptionSegment),
306
404
  style: CaptionStyle.optional(),
405
+ layouts: z2.array(CaptionLayout).optional(),
307
406
  zPlane: CaptionDepth
308
407
  })
309
408
  ]).superRefine((captions, ctx) => {
310
- if (!captions.style) return;
311
- for (const issue of captionFaceIssues(captions.style)) {
312
- ctx.addIssue({ code: "custom", path: ["style", issue.field], message: issue.message });
409
+ if (captions.style) {
410
+ for (const issue of captionFaceIssues(captions.style)) {
411
+ ctx.addIssue({ code: "custom", path: ["style", issue.field], message: issue.message });
412
+ }
413
+ }
414
+ for (const issue of captionLayoutIssues(captions.segments, captions.layouts ?? [], true)) {
415
+ ctx.addIssue({ code: "custom", path: ["layouts", issue.index, issue.field], message: issue.message });
416
+ }
417
+ for (const [index, layout] of (captions.layouts ?? []).entries()) {
418
+ for (const issue of captionFaceIssues({ ...captions.style, ...layout.style })) {
419
+ ctx.addIssue({ code: "custom", path: ["layouts", index, "style", issue.field], message: issue.message });
420
+ }
313
421
  }
314
422
  });
315
423
  function captionsAreBehindSubject(captions) {
@@ -540,6 +648,11 @@ export {
540
648
  Marker,
541
649
  CaptionWord,
542
650
  CaptionStyle,
651
+ CaptionLayoutStyle,
652
+ CaptionLayoutInput,
653
+ CaptionLayout,
654
+ captionSegmentSignature,
655
+ captionLayoutIssues,
543
656
  CaptionSegment,
544
657
  CompositionCaptions,
545
658
  captionsAreBehindSubject,
@@ -5,7 +5,7 @@ import {
5
5
  bundledFontFamilyFaces,
6
6
  isGenericOnlyStack,
7
7
  parseFontStack
8
- } from "./chunk-AVTEPAWI.js";
8
+ } from "./chunk-7RGDRDAR.js";
9
9
 
10
10
  // ../execution/render-harness/src/cardFonts.ts
11
11
  import parseCss from "css-tree/parser";