@mengine/medeo-client 3.0.1-dsl.4 → 3.1.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.
@@ -145,12 +145,8 @@ function assertCaptionContent(value) {
145
145
  }
146
146
  const phonemeFields = {
147
147
  ...selectionFields,
148
- phonemeScript: optional$1(nonempty)
148
+ phonemeScript: optional$1(string)
149
149
  };
150
- /** D190: omitted pronunciation uses the selected source words; an authored pronunciation is never empty. */
151
- function assertPhonemeScript(value) {
152
- if (value !== void 0 && !nonempty(value)) throw new Error("phonemeScript must be nonempty when provided");
153
- }
154
150
  function assertPhoneme(value) {
155
151
  if (!segmentSelection(phonemeFields)(value)) throw new Error("Invalid phoneme");
156
152
  }
@@ -173,9 +169,39 @@ function assertScriptSegment(value) {
173
169
  /** A script's segments: each with its own id, unique within the script. */
174
170
  const segments = (v) => Array.isArray(v) && v.every(segment) && new Set(v.map((s) => s.id)).size === v.length;
175
171
  const locator = {
176
- system: required(oneOf("memota", "memota-speech")),
172
+ system: required(oneOf("memota", "memota-speech", "placeholder")),
177
173
  key: required(nonempty)
178
174
  };
175
+ /**
176
+ * A placeholder medium is an Image with no file, standing in where a legacy VideoDraft placed a
177
+ * visual clip the old editor drew as a blank (D196). The blank one stands in for a clip placed
178
+ * without media (`origin_media_id: ''`); one keyed by a memota media id stands in for media the
179
+ * clip named but Director found missing, so the id survives. Importing keeps the clips' time on
180
+ * them instead of dropping them. Only migration authors them.
181
+ */
182
+ const BLANK_PLACEHOLDER = {
183
+ system: "placeholder",
184
+ key: "blank",
185
+ kind: "image"
186
+ };
187
+ /**
188
+ * Only a memota media id may key a missing-media placeholder: visual parts name media and nothing
189
+ * else, and a key of this shape can never be mistaken for a file.
190
+ */
191
+ const MISSING_MEDIA_KEY = /^media_[0-9A-Za-z]+$/;
192
+ /** The placeholder for a memota media id Director found missing. */
193
+ function missingMediaPlaceholder(mediaId) {
194
+ return {
195
+ system: "placeholder",
196
+ key: mediaId,
197
+ kind: "image"
198
+ };
199
+ }
200
+ function assertPlaceholder(media) {
201
+ if (media.system !== "placeholder") return;
202
+ if (media.kind !== "image" || media.key !== BLANK_PLACEHOLDER.key && !(typeof media.key === "string" && MISSING_MEDIA_KEY.test(media.key))) throw new Error("A placeholder is the blank Image or one standing in for a missing memota media id");
203
+ if (media.storageKey !== void 0 || media.digest !== void 0) throw new Error("A placeholder has no file");
204
+ }
179
205
  const entityFields = {
180
206
  "timeline-entities": {},
181
207
  "track-entities": {
@@ -271,6 +297,7 @@ function assertEntity(table, value) {
271
297
  checkFields(value, entityFields[table]);
272
298
  if (table === "audio-script-entities") assertTranscriptFile(value);
273
299
  if (table === "media-entities") {
300
+ assertPlaceholder(value);
274
301
  if (value.kind === "image" && value.durationMs !== void 0) throw new Error("Image has no intrinsic duration");
275
302
  if (value.kind !== "image" && !positive(value.durationMs)) throw new Error("Finite media requires durationMs");
276
303
  if (["audio", "voiceover-recording"].includes(value.kind) && !nonempty(value.storageKey)) throw new Error("Audio requires storageKey");
@@ -299,6 +326,7 @@ function assertEntityPatch(table, patch, current) {
299
326
  ]) if (Object.hasOwn(patch, key) && patch[key] !== current[key]) throw new Error(`Resource identity field is immutable: ${key}`);
300
327
  }
301
328
  if (table === "media-entities") {
329
+ assertPlaceholder(next);
302
330
  if (Object.hasOwn(patch, "durationMs")) {
303
331
  if (next.kind === "image" ? next.durationMs !== void 0 : !positive(next.durationMs)) throw new Error("durationMs must match the media kind");
304
332
  }
@@ -642,63 +670,6 @@ function assertContainTree(rows, tree) {
642
670
  }
643
671
  }
644
672
  //#endregion
645
- //#region ../medeo-dsl/src/clip-playback.ts
646
- const playable = {
647
- video_clip: ["video", "image"],
648
- speech: ["audio", "voiceover-recording"],
649
- caption: ["caption"],
650
- bgm: ["audio"]
651
- };
652
- function supportsClipSpeed(source) {
653
- return isRecord(source) && [
654
- "video",
655
- "audio",
656
- "voiceover-recording"
657
- ].includes(String(source.kind));
658
- }
659
- /** Content-kind admission is shared by Actions, script compilation and the final transaction. */
660
- function assertClipSourceRole(role, kind) {
661
- if (!playable[role]?.includes(kind)) throw new Error(`A ${role} Track does not play ${kind}`);
662
- }
663
- function assertClipWindow(window, duration) {
664
- if (!Number.isSafeInteger(window.inMs) || !Number.isSafeInteger(window.outMs) || window.inMs < 0 || window.outMs <= window.inMs) throw new Error("Invalid Clip window");
665
- if (duration !== void 0 && window.outMs > duration) throw new Error(`Invalid Clip window: ${window.outMs}ms is past its content's ${duration}ms`);
666
- }
667
- /** Check authored playback facts; a remote merge is still read by projection with diagnostics. */
668
- function assertClipPlayback(clip, source, role) {
669
- if (!isRecord(source)) throw new Error("Invalid Clip source");
670
- const kind = source.kind ?? "caption";
671
- const marker = clip.seqMarker;
672
- assertSeqMarker(marker);
673
- if (marker.outMs <= marker.inMs) throw new Error("Clip window must be nonempty");
674
- if (role !== void 0) assertClipSourceRole(role, kind);
675
- if (marker.rate !== void 0 && !supportsClipSpeed(source)) throw new Error("Only video and sound change speed");
676
- const duration = typeof source.durationMs === "number" ? source.durationMs : void 0;
677
- if (marker.fillTimeline) {
678
- if (kind !== "audio") throw new Error("Only BGM audio fills the Timeline");
679
- if (marker.inMs !== 0 || marker.rate !== void 0 && marker.rate !== 1 || marker.outMs !== duration) throw new Error(`A Clip that fills the Timeline plays its whole source at 1×: inMs 0, outMs ${duration ?? "its length"}, no rate; the Timeline's end cuts it`);
680
- }
681
- assertClipWindow(marker);
682
- }
683
- function clipParents(tree) {
684
- const result = /* @__PURE__ */ new Map();
685
- for (const track of tree) for (const clip of track.children ?? []) result.set(clip.entityId, track.entityId);
686
- return result;
687
- }
688
- /** A new parent or Track role revalidates that binding, without blocking unrelated merged rows. */
689
- function assertClipPlacements(before, after, previousTree, nextTree) {
690
- const previous = clipParents(previousTree);
691
- for (const [id, parent] of clipParents(nextTree)) {
692
- const track = after["track-entities"][parent];
693
- const previousTrack = before["track-entities"][parent];
694
- if (previous.get(id) === parent && (track === void 0 || isRecord(track) && isRecord(previousTrack) && previousTrack.role === track.role)) continue;
695
- const role = requireEntity(after, "track-entities", parent).role;
696
- const clip = after["clip-entities"][id];
697
- if (!clip) continue;
698
- assertClipPlayback(clip, after["media-entities"][clip.source] ?? after["caption-entities"][clip.source], role);
699
- }
700
- }
701
- //#endregion
702
673
  //#region ../medeo-dsl/src/plan.ts
703
674
  /** Include removed nested keys: a partial merge alone would conceal a deleted required field. */
704
675
  function replacementPatch(before, after) {
@@ -714,7 +685,6 @@ function replacementPatch(before, after) {
714
685
  */
715
686
  function assertChangeAdmitted(change) {
716
687
  const { before, after } = change;
717
- const parents = change.containment ? clipParents(change.containment.after) : void 0;
718
688
  for (const [table, rows] of change.entities) for (const [id, fields] of rows) {
719
689
  const value = after[table][id];
720
690
  if (value === void 0) continue;
@@ -729,17 +699,25 @@ function assertChangeAdmitted(change) {
729
699
  }
730
700
  const row = value;
731
701
  for (const field of referenceFields[table] ?? []) if (!old || fields.has(field)) assertReferenceAdmitted(after, table, id, field, row[field]);
732
- if (table === "clip-entities" && (!old || fields.has("seqMarker") || fields.has("source"))) {
733
- const parent = parents?.get(id);
734
- assertClipPlayback(row, after["media-entities"][row.source] ?? after["caption-entities"][row.source], parent === void 0 ? void 0 : requireEntity(after, "track-entities", parent).role);
735
- }
702
+ const marker = row.seqMarker;
703
+ if (table === "clip-entities" && (!old || fields.has("seqMarker")) && marker?.outMs <= marker?.inMs) throw new Error("Clip window must be nonempty");
704
+ if (table === "clip-entities" && (!old || fields.has("seqMarker") || fields.has("source"))) assertFillsWithWholeSource(after, marker, row.source);
736
705
  }
737
706
  if (change.entities.has("timeline-entities") && Object.keys(after["timeline-entities"]).length > 1) throw new Error("Document already has a Timeline");
738
707
  for (const [table, ids] of change.relations) for (const id of ids) {
739
708
  const value = after[table][id];
740
709
  if (value !== void 0) assertRelationAdmitted(after, table, id, value);
741
710
  }
742
- if (change.containment) assertClipPlacements(before, after, change.containment.before, change.containment.after);
711
+ }
712
+ /**
713
+ * A Clip that fills the Timeline plays its whole source at 1×: the player loops a short source and
714
+ * cuts a long one at the Timeline's end (RFC 02 §3.3), so a window or speed of its own has no meaning.
715
+ */
716
+ function assertFillsWithWholeSource(rows, marker, source) {
717
+ if (marker?.fillTimeline !== true) return;
718
+ const media = typeof source === "string" ? rows["media-entities"][source] : void 0;
719
+ const durationMs = typeof media?.durationMs === "number" ? media.durationMs : void 0;
720
+ if (marker.inMs !== 0 || marker.rate !== void 0 && marker.rate !== 1 || durationMs !== void 0 && marker.outMs !== durationMs) throw new Error(`A Clip that fills the Timeline plays its whole source at 1×: inMs 0, outMs ${durationMs ?? "its length"}, no rate; the Timeline's end cuts it`);
743
721
  }
744
722
  //#endregion
745
723
  //#region src/dsl/projection/types.ts
@@ -975,11 +953,15 @@ function requireSource(s, id, role) {
975
953
  return media;
976
954
  }
977
955
  function assertMediaRole(media, role) {
978
- if (!media) throw new Error(`Missing source for ${role} Track`);
979
- assertClipSourceRole(role, media.kind);
956
+ if (!media || !{
957
+ video_clip: ["video", "image"],
958
+ speech: ["audio", "voiceover-recording"],
959
+ bgm: ["audio"],
960
+ caption: []
961
+ }[role].includes(media.kind)) throw new Error(`Source is not allowed on ${role} Track`);
980
962
  }
981
963
  function validateWindow(window, duration) {
982
- assertClipWindow(window, duration);
964
+ if (!Number.isSafeInteger(window.inMs) || !Number.isSafeInteger(window.outMs) || window.inMs < 0 || window.outMs <= window.inMs || duration !== void 0 && window.outMs > duration) throw new Error("Invalid Clip window");
983
965
  }
984
966
  function trackOf(s, id) {
985
967
  requireClip(s, id);
@@ -1267,15 +1249,14 @@ function replaceSourcePlan(s, clipId, input) {
1267
1249
  patch: {
1268
1250
  seqMarker: {
1269
1251
  inMs: input.inMs,
1270
- outMs: input.outMs,
1271
- ...supportsClipSpeed(source.source) ? {} : { rate: void 0 }
1252
+ outMs: input.outMs
1272
1253
  },
1273
1254
  source: source.id
1274
1255
  }
1275
1256
  }];
1276
1257
  }
1277
1258
  //#endregion
1278
- //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.1/node_modules/loro-mirror/dist/schema/resolver.js
1259
+ //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.4/node_modules/loro-mirror/dist/schema/resolver.js
1279
1260
  function getMapFieldSchema(schema, key) {
1280
1261
  if (!schema) return void 0;
1281
1262
  if (Object.prototype.hasOwnProperty.call(schema.definition, key)) return schema.definition[key];
@@ -1305,10 +1286,10 @@ function getChildContainerSchema(schema, childKey) {
1305
1286
  }
1306
1287
  }
1307
1288
  //#endregion
1308
- //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.1/node_modules/loro-mirror/dist/constants.js
1289
+ //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.4/node_modules/loro-mirror/dist/constants.js
1309
1290
  const CID_KEY = "$cid";
1310
1291
  //#endregion
1311
- //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.1/node_modules/loro-mirror/dist/core/container-inference.js
1292
+ //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.4/node_modules/loro-mirror/dist/core/container-inference.js
1312
1293
  function isPlainObjectValue(value) {
1313
1294
  return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date) && !(value instanceof RegExp) && typeof value !== "function";
1314
1295
  }
@@ -1347,7 +1328,7 @@ function getContainerValueKind(value) {
1347
1328
  return "other";
1348
1329
  }
1349
1330
  //#endregion
1350
- //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.1/node_modules/loro-mirror/dist/core/utils.js
1331
+ //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.4/node_modules/loro-mirror/dist/core/utils.js
1351
1332
  /**
1352
1333
  * Utility functions for Loro Mirror core
1353
1334
  */
@@ -1771,7 +1752,7 @@ function safeStringify(value, indent = 2) {
1771
1752
  return JSON.stringify(replacer(value), null, indent);
1772
1753
  }
1773
1754
  //#endregion
1774
- //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.1/node_modules/loro-mirror/dist/schema/validators.js
1755
+ //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.4/node_modules/loro-mirror/dist/schema/validators.js
1775
1756
  const schemaValidationCache = /* @__PURE__ */ new WeakMap();
1776
1757
  function isCacheableValue(value) {
1777
1758
  return typeof value === "object" && value !== null;
@@ -2051,7 +2032,7 @@ function getDefaultValue(schema) {
2051
2032
  }
2052
2033
  }
2053
2034
  //#endregion
2054
- //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.1/node_modules/loro-mirror/dist/schema/index.js
2035
+ //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.4/node_modules/loro-mirror/dist/schema/index.js
2055
2036
  /**
2056
2037
  * Create a schema definition
2057
2038
  */
@@ -2816,7 +2797,7 @@ const produce = new Immer2({
2816
2797
  useStrictShallowCopy: true
2817
2798
  }).produce;
2818
2799
  //#endregion
2819
- //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.1/node_modules/loro-mirror/dist/core/debounce-timer.js
2800
+ //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.4/node_modules/loro-mirror/dist/core/debounce-timer.js
2820
2801
  /**
2821
2802
  * Deadline-based debounce timer.
2822
2803
  *
@@ -2874,7 +2855,7 @@ var DebounceTimer = class {
2874
2855
  }
2875
2856
  };
2876
2857
  //#endregion
2877
- //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.1/node_modules/loro-mirror/dist/core/ephemeral.js
2858
+ //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.4/node_modules/loro-mirror/dist/core/ephemeral.js
2878
2859
  /**
2879
2860
  * Manages ephemeral patches: storage, composition, path resolution, and finalization.
2880
2861
  */
@@ -3149,7 +3130,7 @@ var EphemeralPatchManager = class {
3149
3130
  }
3150
3131
  };
3151
3132
  //#endregion
3152
- //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.1/node_modules/loro-mirror/dist/core/tree-utils.js
3133
+ //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.4/node_modules/loro-mirror/dist/core/tree-utils.js
3153
3134
  function normalizeTreeJson(input, options) {
3154
3135
  if (!Array.isArray(input)) return [];
3155
3136
  return input.map((node) => normalizeTreeNode(node, options));
@@ -3172,7 +3153,7 @@ function isPlainRecord(value) {
3172
3153
  return typeof value === "object" && value !== null && !Array.isArray(value);
3173
3154
  }
3174
3155
  //#endregion
3175
- //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.1/node_modules/loro-mirror/dist/core/loroEventApply.js
3156
+ //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.4/node_modules/loro-mirror/dist/core/loroEventApply.js
3176
3157
  function isJSONObject(v) {
3177
3158
  return typeof v === "object" && v !== null && !Array.isArray(v);
3178
3159
  }
@@ -3594,7 +3575,7 @@ function isIgnoredByAncestor(id, ignoreSet, getContainerById) {
3594
3575
  return false;
3595
3576
  }
3596
3577
  //#endregion
3597
- //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.1/node_modules/loro-mirror/dist/core/diff.js
3578
+ //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.4/node_modules/loro-mirror/dist/core/diff.js
3598
3579
  /**
3599
3580
  * Finds the longest increasing subsequence of a sequence of numbers
3600
3581
  * @param sequence The sequence of numbers
@@ -4325,7 +4306,7 @@ function diffMap(doc, oldState, newState, containerId, schema, inferOptions) {
4325
4306
  return changes;
4326
4307
  }
4327
4308
  //#endregion
4328
- //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.1/node_modules/loro-mirror/dist/core/mirror.js
4309
+ //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.4/node_modules/loro-mirror/dist/core/mirror.js
4329
4310
  /**
4330
4311
  * Mirror core functionality for bidirectional sync between app state and Loro CRDT
4331
4312
  */
@@ -5896,30 +5877,6 @@ const phonemeCodec = codec((v) => {
5896
5877
  assertPhoneme(value);
5897
5878
  return value;
5898
5879
  });
5899
- /** A row-owned binding couples the selection, pronunciation and its immutable file in one LWW value. */
5900
- const phoneticBindingCodec = codec((value) => {
5901
- assertEntity("phonetic-script-entities", value);
5902
- assertPhoneticScript(void 0, { ...value });
5903
- return JSON.stringify({
5904
- phoneme: JSON.parse(phonemeCodec.encode(value.phoneme)),
5905
- system: value.system,
5906
- key: value.key,
5907
- storageKey: value.storageKey,
5908
- digest: value.digest
5909
- });
5910
- }, (raw) => {
5911
- const value = JSON.parse(raw);
5912
- if (!isRecord(value)) throw new Error("Expected a PhoneticScript binding");
5913
- const phoneme = phonemeCodec.decode(JSON.stringify(value.phoneme));
5914
- if (isInvalidAtomic(phoneme)) throw new Error(phoneme.reason);
5915
- const binding = {
5916
- ...value,
5917
- phoneme
5918
- };
5919
- assertEntity("phonetic-script-entities", binding);
5920
- assertPhoneticScript(void 0, binding);
5921
- return binding;
5922
- });
5923
5880
  /** A Clip's sequence marker is stored as one string, so it is taken or replaced whole. */
5924
5881
  const seqMarkerCodec = codec((v) => {
5925
5882
  assertSeqMarker(v);
@@ -5986,7 +5943,13 @@ const medeoDslLoroShape = schema({
5986
5943
  storageKey: schema.String(optional),
5987
5944
  digest: schema.String(optional)
5988
5945
  })),
5989
- "phonetic-script-entities": schema.LoroMapRecord(schema.LoroMap({ binding: atomicValue(phoneticBindingCodec) })),
5946
+ "phonetic-script-entities": schema.LoroMapRecord(schema.LoroMap({
5947
+ phoneme: atomicValue(phonemeCodec),
5948
+ system: schema.String(optional),
5949
+ key: schema.String(optional),
5950
+ storageKey: schema.String(optional),
5951
+ digest: schema.String(optional)
5952
+ })),
5990
5953
  "caption-entities": schema.LoroMapRecord(schema.LoroMap({
5991
5954
  selection: optionalAtomicValue(captionContentCodec),
5992
5955
  text: schema.LoroText(optional),
@@ -6038,78 +6001,20 @@ function documentFormat(doc) {
6038
6001
  }
6039
6002
  var UnsupportedDocumentFormatError = class extends Error {
6040
6003
  code = "unsupported_document_format";
6041
- constructor(message = "Expected a new DSL document; legacy, mixed or uninitialized content is not supported") {
6042
- super(message);
6004
+ constructor() {
6005
+ super("Expected a new DSL document; legacy, mixed or uninitialized content is not supported");
6043
6006
  this.name = "UnsupportedDocumentFormatError";
6044
6007
  }
6045
6008
  };
6046
6009
  /** Run after initial import, before opening an editable handle. Empty new documents still have one Timeline. */
6047
6010
  function assertDslDocument(doc) {
6048
6011
  if (documentFormat(doc) !== "dsl" || doc.getMap("timeline-entities").keys().length !== 1) throw new UnsupportedDocumentFormatError();
6049
- assertPhoneticBindingFormat(doc);
6050
- }
6051
- /** This storage cutover has no field-per-fact reader or mixed-version write path. */
6052
- function assertPhoneticBindingFormat(doc) {
6053
- if (!Object.hasOwn(doc.getShallowValue(), "phonetic-script-entities")) return;
6054
- const table = doc.getMap("phonetic-script-entities");
6055
- for (const id of table.keys()) {
6056
- const row = table.get(id);
6057
- if (!isContainer(row) || row.kind() !== "Map" || row.keys().some((key) => key !== "binding") || typeof row.get("binding") !== "string") throw new UnsupportedDocumentFormatError("PhoneticScript storage requires one atomic binding; old DSL documents need explicit migration");
6058
- }
6059
6012
  }
6060
6013
  //#endregion
6061
6014
  //#region ../medeo-dsl/src/schema/text-styles.ts
6062
6015
  /** How rich-text marks extend at their edges: a language runs on into text typed after it. */
6063
6016
  const textStyles = { language: { expand: "after" } };
6064
6017
  //#endregion
6065
- //#region src/dsl/document/phonetic-binding.ts
6066
- /**
6067
- * Actions and the transaction guard keep their domain-shaped rows. The native schema stores
6068
- * a PhoneticScript's facts under one binding; this lens applies draft edits to that single value.
6069
- * It never reads the retired field-per-fact representation.
6070
- */
6071
- function logicalDslState(native) {
6072
- const phonetics = new Proxy({}, {
6073
- get: (_target, key) => typeof key === "string" ? native["phonetic-script-entities"]?.[key]?.binding : void 0,
6074
- set: (_target, key, value) => {
6075
- if (typeof key !== "string") throw new Error("Invalid PhoneticScript key");
6076
- native["phonetic-script-entities"] ??= {};
6077
- const existing = native["phonetic-script-entities"][key];
6078
- if (existing) existing.binding = value;
6079
- else native["phonetic-script-entities"][key] = { binding: value };
6080
- return true;
6081
- },
6082
- deleteProperty: (_target, key) => {
6083
- if (typeof key === "string") delete native["phonetic-script-entities"][key];
6084
- return true;
6085
- },
6086
- ownKeys: () => Object.keys(native["phonetic-script-entities"] ?? {}),
6087
- has: (_target, key) => typeof key === "string" && Object.hasOwn(native["phonetic-script-entities"] ?? {}, key),
6088
- getOwnPropertyDescriptor: (_target, key) => typeof key === "string" && Object.hasOwn(native["phonetic-script-entities"] ?? {}, key) ? {
6089
- configurable: true,
6090
- enumerable: true,
6091
- writable: true,
6092
- value: native["phonetic-script-entities"][key].binding
6093
- } : void 0
6094
- });
6095
- const view = {};
6096
- for (const key of new Set([
6097
- ...Object.keys(native),
6098
- ...entityTables,
6099
- ...relationTables,
6100
- "contain-relations"
6101
- ])) Object.defineProperty(view, key, {
6102
- configurable: true,
6103
- enumerable: true,
6104
- get: () => key === "phonetic-script-entities" ? phonetics : Reflect.get(native, key),
6105
- set: (value) => {
6106
- if (key === "phonetic-script-entities") throw new Error("Cannot replace the PhoneticScript table");
6107
- Reflect.set(native, key, value);
6108
- }
6109
- });
6110
- return view;
6111
- }
6112
- //#endregion
6113
6018
  //#region src/dsl/document/read.ts
6114
6019
  /** Looking up a missing root must not create it. */
6115
6020
  function rootMap(doc, name) {
@@ -6193,7 +6098,6 @@ function referenceTargets(table, row) {
6193
6098
  }
6194
6099
  }
6195
6100
  function readMedeoDsl(doc, state) {
6196
- const logical = logicalDslState(state);
6197
6101
  const diagnostics = [];
6198
6102
  const diagnostic = (code, path, error) => diagnostics.push({
6199
6103
  code,
@@ -6203,7 +6107,7 @@ function readMedeoDsl(doc, state) {
6203
6107
  const entities = Object.fromEntries(entityTables.map((t) => [t, {}]));
6204
6108
  const relations = Object.fromEntries(relationTables.map((t) => [t, {}]));
6205
6109
  const richText = {};
6206
- for (const table of entityTables) for (const [id, raw] of Object.entries(logical[table] ?? {})) try {
6110
+ for (const table of entityTables) for (const [id, raw] of Object.entries(state[table] ?? {})) try {
6207
6111
  assertId(id);
6208
6112
  let v = raw;
6209
6113
  for (const field of optionalReferences[table] ?? []) {
@@ -6709,11 +6613,7 @@ function validateDraft(tracked) {
6709
6613
  before,
6710
6614
  after: state,
6711
6615
  entities: tracked.entities,
6712
- relations: tracked.relations,
6713
- containment: {
6714
- before: plainTree(before["contain-relations"] ?? []),
6715
- after: plainTree(state["contain-relations"] ?? [])
6716
- }
6616
+ relations: tracked.relations
6717
6617
  });
6718
6618
  if (!tracked.treeChanged) return;
6719
6619
  const oldNodes = nodeIndex(before["contain-relations"] ?? []);
@@ -6790,11 +6690,8 @@ function runTransaction(doc, mirror, edit, options, setReject) {
6790
6690
  const steps = [];
6791
6691
  const newCaptionTexts = /* @__PURE__ */ new Map();
6792
6692
  const bodies = /* @__PURE__ */ new Map();
6793
- const nativeBefore = mirror.getState();
6794
- const before = logicalDslState(nativeBefore);
6795
- const phoneticRows = /* @__PURE__ */ new Map();
6796
- mirror.setState((native) => {
6797
- const raw = logicalDslState(native);
6693
+ const before = mirror.getState();
6694
+ mirror.setState((raw) => {
6798
6695
  const tracked = new TransactionDraft(raw, before);
6799
6696
  setReject((error) => tracked.fail(error));
6800
6697
  /** The current text of a Caption's own Text or a script segment, as this transaction leaves it so far. */
@@ -6986,15 +6883,6 @@ function runTransaction(doc, mirror, edit, options, setReject) {
6986
6883
  for (const step of steps) if (step.type === "text") if ("captionId" in step.target) requireEntity(raw, "caption-entities", step.target.captionId);
6987
6884
  else requireSegment(raw, step.target.audioScriptId, step.target.segmentId);
6988
6885
  assertSelectionsAfter(raw, before, tracked, steps);
6989
- const changedPhonetics = new Set([...tracked.entities.get("phonetic-script-entities")?.keys() ?? [], ...steps.flatMap((step) => step.type !== "text" && step.table === "phonetic-script-entities" ? [step.id] : [])]);
6990
- for (const id of changedPhonetics) {
6991
- const row = raw["phonetic-script-entities"][id];
6992
- phoneticRows.set(id, row ? copyInput(row) : void 0);
6993
- if (!row) continue;
6994
- const old = nativeBefore["phonetic-script-entities"][id];
6995
- if (old) native["phonetic-script-entities"][id] = old;
6996
- else delete native["phonetic-script-entities"][id];
6997
- }
6998
6886
  for (const [id, fields] of tracked.entities.get("caption-entities") ?? []) {
6999
6887
  const row = raw["caption-entities"][id];
7000
6888
  if (fields.has("text") && row?.text !== void 0 && before["caption-entities"][id]?.text === void 0) {
@@ -7003,7 +6891,6 @@ function runTransaction(doc, mirror, edit, options, setReject) {
7003
6891
  }
7004
6892
  }
7005
6893
  for (const table of Object.keys(selectionField)) {
7006
- if (table === "phonetic-script-entities") continue;
7007
6894
  const field = selectionField[table];
7008
6895
  for (const [id, fields] of tracked.entities.get(table) ?? []) {
7009
6896
  const row = raw[table][id];
@@ -7043,8 +6930,6 @@ function runTransaction(doc, mirror, edit, options, setReject) {
7043
6930
  applyTextEdits(text, step.edits);
7044
6931
  continue;
7045
6932
  }
7046
- const phonetic = step.table === "phonetic-script-entities" ? phoneticRows.get(step.id) : void 0;
7047
- if (step.table === "phonetic-script-entities" && !phonetic) continue;
7048
6933
  const row = entityRow(doc, step.table, step.id);
7049
6934
  const field = selectionField[step.table];
7050
6935
  if (step.type === "range") {
@@ -7063,21 +6948,12 @@ function runTransaction(doc, mirror, edit, options, setReject) {
7063
6948
  if (!value.start || !value.end) throw new Error("Unable to construct prepared range");
7064
6949
  }
7065
6950
  if (step.phonemeScript !== void 0) value.phonemeScript = step.phonemeScript;
7066
- if (phonetic) phonetic.phoneme = value;
7067
- else {
7068
- if (!row) throw new Error("Unable to construct prepared range");
7069
- row.set(field, codecs[step.table].encode(value));
7070
- }
7071
- } else if (step.value !== void 0) if (phonetic) phonetic.phoneme = step.value;
7072
- else {
6951
+ if (!row) throw new Error("Unable to construct prepared range");
6952
+ row.set(field, codecs[step.table].encode(value));
6953
+ } else if (step.value !== void 0) {
7073
6954
  if (!row) throw new Error("Prepared selection owner is missing");
7074
6955
  row.set(field, codecs[step.table].encode(step.value));
7075
- }
7076
- else if (row?.get(field) !== void 0) row.delete(field);
7077
- }
7078
- for (const [id, binding] of phoneticRows) {
7079
- if (!binding) continue;
7080
- (entityRow(doc, "phonetic-script-entities", id) ?? doc.getMap("phonetic-script-entities").setContainer(id, new LoroMap())).set("binding", phoneticBindingCodec.encode(binding));
6956
+ } else if (row?.get(field) !== void 0) row.delete(field);
7081
6957
  }
7082
6958
  }
7083
6959
  });
@@ -7124,7 +7000,7 @@ function assertSelectionsAfter(raw, before, tracked, steps) {
7124
7000
  if (!row) continue;
7125
7001
  const phoneme = phonemes.has(id) ? phonemes.get(id) : row.phoneme;
7126
7002
  const old = before["phonetic-script-entities"][id];
7127
- assertPhoneticScript(old ? { ...old } : void 0, {
7003
+ assertPhoneticScript(old, {
7128
7004
  ...row,
7129
7005
  phoneme: isRecord(phoneme) ? phoneme : void 0
7130
7006
  });
@@ -7163,7 +7039,6 @@ var MedeoDsl = class MedeoDsl {
7163
7039
  "tracks",
7164
7040
  "part_library"
7165
7041
  ].some((key) => Object.hasOwn(roots, key))) throw new Error("Expected a new DSL document, not a legacy content model");
7166
- assertPhoneticBindingFormat(doc);
7167
7042
  doc.configTextStyle(textStyles);
7168
7043
  this.mirror = new Mirror({
7169
7044
  doc,
@@ -7227,11 +7102,6 @@ var MedeoDsl = class MedeoDsl {
7227
7102
  editCaptionText(captionId, expectedBodyId, edits, audit) {
7228
7103
  this.transact((tx) => tx.editCaptionText(captionId, edits, expectedBodyId), audit);
7229
7104
  }
7230
- /** Check the session's write capability before an Action starts host-owned resource I/O. */
7231
- assertWritable() {
7232
- this.assertReadyToWrite();
7233
- this.options.mutationGuard?.run(() => {});
7234
- }
7235
7105
  /**
7236
7106
  * A run inside one segment as native Cursors. Local ranges exclude insertions at their start and
7237
7107
  * include insertions at their end. K01 remains deferred.
@@ -7264,7 +7134,13 @@ var MedeoDsl = class MedeoDsl {
7264
7134
  if (this.ownsDoc) this.doc.free();
7265
7135
  }
7266
7136
  mutate(run) {
7267
- this.assertReadyToWrite();
7137
+ this.assertActive();
7138
+ if (this.writing || this.notifying) {
7139
+ const error = /* @__PURE__ */ new Error("A DSL mutation is already running");
7140
+ this.rejectTransaction?.(error);
7141
+ throw error;
7142
+ }
7143
+ if (this.doc.getPendingTxnLength() !== 0) throw new Error("Commit external native edits before a DSL mutation");
7268
7144
  const apply = () => {
7269
7145
  this.writing = true;
7270
7146
  try {
@@ -7279,15 +7155,6 @@ var MedeoDsl = class MedeoDsl {
7279
7155
  if (this.options.mutationGuard) this.options.mutationGuard.run(apply);
7280
7156
  else apply();
7281
7157
  }
7282
- assertReadyToWrite() {
7283
- this.assertActive();
7284
- if (this.writing || this.notifying) {
7285
- const error = /* @__PURE__ */ new Error("A DSL mutation is already running");
7286
- this.rejectTransaction?.(error);
7287
- throw error;
7288
- }
7289
- if (this.doc.getPendingTxnLength() !== 0) throw new Error("Commit external native edits before a DSL mutation");
7290
- }
7291
7158
  notify() {
7292
7159
  if (this.disposed || !this.dirty) return;
7293
7160
  this.dirty = false;
@@ -7314,13 +7181,12 @@ function spokenScripts(snapshot, recordingId) {
7314
7181
  const found = /* @__PURE__ */ new Map();
7315
7182
  const seen = /* @__PURE__ */ new Set();
7316
7183
  const visit = (id, phoneticScriptId) => {
7184
+ if (seen.has(id)) return;
7185
+ seen.add(id);
7317
7186
  if (snapshot["audio-script-entities"][id]) {
7318
- if (!found.has(id)) found.set(id, /* @__PURE__ */ new Set());
7319
- if (phoneticScriptId !== void 0) found.get(id).add(phoneticScriptId);
7187
+ if (!found.has(id)) found.set(id, phoneticScriptId);
7320
7188
  return;
7321
7189
  }
7322
- if (seen.has(id)) return;
7323
- seen.add(id);
7324
7190
  const phonetic = snapshot["phonetic-script-entities"][id];
7325
7191
  if (phonetic) {
7326
7192
  visit(phonetic.phoneme.audioScriptId, id);
@@ -7330,18 +7196,11 @@ function spokenScripts(snapshot, recordingId) {
7330
7196
  };
7331
7197
  visit(recordingId);
7332
7198
  if (!found.size) {
7333
- for (const r of relations) if (r.sourceId === recordingId && snapshot["audio-script-entities"][r.entityId]) found.set(r.entityId, /* @__PURE__ */ new Set());
7199
+ for (const r of relations) if (r.sourceId === recordingId && snapshot["audio-script-entities"][r.entityId]) found.set(r.entityId, void 0);
7334
7200
  }
7335
- return [...found].map(([audioScriptId, ids]) => {
7336
- const phoneticScriptIds = [...ids];
7337
- if (phoneticScriptIds.length === 1) return {
7338
- audioScriptId,
7339
- phoneticScriptId: phoneticScriptIds[0]
7340
- };
7341
- return phoneticScriptIds.length ? {
7342
- audioScriptId,
7343
- phoneticScriptIds
7344
- } : { audioScriptId };
7201
+ return [...found].map(([audioScriptId, phoneticScriptId]) => phoneticScriptId === void 0 ? { audioScriptId } : {
7202
+ audioScriptId,
7203
+ phoneticScriptId
7345
7204
  });
7346
7205
  }
7347
7206
  /** Every AudioScript in the document with its PhoneticScripts, the Speech Clips saying it and the Caption Clips showing it. */
@@ -7386,6 +7245,7 @@ function toAudioScriptViews(snapshot) {
7386
7245
  });
7387
7246
  }
7388
7247
  const layout = resolveDslLayout(snapshot);
7248
+ for (const view of views.values()) placeTranscript(snapshot, layout, view);
7389
7249
  const timbres = Object.values(snapshot["voice-timbre-relations"]);
7390
7250
  for (const track of layout.tracks) for (const { clipId } of track.clips) {
7391
7251
  const source = snapshot["clip-entities"][clipId]?.source;
@@ -7409,7 +7269,6 @@ function toAudioScriptViews(snapshot) {
7409
7269
  clipId,
7410
7270
  recordingId: source,
7411
7271
  ...spoken.phoneticScriptId === void 0 ? {} : { phoneticScriptId: spoken.phoneticScriptId },
7412
- ...spoken.phoneticScriptIds === void 0 ? {} : { phoneticScriptIds: spoken.phoneticScriptIds },
7413
7272
  voices,
7414
7273
  ...applied === void 0 ? {} : { applied }
7415
7274
  });
@@ -7426,23 +7285,19 @@ function toAudioScriptViews(snapshot) {
7426
7285
  segment
7427
7286
  });
7428
7287
  }
7429
- for (const view of views.values()) placeTranscript(snapshot, layout, view);
7430
7288
  return [...views.values()];
7431
7289
  }
7432
7290
  /**
7433
7291
  * A transcript's segments on the Timeline: a segment's time is in the medium the script was read
7434
- * off. An authored Caption anchor identifies its use of that medium; otherwise the earliest
7435
- * Clip holding that source moment is used. The source time is never taken from the Caption.
7292
+ * off, so it lands where the earliest Clip playing that medium holds that moment in its window.
7436
7293
  */
7437
7294
  function placeTranscript(snapshot, layout, view) {
7438
7295
  if (!view.sourceMediaIds.length) return;
7439
7296
  const plays = layout.tracks.flatMap((track) => track.clips).filter((clip) => view.sourceMediaIds.includes(snapshot["clip-entities"][clip.clipId]?.source ?? "")).sort((a, b) => a.startMs - b.startMs);
7440
- for (const [index, segment] of view.segments.entries()) {
7297
+ for (const segment of view.segments) {
7441
7298
  const at = segment.startMs;
7442
7299
  if (at === void 0) continue;
7443
- const anchoredUses = new Set(view.captions.filter((caption) => caption.segment === index).map((caption) => layout.clips[caption.clipId]?.anchorClipId));
7444
- const candidates = plays.filter((c) => c.inMs <= at && at < c.outMs);
7445
- const clip = candidates.find((c) => anchoredUses.has(c.clipId)) ?? candidates[0];
7300
+ const clip = plays.find((c) => c.inMs <= at && at < c.outMs);
7446
7301
  if (clip) segment.timelineMs = Math.round(clip.startMs + (at - clip.inMs) / clip.rate);
7447
7302
  }
7448
7303
  }
@@ -7453,26 +7308,14 @@ function placeTranscript(snapshot, layout, view) {
7453
7308
  * not what a take was synthesized from (D190).
7454
7309
  */
7455
7310
  function appliedSpeech(snapshot, recordingId, spoken) {
7456
- const ids = spoken.phoneticScriptIds ?? (spoken.phoneticScriptId === void 0 ? [] : [spoken.phoneticScriptId]);
7457
- if (!ids.length) return void 0;
7458
- let unknown = false;
7459
- for (const sourceId of ids) {
7460
- const recorded = Object.values(snapshot["derived-from-relations"]).find((r) => r.entityId === recordingId && r.sourceId === sourceId)?.sourceDigest;
7461
- const current = snapshot["phonetic-script-entities"][sourceId]?.digest;
7462
- if (recorded === void 0 || current === void 0) unknown = true;
7463
- else if (recorded !== current) return false;
7464
- }
7465
- return unknown ? void 0 : true;
7311
+ const sourceId = spoken.phoneticScriptId ?? spoken.audioScriptId;
7312
+ const recorded = Object.values(snapshot["derived-from-relations"]).find((r) => r.entityId === recordingId && r.sourceId === sourceId)?.sourceDigest;
7313
+ const current = spoken.phoneticScriptId === void 0 ? void 0 : snapshot["phonetic-script-entities"][sourceId]?.digest;
7314
+ return recorded === void 0 || current === void 0 ? void 0 : recorded === current;
7466
7315
  }
7467
7316
  //#endregion
7468
7317
  //#region src/dsl/voiceover-actions.ts
7469
- function matchesPlainPhoneticOrigin(s, id, scriptId, segmentId) {
7470
- const phonetic = s["phonetic-script-entities"][id];
7471
- const run = s.phoneticText[id];
7472
- const segment = s["audio-script-entities"][scriptId]?.segments.find((item) => item.id === segmentId);
7473
- return phonetic?.phoneme.audioScriptId === scriptId && phonetic.phoneme.segmentId === segmentId && phonetic.phoneme.phonemeScript === void 0 && run?.start === 0 && run.end === segment?.text.length;
7474
- }
7475
- function voiceoverPlan(s, input, result, plainPhoneticIds) {
7318
+ function voiceoverPlan(s, input, result) {
7476
7319
  if (requireTrack(s, input.trackId).role !== "speech" || input.media.kind !== "voiceover-recording") throw new Error("Voiceover requires a speech Track and a recording");
7477
7320
  if (input.captions.length && (!input.captionTrackId || requireTrack(s, input.captionTrackId).role !== "caption")) throw new Error("Caption Track is required");
7478
7321
  const existing = s["clip-entities"][result.clipId];
@@ -7511,44 +7354,15 @@ function voiceoverPlan(s, input, result, plainPhoneticIds) {
7511
7354
  inMs: 0,
7512
7355
  outMs: input.media.durationMs
7513
7356
  }, input.media.durationMs);
7514
- if (phoneticScriptId !== void 0) {
7515
- const recorded = Object.values(s["derived-from-relations"]).find((origin) => origin.entityId === result.mediaId && origin.sourceId === phoneticScriptId);
7516
- if (recorded?.sourceDigest !== void 0 && input.sourceDigest !== void 0 && recorded.sourceDigest !== input.sourceDigest) throw new Error("A recording already has a different synthesis digest");
7517
- if (!recorded || recorded.sourceDigest === void 0 && input.sourceDigest !== void 0) mutations.push({
7518
- type: "set-relation",
7519
- table: "derived-from-relations",
7520
- value: {
7521
- entityId: result.mediaId,
7522
- sourceId: phoneticScriptId,
7523
- ...input.sourceDigest === void 0 ? {} : { sourceDigest: input.sourceDigest }
7524
- }
7525
- });
7526
- } else {
7527
- if (input.sourceDigest !== void 0) throw new Error("A synthesis digest requires its PhoneticScript");
7528
- if (plainPhoneticIds.length !== segmentIds.length) throw new Error("Expected one PhoneticScript id per plain segment");
7529
- segmentIds.forEach((segmentId, i) => {
7530
- const id = plainPhoneticIds[i];
7531
- if (s["phonetic-script-entities"][id]) {
7532
- if (!matchesPlainPhoneticOrigin(s, id, result.audioScriptId, segmentId)) throw new Error("PhoneticScript id already names another synthesis origin");
7533
- } else mutations.push({
7534
- type: "create-entity",
7535
- table: "phonetic-script-entities",
7536
- id,
7537
- value: { phoneme: {
7538
- audioScriptId: result.audioScriptId,
7539
- segmentId
7540
- } }
7541
- });
7542
- if (!Object.values(s["derived-from-relations"]).some((origin) => origin.entityId === result.mediaId && origin.sourceId === id)) mutations.push({
7543
- type: "set-relation",
7544
- table: "derived-from-relations",
7545
- value: {
7546
- entityId: result.mediaId,
7547
- sourceId: id
7548
- }
7549
- });
7550
- });
7551
- }
7357
+ mutations.push({
7358
+ type: "set-relation",
7359
+ table: "derived-from-relations",
7360
+ value: {
7361
+ entityId: result.mediaId,
7362
+ sourceId: phoneticScriptId ?? result.audioScriptId,
7363
+ ...input.sourceDigest === void 0 ? {} : { sourceDigest: input.sourceDigest }
7364
+ }
7365
+ });
7552
7366
  if (input.voice) {
7553
7367
  const id = resourceId("voice", input.voice.system, input.voice.key);
7554
7368
  mutations.push({
@@ -8045,20 +7859,10 @@ var DslEditor = class {
8045
7859
  captionClipIds: input.captions.map((c) => c.clipId ?? this.idFactory("clip")),
8046
7860
  captionIds: input.captions.map(() => this.idFactory("caption"))
8047
7861
  };
8048
- const state = this.dsl.getSnapshot();
8049
- const plainSegments = input.script.segments === void 0 ? state["audio-script-entities"][result.audioScriptId]?.segments.map((segment) => segment.id) ?? [] : result.segmentIds;
8050
- const plainPhoneticIds = input.script.phoneticScriptId === void 0 ? plainSegments.map((segmentId) => {
8051
- const origins = Object.values(state["derived-from-relations"]).filter((origin) => {
8052
- if (origin.entityId !== result.mediaId) return false;
8053
- return matchesPlainPhoneticOrigin(state, origin.sourceId, result.audioScriptId, segmentId);
8054
- });
8055
- if (origins.length > 1) throw new Error("Ambiguous plain recording origin");
8056
- return origins[0]?.sourceId ?? this.idFactory("phonetic-script");
8057
- }) : [];
8058
7862
  this.write("applyVoiceover", {
8059
7863
  input,
8060
7864
  result
8061
- }, voiceoverPlan(state, input, result, plainPhoneticIds), options);
7865
+ }, voiceoverPlan(this.dsl.getSnapshot(), input, result), options);
8062
7866
  return result;
8063
7867
  }
8064
7868
  voiceoverScriptId(script) {
@@ -8075,12 +7879,8 @@ var DslEditor = class {
8075
7879
  * it; an unchanged one keeps its file. Recordings already spoken from it keep their derived-from.
8076
7880
  */
8077
7881
  async setPhoneticScript(input, options) {
8078
- this.dsl.assertWritable();
8079
7882
  const id = input.phoneticScriptId ?? this.idFactory("phonetic-script");
8080
7883
  const range = segmentRange(input);
8081
- assertId(id);
8082
- assertSegmentRange(range, requireSegment(this.dsl.getSnapshot(), range.audioScriptId, range.segmentId).text);
8083
- assertPhonemeScript(input.phonemeScript);
8084
7884
  const file = input.phonemeScript === void 0 ? {} : await this.fileFor(id, input.phonemeScript);
8085
7885
  const exists = Object.hasOwn(this.dsl.getSnapshot()["phonetic-script-entities"], id);
8086
7886
  const cleared = {
@@ -8289,13 +8089,14 @@ function toVideoDraft(snapshot) {
8289
8089
  duration_ms: clip.durationMs
8290
8090
  };
8291
8091
  if (track.role === "video_clip") {
8292
- if (!source || !["image", "video"].includes(source.kind) || source.system !== "memota") fail("Visual playback requires a memota Image or Video");
8092
+ const placeholder = source?.system === "placeholder";
8093
+ if (!source || !["image", "video"].includes(source.kind) || !(source.system === "memota" || placeholder)) fail("Visual playback requires a memota Image or Video, or a placeholder Image");
8293
8094
  if (clip.fillTimeline) fail("VideoDraft cannot express timeline-filling visual content");
8294
8095
  parts[clip.clipId] = { video_clip: {
8295
8096
  ...common,
8296
8097
  play_in: clip.inMs,
8297
8098
  play_out: clip.outMs,
8298
- origin_media_id: source.key,
8099
+ origin_media_id: placeholder && source.key === BLANK_PLACEHOLDER.key ? "" : source.key,
8299
8100
  volume: clip.volume,
8300
8101
  ...snapshot["clip-entities"][clip.clipId].seqMarker.rate === void 0 ? {} : { speed_shift: {
8301
8102
  category: "linear",
@@ -8304,8 +8105,7 @@ function toVideoDraft(snapshot) {
8304
8105
  } }
8305
8106
  } };
8306
8107
  } else if (track.role === "speech") {
8307
- if (!source || source.kind !== "audio" && source.kind !== "voiceover-recording") fail("Speech playback requires an Audio or VoiceoverRecording");
8308
- if (source.system !== "memota" && !(source.kind === "voiceover-recording" && source.system === "memota-speech")) fail("Speech playback requires a Memota audio file");
8108
+ if (!source || source.kind !== "voiceover-recording" || source.system !== "memota-speech") fail("Legacy Speech requires a memota-speech VoiceoverRecording");
8309
8109
  requireWholeAudio(source, clip, fail);
8310
8110
  if (clip.fillTimeline) fail("Legacy Speech cannot express timeline fill");
8311
8111
  const voices = Object.values(snapshot["voice-timbre-relations"]).filter((r) => r.recordingId === sourceId).flatMap((r) => snapshot["voice-entities"][r.voiceId] ? [snapshot["voice-entities"][r.voiceId]] : []);
@@ -8322,7 +8122,7 @@ function toVideoDraft(snapshot) {
8322
8122
  audio_storage_key: source.storageKey,
8323
8123
  origin_speech_id: source.key,
8324
8124
  volume: clip.volume,
8325
- audio_script: source.kind === "voiceover-recording" ? spokenText(snapshot, sourceId) : "",
8125
+ audio_script: spokenText(snapshot, sourceId),
8326
8126
  voice: voice ? {
8327
8127
  id: voice.key,
8328
8128
  name: voice.name
@@ -8418,15 +8218,6 @@ function spokenText(snapshot, recordingId) {
8418
8218
  if (!first) return "";
8419
8219
  if (first.phoneticScriptId !== void 0) return snapshot.phoneticText[first.phoneticScriptId]?.text ?? "";
8420
8220
  const script = snapshot["audio-script-entities"][first.audioScriptId];
8421
- if (first.phoneticScriptIds !== void 0 && script) {
8422
- const ids = first.phoneticScriptIds;
8423
- const selections = ids.map((id) => snapshot["phonetic-script-entities"][id]);
8424
- if (!(ids.length === script.segments.length && script.segments.every((segment) => {
8425
- const matching = ids.filter((id) => snapshot["phonetic-script-entities"][id]?.phoneme.segmentId === segment.id);
8426
- const run = matching.length === 1 ? snapshot.phoneticText[matching[0]] : void 0;
8427
- return run?.start === 0 && run.end === segment.text.length;
8428
- })) || selections.some((selection) => selection.phoneme.phonemeScript !== void 0)) projectionError("ambiguous-script", `media-entities/${recordingId}`, "Multiple phonetic sources require complete plain selections of one script");
8429
- }
8430
8221
  return script ? script.segments.map((segment) => segment.text).join("\n") : "";
8431
8222
  }
8432
8223
  //#endregion
@@ -8517,6 +8308,7 @@ function prepare(input, options) {
8517
8308
  if (facts.has(id)) fail("mediaFacts", `Duplicate facts for ${fact.system}/${fact.key}`);
8518
8309
  facts.set(id, fact);
8519
8310
  }
8311
+ for (const fact of facts.values()) if (fact.system === "placeholder" && facts.has(resourceId("media", "memota", fact.key))) fail("mediaFacts", `Media cannot be both present and missing: ${fact.key}`);
8520
8312
  const register = (media) => {
8521
8313
  const id = resourceId("media", media.system, media.key), old = imported.get(id);
8522
8314
  if (old) {
@@ -8542,6 +8334,15 @@ function prepare(input, options) {
8542
8334
  const mediaByPart = /* @__PURE__ */ new Map(), scriptByRecording = /* @__PURE__ */ new Map(), scriptsByPart = /* @__PURE__ */ new Map();
8543
8335
  const voices = /* @__PURE__ */ new Set();
8544
8336
  for (const [id, p] of Object.entries(library)) if (p.video_clip) {
8337
+ if (p.video_clip.origin_media_id === "") {
8338
+ mediaByPart.set(id, register(BLANK_PLACEHOLDER));
8339
+ continue;
8340
+ }
8341
+ const missing = p.video_clip.origin_media_id && facts.get(resourceId("media", "placeholder", p.video_clip.origin_media_id));
8342
+ if (missing) {
8343
+ mediaByPart.set(id, register(missing));
8344
+ continue;
8345
+ }
8545
8346
  const fact = factFor("memota", p.video_clip.origin_media_id);
8546
8347
  if (fact.kind !== "image" && fact.kind !== "video") fail(`part_library/${id}`, "Expected Image or Video facts");
8547
8348
  mediaByPart.set(id, register(fact));
@@ -8765,5 +8566,122 @@ function importedScript(id, text, mintSegment) {
8765
8566
  segments
8766
8567
  };
8767
8568
  }
8569
+ function normalizeLegacyVideoDraft(input, mediaFacts = []) {
8570
+ const draft = structuredClone(input);
8571
+ const diagnostics = [];
8572
+ const note = (code, path, message) => diagnostics.push({
8573
+ code,
8574
+ path,
8575
+ message
8576
+ });
8577
+ const library = draft.part_library ?? {};
8578
+ const tracks = [
8579
+ ...draft.above_main_tracks ?? [],
8580
+ ...draft.main_track ? [draft.main_track] : [],
8581
+ ...draft.below_main_tracks ?? []
8582
+ ];
8583
+ const facts = new Map(mediaFacts.map((fact) => [resourceId("media", fact.system, fact.key), fact]));
8584
+ const round = (owner, field, path) => {
8585
+ const fields = owner;
8586
+ const value = fields[field];
8587
+ if (typeof value !== "number" || !Number.isFinite(value) || Number.isSafeInteger(value)) return;
8588
+ const rounded = Math.round(value);
8589
+ fields[field] = rounded;
8590
+ note("legacy-time-rounded", `${path}/${field}`, `${value} → ${rounded}`);
8591
+ };
8592
+ for (const track of tracks) for (const item of track.items ?? []) round(item, "abs_time_position", `tracks/${track.id}/${item.part_id}`);
8593
+ for (const [id, part] of Object.entries(library)) {
8594
+ const path = `part_library/${id}`;
8595
+ if (part.video_clip) for (const f of [
8596
+ "play_in",
8597
+ "play_out",
8598
+ "duration_ms"
8599
+ ]) round(part.video_clip, f, path);
8600
+ if (part.speech) for (const f of ["duration_ms", "media_duration_ms"]) round(part.speech, f, path);
8601
+ if (part.caption) for (const f of [
8602
+ "duration_ms",
8603
+ "initial_duration_ms",
8604
+ "start_ms"
8605
+ ]) round(part.caption, f, path);
8606
+ if (part.bgm) round(part.bgm, "duration_ms", path);
8607
+ }
8608
+ for (const aggregation of draft.part_aggregations ?? []) for (const attachment of aggregation.attachments ?? []) round(attachment, "relative_time_position", `part_aggregations/${aggregation.body_part_id}/${attachment.part_id}`);
8609
+ const dropped = /* @__PURE__ */ new Set();
8610
+ const drop = (id, why) => {
8611
+ dropped.add(id);
8612
+ note("legacy-part-dropped", `part_library/${id}`, why);
8613
+ };
8614
+ for (const [id, part] of Object.entries(library)) {
8615
+ const path = `part_library/${id}`;
8616
+ const clip = part.video_clip;
8617
+ const sound = part.bgm ?? part.speech;
8618
+ const fact = part.bgm ? facts.get(resourceId("media", "memota", part.bgm.origin_media_id ?? "")) : part.speech && facts.get(resourceId("media", "memota-speech", part.speech.origin_speech_id ?? ""));
8619
+ if (sound && fact && "storageKey" in fact && fact.storageKey && fact.storageKey !== sound.audio_storage_key) {
8620
+ note("legacy-storage-key-current", `${path}/audio_storage_key`, `${sound.audio_storage_key} → ${fact.storageKey}`);
8621
+ sound.audio_storage_key = fact.storageKey;
8622
+ }
8623
+ if (!clip) continue;
8624
+ const rate = clip.speed_shift?.config?.linear?.speed ?? 1;
8625
+ const inMs = clip.play_in ?? 0;
8626
+ const outMs = clip.play_out ?? (clip.duration_ms === void 0 ? void 0 : inMs + Math.round(clip.duration_ms * rate));
8627
+ const source = facts.get(resourceId("media", "memota", clip.origin_media_id ?? ""));
8628
+ let windowEnd = outMs;
8629
+ if (source?.kind === "video" && outMs !== void 0 && outMs > source.durationMs) {
8630
+ windowEnd = source.durationMs;
8631
+ if (windowEnd > inMs) {
8632
+ note("legacy-window-cut", `${path}/play_out`, `${outMs} → ${windowEnd} (source length)`);
8633
+ clip.play_out = windowEnd;
8634
+ if (clip.duration_ms !== void 0) clip.duration_ms = Math.max(0, Math.round((windowEnd - inMs) / rate));
8635
+ }
8636
+ }
8637
+ if (windowEnd === void 0) continue;
8638
+ const length = windowEnd - inMs;
8639
+ if (length <= 0) drop(id, `Empty window ${inMs}–${windowEnd}`);
8640
+ else if (clip.origin_media_id === "" && length / rate < 40) drop(id, `Placeholder sliver of ${length / rate}ms`);
8641
+ }
8642
+ for (const id of dropped) delete library[id];
8643
+ for (const track of tracks) track.items = track.items?.filter((item) => !dropped.has(item.part_id ?? ""));
8644
+ const abs = /* @__PURE__ */ new Map();
8645
+ for (const track of tracks) for (const item of track.items ?? []) abs.set(item.part_id ?? "", item.abs_time_position ?? 0);
8646
+ const mainOrder = new Map((draft.main_track?.items ?? []).map((item, index) => [item.part_id ?? "", index]));
8647
+ const candidates = /* @__PURE__ */ new Map();
8648
+ for (const aggregation of draft.part_aggregations ?? []) {
8649
+ const host = aggregation.body_part_id ?? "";
8650
+ for (const attachment of aggregation.attachments ?? []) {
8651
+ const speech = attachment.part_id ?? "";
8652
+ const path = `part_aggregations/${host}/${speech}`;
8653
+ if (!library[speech]?.speech) note("legacy-attachment-dropped", path, "Not a placed Speech");
8654
+ else if (!library[host]?.video_clip || !mainOrder.has(host)) note("legacy-attachment-dropped", path, "Host is not a placed visual clip");
8655
+ else candidates.set(speech, [...candidates.get(speech) ?? [], {
8656
+ host,
8657
+ offset: attachment.relative_time_position ?? 0
8658
+ }]);
8659
+ }
8660
+ }
8661
+ const chosen = /* @__PURE__ */ new Map();
8662
+ for (const [speech, list] of candidates) {
8663
+ if (list.length === 1) {
8664
+ chosen.set(speech, list[0]);
8665
+ continue;
8666
+ }
8667
+ const playing = list.filter((c) => (abs.get(c.host) ?? 0) + c.offset === abs.get(speech)).sort((a, b) => mainOrder.get(a.host) - mainOrder.get(b.host));
8668
+ const pick = playing[0] ?? list[0];
8669
+ chosen.set(speech, pick);
8670
+ note(playing.length ? "legacy-speech-host-chosen" : "legacy-speech-host-unmatched", `part_aggregations/${pick.host}/${speech}`, `${list.length} hosts: ${list.map((c) => c.host).join(", ")}`);
8671
+ }
8672
+ const byHost = /* @__PURE__ */ new Map();
8673
+ for (const [speech, { host, offset }] of chosen) byHost.set(host, [...byHost.get(host) ?? [], {
8674
+ part_id: speech,
8675
+ relative_time_position: offset
8676
+ }]);
8677
+ if (draft.part_aggregations) draft.part_aggregations = [...new Set(draft.part_aggregations.map((a) => a.body_part_id ?? ""))].filter((host) => byHost.has(host)).map((host) => ({
8678
+ body_part_id: host,
8679
+ attachments: byHost.get(host)
8680
+ }));
8681
+ return {
8682
+ draft,
8683
+ diagnostics
8684
+ };
8685
+ }
8768
8686
  //#endregion
8769
- export { tupleHash as S, resolveDslLayout as _, DslEditor as a, relationId as b, MedeoDsl as c, assertPhoneticBindingFormat as d, documentFormat as f, timeAnchorCodec as g, isInvalidAtomic as h, createInitialMedeoDsl as i, UnsupportedDocumentFormatError as l, captionContentCodec as m, toVideoDraft as n, spokenScripts as o, medeoDslLoroShape as p, DEFAULT_UNIT_TIME_MS as r, toAudioScriptViews as s, fromVideoDraft as t, assertDslDocument as u, DslProjectionError as v, resourceId as x, defaultIdFactory as y };
8687
+ export { tupleHash as C, resourceId as S, DslProjectionError as _, createInitialMedeoDsl as a, defaultIdFactory as b, MedeoDsl as c, documentFormat as d, medeoDslLoroShape as f, resolveDslLayout as g, timeAnchorCodec as h, DEFAULT_UNIT_TIME_MS as i, UnsupportedDocumentFormatError as l, isInvalidAtomic as m, fromVideoDraft as n, DslEditor as o, captionContentCodec as p, toVideoDraft as r, toAudioScriptViews as s, normalizeLegacyVideoDraft as t, assertDslDocument as u, BLANK_PLACEHOLDER as v, relationId as x, missingMediaPlaceholder as y };