@mengine/medeo-client 1.2.1-alpha.9 → 1.3.1-alpha.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/README.md CHANGED
@@ -10,15 +10,43 @@ asset facts, not an entity store implementation.
10
10
  For entity-authoritative documents:
11
11
 
12
12
  1. `EntityGraphHttpClient.fetchState()` obtains the complete graph and revision.
13
- 2. `importMediaAsset` reuses or creates media/Asset bindings from recalled facts.
14
- Video duration must come from asset metadata, never a clip trim window. Asset
15
- and media entities are not one-to-one; ambiguous graphs require an explicit
16
- `MediaAssetSelection` (fourth argument), not an arbitrary first match.
13
+ 2. `ensureEditorFoundation` gets or creates one Timeline and four role Tracks,
14
+ retaining all existing identities and settings. `importMediaAsset` groups and
15
+ deduplicates only by direct media variants carrying the same external asset
16
+ id `(system, key)`. Imports mint exactly one media variant Entity
17
+ (Video/Image/Audio/Voice) that carries the Asset locator itself; no separate
18
+ `asset` Entity and no `physical-asset` Relation are created, and the returned
19
+ identity is that single Entity. Video duration must come from asset
20
+ metadata, never a clip trim window. Dual-entity media graphs (`asset` row
21
+ plus a `physical-asset` binding) are not reused and fail explicitly instead
22
+ of minting a second identity. This policy is local to the MEngine editor, not
23
+ the general-purpose DSL.
17
24
  3. `EntityTimelineEditor` edits Clip/Marker/Track relations and payloads.
18
25
  4. `EntityGraphHttpClient.commit(baseState, nextRows)` performs revision CAS,
19
26
  including explicit net deletions. The server writes entities and the derived
20
27
  Loro read view in the same transaction.
21
28
 
29
+ Repeated placement reuses the same single media identity but creates independent
30
+ Clips and SequenceMarkers. Asset generation alone still does not create editor
31
+ Entities. Two commit-level guards protect the single-identity policy:
32
+ `assertCanonicalEditorResources(rows)` checks one complete state — every media
33
+ variant carries its own external locator, one external asset id has exactly one
34
+ claimant, and no media variant is bound through a `physical-asset` Relation.
35
+ The exported `assertMediaAssetWritePolicy(before, after)` checks one CAS
36
+ transition and is reusable by the host tool and the entity stores: it applies
37
+ the same rules to the complete after state, and a self-carried identity cannot
38
+ be rewritten to another asset id. The editor applies it to every transaction
39
+ and `EntityGraphHttpClient.commit` applies it to `(baseState.rows, nextRows)`
40
+ before any request. There is no historical exemption: dual-entity media graphs
41
+ are rejected rather than kept editable — the shared `decodeEntityRelationRows`
42
+ boundary already fails rows whose media variants lack `external` or carry a
43
+ `physical-asset` binding, so editors and commits never observe them — deleting
44
+ media is an explicit caller decision, and no implicit historical merge or
45
+ identity rewriting occurs. The
46
+ compatibility reader reads physical facts directly from the media variant;
47
+ Caption inline text still resolves its inline Asset through
48
+ `physical-asset`.
49
+
22
50
  `SequenceMarker` owns source/target ranges, duration, and time remapping. `Clip`
23
51
  owns clip properties such as volume; `Track` owns track properties. References
24
52
  between entities belong in Relations, not in payload IDs. There is no separate
@@ -39,19 +67,23 @@ cold-start reconciliation must not POST legacy `/updates`. Entity writes use
39
67
  the independent graph CAS endpoint.
40
68
 
41
69
  Anchored placement is a `clip-anchor` Relation from child Clip to host Clip
42
- plus `SequenceMarker.anchorOffset`. Voice anchors to a visual Clip; Caption
43
- anchors to a Voice Clip. `resolveEntityTimelineLayout` resolves this graph
70
+ plus `SequenceMarker.anchorOffset`. Voice and Caption can be independently
71
+ placed or explicitly anchored; generation relations do not choose a host. `resolveEntityTimelineLayout` resolves this graph
44
72
  without reparenting or synthesizing content. Move/delete operations must write
45
73
  their follow, keep-absolute, cascade, or detach decisions into the same entity
46
74
  plan. A speech-overlap shift is explicit placement on the real visual Clips;
47
75
  an uncovered time interval does not require an invented empty-media entity.
48
76
 
49
77
  Voice links to its AudioScript through `audio-script-render`; Caption links to
50
- the same script and Voice through provenance/alignment relations. External
51
- speech identity and physical storage key belong to Asset, separately from the
52
- placed Clip identity. Caption styles belong to Caption; text can have a real
53
- inline text Asset rather than a fabricated Memota ID. Background Audio's
54
- Marker declares `durationPolicy: 'timeline'`: its source duration stays factual,
78
+ its source script and optional alignment Voice through separate provenance/alignment relations. External
79
+ speech identity and physical storage key belong to the Voice Entity itself.
80
+ `insertCaptionClip` creates a complete independent Caption placement;
81
+ `upsertVoiceoverTake` accepts explicit `placement` for independent voice.
82
+ AudioScript carries only text. `audio-script-marker` attaches directly assigned
83
+ `segmentRanges` timing; those annotation Markers cannot be Clip display Markers.
84
+ Caption styles belong to Caption; text can have a real inline text Asset rather
85
+ than a fabricated Memota ID. Background Audio's Marker declares
86
+ `durationPolicy: 'timeline'`: its source duration stays factual,
55
87
  while the view fills the non-BGM timeline duration.
56
88
 
57
89
  `migrateLegacyTimelineToEntities` is a one-time migration utility, **not** an
