@mengine/medeo-tool 1.4.1-alpha.5 → 2.0.1-alpha.11
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 +9 -0
- package/dist/{entity-contract-DlmUSouB.d.mts → entity-contract-Cf3AiSe7.d.mts} +45 -43
- package/dist/{entity-sandbox-TaUVT3on.mjs → entity-sandbox-BTR2cRl1.mjs} +255 -139
- package/dist/entity-sandbox-BTR2cRl1.mjs.map +1 -0
- package/dist/index.d.mts +37 -12
- package/dist/index.mjs +131 -137
- package/dist/index.mjs.map +1 -1
- package/dist/sandbox-api.d.mts +42 -44
- package/dist/worker-entry.d.mts +2 -1
- package/dist/worker-entry.mjs +105 -111
- package/dist/worker-entry.mjs.map +1 -1
- package/package.json +3 -3
- package/dist/entity-sandbox-TaUVT3on.mjs.map +0 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as UpdateEntityInput, S as UnlinkRelationInput, _ as LinkRelationInput, a as DeleteEntityInput, b as SandboxEntity, c as EntitySandboxCheckpoint, d as JsonObject, f as JsonPrimitive, g as LinkGeneratedRelationInput, h as KnownRelationKind, i as CreateEntityInput, l as EntityStoreSnapshot, m as KnownEntityKind, n as BusinessEntityFacade, o as EntityCommand, p as JsonValue, r as BusinessRelationFacade, s as EntityPlanState, t as AuthorableRelationKind, u as EntityUpdateInput, v as RelationUpdateInput, x as SandboxRelation, y as ResourceEntityKind } from "./entity-contract-
|
|
1
|
+
import { C as UpdateEntityInput, S as UnlinkRelationInput, _ as LinkRelationInput, a as DeleteEntityInput, b as SandboxEntity, c as EntitySandboxCheckpoint, d as JsonObject, f as JsonPrimitive, g as LinkGeneratedRelationInput, h as KnownRelationKind, i as CreateEntityInput, l as EntityStoreSnapshot, m as KnownEntityKind, n as BusinessEntityFacade, o as EntityCommand, p as JsonValue, r as BusinessRelationFacade, s as EntityPlanState, t as AuthorableRelationKind, u as EntityUpdateInput, v as RelationUpdateInput, x as SandboxRelation, y as ResourceEntityKind } from "./entity-contract-Cf3AiSe7.mjs";
|
|
2
2
|
import { JournalEntry, ManualSyncDoc, MediaAssetFact, PartIdFactory, SemanticOpName, VideoDocument, VideoDraft } from "@mengine/medeo-client";
|
|
3
3
|
|
|
4
4
|
//#region src/document/compact-projection.d.ts
|
|
@@ -11,6 +11,14 @@ import { JournalEntry, ManualSyncDoc, MediaAssetFact, PartIdFactory, SemanticOpN
|
|
|
11
11
|
* `fromVideoDocument`.
|
|
12
12
|
*/
|
|
13
13
|
interface CompactProjectionOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Document identity for the header. Stated by the caller: the document
|
|
16
|
+
* content no longer carries its own id or revision (business metadata left
|
|
17
|
+
* the CRDT), and the model needs to know which draft this summary describes.
|
|
18
|
+
* Omitted values keep the header's fixed shape with the historical blanks.
|
|
19
|
+
*/
|
|
20
|
+
docId?: string;
|
|
21
|
+
revision?: number;
|
|
14
22
|
/** Only render these parts (header still reports the full timeline total). Default = all. */
|
|
15
23
|
onlyPartIds?: ReadonlySet<string>;
|
|
16
24
|
/** Caption text preview truncation length. Default 24. */
|
|
@@ -30,7 +38,10 @@ declare function collectAffectedPartIds(journal: readonly JournalEntry[]): Set<s
|
|
|
30
38
|
* Render a ChangePlan preview: header + rows for journal-affected parts only.
|
|
31
39
|
* Empty journal → empty `onlyPartIds` (header alone), matching the T2 contract.
|
|
32
40
|
*/
|
|
33
|
-
declare function renderPreview(document: VideoDocument, journal: readonly JournalEntry[]
|
|
41
|
+
declare function renderPreview(document: VideoDocument, journal: readonly JournalEntry[], identity?: {
|
|
42
|
+
docId?: string;
|
|
43
|
+
revision?: number;
|
|
44
|
+
}): string;
|
|
34
45
|
//#endregion
|
|
35
46
|
//#region src/entity/entity-asset.d.ts
|
|
36
47
|
/** Immutable resource content resolved by the host for a document Entity. */
|
|
@@ -39,7 +50,17 @@ interface EntityAssetContent {
|
|
|
39
50
|
content: JsonValue;
|
|
40
51
|
}
|
|
41
52
|
/** Host-only I/O. The sandbox supplies an Entity, never an arbitrary Asset query. */
|
|
42
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Resolve one entity's stored resource.
|
|
55
|
+
*
|
|
56
|
+
* The Asset travels with the entity because the locator lives on the Asset
|
|
57
|
+
* alone: an entity made from stored bytes names them through a `from-asset`
|
|
58
|
+
* Relation, never on its own row, so a loader given only the entity would have
|
|
59
|
+
* nothing to look up.
|
|
60
|
+
*/
|
|
61
|
+
type EntityAssetLoader = (docId: string, entity: SandboxEntity, asset: SandboxEntity) => Promise<EntityAssetContent>;
|
|
62
|
+
/** Program-only initial resource creation; never exposed as a sandbox API. */
|
|
63
|
+
type EntityAssetWriter = (docId: string, entity: SandboxEntity, content: JsonValue) => Promise<EntityAssetContent>;
|
|
43
64
|
//#endregion
|
|
44
65
|
//#region src/entity/entity-sandbox.d.ts
|
|
45
66
|
type DomainIdFactory = (prefix: 'entity' | 'relation') => string;
|
|
@@ -61,6 +82,12 @@ interface ChangePlan {
|
|
|
61
82
|
logs: string[];
|
|
62
83
|
}
|
|
63
84
|
interface EditSandboxSessionOptions {
|
|
85
|
+
/**
|
|
86
|
+
* Document this session edits. Stated by the host: the document content no
|
|
87
|
+
* longer carries its own identity (business metadata left the CRDT), and a
|
|
88
|
+
* plan must name the document it will be committed against.
|
|
89
|
+
*/
|
|
90
|
+
docId?: string;
|
|
64
91
|
idFactory?: PartIdFactory;
|
|
65
92
|
onEntry?: (entry: JournalEntry) => void;
|
|
66
93
|
onLog?: (line: string) => void;
|
|
@@ -87,7 +114,10 @@ interface ConsoleShim {
|
|
|
87
114
|
* injects loaders that break worker boot.
|
|
88
115
|
*/
|
|
89
116
|
interface RunEditScriptOptions {
|
|
90
|
-
|
|
117
|
+
/** Document the plan will be committed against; stated by the host. */
|
|
118
|
+
docId: string;
|
|
119
|
+
loadEntityAsset?: (entity: SandboxEntity, asset: SandboxEntity) => Promise<EntityAssetContent>;
|
|
120
|
+
writeEntityAsset?: (entity: SandboxEntity, content: EntityAssetContent['content']) => Promise<EntityAssetContent>;
|
|
91
121
|
document: VideoDocument;
|
|
92
122
|
baseVersion: string;
|
|
93
123
|
script: string;
|
|
@@ -376,13 +406,6 @@ interface CreateMedeoToolOptions {
|
|
|
376
406
|
userId?: ContextualValue<string>;
|
|
377
407
|
/** Stable agent peer id. Supply a host-scoped value so audit provenance is durable. */
|
|
378
408
|
peerId?: ContextualValue<string>;
|
|
379
|
-
/**
|
|
380
|
-
* Load the authoritative legacy draft used to create a missing Mengine
|
|
381
|
-
* document. The tool owns the get-or-create flow: it first probes Mengine,
|
|
382
|
-
* converts this draft into a VideoDocument only on a 404, bootstraps the
|
|
383
|
-
* snapshot, and tolerates a concurrent creator winning the race.
|
|
384
|
-
*/
|
|
385
|
-
loadInitialDraft?: (docId: string) => Promise<VideoDraft>;
|
|
386
409
|
/**
|
|
387
410
|
* Resolve factual generation lineage by external asset id after a confirmed
|
|
388
411
|
* entity commit. Return every known generation record involving the given
|
|
@@ -395,6 +418,8 @@ interface CreateMedeoToolOptions {
|
|
|
395
418
|
/** Assemble optional Caption artifacts by immutable entity ID; never exposed to scripts. */
|
|
396
419
|
loadCaptionAssets?: CaptionAssetsLoader;
|
|
397
420
|
loadEntityAsset?: EntityAssetLoader;
|
|
421
|
+
/** Program-side persistence for immutable AudioScript resources. */
|
|
422
|
+
writeEntityAsset?: EntityAssetWriter;
|
|
398
423
|
fetchImpl?: typeof fetch;
|
|
399
424
|
/** @deprecated ManualSyncDoc has no SSE or reconnect loop. */
|
|
400
425
|
sseReconnectDelayMs?: number;
|
|
@@ -567,5 +592,5 @@ declare function materializeResources(options: EntityHttpClientOptions & {
|
|
|
567
592
|
loadGenerationFacts?: GenerationFactsLoader;
|
|
568
593
|
}, resources: readonly GeneratedResource[]): Promise<readonly string[]>;
|
|
569
594
|
//#endregion
|
|
570
|
-
export { type AssetGenerationFact, type AuthorableRelationKind, type BusinessEntityFacade, type BusinessRelationFacade, type CaptionAssetAssemblyOutcome, type CaptionAssetFact, type CaptionAssetsLoader, type ChangePlan, type CommitPlan, type CommitPlanOptions, type CommitPlanResult, type CompactProjectionOptions, type ConsoleShim, type CreateEntityInput, type CreateMedeoToolOptions, type DeleteEntityInput, type EditSandboxSessionOptions, type EditScriptResult, type EntityAssetContent, type EntityAssetLoader, type EntityCommand, type EntityCommitResult, type EntityPlanState, type EntitySandboxCheckpoint, type EntityStoreSnapshot, type EntityUpdateInput, type GeneratedResource, type GenerationFactsLoader, type GenerationSyncOutcome, type JsonObject, type JsonPrimitive, type JsonValue, type KnownEntityKind, type KnownRelationKind, type LinkGeneratedRelationInput, type LinkRelationInput, MEDEO_TOOL_DESCRIPTION, MEDEO_TOOL_NAME, MEDEO_TOOL_PARAMETERS, type MedeoCommitResult, type MedeoInitialDraft, type MedeoModelContext, type MedeoModelContextInput, type MedeoTool, type MedeoToolInput, type MedeoToolOp, type MedeoToolResult, type RelationUpdateInput, type ResourceEntityKind, type RunEditScriptOptions, type SandboxEntity, type SandboxRelation, type UnlinkRelationInput, type UpdateEntityInput, collectAffectedPartIds, commitPlan, createMedeoTool, materializeResources, renderCompactProjection, renderPreview, runEditScript };
|
|
595
|
+
export { type AssetGenerationFact, type AuthorableRelationKind, type BusinessEntityFacade, type BusinessRelationFacade, type CaptionAssetAssemblyOutcome, type CaptionAssetFact, type CaptionAssetsLoader, type ChangePlan, type CommitPlan, type CommitPlanOptions, type CommitPlanResult, type CompactProjectionOptions, type ConsoleShim, type CreateEntityInput, type CreateMedeoToolOptions, type DeleteEntityInput, type EditSandboxSessionOptions, type EditScriptResult, type EntityAssetContent, type EntityAssetLoader, type EntityAssetWriter, type EntityCommand, type EntityCommitResult, type EntityPlanState, type EntitySandboxCheckpoint, type EntityStoreSnapshot, type EntityUpdateInput, type GeneratedResource, type GenerationFactsLoader, type GenerationSyncOutcome, type JsonObject, type JsonPrimitive, type JsonValue, type KnownEntityKind, type KnownRelationKind, type LinkGeneratedRelationInput, type LinkRelationInput, MEDEO_TOOL_DESCRIPTION, MEDEO_TOOL_NAME, MEDEO_TOOL_PARAMETERS, type MedeoCommitResult, type MedeoInitialDraft, type MedeoModelContext, type MedeoModelContextInput, type MedeoTool, type MedeoToolInput, type MedeoToolOp, type MedeoToolResult, type RelationUpdateInput, type ResourceEntityKind, type RunEditScriptOptions, type SandboxEntity, type SandboxRelation, type UnlinkRelationInput, type UpdateEntityInput, collectAffectedPartIds, commitPlan, createMedeoTool, materializeResources, renderCompactProjection, renderPreview, runEditScript };
|
|
571
596
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { a as createEntityId, i as businessState, o as createRelationId, s as isMediaAssetVariantKind, t as EntitySandbox } from "./entity-sandbox-BTR2cRl1.mjs";
|
|
2
|
+
import { ManualSyncDoc, MengineHttpClient, ValidationError, base64ToBytes, bytesToBase64, compileEntityRows, createPlainMemoryAdapter, decodeDocVersionMark, effectiveVideoClipDurationMs, encodeDocVersionMark, replayJournal, solveVideoDocument, speedOf } from "@mengine/medeo-client";
|
|
3
3
|
import { Worker } from "node:worker_threads";
|
|
4
4
|
import { createHash, randomUUID } from "node:crypto";
|
|
5
5
|
//#region src/document/compact-projection.ts
|
|
@@ -79,7 +79,7 @@ function renderCompactProjection(document, options) {
|
|
|
79
79
|
rows.push(`${tag} ${partId} ${lane} [${abs},${abs + dur}) ${attrs}`);
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
-
return [`# draft=${
|
|
82
|
+
return [`# draft=${options?.docId ?? ""} v=${options?.revision ?? 0} duration=${solved.durationMs} parts=${totalParts} shown=${rows.length}`, ...rows].join("\n");
|
|
83
83
|
}
|
|
84
84
|
//#endregion
|
|
85
85
|
//#region src/sandbox/preview.ts
|
|
@@ -132,8 +132,12 @@ function collectFromValue(value, ids) {
|
|
|
132
132
|
* Render a ChangePlan preview: header + rows for journal-affected parts only.
|
|
133
133
|
* Empty journal → empty `onlyPartIds` (header alone), matching the T2 contract.
|
|
134
134
|
*/
|
|
135
|
-
function renderPreview(document, journal) {
|
|
136
|
-
|
|
135
|
+
function renderPreview(document, journal, identity) {
|
|
136
|
+
const onlyPartIds = journal.length === 0 ? /* @__PURE__ */ new Set() : collectAffectedPartIds(journal);
|
|
137
|
+
return renderCompactProjection(document, {
|
|
138
|
+
...identity,
|
|
139
|
+
onlyPartIds
|
|
140
|
+
});
|
|
137
141
|
}
|
|
138
142
|
//#endregion
|
|
139
143
|
//#region src/sandbox/node-host.ts
|
|
@@ -165,6 +169,7 @@ function runEditScript(options) {
|
|
|
165
169
|
let timer;
|
|
166
170
|
const worker = new Worker(workerEntryUrl, {
|
|
167
171
|
workerData: {
|
|
172
|
+
docId: options.docId,
|
|
168
173
|
document: options.document,
|
|
169
174
|
script: options.script,
|
|
170
175
|
inputs: options.inputs,
|
|
@@ -214,11 +219,14 @@ function runEditScript(options) {
|
|
|
214
219
|
armTimeout();
|
|
215
220
|
return;
|
|
216
221
|
}
|
|
217
|
-
if (message.t === "entity-asset") {
|
|
222
|
+
if (message.t === "entity-asset" || message.t === "write-entity-asset") {
|
|
218
223
|
(async () => {
|
|
219
224
|
try {
|
|
220
|
-
|
|
221
|
-
|
|
225
|
+
const result = message.t === "write-entity-asset" ? await (options.writeEntityAsset ?? (() => {
|
|
226
|
+
throw new Error("Entity Asset writer is unavailable");
|
|
227
|
+
}))(message.entity, message.content) : await (options.loadEntityAsset ?? (() => {
|
|
228
|
+
throw new Error("Entity Asset loader is unavailable");
|
|
229
|
+
}))(message.entity, message.asset);
|
|
222
230
|
if (!settled) worker.postMessage({
|
|
223
231
|
t: "entity-asset-result",
|
|
224
232
|
requestId: message.requestId,
|
|
@@ -272,7 +280,7 @@ function runEditScript(options) {
|
|
|
272
280
|
ok: true,
|
|
273
281
|
plan: {
|
|
274
282
|
plan_kind: message.planKind,
|
|
275
|
-
doc_id: options.
|
|
283
|
+
doc_id: options.docId,
|
|
276
284
|
base_version: options.baseVersion,
|
|
277
285
|
ops: ops.slice(),
|
|
278
286
|
...message.loroUpdate ? { loro_update: message.loroUpdate } : {},
|
|
@@ -338,7 +346,7 @@ function runEditScript(options) {
|
|
|
338
346
|
async function assembleCaptionAssets(input) {
|
|
339
347
|
try {
|
|
340
348
|
const state = await input.client.fetchState();
|
|
341
|
-
const candidates = state.entities.filter((entity) => entity.entity_kind === "caption" && !state.relations.some((relation) => relation.relation_kind === "
|
|
349
|
+
const candidates = state.entities.filter((entity) => entity.entity_kind === "caption" && !state.relations.some((relation) => relation.relation_kind === "from-asset" && (relation.endpoint_0_entity_id === entity.entity_id || relation.endpoint_1_entity_id === entity.entity_id)));
|
|
342
350
|
if (!candidates.length) return { status: "current" };
|
|
343
351
|
const ids = new Set(candidates.map((entity) => entity.entity_id));
|
|
344
352
|
const facts = await input.loadAssets(input.docId, [...ids]);
|
|
@@ -353,11 +361,8 @@ async function assembleCaptionAssets(input) {
|
|
|
353
361
|
continue;
|
|
354
362
|
}
|
|
355
363
|
bound.set(fact.captionEntityId, fact.assetId);
|
|
356
|
-
const matches = entities.filter((entity) =>
|
|
357
|
-
|
|
358
|
-
return external !== null && typeof external === "object" && !Array.isArray(external) && external.system === "memota" && external.key === fact.assetId;
|
|
359
|
-
});
|
|
360
|
-
if (matches.length > 1 || matches[0] && matches[0].entity_kind !== "asset") throw new Error(`Conflicting resource identity for Caption Asset ${fact.assetId}`);
|
|
364
|
+
const matches = entities.filter((entity) => entity.entity_kind === "asset" && entity.payload.system === "memota" && entity.payload.key === fact.assetId);
|
|
365
|
+
if (matches.length > 1) throw new Error(`Conflicting resource identity for Caption Asset ${fact.assetId}`);
|
|
361
366
|
let asset = matches[0];
|
|
362
367
|
if (asset && asset.payload.storageKey !== fact.storageKey) throw new Error(`Conflicting storage key for Caption Asset ${fact.assetId}`);
|
|
363
368
|
if (!asset) {
|
|
@@ -365,10 +370,8 @@ async function assembleCaptionAssets(input) {
|
|
|
365
370
|
entity_id: stableId$1("asset", fact.assetId),
|
|
366
371
|
entity_kind: "asset",
|
|
367
372
|
payload: {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
key: fact.assetId
|
|
371
|
-
},
|
|
373
|
+
system: "memota",
|
|
374
|
+
key: fact.assetId,
|
|
372
375
|
storageKey: fact.storageKey
|
|
373
376
|
}
|
|
374
377
|
};
|
|
@@ -377,7 +380,7 @@ async function assembleCaptionAssets(input) {
|
|
|
377
380
|
}
|
|
378
381
|
relations.push({
|
|
379
382
|
relation_id: stableId$1("relation", fact.captionEntityId, asset.entity_id),
|
|
380
|
-
relation_kind: "
|
|
383
|
+
relation_kind: "from-asset",
|
|
381
384
|
endpoint_0_entity_id: fact.captionEntityId,
|
|
382
385
|
endpoint_1_entity_id: asset.entity_id,
|
|
383
386
|
metadata: {},
|
|
@@ -429,11 +432,12 @@ const KNOWN_RELATION_KINDS = [
|
|
|
429
432
|
"marker-content",
|
|
430
433
|
"axvideo-marker",
|
|
431
434
|
"marker-timeline",
|
|
432
|
-
"
|
|
435
|
+
"from-asset",
|
|
433
436
|
"generated",
|
|
434
437
|
"caption-alignment",
|
|
435
438
|
"clip-anchor",
|
|
436
439
|
"phonetic-script-render",
|
|
440
|
+
"voice-timbre",
|
|
437
441
|
"audio-script-source",
|
|
438
442
|
"audio-script-marker"
|
|
439
443
|
];
|
|
@@ -685,33 +689,39 @@ async function syncGeneratedRelations(input) {
|
|
|
685
689
|
};
|
|
686
690
|
}
|
|
687
691
|
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
692
|
+
/** The locator an entity reaches through `from-asset`; only Assets state one. */
|
|
693
|
+
function assetLocatorOf(state, entityId) {
|
|
694
|
+
const assetIds = state.relations.filter((relation) => relation.relation_kind === "from-asset").map((relation) => relation.endpoint_0_entity_id === entityId ? relation.endpoint_1_entity_id : relation.endpoint_1_entity_id === entityId ? relation.endpoint_0_entity_id : void 0).filter((id) => id !== void 0);
|
|
695
|
+
for (const assetId of assetIds) {
|
|
696
|
+
const asset = state.entities.find((row) => row.entity_id === assetId && row.entity_kind === "asset");
|
|
697
|
+
const system = asset?.payload.system;
|
|
698
|
+
const key = asset?.payload.key;
|
|
699
|
+
if (typeof system !== "string" || !ASSET_SYSTEMS.has(system)) continue;
|
|
700
|
+
if (typeof key !== "string" || key.length === 0 || key.trim() !== key) continue;
|
|
701
|
+
return {
|
|
702
|
+
system,
|
|
703
|
+
key
|
|
704
|
+
};
|
|
705
|
+
}
|
|
696
706
|
}
|
|
697
|
-
/**
|
|
707
|
+
/**
|
|
708
|
+
* Media reaches its Asset through `from-asset`, so lineage lookup follows that
|
|
709
|
+
* Relation — the locator is never copied onto the medium's own row.
|
|
710
|
+
*/
|
|
698
711
|
function resolveMediaByAssetKey(state, factKeys) {
|
|
699
712
|
const systemByKey = /* @__PURE__ */ new Map();
|
|
700
|
-
for (const entity of state.entities) {
|
|
701
|
-
const key = assetKeyOf(entity);
|
|
702
|
-
if (key === void 0 || !factKeys?.has(key)) continue;
|
|
703
|
-
const system = entity.payload.external.system;
|
|
704
|
-
if (systemByKey.has(key) && systemByKey.get(key) !== system) throw new Error(`Ambiguous generation asset id ${key} across media and speech namespaces`);
|
|
705
|
-
systemByKey.set(key, system);
|
|
706
|
-
}
|
|
707
713
|
const resolved = /* @__PURE__ */ new Map();
|
|
708
714
|
for (const entity of state.entities) {
|
|
709
715
|
if (!isMediaAssetVariantKind(entity.entity_kind)) continue;
|
|
710
|
-
const
|
|
711
|
-
if (
|
|
712
|
-
|
|
716
|
+
const locator = assetLocatorOf(state, entity.entity_id);
|
|
717
|
+
if (locator === void 0) continue;
|
|
718
|
+
if (factKeys?.has(locator.key)) {
|
|
719
|
+
if (systemByKey.has(locator.key) && systemByKey.get(locator.key) !== locator.system) throw new Error(`Ambiguous generation asset id ${locator.key} across media and speech namespaces`);
|
|
720
|
+
systemByKey.set(locator.key, locator.system);
|
|
721
|
+
}
|
|
722
|
+
const matches = resolved.get(locator.key) ?? [];
|
|
713
723
|
matches.push(entity.entity_id);
|
|
714
|
-
resolved.set(key, matches);
|
|
724
|
+
resolved.set(locator.key, matches);
|
|
715
725
|
}
|
|
716
726
|
return resolved;
|
|
717
727
|
}
|
|
@@ -738,25 +748,6 @@ function isRecord$1(value) {
|
|
|
738
748
|
/** @generated by gen:sandbox-dts. DO NOT EDIT. */
|
|
739
749
|
const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
740
750
|
"/** @generated by gen:sandbox-dts. Entity-native editor contract; DO NOT EDIT. */",
|
|
741
|
-
"export interface BoundedDerivedSequencePayload extends JsonObject {",
|
|
742
|
-
" extent: {",
|
|
743
|
-
" kind: 'bounded';",
|
|
744
|
-
" start: number;",
|
|
745
|
-
" end: number;",
|
|
746
|
-
" };",
|
|
747
|
-
" sampling: 'derived';",
|
|
748
|
-
" coordinateSpace: JsonValue;",
|
|
749
|
-
"}",
|
|
750
|
-
"export interface BoundedNativeSequencePayload extends JsonObject {",
|
|
751
|
-
" /** Factual coordinates from recalled media metadata; never invent an end/duration. */",
|
|
752
|
-
" extent: {",
|
|
753
|
-
" kind: 'bounded';",
|
|
754
|
-
" start: number;",
|
|
755
|
-
" end: number;",
|
|
756
|
-
" };",
|
|
757
|
-
" sampling: 'native';",
|
|
758
|
-
" coordinateSpace: JsonValue;",
|
|
759
|
-
"}",
|
|
760
751
|
"/** Business editing surface. Reads are assembled snapshots; writes preserve native operation intent. */",
|
|
761
752
|
"export interface BusinessEntityFacade {",
|
|
762
753
|
" list(): SandboxEntity[];",
|
|
@@ -805,17 +796,20 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
805
796
|
" content: JsonValue;",
|
|
806
797
|
"}",
|
|
807
798
|
"export interface EntityPayloadByKind {",
|
|
808
|
-
"
|
|
799
|
+
" /** Its Timeline decides how long it runs, so it stores no length. */",
|
|
800
|
+
" axvideo: JsonObject;",
|
|
809
801
|
" timeline: JsonObject;",
|
|
810
802
|
" track: JsonObject & {",
|
|
811
803
|
" hidden?: boolean;",
|
|
812
804
|
" role?: string;",
|
|
813
805
|
" };",
|
|
814
806
|
" clip: JsonObject;",
|
|
815
|
-
" video:
|
|
816
|
-
" audio
|
|
817
|
-
"
|
|
818
|
-
"
|
|
807
|
+
" video: OwnDurationPayload;",
|
|
808
|
+
" /** Every playable sound, including video original audio and synthesized voiceovers. */",
|
|
809
|
+
" audio: OwnDurationPayload;",
|
|
810
|
+
" /** A timbre identity, never playable content; the rendered take is an `audio` entity. */",
|
|
811
|
+
" voice: VoiceIdentityPayload;",
|
|
812
|
+
" image: NoDurationPayload;",
|
|
819
813
|
" 'sequence-marker': JsonObject & {",
|
|
820
814
|
" sourceRange: {",
|
|
821
815
|
" start: number;",
|
|
@@ -852,7 +846,7 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
852
846
|
" phonemeScript?: string;",
|
|
853
847
|
" prosody?: JsonObject;",
|
|
854
848
|
" };",
|
|
855
|
-
" caption:
|
|
849
|
+
" caption: OwnDurationPayload & {",
|
|
856
850
|
" baseEntityIds: string[];",
|
|
857
851
|
" selection: CaptionTextSelection;",
|
|
858
852
|
" style?: JsonObject;",
|
|
@@ -942,6 +936,7 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
942
936
|
" | 'caption-alignment'",
|
|
943
937
|
" | 'clip-anchor'",
|
|
944
938
|
" | 'phonetic-script-render'",
|
|
939
|
+
" | 'voice-timbre'",
|
|
945
940
|
" | 'audio-script-source'",
|
|
946
941
|
" | 'audio-script-marker';",
|
|
947
942
|
"interface LinkRelationBase {",
|
|
@@ -954,12 +949,18 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
954
949
|
" relation_kind: KnownRelationKind;",
|
|
955
950
|
" metadata?: JsonObject;",
|
|
956
951
|
"}",
|
|
957
|
-
"
|
|
958
|
-
"
|
|
959
|
-
"
|
|
960
|
-
"
|
|
961
|
-
"
|
|
962
|
-
"
|
|
952
|
+
"/** A still frame runs for as long as its use asks, so it states no length. */",
|
|
953
|
+
"export interface NoDurationPayload extends JsonObject {",
|
|
954
|
+
" durationMs: null;",
|
|
955
|
+
"}",
|
|
956
|
+
"/**",
|
|
957
|
+
" * Content states how long it runs and nothing else: where it is taken from and",
|
|
958
|
+
" * where it lands both belong to its Sequence Marker.",
|
|
959
|
+
" */",
|
|
960
|
+
"export interface OwnDurationPayload extends JsonObject {",
|
|
961
|
+
" /** Factual whole milliseconds from recalled media metadata; never invented. */",
|
|
962
|
+
" durationMs: number;",
|
|
963
|
+
"}",
|
|
963
964
|
"export interface RelationUpdateInput {",
|
|
964
965
|
" relation_id: string;",
|
|
965
966
|
" changes: FieldChange[];",
|
|
@@ -982,22 +983,22 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
982
983
|
" text: string;",
|
|
983
984
|
" language?: string;",
|
|
984
985
|
"};",
|
|
985
|
-
"/**
|
|
986
|
+
"/**",
|
|
987
|
+
" * Stored own fields; a variant may obtain required content fields from its",
|
|
988
|
+
" * declared bases.",
|
|
989
|
+
" *",
|
|
990
|
+
" * Caption and Audio may also be declared resource-only: the ASR transcript and",
|
|
991
|
+
" * the synthesized voiceover are attached by the host, which then fills in the",
|
|
992
|
+
" * remaining factual fields.",
|
|
993
|
+
" */",
|
|
986
994
|
"export type StoredEntityPayload<K extends KnownEntityKind> =",
|
|
987
995
|
" | EntityPayloadByKind[K]",
|
|
988
|
-
"
|
|
996
|
+
" // Resource-only: the host fills the factual fields after reading the Asset.",
|
|
997
|
+
" | (K extends 'caption' | 'audio' ? Record<string, never> : never)",
|
|
989
998
|
" | (JsonObject &",
|
|
990
999
|
" Partial<EntityPayloadByKind[K]> & {",
|
|
991
1000
|
" baseEntityIds: string[];",
|
|
992
1001
|
" });",
|
|
993
|
-
"export interface UnboundedConstantSequencePayload extends JsonObject {",
|
|
994
|
-
" extent: {",
|
|
995
|
-
" kind: 'unbounded';",
|
|
996
|
-
" start: number;",
|
|
997
|
-
" };",
|
|
998
|
-
" sampling: 'constant';",
|
|
999
|
-
" coordinateSpace: JsonValue;",
|
|
1000
|
-
"}",
|
|
1001
1002
|
"export interface UnlinkRelationInput {",
|
|
1002
1003
|
" relation_id: string;",
|
|
1003
1004
|
"}",
|
|
@@ -1006,9 +1007,17 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
1006
1007
|
" entity_id: string;",
|
|
1007
1008
|
" payload: JsonObject;",
|
|
1008
1009
|
"}",
|
|
1010
|
+
"/** The voice-library timbre a synthesized Audio was rendered with. */",
|
|
1011
|
+
"export type VoiceIdentityPayload = JsonObject & {",
|
|
1012
|
+
" voice: {",
|
|
1013
|
+
" system: 'voice-library';",
|
|
1014
|
+
" key: string;",
|
|
1015
|
+
" name?: string;",
|
|
1016
|
+
" };",
|
|
1017
|
+
"};",
|
|
1009
1018
|
"export declare const entities: BusinessEntityFacade;",
|
|
1010
1019
|
"export declare const relations: BusinessRelationFacade;",
|
|
1011
|
-
"/** Resolve this Entity's attached resource through the host; await before using an uninitialized Caption. */",
|
|
1020
|
+
"/** Resolve this Entity's attached resource through the host; await before using an uninitialized Caption or Voice. */",
|
|
1012
1021
|
"export declare function rgetAssetFromEntity(entityId: string): Promise<EntityAssetContent>;",
|
|
1013
1022
|
"export declare function checkpoint(): EntitySandboxCheckpoint;",
|
|
1014
1023
|
"export declare function rollbackTo(cp: EntitySandboxCheckpoint): void;",
|
|
@@ -1038,19 +1047,21 @@ The host supplies the current document. Do not invent or request a document id.
|
|
|
1038
1047
|
|
|
1039
1048
|
Entity reads return assembled snapshots. Mutating returned objects has no persistence effect. Create declares own fields. Update requires a nonempty changes array and atomically applies its ordered operations to assembled fields, routing each to its declaring owner. DeclareFields explicitly establishes own fields or an override; it is not the normal way to edit inherited content. Deletion requires explicitly handling incident relations, variant references and protected project attachments; it never deletes external resources or cascades business entities.
|
|
1040
1049
|
|
|
1041
|
-
FieldChange paths start at payload for an entity, and at metadata or trace for a relation. Use strings for fields and {elementId:'...'} for list members, never numeric indices. Segment identity is segmentId; reference identity is the referenced id. set writes scalars or schema-declared atomic values, including complete sourceRange, targetRange,
|
|
1050
|
+
FieldChange paths start at payload for an entity, and at metadata or trace for a relation. Use strings for fields and {elementId:'...'} for list members, never numeric indices. Segment identity is segmentId; reference identity is the referenced id. set writes scalars or schema-declared atomic values, including complete sourceRange, targetRange, duration, timeRemapping, selection, textRange and external. It cannot replace collaborative text, lists or maps. Edit text with text.splice (Unicode code-point index and deleteCount), and segments/segmentRanges with list.insert, list.move or list.remove. beforeElementId:null means append; moving before itself, duplicate identity or an absent anchor is an error. Unset removes optional fields only. Map fields are edited through leaf paths; initialize a new own map via declareFields. baseEntityIds is unordered composition membership: insert/remove are supported, moving bases cannot resolve field conflicts. Independent fields and list-item text edits retain native Loro identities.
|
|
1042
1051
|
|
|
1043
1052
|
Entities own data. Relations associate two existing entities; endpoint_0 and endpoint_1 retain their declared positions. Each relation kind defines direction and meaning; neither relation listing order nor endpoint storage order implies a global business order. Relations.update changes metadata/trace fields only; kind/endpoints require explicit unlink plus link with a new identity. Generated relations use generated(output,input). Ordinary associations do not implement composition.
|
|
1044
1053
|
|
|
1045
1054
|
Variants directly hold baseEntityIds. Assembly validates every base before applying explicit own overrides: two bases supplying the same field are an error even if values match or the variant declares that field. Missing/cyclic bases fail. Consumers operate assembled fields without copying inherited content or resolving owners themselves. An owned-field edit creates a new immutable entity version. Base changes automatically advance variant base references and project attachments without versioning an unchanged variant. A variant-owned change versions the variant and retains unchanged bases. Do not manually clone entities for versioning. Use the current plan's ids throughout that execution; refresh after commit.
|
|
1046
1055
|
|
|
1047
|
-
Generation tools return resource references. An asset id
|
|
1056
|
+
Generation tools return resource references. An asset id names an Asset entity ({system:'memota',key:assetId}); every entity made from it links to that Asset with from-asset(entity,asset) and copies no locator of its own. An asset id is never an entity id, baseEntityId or relation endpoint. Physical resources are host infrastructure. The sandbox has no direct resource lookup/CRUD by asset id. rgetAssetFromEntity takes only an existing business entity id and returns {assetId,content} for its attached resource. For output_caption, create a Caption, link it with from-asset to the Asset for that assetId, then await rgetAssetFromEntity(captionId). The host initializes the complete AudioScript and one Caption per resource segment in the same causal plan when selection is omitted. The requested Caption represents the first segment; additional Caption entities share that same script and external reference. After awaiting, discover ALL resource Captions with entities.list filtered by entity_kind and their from-asset Asset, inspect each singular selection/durationMs, and place each in its own Clip/SequenceMarker. Discover them by following from-asset from the Asset. An explicit selection initializes only that selected Caption. Never place the first Caption over the whole transcript; Without explicit bases, the host creates an independent AudioScript for that resource. With bases, it resolves their unique AudioScript text owner and preserves other nonconflicting bases. It never routes text through the panel selection. Read the editable fields with entities.get afterwards. Repeated reads preserve edited text and return the original immutable resource content. Unawaited initialization is completed before validation; failure prevents publication. Resource-free Caption composition from AudioScript is also valid.
|
|
1048
1057
|
|
|
1049
|
-
Project initialization creates one current Timeline, the four standard Tracks and one attached AudioScript with segments:[]. A project may contain multiple independent complete AudioScripts (for example a generated script and an uploaded video transcript). The project AudioScript attachment selects only the panel view; it is not a singleton or membership constraint. Locate scripts with entities.list and choose the appropriate source. AudioScript owns independent multi-segment text; each can have multiple Caption variants. Caption and PhoneticScript compose it through baseEntityIds. Caption owns exactly one selection object ({segmentId,textRange?}), never an array or the legacy selections field. It selects one script segment or a contiguous substring, and owns only that selection's intrinsic timing and style;
|
|
1058
|
+
Project initialization creates one current Timeline, the four standard Tracks and one attached AudioScript with segments:[]. A project may contain multiple independent complete AudioScripts (for example a generated script and an uploaded video transcript). The project AudioScript attachment selects only the panel view; it is not a singleton or membership constraint. Locate scripts with entities.list and choose the appropriate source. AudioScript owns independent multi-segment text; each can have multiple Caption variants. Caption and PhoneticScript compose it through baseEntityIds. Caption locates its text as AudioScript plus index: it owns exactly one selection object ({segmentId,textRange?}), never an array or the legacy selections field. segmentId is that segment's stable identity rather than its ordinal, so a concurrent insert or re-segmentation cannot slide a Caption onto other text. It selects one script segment or a contiguous substring, and owns only that selection's intrinsic timing and style; selection textRange is a half-open code-point range and can split display text without rewriting the script. PhoneticScript owns pronunciation/prosody; the voiceover is an Audio rendered from it and related by phonetic-script-render(audio,phoneticScript). Voice is only a timbre identity ({voice:{system:'voice-library',key}}) and is never playable; link the rendered Audio to it with voice-timbre(audio,voice). Speech and caption occupy separate Clips. Caption carries Sequence: it owns its display timing and can be Clip content directly. Subtitles normally accompany sound; use source media or render a voiceover Audio from PhoneticScript when the request requires narration. Do not fabricate a sound resource.
|
|
1050
1059
|
|
|
1051
|
-
|
|
1060
|
+
For Speech resources, create an Audio and link it with from-asset to the Asset {system:'memota-speech',key:speechId}, then await rgetAssetFromEntity(audioId) before linking its placement. The host supplies the factual durationMs and assembles physical storage facts internally; the voice-library identity stays on its own Voice entity. Read the visible fields through entities.get; never invent them or use a Speech ID as a Memota media ID. A TTS voiceover still requires its real PhoneticScript relation; resource loading does not invent script content.
|
|
1052
1061
|
|
|
1053
|
-
|
|
1062
|
+
Direct AudioScript creation saves its segments as a resource before publication. Its assembled segments remain readable and editable; committed text changes preserve native Loro operations and save a new immutable resource, automatically advancing the external reference. AudioScripts initialized through Caption resources keep inline segments without an additional script resource. Do not manage these resources yourself. AudioScript has no intrinsic time and cannot be Clip content. ASR input Audio/Video associates via audio-script-source(script,source). Its external annotation SequenceMarker associates via audio-script-marker(script,marker) and receives assigned segmentRanges, not references to another marker. Caption intrinsic timing and its Clip's display SequenceMarker are distinct. Move/stretch display by editing only the placement marker.
|
|
1063
|
+
|
|
1064
|
+
To place content, create a Clip and a SequenceMarker, then link track-clip(track,clip), clip-marker(clip,marker) and marker-content(marker,content). Existing track membership uses timeline-track(timeline,track). The editor has one track of each role: video_clip for Image/Video, speech for the voiceover Audio, caption for Caption, bgm for Audio. Content states only its own durationMs in whole milliseconds, never a position: where it is taken from is Marker.sourceRange and where it lands is Marker.targetRange. A still Image states durationMs:null and each use decides how long it runs; an AXVideo stores none because its Timeline decides it. Inspect factual media durations; never invent one. Each placement is exactly one of Clip.order (sequential), Marker.targetRange (absolute) or clip-anchor(childClip,hostClip) plus Marker.anchorOffset. Marker.sourceRange selects source, duration describes playback/display. Whole milliseconds are required by this editor projection. Clip.volume is decibels (-60..20, 0 original). Linear timeRemapping is {kind:'linear',rate:2,mode:'constant'}, with duration matching the rounded source span/rate. BGM can use durationPolicy:'timeline'. Nonlinear speed and multiple overlay tracks are unsupported. Caption visibility is the caption Track's hidden field. Caption style is its style map. Handle structural links explicitly when moving/deleting; there is no implicit cascade.
|
|
1054
1065
|
|
|
1055
1066
|
checkpoint returns an opaque token valid only in this execution. rollbackTo restores that point and invalidates later checkpoints. Returned read snapshots and inputs are not writable document authorities. Use only the declarations below.
|
|
1056
1067
|
`.trim();
|
|
@@ -1213,7 +1224,7 @@ async function commitPlan(doc, plan, options) {
|
|
|
1213
1224
|
* integrity throws are not wrapped.
|
|
1214
1225
|
*/
|
|
1215
1226
|
async function commitPlanPreflight(doc, plan) {
|
|
1216
|
-
const scratch = createPlainMemoryAdapter(doc.snapshot());
|
|
1227
|
+
const scratch = createPlainMemoryAdapter(doc.snapshot(), { readOnly: doc.isEntityDocument() });
|
|
1217
1228
|
for (let index = 0; index < plan.ops.length; index++) {
|
|
1218
1229
|
const entry = plan.ops[index];
|
|
1219
1230
|
if (entry == null) continue;
|
|
@@ -1405,13 +1416,13 @@ function createMedeoTool(options) {
|
|
|
1405
1416
|
const existing = documents.get(docId);
|
|
1406
1417
|
if (existing != null) return await existing;
|
|
1407
1418
|
const created = (async () => {
|
|
1408
|
-
return await
|
|
1419
|
+
return await openDocument(new MengineHttpClient({
|
|
1409
1420
|
docId,
|
|
1410
1421
|
httpOrigin: requiredContext(options.httpOrigin, docId, "httpOrigin"),
|
|
1411
1422
|
...options.authToken !== void 0 ? { authToken: () => optionalContext(options.authToken, docId) } : {},
|
|
1412
1423
|
...options.userId !== void 0 ? { userId: () => optionalContext(options.userId, docId) } : {},
|
|
1413
1424
|
...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {}
|
|
1414
|
-
}),
|
|
1425
|
+
}), optionalContext(options.peerId, docId));
|
|
1415
1426
|
})();
|
|
1416
1427
|
documents.set(docId, created);
|
|
1417
1428
|
try {
|
|
@@ -1451,36 +1462,14 @@ function createMedeoTool(options) {
|
|
|
1451
1462
|
if (documentTails.get(docId) === tail) documentTails.delete(docId);
|
|
1452
1463
|
}
|
|
1453
1464
|
}
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
if (options.loadInitialDraft === void 0) throw error;
|
|
1463
|
-
}
|
|
1464
|
-
const document = toVideoDocument(await options.loadInitialDraft(docId));
|
|
1465
|
-
if (Object.keys(document.part_library ?? {}).length > 0) throw new Error("Legacy content cannot bootstrap a Loro entity project");
|
|
1466
|
-
const seed = createMirrorVideoDocument(document, {
|
|
1467
|
-
...peerId !== void 0 ? { peerId } : {},
|
|
1468
|
-
origin: "mengine.medeo_tool.bootstrap"
|
|
1469
|
-
});
|
|
1470
|
-
const foundation = ensureEditorFoundation({
|
|
1471
|
-
entities: [],
|
|
1472
|
-
relations: []
|
|
1473
|
-
});
|
|
1474
|
-
const entities = LoroEntityDocument.create(foundation.rows, {
|
|
1475
|
-
timelineEntityId: foundation.timelineEntityId,
|
|
1476
|
-
audioScriptEntityId: foundation.audioScriptEntityId
|
|
1477
|
-
});
|
|
1478
|
-
seed.import(entities.doc.export({ mode: "snapshot" }));
|
|
1479
|
-
try {
|
|
1480
|
-
await client.bootstrapSnapshot(seed.export({ mode: "snapshot" }));
|
|
1481
|
-
} catch (error) {
|
|
1482
|
-
if (!(error instanceof MengineHttpRequestError) || error.status !== 400) throw error;
|
|
1483
|
-
}
|
|
1465
|
+
/**
|
|
1466
|
+
* Open an existing Mengine document. Creation belongs to Director, which
|
|
1467
|
+
* initializes every project's document (`POST .../initialize`) before any
|
|
1468
|
+
* agent edit; Mengine's `bootstrap` is the internal Legacy-migration route
|
|
1469
|
+
* and is deliberately absent from the client SDK. A 404 here is a real
|
|
1470
|
+
* missing document, not something this tool may paper over.
|
|
1471
|
+
*/
|
|
1472
|
+
async function openDocument(client, peerId) {
|
|
1484
1473
|
return await ManualSyncDoc.open({
|
|
1485
1474
|
client,
|
|
1486
1475
|
...peerId !== void 0 ? { peerId } : {}
|
|
@@ -1652,10 +1641,12 @@ function createMedeoTool(options) {
|
|
|
1652
1641
|
const document = doc.snapshot();
|
|
1653
1642
|
const baseVersion = encodeDocVersionMark(doc.versionMark());
|
|
1654
1643
|
const result = await runEditScript({
|
|
1644
|
+
docId: input.doc_id,
|
|
1655
1645
|
document,
|
|
1656
1646
|
baseVersion,
|
|
1657
1647
|
entityState,
|
|
1658
|
-
loadEntityAsset: options.loadEntityAsset ? (entity) => options.loadEntityAsset(input.doc_id, entity) : void 0,
|
|
1648
|
+
loadEntityAsset: options.loadEntityAsset ? (entity, asset) => options.loadEntityAsset(input.doc_id, entity, asset) : void 0,
|
|
1649
|
+
writeEntityAsset: options.writeEntityAsset ? (entity, content) => options.writeEntityAsset(input.doc_id, entity, content) : void 0,
|
|
1659
1650
|
script: input.script,
|
|
1660
1651
|
...input.inputs !== void 0 ? { inputs: input.inputs } : {},
|
|
1661
1652
|
timeoutMs: input.timeout_ms ?? options.sandbox?.timeoutMs ?? 3e4,
|
|
@@ -1791,20 +1782,24 @@ async function materializeResources(options, resources) {
|
|
|
1791
1782
|
const client = new EntityHttpClient(options);
|
|
1792
1783
|
const state = await client.fetchState();
|
|
1793
1784
|
let resourceKey = "";
|
|
1785
|
+
let mintOrdinal = 0;
|
|
1794
1786
|
const sandbox = new EntitySandbox({
|
|
1795
1787
|
state,
|
|
1796
|
-
idFactory: (prefix) => stableId(prefix, options.docId, resourceKey)
|
|
1788
|
+
idFactory: (prefix) => stableId(prefix, options.docId, resourceKey, String(mintOrdinal++))
|
|
1797
1789
|
});
|
|
1798
1790
|
const ids = [];
|
|
1799
1791
|
for (const resource of resources) {
|
|
1800
1792
|
resourceKey = `${resource.kind}:${resource.assetId}`;
|
|
1793
|
+
mintOrdinal = 0;
|
|
1801
1794
|
if (resource.kind !== "caption") {
|
|
1802
1795
|
ids.push(sandbox.entities.ensureMedia(resource).contentEntityId);
|
|
1803
1796
|
continue;
|
|
1804
1797
|
}
|
|
1805
|
-
const
|
|
1798
|
+
const rows = sandbox.entities.list();
|
|
1799
|
+
const assetRow = rows.find((row) => row.entity_kind === "asset" && row.payload.key === resource.assetId);
|
|
1800
|
+
const existingCaptions = assetRow === void 0 ? [] : rows.filter((row) => row.entity_kind === "caption" && sandbox.entities.assetIdOf(row.entity_id) === assetRow.entity_id);
|
|
1806
1801
|
if (existingCaptions.length) {
|
|
1807
|
-
if (
|
|
1802
|
+
if (assetRow?.payload.storageKey !== resource.storageKey) throw new Error("Conflicting Caption resource identity");
|
|
1808
1803
|
ids.push(...existingCaptions.map((caption) => caption.entity_id));
|
|
1809
1804
|
continue;
|
|
1810
1805
|
}
|
|
@@ -1825,6 +1820,11 @@ async function materializeResources(options, resources) {
|
|
|
1825
1820
|
entity_kind: "audio-script",
|
|
1826
1821
|
payload: { segments }
|
|
1827
1822
|
});
|
|
1823
|
+
const captionAssetId = sandbox.entities.ensureAsset({
|
|
1824
|
+
system: "memota",
|
|
1825
|
+
key: resource.assetId,
|
|
1826
|
+
storageKey: resource.storageKey
|
|
1827
|
+
});
|
|
1828
1828
|
const ranges = resource.segments.map((segment, index) => ({
|
|
1829
1829
|
segmentId: segments[index].segmentId,
|
|
1830
1830
|
startMs: segment.startMs,
|
|
@@ -1837,22 +1837,16 @@ async function materializeResources(options, resources) {
|
|
|
1837
1837
|
entity_kind: "caption",
|
|
1838
1838
|
payload: {
|
|
1839
1839
|
baseEntityIds: [scriptId],
|
|
1840
|
-
external: {
|
|
1841
|
-
system: "memota",
|
|
1842
|
-
key: resource.assetId
|
|
1843
|
-
},
|
|
1844
|
-
storageKey: resource.storageKey,
|
|
1845
1840
|
selection: { segmentId: range.segmentId },
|
|
1846
1841
|
segmentRanges: [range],
|
|
1847
|
-
|
|
1848
|
-
kind: "bounded",
|
|
1849
|
-
start: range.startMs,
|
|
1850
|
-
end: range.endMs
|
|
1851
|
-
},
|
|
1852
|
-
sampling: "native",
|
|
1853
|
-
coordinateSpace: "milliseconds"
|
|
1842
|
+
durationMs: range.endMs - range.startMs
|
|
1854
1843
|
}
|
|
1855
1844
|
});
|
|
1845
|
+
sandbox.relations.link({
|
|
1846
|
+
relation_kind: "from-asset",
|
|
1847
|
+
endpoint_0_entity_id: captionId,
|
|
1848
|
+
endpoint_1_entity_id: captionAssetId
|
|
1849
|
+
});
|
|
1856
1850
|
ids.push(captionId);
|
|
1857
1851
|
}
|
|
1858
1852
|
const markerId = sandbox.entities.create({
|