@mengine/medeo-tool 2.0.1-alpha.11 → 2.0.1-alpha.13

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.
@@ -9,7 +9,7 @@ interface JsonObject {
9
9
  type KnownEntityKind = 'axvideo' | 'timeline' | 'track' | 'clip' | 'asset' | 'video' | 'audio' | 'voice' | 'image' | 'sequence-marker' | 'viewport' | 'audio-script' | 'phonetic-script' | 'caption';
10
10
  /** Asset identity, either an old physical-only row or a directly composed media variant. */
11
11
  type ResourceEntityKind = 'image' | 'video' | 'audio';
12
- type KnownRelationKind = 'timeline-track' | 'track-clip' | 'clip-marker' | 'marker-content' | 'axvideo-marker' | 'marker-timeline' | 'from-asset' | 'generated' | 'caption-alignment' | 'clip-anchor' | 'phonetic-script-render' | 'voice-timbre' | 'audio-script-source' | 'audio-script-marker';
12
+ type KnownRelationKind = 'timeline-track' | 'track-clip' | 'clip-marker' | 'marker-content' | 'axvideo-marker' | 'marker-timeline' | 'from-asset' | 'to-asset' | 'generated' | 'caption-alignment' | 'clip-anchor' | 'phonetic-script-render' | 'voice-timbre' | 'audio-script-source' | 'audio-script-marker';
13
13
  type AuthorableRelationKind = KnownRelationKind;
14
14
  /**
15
15
  * Content states how long it runs and nothing else: where it is taken from and
@@ -58,7 +58,7 @@ interface EntityPayloadByKind {
58
58
  role?: string;
59
59
  };
60
60
  clip: JsonObject;
61
- /** Where the bytes live. Reached from other entities by from-asset. */
61
+ /** Where the bytes live. Reached from other entities by from-asset or to-asset. */
62
62
  asset: AssetPayload;
63
63
  video: OwnDurationPayload;
64
64
  /** Every playable sound, including video original audio and synthesized voiceovers. */
@@ -147,6 +147,18 @@ type CreateEntityInput = { [K in KnownEntityKind]: {
147
147
  entity_id?: string;
148
148
  entity_kind: K;
149
149
  payload: StoredEntityPayload<K>;
150
+ /**
151
+ * The stored resource this entity is made from.
152
+ *
153
+ * Naming it here is the only way to say where an entity's bytes come from:
154
+ * the Asset row and the `from-asset` Relation that reaches it are assembled
155
+ * by the host, never authored. Only a kind that declares `Asset` — video,
156
+ * audio, image, caption, audio-script, phonetic-script — accepts one.
157
+ */
158
+ asset?: {
159
+ system: 'memota' | 'memota-speech';
160
+ key: string;
161
+ };
150
162
  } }[KnownEntityKind];
151
163
  /** Patch supplied fields on the assembled entity; omitted fields remain unchanged. */
152
164
  interface UpdateEntityInput {
@@ -240,4 +252,4 @@ interface BusinessRelationFacade {
240
252
  }
241
253
  //#endregion
242
254
  export { UpdateEntityInput as C, UnlinkRelationInput as S, LinkRelationInput as _, DeleteEntityInput as a, SandboxEntity as b, EntitySandboxCheckpoint as c, JsonObject as d, JsonPrimitive as f, LinkGeneratedRelationInput as g, KnownRelationKind as h, CreateEntityInput as i, EntityStoreSnapshot as l, KnownEntityKind as m, BusinessEntityFacade as n, EntityCommand as o, JsonValue as p, BusinessRelationFacade as r, EntityPlanState as s, AuthorableRelationKind as t, EntityUpdateInput as u, RelationUpdateInput as v, SandboxRelation as x, ResourceEntityKind as y };
243
- //# sourceMappingURL=entity-contract-Cf3AiSe7.d.mts.map
255
+ //# sourceMappingURL=entity-contract-Dy0cozPt.d.mts.map
@@ -15,8 +15,9 @@ import { applyFieldChanges, assertCanonicalEditorResources, assertFieldChanges,
15
15
  * `Voice` declares no `Sequence` — it is a timbre identity, not playable
16
16
  * content; a rendered voiceover is an `audio` entity.
17
17
  *
18
- * No declaration says anything about resources: whether an entity has one, and
19
- * which, is the `from-asset` Relation's answer alone.
18
+ * `Asset` says only that an entity of that kind may be bound to stored bytes,
19
+ * and asks nothing of its payload. Which bytes, and which way the content
20
+ * flowed, is the `from-asset` / `to-asset` Relation's answer alone.
20
21
  */
21
22
  const ENTITY_KINDS = Object.freeze({
22
23
  axvideo: Object.freeze([
@@ -33,27 +34,27 @@ const ENTITY_KINDS = Object.freeze({
33
34
  "Sequence",
34
35
  "Visual",
35
36
  "Audible",
36
- "FromAsset"
37
+ "Asset"
37
38
  ]),
38
39
  audio: Object.freeze([
39
40
  "Sequence",
40
41
  "Audible",
41
- "FromAsset"
42
+ "Asset"
42
43
  ]),
43
44
  image: Object.freeze([
44
45
  "Sequence",
45
46
  "Visual",
46
- "FromAsset"
47
+ "Asset"
47
48
  ]),
48
49
  voice: Object.freeze(["Container", "IPBound"]),
49
50
  "sequence-marker": Object.freeze([]),
50
51
  viewport: Object.freeze([]),
51
- "audio-script": Object.freeze(["SegmentText", "FromAsset"]),
52
- "phonetic-script": Object.freeze(["SegmentText"]),
52
+ "audio-script": Object.freeze(["SegmentText", "Asset"]),
53
+ "phonetic-script": Object.freeze(["SegmentText", "Asset"]),
53
54
  caption: Object.freeze([
54
55
  "SegmentText",
55
56
  "Sequence",
56
- "FromAsset"
57
+ "Asset"
57
58
  ])
58
59
  });
59
60
  /** What a known kind declares; extension kinds declare nothing through this table. */
@@ -347,15 +348,6 @@ function entityToRow(entity) {
347
348
  //#endregion
348
349
  //#region ../medeo-dsl/src/invariants.ts
349
350
  /** An entity is made from at most one Asset; two would leave its bytes ambiguous. */
350
- function validateAssetSource(entity, index) {
351
- if (entity.current().entityKind === "asset") return [];
352
- if ([...index.relationsOf(entity)].filter((relation) => relation.kind === "from-asset").length <= 1) return [];
353
- return [{
354
- code: "asset_source_cardinality",
355
- entityId: entity.entityId,
356
- message: `Entity "${entity.entityId}" is made from more than one Asset`
357
- }];
358
- }
359
351
  /** Validates one complete set of entities and its authoritative Relation rows. */
360
352
  function validateEntityRelationSet(entityRefs, index, options) {
361
353
  const { entities, issues } = collectRelatedEntities(entityRefs, index);
@@ -377,7 +369,6 @@ function validateEntityRelationSet(entityRefs, index, options) {
377
369
  const current = entity.current();
378
370
  const entityIssues = validateEntity(entity, entityIds);
379
371
  issues.push(...entityIssues);
380
- issues.push(...validateAssetSource(entity, index));
381
372
  if (entityIssues.some((issue) => issue.code === "invalid_entity_payload")) continue;
382
373
  if (current.entityKind === "sequence-marker") {
383
374
  const marker = entity;
@@ -1021,10 +1012,10 @@ const markerContentRelationSpec = Object.freeze({
1021
1012
  /**
1022
1013
  * `from-asset(entity, asset)` — the entity was made from that stored resource.
1023
1014
  *
1024
- * Eligibility is declared: the non-Asset endpoint must carry the `FromAsset`
1025
- * kind, which says an entity of that kind can be made from stored bytes and
1026
- * asks nothing of its payload. The bytes stay named in exactly one place, so
1027
- * no entity copies the locator into its own row and one Asset can back several
1015
+ * Eligibility is declared: the non-Asset endpoint must carry the `Asset` kind,
1016
+ * which says an entity of that kind can be bound to stored bytes and asks
1017
+ * nothing of its payload. The bytes stay named in exactly one place, so no
1018
+ * entity copies the locator into its own row and one Asset can back several
1028
1019
  * entities.
1029
1020
  */
1030
1021
  const fromAssetRelationSpec = Object.freeze({
@@ -1032,6 +1023,26 @@ const fromAssetRelationSpec = Object.freeze({
1032
1023
  validateEndpoints: (endpoints) => hasExactlyOneAsset(endpoints),
1033
1024
  validateMetadata: isAssetBinding
1034
1025
  });
1026
+ /**
1027
+ * `to-asset(entity, asset)` — the entity's content was written out to that
1028
+ * stored resource.
1029
+ *
1030
+ * The mirror of `from-asset`, and deliberately a separate relation kind rather
1031
+ * than a second declared kind: eligibility is the same either way, but which
1032
+ * way the content flowed is what says whether an entity may still be edited.
1033
+ * An entity reached by `from-asset` was produced from bytes it does not own,
1034
+ * so changing it would contradict them. An entity with a `to-asset` owns its
1035
+ * content and the resource is a snapshot of it, which is what goes stale and
1036
+ * gets written again. A transcript that is later edited carries both at once,
1037
+ * and only the distinct relation kinds keep them apart.
1038
+ *
1039
+ * Writing a snapshot is host assembly; nothing a script authors creates one.
1040
+ */
1041
+ const toAssetRelationSpec = Object.freeze({
1042
+ kind: "to-asset",
1043
+ validateEndpoints: (endpoints) => hasExactlyOneAsset(endpoints),
1044
+ validateMetadata: isAssetBinding
1045
+ });
1035
1046
  /** `generated(output, input)` means endpoint 0 was generated from endpoint 1. */
1036
1047
  const generatedRelationSpec = Object.freeze({
1037
1048
  kind: "generated",
@@ -1094,6 +1105,7 @@ const builtInRelationSpecs = Object.freeze([
1094
1105
  axVideoMarkerRelationSpec,
1095
1106
  markerTimelineRelationSpec,
1096
1107
  fromAssetRelationSpec,
1108
+ toAssetRelationSpec,
1097
1109
  generatedRelationSpec,
1098
1110
  captionAlignmentRelationSpec,
1099
1111
  clipAnchorRelationSpec,
@@ -1122,12 +1134,12 @@ function hasMarkerAndSequence(endpoints) {
1122
1134
  const second = endpoints[1].current();
1123
1135
  return first.entityKind === "sequence-marker" && hasSequence(second) || second.entityKind === "sequence-marker" && hasSequence(first);
1124
1136
  }
1125
- /** Exactly one end is the Asset, so the other end is unambiguously the maker. */
1137
+ /** Exactly one end is the Asset, so the other end is unambiguously the bound entity. */
1126
1138
  function hasExactlyOneAsset(endpoints) {
1127
1139
  const kinds = endpoints.map((endpoint) => endpoint.current().entityKind);
1128
1140
  const assets = kinds.map((kind) => kind === "asset");
1129
1141
  if (assets[0] === assets[1]) return false;
1130
- return declaresKind(assets[0] ? kinds[1] : kinds[0], "FromAsset");
1142
+ return declaresKind(assets[0] ? kinds[1] : kinds[0], "Asset");
1131
1143
  }
1132
1144
  function isGeneratedMedia(entity) {
1133
1145
  return entity.entityKind === "video" || entity.entityKind === "image" || entity.entityKind === "audio";
@@ -1467,6 +1479,8 @@ function errorMessage(error) {
1467
1479
  //#region src/sandbox/business-facades.ts
1468
1480
  /** Resource IDs are entity data; physical storage paths remain host-owned. */
1469
1481
  const infrastructureFields = new Set(["storageKey", "assetContentHash"]);
1482
+ /** Both directions of an Asset binding are host assembly, whoever asks. */
1483
+ const assetBindingKinds = new Set(["from-asset", "to-asset"]);
1470
1484
  function businessEntity(entity) {
1471
1485
  return {
1472
1486
  ...entity,
@@ -1479,7 +1493,17 @@ function businessState(state) {
1479
1493
  revision: state.revision,
1480
1494
  audioScriptEntityId: state.audioScriptEntityId,
1481
1495
  entities: state.entities.filter((row) => !assets.has(row.entity_id)).map(businessEntity),
1482
- relations: state.relations.filter((row) => row.relation_kind !== "from-asset" && !assets.has(row.endpoint_0_entity_id) && !assets.has(row.endpoint_1_entity_id))
1496
+ relations: state.relations.filter((row) => !assetBindingKinds.has(row.relation_kind) && !assets.has(row.endpoint_0_entity_id) && !assets.has(row.endpoint_1_entity_id))
1497
+ };
1498
+ }
1499
+ /** Only a kind that declares Asset can be bound to stored bytes. */
1500
+ function assetLocator(value, entityKind) {
1501
+ if (!declaresKind(entityKind, "Asset")) throw new Error(`Entity kind "${entityKind}" cannot be made from an Asset`);
1502
+ if (!isJsonObject(value) || value.system !== "memota" && value.system !== "memota-speech") throw new Error("asset.system must be \"memota\" or \"memota-speech\"");
1503
+ if (typeof value.key !== "string" || value.key.trim() !== value.key || value.key === "") throw new Error("asset.key must be a trimmed, non-empty resource id");
1504
+ return {
1505
+ system: value.system,
1506
+ key: value.key
1483
1507
  };
1484
1508
  }
1485
1509
  /** Keep infrastructure rows available to assembly without exposing their management to scripts. */
@@ -1494,7 +1518,7 @@ function businessFacades(entities, relations, onDirectScriptWrite) {
1494
1518
  for (const id of payload.baseEntityIds) if (typeof id === "string") assertBusiness(id);
1495
1519
  }
1496
1520
  };
1497
- const visibleRelation = (relation) => relation.relation_kind !== "from-asset" && !isAsset(relation.endpoint_0_entity_id) && !isAsset(relation.endpoint_1_entity_id);
1521
+ const visibleRelation = (relation) => !assetBindingKinds.has(relation.relation_kind) && !isAsset(relation.endpoint_0_entity_id) && !isAsset(relation.endpoint_1_entity_id);
1498
1522
  return {
1499
1523
  entities: {
1500
1524
  list: () => entities.list().filter((entity) => entity.entity_kind !== "asset").map(businessEntity),
@@ -1506,7 +1530,14 @@ function businessFacades(entities, relations, onDirectScriptWrite) {
1506
1530
  if (input.entity_kind === "asset") throw new Error("Asset entities are managed by host assembly");
1507
1531
  assertBusinessPayload(input.payload);
1508
1532
  if (input.entity_id && entities.get(input.entity_id)) throw new Error(`Entity id ${input.entity_id} already exists`);
1509
- const id = entities.create(input);
1533
+ const asset = input.asset === void 0 ? void 0 : assetLocator(input.asset, input.entity_kind);
1534
+ const { asset: _named, ...row } = input;
1535
+ const id = entities.create(row);
1536
+ if (asset !== void 0) relations.link({
1537
+ relation_kind: "from-asset",
1538
+ endpoint_0_entity_id: id,
1539
+ endpoint_1_entity_id: entities.ensureAsset(asset)
1540
+ });
1510
1541
  if (input.entity_kind === "audio-script") onDirectScriptWrite?.(id);
1511
1542
  return id;
1512
1543
  },
@@ -1541,7 +1572,7 @@ function businessFacades(entities, relations, onDirectScriptWrite) {
1541
1572
  list: () => relations.list().filter(visibleRelation),
1542
1573
  of: (id, kind) => relations.of(id, kind).filter(visibleRelation),
1543
1574
  link: (input) => {
1544
- if (input.relation_kind === "from-asset") throw new Error("Asset bindings are managed by host assembly");
1575
+ if (assetBindingKinds.has(input.relation_kind)) throw new Error("Asset bindings are managed by host assembly");
1545
1576
  assertBusiness(input.endpoint_0_entity_id);
1546
1577
  assertBusiness(input.endpoint_1_entity_id);
1547
1578
  return relations.link(input);
@@ -1630,7 +1661,7 @@ var EntitySandbox = class {
1630
1661
  decodeEntityRelationRows(rows, numericMarkerComparators);
1631
1662
  if (this.state.revision !== 0 || rows.entities.length !== 0 || this.commands.length !== 0) {
1632
1663
  assertCanonicalEditorResources(rows);
1633
- readDocumentAudioScript({
1664
+ if (this.state.audioScriptEntityId !== null) readDocumentAudioScript({
1634
1665
  rows,
1635
1666
  audioScriptEntityId: this.state.audioScriptEntityId
1636
1667
  });
@@ -1896,7 +1927,7 @@ var EntitySandbox = class {
1896
1927
  }
1897
1928
  for (const row of rows.relations) {
1898
1929
  if (this.state.relations.some((relation) => relation.relation_id === row.relationId)) continue;
1899
- if (row.relationKind !== "timeline-track" && row.relationKind !== "voice-timbre" && row.relationKind !== "from-asset") throw new Error(`Unexpected resource Relation ${row.relationKind}`);
1930
+ if (row.relationKind !== "timeline-track" && row.relationKind !== "voice-timbre" && row.relationKind !== "from-asset" && row.relationKind !== "to-asset") throw new Error(`Unexpected resource Relation ${row.relationKind}`);
1900
1931
  this.link({
1901
1932
  relation_id: row.relationId,
1902
1933
  relation_kind: row.relationKind,
@@ -2202,4 +2233,4 @@ const numericMarkerComparators = { compareMarkerPoints: (_marker, _range, left,
2202
2233
  //#endregion
2203
2234
  export { createEntityId as a, businessState as i, toDslRows as n, createRelationId as o, businessFacades as r, isMediaAssetVariantKind as s, EntitySandbox as t };
2204
2235
 
2205
- //# sourceMappingURL=entity-sandbox-BTR2cRl1.mjs.map
2236
+ //# sourceMappingURL=entity-sandbox-CulJBYrI.mjs.map