@@ -130,7 +130,7 @@ const captionStyle$1 = schema.LoroMap({
130
130
  const captionPart = schema.LoroMap({
131
131
  id: schema.String(),
132
132
  initial_duration_ms: schema.Number(),
133
- speech_part_id: schema.String(),
133
+ speech_part_id: schema.String({ required: false }),
134
134
  text: schema.String(),
135
135
  start_ms: schema.Number(),
136
136
  style: captionStyle$1
@@ -819,7 +819,7 @@ const captionPartSchema = z.object({
819
819
  id: z.string().optional(),
820
820
  kind: z.literal("caption"),
821
821
  initial_duration_ms: finiteNumber,
822
- speech_part_id: z.string(),
822
+ speech_part_id: z.string().optional(),
823
823
  text: z.string(),
824
824
  start_ms: finiteNumber,
825
825
  style: z.object({
@@ -1489,45 +1489,6 @@ function emptyLaneTracks() {
1489
1489
  }));
1490
1490
  }
1491
1491
  //#endregion
1492
- //#region src/document/entity-projection-values.ts
1493
- /** A graph cannot be represented faithfully by the compatibility reader. */
1494
- var EntityTimelineProjectionError = class extends Error {
1495
- constructor(message) {
1496
- super(message);
1497
- this.name = "EntityTimelineProjectionError";
1498
- }
1499
- };
1500
- function fail(message) {
1501
- throw new EntityTimelineProjectionError(message);
1502
- }
1503
- function object(value) {
1504
- return value !== void 0 && value !== null && typeof value === "object" && !Array.isArray(value);
1505
- }
1506
- function finite(value, label) {
1507
- if (typeof value !== "number" || !Number.isFinite(value)) fail(`${label} must be finite`);
1508
- return value;
1509
- }
1510
- function positive(value, label) {
1511
- const result = finite(value, label);
1512
- if (result <= 0) fail(`${label} must be positive`);
1513
- return result;
1514
- }
1515
- function milliseconds(value, label) {
1516
- const result = finite(value, label);
1517
- if (!Number.isSafeInteger(result) || result < 0) fail(`${label} must be non-negative whole milliseconds`);
1518
- return result;
1519
- }
1520
- function range(value, label) {
1521
- if (!object(value)) fail(`${label} must be a numeric range`);
1522
- const start = finite(value.start, `${label}.start`);
1523
- const end = finite(value.end, `${label}.end`);
1524
- if (start >= end) fail(`${label} must be non-empty`);
1525
- return {
1526
- start,
1527
- end
1528
- };
1529
- }
1530
- //#endregion
1531
1492
  //#region ../medeo-dsl/src/entities.ts
1532
1493
  function hasSequence(entity) {
1533
1494
  if (isKnownNonSequenceKind(entity.entityKind) || isReservedEntityKind(entity.entityKind)) return false;
@@ -1544,7 +1505,10 @@ function isSequenceFields(value) {
1544
1505
  return "coordinateSpace" in value && value.coordinateSpace !== void 0;
1545
1506
  }
1546
1507
  function isKnownSequenceKind(kind) {
1547
- return kind === "video" || kind === "audio" || kind === "voice" || kind === "image" || kind === "caption" || kind === "axvideo";
1508
+ return isMediaAssetVariantKind(kind) || kind === "caption" || kind === "axvideo";
1509
+ }
1510
+ function isMediaAssetVariantKind(kind) {
1511
+ return kind === "video" || kind === "image" || kind === "audio" || kind === "voice";
1548
1512
  }
1549
1513
  function isKnownEntityKind(kind) {
1550
1514
  return isKnownSequenceKind(kind) || isKnownNonSequenceKind(kind);
@@ -1573,6 +1537,18 @@ function createId(value, label) {
1573
1537
  }
1574
1538
  //#endregion
1575
1539
  //#region ../medeo-dsl/src/invariants.ts
1540
+ /**
1541
+ * Media variants own their Asset identity directly: a `physical-asset`
1542
+ * Relation may not bind them. Caption inline text (and other non-media
1543
+ * Sequence entities) keep their Asset binding.
1544
+ */
1545
+ function validateMediaAssetIdentity(entity, index) {
1546
+ return [...index.relationsOf(entity)].filter((relation) => relation.kind === "physical-asset").map(() => ({
1547
+ code: "media_physical_asset_forbidden",
1548
+ entityId: entity.entityId,
1549
+ message: `Media entity "${entity.entityId}" owns its Asset identity; remove its physical-asset Relation`
1550
+ }));
1551
+ }
1576
1552
  /** Validates one complete set of entities and its authoritative Relation rows. */
1577
1553
  function validateEntityRelationSet(entityRefs, index, options) {
1578
1554
  const { entities, issues } = collectRelatedEntities(entityRefs, index);
@@ -1581,6 +1557,7 @@ function validateEntityRelationSet(entityRefs, index, options) {
1581
1557
  const current = entity.current();
1582
1558
  const entityIssues = validateEntity(entity, entityIds);
1583
1559
  issues.push(...entityIssues);
1560
+ if (isMediaAssetVariantKind(current.entityKind)) issues.push(...validateMediaAssetIdentity(entity, index));
1584
1561
  if (entityIssues.some((issue) => issue.code === "invalid_entity_payload")) continue;
1585
1562
  if (current.entityKind === "sequence-marker") {
1586
1563
  const marker = entity;
@@ -1608,7 +1585,39 @@ function validateMarkerUse(marker, index) {
1608
1585
  const axVideoEdges = ofKind(relations, "axvideo-marker");
1609
1586
  const contentEdges = ofKind(relations, "marker-content");
1610
1587
  const timelineEdges = ofKind(relations, "marker-timeline");
1588
+ const scriptEdges = ofKind(relations, "audio-script-marker");
1611
1589
  const issues = [];
1590
+ if (scriptEdges.length > 0) {
1591
+ if (clipEdges.length + axVideoEdges.length + contentEdges.length + timelineEdges.length > 0) issues.push({
1592
+ code: "marker_container_xor",
1593
+ entityId: marker.entityId,
1594
+ message: "An AudioScript annotation Marker must not carry Clip, AXVideo, content, or Timeline relations"
1595
+ });
1596
+ const ranges = marker.current().segmentRanges;
1597
+ if (!ranges?.length) issues.push({
1598
+ code: "invalid_entity_payload",
1599
+ entityId: marker.entityId,
1600
+ message: "An AudioScript annotation Marker requires segmentRanges"
1601
+ });
1602
+ for (const relation of scriptEdges) {
1603
+ const script = relation.other(marker)?.deref()?.current();
1604
+ if (script?.entityKind !== "audio-script") continue;
1605
+ const segments = script.segments;
1606
+ if (!Array.isArray(segments)) continue;
1607
+ const segmentIds = new Set(segments.map((segment) => segment.segmentId));
1608
+ if (ranges?.some((range) => !segmentIds.has(range.segmentId))) issues.push({
1609
+ code: "invalid_entity_payload",
1610
+ entityId: marker.entityId,
1611
+ message: "Annotation segmentRanges must name segments of the related AudioScript"
1612
+ });
1613
+ }
1614
+ return issues;
1615
+ }
1616
+ if (marker.current().segmentRanges !== void 0) issues.push({
1617
+ code: "invalid_entity_payload",
1618
+ entityId: marker.entityId,
1619
+ message: "segmentRanges belongs to an AudioScript annotation Marker, not a display Marker"
1620
+ });
1612
1621
  if (clipEdges.length + axVideoEdges.length !== 1) issues.push({
1613
1622
  code: "marker_container_xor",
1614
1623
  entityId: marker.entityId,
@@ -1711,10 +1720,10 @@ function validateClipPlacement(clip, index) {
1711
1720
  const content = ofKind([...index.relationsOf(marker)], "marker-content")[0]?.other(marker)?.deref()?.current();
1712
1721
  const track = ofKind([...index.relationsOf(clip)], "track-clip")[0]?.other(clip)?.deref()?.current();
1713
1722
  const trackRole = track?.entityKind === "track" ? track.role : void 0;
1714
- if (content?.entityKind !== "audio" || trackRole !== "bgm" || !hasOrder) issues.push({
1723
+ if (content?.entityKind !== "audio" || trackRole !== "bgm" || !hasOrder && !hasTarget) issues.push({
1715
1724
  code: "timeline_duration_policy_invalid",
1716
1725
  entityId: marker.entityId,
1717
- message: "Marker durationPolicy \"timeline\" is only valid for ordered Audio Clips on the bgm Track"
1726
+ message: "Marker durationPolicy \"timeline\" is only valid for ordered or absolutely placed Audio Clips on the bgm Track"
1718
1727
  });
1719
1728
  }
1720
1729
  return issues;
@@ -1824,11 +1833,13 @@ function validateKnownEntityPayload(entity) {
1824
1833
  case "voice":
1825
1834
  case "caption":
1826
1835
  validateSequencePayload(value, "bounded", "native", problems);
1836
+ if (entity.entityKind !== "caption") validateMediaAssetFields(value, problems);
1827
1837
  if (entity.entityKind === "voice") validateVoicePayload(value, problems);
1828
1838
  if (entity.entityKind === "caption") validateCaptionPayload(value, problems);
1829
1839
  break;
1830
1840
  case "image":
1831
1841
  validateSequencePayload(value, "unbounded", "constant", problems);
1842
+ validateMediaAssetFields(value, problems);
1832
1843
  break;
1833
1844
  case "axvideo":
1834
1845
  validateSequencePayload(value, "bounded", "derived", problems);
@@ -1876,22 +1887,64 @@ function validateMarkerPayload(value, problems) {
1876
1887
  } else if (duration.mode !== "from-source") problems.push("duration.mode must be \"from-source\" or \"fixed\"");
1877
1888
  if (value.anchorOffset === void 0 && Object.hasOwn(value, "anchorOffset")) problems.push("anchorOffset cannot be undefined when present");
1878
1889
  if (value.durationPolicy !== void 0 && value.durationPolicy !== "timeline") problems.push("durationPolicy must be \"timeline\" when present");
1890
+ if (value.segmentRanges !== void 0) validateSegmentRanges(value.segmentRanges, problems);
1879
1891
  }
1880
- function validateAssetPayload(value, problems) {
1881
- const external = value.external;
1882
- if (external !== void 0) if (!isRecord(external)) problems.push("external must be an object when present");
1883
- else {
1884
- if (external.system !== "memota" && external.system !== "memota-speech") problems.push("external.system must be \"memota\" or \"memota-speech\"");
1885
- if (typeof external.key !== "string" || external.key.trim() === "") problems.push("external.key must be a non-empty string");
1892
+ /** Annotation Marker time values are directly assigned facts; no cross-references are allowed. */
1893
+ function validateSegmentRanges(value, problems) {
1894
+ if (!Array.isArray(value)) {
1895
+ problems.push("segmentRanges must be an array when present");
1896
+ return;
1886
1897
  }
1887
- if (value.storageKey !== void 0 && (typeof value.storageKey !== "string" || value.storageKey.trim() === "")) problems.push("storageKey must be a non-empty string when present");
1898
+ const seen = /* @__PURE__ */ new Set();
1899
+ for (const [index, entry] of value.entries()) {
1900
+ if (!isRecord(entry)) {
1901
+ problems.push(`segmentRanges[${index}] must be an object`);
1902
+ continue;
1903
+ }
1904
+ if (typeof entry.segmentId !== "string") problems.push(`segmentRanges[${index}].segmentId must be a string`);
1905
+ else if (seen.has(entry.segmentId)) problems.push(`segmentRanges[${index}].segmentId must be unique within the Marker`);
1906
+ else seen.add(entry.segmentId);
1907
+ for (const key of ["startMs", "endMs"]) {
1908
+ const point = entry[key];
1909
+ if (typeof point !== "number" || !Number.isFinite(point)) problems.push(`segmentRanges[${index}].${key} must be a finite number`);
1910
+ }
1911
+ if (typeof entry.startMs === "number" && Number.isFinite(entry.startMs) && typeof entry.endMs === "number" && Number.isFinite(entry.endMs) && entry.startMs > entry.endMs) problems.push(`segmentRanges[${index}].startMs must not exceed segmentRanges[${index}].endMs`);
1912
+ }
1913
+ }
1914
+ function validateAssetPayload(value, problems) {
1915
+ validateExternalLocator(value, problems);
1916
+ validateStorageKey(value, problems);
1888
1917
  const inline = value.inline;
1889
1918
  if (inline !== void 0) if (!isRecord(inline)) problems.push("inline must be an object when present");
1890
1919
  else {
1891
1920
  if (inline.mediaType !== "text/plain") problems.push("inline.mediaType must be \"text/plain\"");
1892
1921
  if (typeof inline.text !== "string") problems.push("inline.text must be a string");
1893
1922
  }
1894
- if (external !== void 0 && inline !== void 0) problems.push("Asset must not combine external and inline physical locations");
1923
+ if (value.external !== void 0 && value.inline !== void 0) problems.push("Asset must not combine external and inline physical locations");
1924
+ }
1925
+ /**
1926
+ * Media variants own their Asset locator directly: `external` is required and
1927
+ * inline text stays an Asset-kind-only value so the two physical-location
1928
+ * vocabularies cannot mix.
1929
+ */
1930
+ function validateMediaAssetFields(value, problems) {
1931
+ if (value.external === void 0) problems.push("external is required; media variants own their Asset identity");
1932
+ else validateExternalLocator(value, problems);
1933
+ validateStorageKey(value, problems);
1934
+ if (value.inline !== void 0) problems.push("inline is an Asset-kind field; media variants use external");
1935
+ }
1936
+ function validateExternalLocator(value, problems) {
1937
+ const external = value.external;
1938
+ if (external === void 0) return;
1939
+ if (!isRecord(external)) {
1940
+ problems.push("external must be an object when present");
1941
+ return;
1942
+ }
1943
+ if (external.system !== "memota" && external.system !== "memota-speech") problems.push("external.system must be \"memota\" or \"memota-speech\"");
1944
+ if (typeof external.key !== "string" || external.key.trim() === "") problems.push("external.key must be a non-empty string");
1945
+ }
1946
+ function validateStorageKey(value, problems) {
1947
+ if (value.storageKey !== void 0 && (typeof value.storageKey !== "string" || value.storageKey.trim() === "")) problems.push("storageKey must be a non-empty string when present");
1895
1948
  }
1896
1949
  function validateVoicePayload(value, problems) {
1897
1950
  const voice = value.voice;
@@ -1950,6 +2003,15 @@ function validateScriptPayload(value, problems) {
1950
2003
  problems.push(`segments[${index}] must be an object`);
1951
2004
  continue;
1952
2005
  }
2006
+ for (const key of [
2007
+ "startMs",
2008
+ "endMs",
2009
+ "start_ms",
2010
+ "end_ms",
2011
+ "sourceRange",
2012
+ "targetRange",
2013
+ "duration"
2014
+ ]) if (Object.hasOwn(segment, key)) problems.push(`segments[${index}].${key} is timing; attach an AudioScript annotation Marker instead`);
1953
2015
  if (typeof segment.segmentId !== "string") problems.push(`segments[${index}].segmentId must be a string`);
1954
2016
  if (typeof segment.text !== "string") problems.push(`segments[${index}].text must be a string`);
1955
2017
  if (segment.language !== void 0 && typeof segment.language !== "string") problems.push(`segments[${index}].language must be a string when present`);
@@ -2008,7 +2070,9 @@ function isOwnedLocalEntityValuePath(entityKind, path) {
2008
2070
  "axvideo"
2009
2071
  ].includes(entityKind) && /^(?:sampling|coordinateSpace|coordinateSpace\.unit|extent\.kind)$/.test(path)) return true;
2010
2072
  if (entityKind === "sequence-marker" && /^(?:durationPolicy|duration\.mode|timeRemapping\.(?:kind|mode))$/.test(path)) return true;
2073
+ if (entityKind === "sequence-marker" && /^segmentRanges\[\d+\]\.segmentId$/.test(path)) return true;
2011
2074
  if (entityKind === "asset" && /^(?:external\.(?:system|key)|storageKey|inline\.(?:mediaType|text))$/.test(path)) return true;
2075
+ if (isMediaAssetVariantKind(entityKind) && /^(?:external\.(?:system|key)|storageKey)$/.test(path)) return true;
2012
2076
  if (entityKind === "voice" && /^voice\.(?:system|key|name)$/.test(path)) return true;
2013
2077
  if (entityKind === "caption" && (path === "text" || path.startsWith("style."))) return true;
2014
2078
  if ((entityKind === "audio-script" || entityKind === "phonetic-script") && /^segments\[\d+\]\.(?:segmentId|text|language)$/.test(path)) return true;
@@ -2017,6 +2081,7 @@ function isOwnedLocalEntityValuePath(entityKind, path) {
2017
2081
  function isOwnedLocalIdPath(entityKind, path) {
2018
2082
  if (path === "lifecycle.actorId") return true;
2019
2083
  if ((entityKind === "audio-script" || entityKind === "phonetic-script") && /^segments\[\d+\]\.segmentId$/.test(path)) return true;
2084
+ if (entityKind === "sequence-marker" && /^segmentRanges\[\d+\]\.segmentId$/.test(path)) return true;
2020
2085
  if (entityKind === "asset" && /^(?:tracks\[\d+\]\.trackId|renditions\[\d+\]\.renditionId)$/.test(path)) return true;
2021
2086
  return false;
2022
2087
  }
@@ -2144,6 +2209,23 @@ const audioScriptRenderRelationSpec = Object.freeze({
2144
2209
  },
2145
2210
  validateMetadata: isEmptyMetadata
2146
2211
  });
2212
+ /** `audio-script-source(script, source)` means endpoint 0 was transcribed from endpoint 1. */
2213
+ const audioScriptSourceRelationSpec = Object.freeze({
2214
+ kind: "audio-script-source",
2215
+ validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["audio-script"]), new Set(["audio", "video"])),
2216
+ validateMetadata: isEmptyMetadata
2217
+ });
2218
+ /**
2219
+ * `audio-script-marker(script, marker)` attaches an annotation Sequence Marker
2220
+ * whose `segmentRanges` hold directly assigned per-Segment time values. The
2221
+ * script keeps no Sequence and no Clip admission; annotation Markers cannot
2222
+ * enter Clip/AXVideo use chains (enforced by the marker-use invariant).
2223
+ */
2224
+ const audioScriptMarkerRelationSpec = Object.freeze({
2225
+ kind: "audio-script-marker",
2226
+ validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["audio-script"]), new Set(["sequence-marker"])),
2227
+ validateMetadata: isEmptyMetadata
2228
+ });
2147
2229
  /** Built-in kinds are reserved; callers may add specs only under new names. */
2148
2230
  const builtInRelationSpecs = Object.freeze([
2149
2231
  timelineTrackRelationSpec,
@@ -2158,7 +2240,9 @@ const builtInRelationSpecs = Object.freeze([
2158
2240
  captionProvenanceRelationSpec,
2159
2241
  captionAlignmentRelationSpec,
2160
2242
  clipAnchorRelationSpec,
2161
- audioScriptRenderRelationSpec
2243
+ audioScriptRenderRelationSpec,
2244
+ audioScriptSourceRelationSpec,
2245
+ audioScriptMarkerRelationSpec
2162
2246
  ]);
2163
2247
  function emptySpec(kind, a, b) {
2164
2248
  return metadataSpec(kind, a, b, isEmptyMetadata);
@@ -2255,7 +2339,7 @@ var BiRelationIndex = class {
2255
2339
  byRelationId = /* @__PURE__ */ new Map();
2256
2340
  link(input) {
2257
2341
  if (input.spec.kind === "generated") throw new Error("Author generated Relations with linkGenerated({ output, input })");
2258
- if (input.spec.kind === "clip-anchor" || input.spec.kind === "audio-script-render") throw new Error(`Author ordered ${input.spec.kind} Relations with the dedicated role-named method`);
2342
+ if (input.spec.kind === "clip-anchor" || input.spec.kind === "audio-script-render" || input.spec.kind === "audio-script-source") throw new Error(`Author ordered ${input.spec.kind} Relations with the dedicated role-named method`);
2259
2343
  return this.linkValidated(input);
2260
2344
  }
2261
2345
  /** Author `generated(output, input)` without exposing positional arguments. */
@@ -2288,6 +2372,16 @@ var BiRelationIndex = class {
2288
2372
  trace: input.trace
2289
2373
  });
2290
2374
  }
2375
+ /** Author `audio-script-source(script, source)` without exposing positional arguments. */
2376
+ linkAudioScriptSource(input) {
2377
+ return this.linkValidated({
2378
+ relationId: input.relationId,
2379
+ spec: audioScriptSourceRelationSpec,
2380
+ endpoints: [input.script, input.source],
2381
+ metadata: {},
2382
+ trace: input.trace
2383
+ });
2384
+ }
2291
2385
  /**
2292
2386
  * Rehydrate a persisted row after resolving its spec and endpoint refs.
2293
2387
  *
@@ -2507,6 +2601,45 @@ function errorMessage(error) {
2507
2601
  return error instanceof Error ? error.message : String(error);
2508
2602
  }
2509
2603
  //#endregion
2604
+ //#region src/document/entity-projection-values.ts
2605
+ /** A graph cannot be represented faithfully by the compatibility reader. */
2606
+ var EntityTimelineProjectionError = class extends Error {
2607
+ constructor(message) {
2608
+ super(message);
2609
+ this.name = "EntityTimelineProjectionError";
2610
+ }
2611
+ };
2612
+ function fail(message) {
2613
+ throw new EntityTimelineProjectionError(message);
2614
+ }
2615
+ function object(value) {
2616
+ return value !== void 0 && value !== null && typeof value === "object" && !Array.isArray(value);
2617
+ }
2618
+ function finite(value, label) {
2619
+ if (typeof value !== "number" || !Number.isFinite(value)) fail(`${label} must be finite`);
2620
+ return value;
2621
+ }
2622
+ function positive(value, label) {
2623
+ const result = finite(value, label);
2624
+ if (result <= 0) fail(`${label} must be positive`);
2625
+ return result;
2626
+ }
2627
+ function milliseconds(value, label) {
2628
+ const result = finite(value, label);
2629
+ if (!Number.isSafeInteger(result) || result < 0) fail(`${label} must be non-negative whole milliseconds`);
2630
+ return result;
2631
+ }
2632
+ function range(value, label) {
2633
+ if (!object(value)) fail(`${label} must be a numeric range`);
2634
+ const start = finite(value.start, `${label}.start`);
2635
+ const end = finite(value.end, `${label}.end`);
2636
+ if (start >= end) fail(`${label} must be non-empty`);
2637
+ return {
2638
+ start,
2639
+ end
2640
+ };
2641
+ }
2642
+ //#endregion
2510
2643
  //#region src/document/entity-timeline-layout.ts
2511
2644
  /** Validated graph lookup shared by the compatibility layout and projection. */
2512
2645
  var EntityProjectionGraph = class {
@@ -2643,7 +2776,7 @@ function resolveGraphLayout(graph) {
2643
2776
  let start = fact.absoluteStart ?? 0;
2644
2777
  if (fact.mode === "anchored") {
2645
2778
  const host = resolve(fact.anchorId);
2646
- if (fact.role === "speech" && facts.get(host.clipEntityId).role !== "video_clip") fail("An anchored Voice must use a visual Clip as its host");
2779
+ if (fact.role === "speech" && !["video_clip", "bgm"].includes(facts.get(host.clipEntityId).role)) fail("An anchored Voice must use a visual or BGM Clip as its host");
2647
2780
  start = host.start + fact.anchorOffset;
2648
2781
  } else if (fact.predecessor !== void 0) {
2649
2782
  const prior = resolve(fact.predecessor);
@@ -2671,7 +2804,6 @@ function resolveGraphLayout(graph) {
2671
2804
  const durationMs = Math.max(0, ...[...resolved.values()].filter((clip) => facts.get(clip.clipEntityId).role !== "bgm").map((clip) => clip.start + clip.duration));
2672
2805
  for (const [id, fact] of facts) if (fact.fillTimeline) {
2673
2806
  const clip = resolved.get(id);
2674
- if (clip.start !== 0) fail("Timeline-filling BGM must start at zero");
2675
2807
  resolved.set(id, {
2676
2808
  ...clip,
2677
2809
  duration: durationMs
@@ -2710,8 +2842,8 @@ function projectEntityTimeline(rows, meta) {
2710
2842
  const content = graph.entity(placement.contentEntityId);
2711
2843
  const volume = volumeSchema.safeParse(clip.payload.volume ?? 0);
2712
2844
  if (!volume.success) fail("Clip volume must be decibels between -60 and 20");
2713
- const asset = graph.one(content.entityId, "physical-asset", "asset");
2714
2845
  if (track.role === "video_clip") {
2846
+ const asset = mediaAssetFacts(graph, content);
2715
2847
  const source = range(marker.payload.sourceRange, "sourceRange");
2716
2848
  const external = externalLocator(asset, "memota");
2717
2849
  const speed = videoSpeed(content, marker, placement);
@@ -2725,6 +2857,7 @@ function projectEntityTimeline(rows, meta) {
2725
2857
  ...speed === void 0 ? {} : { speed_shift: speed }
2726
2858
  } };
2727
2859
  } else if (track.role === "speech") {
2860
+ const asset = mediaAssetFacts(graph, content);
2728
2861
  requireUntrimmedAudio(content, marker, placement);
2729
2862
  const external = externalLocator(asset, "memota-speech");
2730
2863
  const script = graph.one(content.entityId, "audio-script-render", "audio-script");
@@ -2748,27 +2881,25 @@ function projectEntityTimeline(rows, meta) {
2748
2881
  } else if (track.role === "caption") {
2749
2882
  const text = content.payload.text;
2750
2883
  if (typeof text !== "string") fail("Caption " + content.entityId + " requires its text");
2884
+ const asset = graph.one(content.entityId, "physical-asset", "asset");
2751
2885
  const inline = asset.payload.inline;
2752
2886
  if (inline !== void 0 && (!object(inline) || inline.mediaType !== "text/plain" || inline.text !== text)) fail("Inline Caption Asset and Caption text must agree");
2753
2887
  if (inline === void 0 && !object(asset.payload.external)) fail("Caption Asset must identify physical text or contain actual inline text");
2754
- if (placement.anchorClipEntityId === void 0) fail("Caption placement requires an explicit voice Clip anchor");
2755
- const anchor = layout.clips.get(placement.anchorClipEntityId);
2756
- if (anchor === void 0 || graph.entity(anchor.contentEntityId).entityKind !== "voice") fail("Caption anchor must resolve a placed Voice");
2757
- if (graph.one(content.entityId, "caption-alignment").entityId !== anchor.contentEntityId) fail("Caption alignment must agree with its placed Voice");
2758
- const script = graph.one(content.entityId, "caption-provenance", "audio-script");
2759
- if (graph.one(anchor.contentEntityId, "audio-script-render", "audio-script").entityId !== script.entityId) fail("Caption provenance must agree with the Voice AudioScript");
2760
2888
  if (marker.payload.timeRemapping !== void 0) fail("Caption timeRemapping is not supported by this reader");
2761
2889
  const style = captionStyle(content.payload.style);
2890
+ const anchor = placement.anchorClipEntityId === void 0 ? void 0 : layout.clips.get(placement.anchorClipEntityId);
2891
+ const anchoredToSpeech = anchor !== void 0 && trackRoleOf(layout, anchor.trackEntityId) === "speech";
2762
2892
  parts[clip.entityId] = { caption: {
2763
2893
  id: clip.entityId,
2764
2894
  kind: "caption",
2765
2895
  text,
2766
- speech_part_id: placement.anchorClipEntityId,
2767
- start_ms: placement.anchorOffset,
2896
+ speech_part_id: anchoredToSpeech ? placement.anchorClipEntityId : void 0,
2897
+ start_ms: anchoredToSpeech ? placement.anchorOffset : 0,
2768
2898
  initial_duration_ms: placement.duration,
2769
2899
  ...style === void 0 ? {} : { style }
2770
2900
  } };
2771
2901
  } else {
2902
+ const asset = mediaAssetFacts(graph, content);
2772
2903
  const external = externalLocator(asset, "memota");
2773
2904
  if (marker.payload.durationPolicy !== "timeline") fail("Background Audio requires an explicit timeline-fill durationPolicy");
2774
2905
  if (marker.payload.timeRemapping !== void 0) fail("BGM timeRemapping is not supported by this reader");
@@ -2815,6 +2946,23 @@ function projectEntityTimeline(rows, meta) {
2815
2946
  assertValidVideoDocument(document);
2816
2947
  return document;
2817
2948
  }
2949
+ function trackRoleOf(layout, trackEntityId) {
2950
+ return layout.tracks.find((track) => track.entityId === trackEntityId)?.role;
2951
+ }
2952
+ /**
2953
+ * Media facts are read from the variant itself: a media variant owns its
2954
+ * Asset identity and carries `external`/`storageKey` directly. Dual-entity
2955
+ * media graphs are rejected instead of being resolved through a legacy
2956
+ * `physical-asset` binding.
2957
+ */
2958
+ function mediaAssetFacts(graph, content) {
2959
+ if (graph.adjacent(content.entityId, "physical-asset").length > 0) fail("Media " + content.entityId + " owns its Asset identity; remove its physical-asset Relation");
2960
+ if (!object(content.payload.external)) fail("Media " + content.entityId + " must carry its own Asset external identity");
2961
+ return {
2962
+ entityId: String(content.entityId),
2963
+ payload: content.payload
2964
+ };
2965
+ }
2818
2966
  function externalLocator(asset, system) {
2819
2967
  const value = asset.payload.external;
2820
2968
  if (!object(value) || value.system !== system || typeof value.key !== "string" || value.key.trim() === "") fail("Asset " + asset.entityId + " requires an explicit " + system + " external key");
@@ -3203,4 +3351,4 @@ function createPlainMemoryAdapter(document, options) {
3203
3351
  return new PlainMemoryAdapter(document, options);
3204
3352
  }
3205
3353
  //#endregion
3206
- export { base64ToBytes as $, findLaneTrack as A, syncAggregatedClipsTimePosition as B, VideoDocumentValidationError as C, videoDocumentSchema as D, partUnionSchema as E, fillMainTrackTimeGaps as F, DEFAULT_UNIT_TIME_MS as G, isEmptyVideoClip as H, reassignSpeechesToVideoClipsByTime as I, effectiveVideoClipDurationMs as J, ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION as K, recalculateTimelineDuration as L, solveVideoDocument as M, cascadeAfterVideoClipChanges as N, LANE_KINDS_IN_STACK_ORDER as O, arrangeMainTrackSeamlessly as P, videoDocumentMirrorSchema as Q, resolveAllSpeechOverlaps as R, toVideoDocument as S, validateVideoDocument as T, partDurationMs as U, TIMELINE_SKELETON_DURATION_MS as V, safeDurationMs as W, partUnionToDraft as X, speedOf as Y, recordEntries as Z, EntityTimelineProjectionError as _, applyEntityTimelineProjection as a, derivePositionFromAbs as b, writeVideoDocumentToDraft as c, resolveEntityTimelineLayout as d, bytesToBase64 as et, decodeEntityRelationRows as f, hasSequence as g, createRelationId as h, MirrorVideoDocumentAdapter as i, laneTrackId as j, ensureLaneTrack as k, readVideoDocumentFromDraft as l, createEntityId as m, createPlainMemoryAdapter as n, createMirrorVideoDocument as o, isJsonObject as p, VIDEO_DOCUMENT_SCHEMA_VERSION as q, generatePartId as r, createMirrorVideoDocumentAdapter as s, PlainMemoryAdapter as t, projectEntityTimeline as u, buildInitialVideoDocument as v, assertValidVideoDocument as w, fromVideoDocument as x, buildSpeechHostMap as y, resolveSpeechOverlapByShiftingVideos as z };
3354
+ export { videoDocumentMirrorSchema as $, ensureLaneTrack as A, resolveSpeechOverlapByShiftingVideos as B, toVideoDocument as C, partUnionSchema as D, validateVideoDocument as E, arrangeMainTrackSeamlessly as F, safeDurationMs as G, TIMELINE_SKELETON_DURATION_MS as H, fillMainTrackTimeGaps as I, VIDEO_DOCUMENT_SCHEMA_VERSION as J, DEFAULT_UNIT_TIME_MS as K, reassignSpeechesToVideoClipsByTime as L, laneTrackId as M, solveVideoDocument as N, videoDocumentSchema as O, cascadeAfterVideoClipChanges as P, recordEntries as Q, recalculateTimelineDuration as R, fromVideoDocument as S, assertValidVideoDocument as T, isEmptyVideoClip as U, syncAggregatedClipsTimePosition as V, partDurationMs as W, speedOf as X, effectiveVideoClipDurationMs as Y, partUnionToDraft as Z, hasSequence as _, applyEntityTimelineProjection as a, buildSpeechHostMap as b, writeVideoDocumentToDraft as c, resolveEntityTimelineLayout as d, base64ToBytes as et, EntityTimelineProjectionError as f, createRelationId as g, createEntityId as h, MirrorVideoDocumentAdapter as i, findLaneTrack as j, LANE_KINDS_IN_STACK_ORDER as k, readVideoDocumentFromDraft as l, isJsonObject as m, createPlainMemoryAdapter as n, createMirrorVideoDocument as o, decodeEntityRelationRows as p, ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION as q, generatePartId as r, createMirrorVideoDocumentAdapter as s, PlainMemoryAdapter as t, bytesToBase64 as tt, projectEntityTimeline as u, isMediaAssetVariantKind as v, VideoDocumentValidationError as w, derivePositionFromAbs as x, buildInitialVideoDocument as y, resolveAllSpeechOverlaps as z };
@@ -487,7 +487,11 @@ type SpeechPart = Omit<SpeechPart$1, 'duration_ms'> & {
487
487
  *
488
488
  * `speech_part_id` / `start_ms` are kept as resource-intrinsic facts (which
489
489
  * speech it was split from, and its offset in that source) — reference/17
490
- * principle 4.
490
+ * principle 4. An independently placed Caption (its own Clip and Sequence
491
+ * Marker, no Voice take) has no owning speech: both fields stay unset and its
492
+ * position is expressed entirely by the Clip's placement. Generation source
493
+ * (AudioScript provenance) and Voice alignment are entity Relations, decoupled
494
+ * from this display-side legacy view.
491
495
  */
492
496
  type CaptionPart = Omit<CaptionPart$1, 'duration_ms'> & {
493
497
  initial_duration_ms?: number | undefined;
@@ -893,7 +897,7 @@ type JsonObject = {
893
897
  };
894
898
  //#endregion
895
899
  //#region ../medeo-dsl/src/relations.d.ts
896
- type KnownRelationKind = 'timeline-track' | 'track-clip' | 'clip-marker' | 'marker-content' | 'axvideo-marker' | 'marker-timeline' | 'physical-asset' | 'generated' | 'phonetic-script-provenance' | 'caption-provenance' | 'caption-alignment' | 'clip-anchor' | 'audio-script-render';
900
+ type KnownRelationKind = 'timeline-track' | 'track-clip' | 'clip-marker' | 'marker-content' | 'axvideo-marker' | 'marker-timeline' | 'physical-asset' | 'generated' | 'phonetic-script-provenance' | 'caption-provenance' | 'caption-alignment' | 'clip-anchor' | 'audio-script-render' | 'audio-script-source' | 'audio-script-marker';
897
901
  type RelationKind = KnownRelationKind | (string & {});
898
902
  type RelationTrace = JsonObject;
899
903
  /**
@@ -1606,7 +1610,7 @@ declare const partUnionSchema: z.ZodUnion<readonly [z.ZodObject<{
1606
1610
  id: z.ZodOptional<z.ZodString>;
1607
1611
  kind: z.ZodLiteral<"caption">;
1608
1612
  initial_duration_ms: z.ZodNumber;
1609
- speech_part_id: z.ZodString;
1613
+ speech_part_id: z.ZodOptional<z.ZodString>;
1610
1614
  text: z.ZodString;
1611
1615
  start_ms: z.ZodNumber;
1612
1616
  style: z.ZodOptional<z.ZodObject<{
@@ -1709,7 +1713,7 @@ declare const videoDocumentSchema: z.ZodObject<{
1709
1713
  id: z.ZodOptional<z.ZodString>;
1710
1714
  kind: z.ZodLiteral<"caption">;
1711
1715
  initial_duration_ms: z.ZodNumber;
1712
- speech_part_id: z.ZodString;
1716
+ speech_part_id: z.ZodOptional<z.ZodString>;
1713
1717
  text: z.ZodString;
1714
1718
  start_ms: z.ZodNumber;
1715
1719
  style: z.ZodOptional<z.ZodObject<{
@@ -1736,4 +1740,4 @@ declare const videoDocumentSchema: z.ZodObject<{
1736
1740
  }, z.core.$loose>]>>>;
1737
1741
  }, z.core.$loose>;
1738
1742
  //#endregion
1739
- export { EntityId as $, generatePartId as A, CaptionPart$1 as At, ResolvedEntityClipPlacement as B, PlannedSemanticOpKind as C, VideoDocumentValidationIssue as Ct, SnapshotReadable as D, effectiveVideoClipDurationMs as Dt, SchemaValidator as E, VideoDraftPartUnion as Et, videoDocumentMirrorSchema as F, Track$1 as Ft, EntityTimelineProjectionError as G, ResolvedEntityTrack as H, VideoDocumentValidationError as I, TrackItem$1 as It, JsonValue as J, EntityRelationRows as K, assertValidVideoDocument as L, TrackItemDraft as M, PartAggregation as Mt, VideoDocumentDraft as N, SpeedShift as Nt, ValidationError as O, speedOf as Ot, VideoDocumentMirrorSchema as P, Timeline$1 as Pt, VoiceDescriptor as Q, validateVideoDocument as R, ImplementedSemanticOpKind as S, VideoDocumentSchemaVersion as St, isImplementedSemanticOpKind as T, VideoDraft as Tt, resolveEntityTimelineLayout as U, ResolvedEntityTimelineLayout as V, projectEntityTimeline as W, SequenceDuration as X, CaptionStyleFields as Y, SequenceRange as Z, SemanticEditor as _, TrackItem as _t, PlainMemoryAdapter as a, derivePositionFromAbs as at, TransactAudit as b, VideoClipPart as bt, MirrorVideoDocumentAdapter as c, BgmPart as ct, createMirrorVideoDocument as d, ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION as dt, InitialDocumentFacts as et, createMirrorVideoDocumentAdapter as f, PartKind as ft, SemanticDocumentAdapter as g, Track as gt, OpActor as h, Timeline as ht, JournalEntry as i, buildSpeechHostMap as it, TrackDraft as j, CaptionStyle as jt, PartIdFactory as k, Attachment as kt, MirrorVideoDocumentOptions as l, CaptionPart as lt, CommitOptions as m, SpeechPart as mt, videoDocumentSchema as n, DerivedItemPosition as nt, PlainMemoryAdapterOptions as o, fromVideoDocument as ot, writeVideoDocumentToDraft as p, PartUnion as pt, JsonObject as q, readVideoDocumentFromDraft as r, SpeechHostMap as rt, createPlainMemoryAdapter as s, toVideoDocument as st, partUnionSchema as t, buildInitialVideoDocument as tt, applyEntityTimelineProjection as u, DEFAULT_UNIT_TIME_MS as ut, SemanticOpInput as v, TrackItemTimePosition as vt, SemanticOpKind as w, VideoDocumentValidationIssueCode as wt, IMPLEMENTED_SEMANTIC_OP_KINDS as x, VideoDocument as xt, SemanticOpName as y, VIDEO_DOCUMENT_SCHEMA_VERSION as yt, EntityTimelineTrackRole as z };
1743
+ export { VoiceDescriptor as $, generatePartId as A, Attachment as At, ResolvedEntityClipPlacement as B, PlannedSemanticOpKind as C, VideoDocumentSchemaVersion as Ct, SnapshotReadable as D, VideoDraftPartUnion as Dt, SchemaValidator as E, VideoDraft as Et, videoDocumentMirrorSchema as F, Timeline$1 as Ft, EntityTimelineProjectionError as G, ResolvedEntityTrack as H, VideoDocumentValidationError as I, Track$1 as It, JsonObject as J, EntityRelationRows as K, assertValidVideoDocument as L, TrackItem$1 as Lt, TrackItemDraft as M, CaptionStyle as Mt, VideoDocumentDraft as N, PartAggregation as Nt, ValidationError as O, effectiveVideoClipDurationMs as Ot, VideoDocumentMirrorSchema as P, SpeedShift as Pt, SequenceRange as Q, validateVideoDocument as R, ImplementedSemanticOpKind as S, VideoDocument as St, isImplementedSemanticOpKind as T, VideoDocumentValidationIssueCode as Tt, resolveEntityTimelineLayout as U, ResolvedEntityTimelineLayout as V, projectEntityTimeline as W, CaptionStyleFields as X, JsonValue as Y, SequenceDuration as Z, SemanticEditor as _, Track as _t, PlainMemoryAdapter as a, buildSpeechHostMap as at, TransactAudit as b, VIDEO_DOCUMENT_SCHEMA_VERSION as bt, MirrorVideoDocumentAdapter as c, toVideoDocument as ct, createMirrorVideoDocument as d, DEFAULT_UNIT_TIME_MS as dt, EntityId as et, createMirrorVideoDocumentAdapter as f, ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION as ft, SemanticDocumentAdapter as g, Timeline as gt, OpActor as h, SpeechPart as ht, JournalEntry as i, SpeechHostMap as it, TrackDraft as j, CaptionPart$1 as jt, PartIdFactory as k, speedOf as kt, MirrorVideoDocumentOptions as l, BgmPart as lt, CommitOptions as m, PartUnion as mt, videoDocumentSchema as n, buildInitialVideoDocument as nt, PlainMemoryAdapterOptions as o, derivePositionFromAbs as ot, writeVideoDocumentToDraft as p, PartKind as pt, EntityRow as q, readVideoDocumentFromDraft as r, DerivedItemPosition as rt, createPlainMemoryAdapter as s, fromVideoDocument as st, partUnionSchema as t, InitialDocumentFacts as tt, applyEntityTimelineProjection as u, CaptionPart as ut, SemanticOpInput as v, TrackItem as vt, SemanticOpKind as w, VideoDocumentValidationIssue as wt, IMPLEMENTED_SEMANTIC_OP_KINDS as x, VideoClipPart as xt, SemanticOpName as y, TrackItemTimePosition as yt, EntityTimelineTrackRole as z };