@mengine/medeo-tool 1.2.1-alpha.9 → 1.3.1-alpha.8
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 +39 -14
- package/dist/{entity-contract-DycLxdQ5.d.mts → entity-contract-Dm3AMEC9.d.mts} +89 -33
- package/dist/index.d.mts +76 -76
- package/dist/index.mjs +268 -141
- package/dist/index.mjs.map +1 -1
- package/dist/sandbox-api.d.mts +145 -43
- package/dist/{script-session-CHyIUBkO.mjs → script-session-C2uQHYt4.mjs} +689 -132
- package/dist/script-session-C2uQHYt4.mjs.map +1 -0
- package/dist/worker-entry.d.mts +1 -1
- package/dist/worker-entry.mjs +35 -11
- package/dist/worker-entry.mjs.map +1 -1
- package/package.json +2 -2
- package/dist/script-session-CHyIUBkO.mjs.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SchemaValidator, SemanticEditor, createEditSandbox, effectiveVideoClipDurationMs, fromVideoDocument, solveVideoDocument, speedOf } from "@mengine/medeo-client";
|
|
1
|
+
import { SchemaValidator, SemanticEditor, assertCanonicalEditorResources, createEditSandbox, effectiveVideoClipDurationMs, ensureEditorFoundation, fromVideoDocument, importMediaAsset, readDocumentAudioScript, solveVideoDocument, speedOf } from "@mengine/medeo-client";
|
|
2
2
|
//#region src/document/compact-projection.ts
|
|
3
3
|
const DEFAULT_TEXT_PREVIEW_LENGTH = 24;
|
|
4
4
|
/** Kind tag shown in the first column (`video_clip` → `clip`). */
|
|
@@ -149,7 +149,10 @@ function isSequenceFields(value) {
|
|
|
149
149
|
return "coordinateSpace" in value && value.coordinateSpace !== void 0;
|
|
150
150
|
}
|
|
151
151
|
function isKnownSequenceKind(kind) {
|
|
152
|
-
return kind
|
|
152
|
+
return isMediaAssetVariantKind(kind) || kind === "caption" || kind === "axvideo";
|
|
153
|
+
}
|
|
154
|
+
function isMediaAssetVariantKind(kind) {
|
|
155
|
+
return kind === "video" || kind === "image" || kind === "audio" || kind === "voice";
|
|
153
156
|
}
|
|
154
157
|
function isKnownEntityKind(kind) {
|
|
155
158
|
return isKnownSequenceKind(kind) || isKnownNonSequenceKind(kind);
|
|
@@ -177,15 +180,281 @@ function createId(value, label) {
|
|
|
177
180
|
return value;
|
|
178
181
|
}
|
|
179
182
|
//#endregion
|
|
183
|
+
//#region ../medeo-dsl/src/json-values.ts
|
|
184
|
+
const nativeObjectConstructorSource = Function.prototype.toString.call(Object);
|
|
185
|
+
function isJsonObject(value) {
|
|
186
|
+
return isJsonValue(value, /* @__PURE__ */ new Set()) && !Array.isArray(value) && value !== null;
|
|
187
|
+
}
|
|
188
|
+
function isJsonValue(value, ancestors) {
|
|
189
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
190
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
191
|
+
if (typeof value !== "object") return false;
|
|
192
|
+
if (!Array.isArray(value) && !isPlainObject(value)) return false;
|
|
193
|
+
if (ancestors.has(value)) return false;
|
|
194
|
+
ancestors.add(value);
|
|
195
|
+
const valid = Array.isArray(value) ? value.every((item) => isJsonValue(item, ancestors)) : Object.values(value).every((item) => isJsonValue(item, ancestors));
|
|
196
|
+
ancestors.delete(value);
|
|
197
|
+
return valid;
|
|
198
|
+
}
|
|
199
|
+
/** Recognize an ordinary object from any VM realm without admitting class instances. */
|
|
200
|
+
function isPlainObject(value) {
|
|
201
|
+
try {
|
|
202
|
+
const prototype = Object.getPrototypeOf(value);
|
|
203
|
+
if (prototype === null) return true;
|
|
204
|
+
if (Object.getPrototypeOf(prototype) !== null) return false;
|
|
205
|
+
const constructor = Object.getOwnPropertyDescriptor(prototype, "constructor")?.value;
|
|
206
|
+
return typeof constructor === "function" && constructor.prototype === prototype && Function.prototype.toString.call(constructor) === nativeObjectConstructorSource;
|
|
207
|
+
} catch {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
//#endregion
|
|
212
|
+
//#region ../medeo-dsl/src/composition.ts
|
|
213
|
+
/** Assembly fails explicitly when declared bases cannot provide valid, unambiguous content. */
|
|
214
|
+
var ScriptCompositionError = class extends Error {
|
|
215
|
+
code;
|
|
216
|
+
entityId;
|
|
217
|
+
constructor(code, entityId, message) {
|
|
218
|
+
super(message);
|
|
219
|
+
this.code = code;
|
|
220
|
+
this.entityId = entityId;
|
|
221
|
+
this.name = "ScriptCompositionError";
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
/** Direct base IDs are structural fields, never ordinary Relation endpoints. */
|
|
225
|
+
function variantBaseEntityIds(entity) {
|
|
226
|
+
if (!Object.hasOwn(entity.payload, "baseEntityIds")) return [];
|
|
227
|
+
const ids = entity.payload.baseEntityIds;
|
|
228
|
+
if (!Array.isArray(ids) || ids.length === 0 || !ids.every((id) => typeof id === "string" && id.trim() === id && id.length > 0) || new Set(ids).size !== ids.length) throw new ScriptCompositionError("invalid_bases", entity.entityId, `Entity "${entity.entityId}" requires non-empty, unique baseEntityIds`);
|
|
229
|
+
return ids.map(createEntityId);
|
|
230
|
+
}
|
|
231
|
+
/** Validate all base providers before applying explicit own fields; ordering cannot resolve ambiguity. */
|
|
232
|
+
function assembleEntityContent(rows, entityId) {
|
|
233
|
+
const byId = /* @__PURE__ */ new Map();
|
|
234
|
+
for (const row of rows.entities) {
|
|
235
|
+
if (byId.has(row.entityId)) throw new ScriptCompositionError("composition_ambiguous", row.entityId, `duplicate entity id "${row.entityId}"`);
|
|
236
|
+
byId.set(row.entityId, row);
|
|
237
|
+
}
|
|
238
|
+
const active = /* @__PURE__ */ new Set();
|
|
239
|
+
const cache = /* @__PURE__ */ new Map();
|
|
240
|
+
const visit = (id) => {
|
|
241
|
+
const cached = cache.get(id);
|
|
242
|
+
if (cached !== void 0) return cached;
|
|
243
|
+
if (active.has(id)) throw new ScriptCompositionError("composition_cycle", id, `Cyclic baseEntityIds at "${id}"`);
|
|
244
|
+
const row = byId.get(id);
|
|
245
|
+
if (row === void 0) throw new ScriptCompositionError("composition_dangling", id, `Missing base entity "${id}" in this document`);
|
|
246
|
+
if (!isJsonObject(row.payload) || Object.hasOwn(row.payload, "entityId") || Object.hasOwn(row.payload, "entityKind")) throw new ScriptCompositionError("invalid_bases", id, `Entity "${id}" requires JSON own fields without reserved identity fields`);
|
|
247
|
+
active.add(id);
|
|
248
|
+
const inherited = {};
|
|
249
|
+
const providers = /* @__PURE__ */ new Map();
|
|
250
|
+
for (const baseId of variantBaseEntityIds(row)) {
|
|
251
|
+
const base = visit(baseId);
|
|
252
|
+
for (const [field, value] of Object.entries(base.payload)) {
|
|
253
|
+
if (field === "baseEntityIds") continue;
|
|
254
|
+
const previous = providers.get(field);
|
|
255
|
+
if (previous !== void 0) throw new ScriptCompositionError("field_conflict", id, `Entity "${id}" field "${field}" conflicts between bases "${previous}" and "${baseId}"; own fields cannot resolve base ambiguity`);
|
|
256
|
+
providers.set(field, baseId);
|
|
257
|
+
Object.defineProperty(inherited, field, {
|
|
258
|
+
value,
|
|
259
|
+
enumerable: true,
|
|
260
|
+
configurable: true,
|
|
261
|
+
writable: true
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const assembled = {
|
|
266
|
+
...row,
|
|
267
|
+
payload: JSON.parse(JSON.stringify({
|
|
268
|
+
...inherited,
|
|
269
|
+
...row.payload
|
|
270
|
+
}))
|
|
271
|
+
};
|
|
272
|
+
active.delete(id);
|
|
273
|
+
cache.set(id, assembled);
|
|
274
|
+
return assembled;
|
|
275
|
+
};
|
|
276
|
+
return visit(entityId);
|
|
277
|
+
}
|
|
278
|
+
/** Resolve a field's declaring entity after validating the entire composition. */
|
|
279
|
+
function resolveEntityFieldOwner(rows, entityId, field) {
|
|
280
|
+
assembleEntityContent(rows, entityId);
|
|
281
|
+
const byId = new Map(rows.entities.map((row) => [row.entityId, row]));
|
|
282
|
+
const find = (id) => {
|
|
283
|
+
const row = byId.get(id);
|
|
284
|
+
if (Object.hasOwn(row.payload, field)) return id;
|
|
285
|
+
if (field === "baseEntityIds") return void 0;
|
|
286
|
+
for (const baseId of variantBaseEntityIds(row)) {
|
|
287
|
+
const owner = find(baseId);
|
|
288
|
+
if (owner !== void 0) return owner;
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
return find(entityId) ?? entityId;
|
|
292
|
+
}
|
|
293
|
+
/** Patch assembled fields without copying inherited fields into the variant's stored payload. */
|
|
294
|
+
function updateEntityFields(rows, entityId, fields) {
|
|
295
|
+
assembleEntityContent(rows, entityId);
|
|
296
|
+
if (!isJsonObject(fields) || Object.hasOwn(fields, "entityId") || Object.hasOwn(fields, "entityKind")) throw new ScriptCompositionError("invalid_bases", entityId, "Field updates require JSON without reserved identity fields");
|
|
297
|
+
const updates = /* @__PURE__ */ new Map();
|
|
298
|
+
for (const [field, value] of Object.entries(fields)) {
|
|
299
|
+
const ownerId = resolveEntityFieldOwner(rows, entityId, field);
|
|
300
|
+
const owner = updates.get(ownerId) ?? rows.entities.find((row) => row.entityId === ownerId);
|
|
301
|
+
updates.set(ownerId, {
|
|
302
|
+
...owner,
|
|
303
|
+
payload: {
|
|
304
|
+
...owner.payload,
|
|
305
|
+
[field]: value
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
return [...updates.values()].map((row) => JSON.parse(JSON.stringify(row)));
|
|
310
|
+
}
|
|
311
|
+
/** Find the actual AudioScript text owner through direct variant bases. */
|
|
312
|
+
function findComposedAudioScript(rows, variantEntityId, variantKind) {
|
|
313
|
+
const variant = requireEntityKind(rows, variantEntityId, variantKind);
|
|
314
|
+
assembleEntityContent(rows, variantEntityId);
|
|
315
|
+
const sources = /* @__PURE__ */ new Map();
|
|
316
|
+
const seen = /* @__PURE__ */ new Set();
|
|
317
|
+
const visit = (row) => {
|
|
318
|
+
for (const id of variantBaseEntityIds(row)) {
|
|
319
|
+
if (seen.has(id)) continue;
|
|
320
|
+
seen.add(id);
|
|
321
|
+
const base = rows.entities.find((candidate) => candidate.entityId === id);
|
|
322
|
+
if (base.entityKind === "audio-script") sources.set(id, assembleEntityContent(rows, id));
|
|
323
|
+
else visit(base);
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
visit(variant);
|
|
327
|
+
if (sources.size !== 1) throw new ScriptCompositionError(sources.size === 0 ? "composition_missing" : "composition_ambiguous", variantEntityId, `${variantKind} "${variantEntityId}" baseEntityIds must resolve one AudioScript text owner, got ${sources.size}`);
|
|
328
|
+
return [...sources.values()][0];
|
|
329
|
+
}
|
|
330
|
+
function scriptSegments(script) {
|
|
331
|
+
const segments = script.payload.segments;
|
|
332
|
+
if (!Array.isArray(segments)) throw new ScriptCompositionError("composition_dangling", script.entityId, `AudioScript "${script.entityId}" requires segments`);
|
|
333
|
+
if (!segments.every(isScriptSegment) || new Set(segments.map((s) => s.segmentId)).size !== segments.length) throw new ScriptCompositionError("invalid_script", script.entityId, "AudioScript requires valid segments with unique local segmentId and text");
|
|
334
|
+
return segments;
|
|
335
|
+
}
|
|
336
|
+
function isScriptSegment(value) {
|
|
337
|
+
if (typeof value !== "object" || value == null || Array.isArray(value)) return false;
|
|
338
|
+
const segment = value;
|
|
339
|
+
return typeof segment.segmentId === "string" && segment.segmentId.trim() !== "" && typeof segment.text === "string" && (segment.language === void 0 || typeof segment.language === "string");
|
|
340
|
+
}
|
|
341
|
+
/** Assemble selected text after base-field validation and explicit variant overrides. */
|
|
342
|
+
function assembleCaptionContent(rows, captionEntityId) {
|
|
343
|
+
requireEntityKind(rows, captionEntityId, "caption");
|
|
344
|
+
const caption = assembleEntityContent(rows, captionEntityId);
|
|
345
|
+
const script = findComposedAudioScript(rows, captionEntityId, "caption");
|
|
346
|
+
const segments = selectAudioScriptSegments({
|
|
347
|
+
...script,
|
|
348
|
+
payload: caption.payload
|
|
349
|
+
}, captionSelections(caption));
|
|
350
|
+
return {
|
|
351
|
+
caption,
|
|
352
|
+
audioScript: script,
|
|
353
|
+
segments,
|
|
354
|
+
text: segments.map((segment) => segment.text).join("")
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Assemble the complete Phonetic Script content for TTS: the composed
|
|
359
|
+
* AudioScript's base text plus the variant's own phoneme and prosody fields.
|
|
360
|
+
*/
|
|
361
|
+
function assemblePhoneticScriptContent(rows, phoneticScriptEntityId) {
|
|
362
|
+
requireEntityKind(rows, phoneticScriptEntityId, "phonetic-script");
|
|
363
|
+
const phoneticScript = assembleEntityContent(rows, phoneticScriptEntityId);
|
|
364
|
+
const audioScript = findComposedAudioScript(rows, phoneticScriptEntityId, "phonetic-script");
|
|
365
|
+
const segments = scriptSegments({
|
|
366
|
+
...audioScript,
|
|
367
|
+
payload: phoneticScript.payload
|
|
368
|
+
});
|
|
369
|
+
return {
|
|
370
|
+
phoneticScript,
|
|
371
|
+
audioScript,
|
|
372
|
+
segments,
|
|
373
|
+
text: segments.map((segment) => segment.text).join("")
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
function captionSelections(caption) {
|
|
377
|
+
const selections = caption.payload.selections;
|
|
378
|
+
if (!Array.isArray(selections) || selections.length === 0) throw new ScriptCompositionError("empty_selection", caption.entityId, `Caption "${caption.entityId}" requires a non-empty segment selection into its AudioScript`);
|
|
379
|
+
if (!selections.every(isSegmentSelection)) throw new ScriptCompositionError("invalid_selection", caption.entityId, "Caption selections require a segmentId and optional textRange");
|
|
380
|
+
return selections;
|
|
381
|
+
}
|
|
382
|
+
function isSegmentSelection(value) {
|
|
383
|
+
return typeof value === "object" && value != null && !Array.isArray(value) && typeof value.segmentId === "string" && value.segmentId.trim() !== "" && Object.keys(value).every((key) => key === "segmentId" || key === "textRange");
|
|
384
|
+
}
|
|
385
|
+
/** Select source text without creating another authoritative text field. */
|
|
386
|
+
function selectAudioScriptSegments(script, selections) {
|
|
387
|
+
if (!Array.isArray(selections) || selections.length === 0 || !selections.every(isSegmentSelection) || new Set(selections.map((s) => s.segmentId)).size !== selections.length) throw new ScriptCompositionError("invalid_selection", script.entityId, "Caption selections must be non-empty and name unique source segments");
|
|
388
|
+
const bySegmentId = new Map(scriptSegments(script).map((segment) => [segment.segmentId, segment]));
|
|
389
|
+
const selected = selections.map((selection) => {
|
|
390
|
+
const segment = bySegmentId.get(selection.segmentId);
|
|
391
|
+
if (segment === void 0) throw new ScriptCompositionError("unknown_segment", script.entityId, `Caption selection "${selection.segmentId}" does not name a segment of AudioScript "${script.entityId}"`);
|
|
392
|
+
if (selection.textRange === void 0) return segment;
|
|
393
|
+
const range = selection.textRange;
|
|
394
|
+
const points = Array.from(segment.text);
|
|
395
|
+
if (typeof range !== "object" || range === null || !Number.isSafeInteger(range.start) || !Number.isSafeInteger(range.end) || range.start < 0 || range.end <= range.start || range.end > points.length || Object.keys(range).some((key) => key !== "start" && key !== "end")) throw new ScriptCompositionError("invalid_selection", script.entityId, "Caption textRange must be a non-empty half-open Unicode code-point range within its source segment");
|
|
396
|
+
return {
|
|
397
|
+
...segment,
|
|
398
|
+
text: points.slice(range.start, range.end).join("")
|
|
399
|
+
};
|
|
400
|
+
});
|
|
401
|
+
if (!selected.map((segment) => segment.text).join("").trim()) throw new ScriptCompositionError("empty_selection", script.entityId, "Caption selection must contain visible source text");
|
|
402
|
+
return selected;
|
|
403
|
+
}
|
|
404
|
+
function requireEntityKind(rows, entityIdValue, entityKind) {
|
|
405
|
+
const entity = rows.entities.find((candidate) => candidate.entityId === entityIdValue);
|
|
406
|
+
if (entity === void 0) throw new ScriptCompositionError("composition_dangling", entityIdValue, `Entity "${entityIdValue}" does not exist`);
|
|
407
|
+
if (entity.entityKind !== entityKind) throw new ScriptCompositionError("composition_dangling", entityIdValue, `Entity "${entityIdValue}" must have kind "${entityKind}", got "${entity.entityKind}"`);
|
|
408
|
+
return entity;
|
|
409
|
+
}
|
|
410
|
+
//#endregion
|
|
411
|
+
//#region ../medeo-dsl/src/rows.ts
|
|
412
|
+
function entityToRow(entity) {
|
|
413
|
+
const { entityId, entityKind, ...payload } = entity;
|
|
414
|
+
if (!isJsonObject(payload)) throw new Error(`Entity "${entityId}" payload must contain only JSON values`);
|
|
415
|
+
return {
|
|
416
|
+
entityId,
|
|
417
|
+
entityKind,
|
|
418
|
+
payload
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
//#endregion
|
|
180
422
|
//#region ../medeo-dsl/src/invariants.ts
|
|
423
|
+
/**
|
|
424
|
+
* Media variants own their Asset identity directly: a `physical-asset`
|
|
425
|
+
* Relation may not bind them. Caption text is composed domain content, so
|
|
426
|
+
* Captions do not bind Physical Assets either; the Relation vocabulary stays
|
|
427
|
+
* open for future physical pipeline semantics.
|
|
428
|
+
*/
|
|
429
|
+
function validateMediaAssetIdentity(entity, index) {
|
|
430
|
+
return [...index.relationsOf(entity)].filter((relation) => relation.kind === "physical-asset").map(() => ({
|
|
431
|
+
code: "media_physical_asset_forbidden",
|
|
432
|
+
entityId: entity.entityId,
|
|
433
|
+
message: `Media entity "${entity.entityId}" owns its Asset identity; remove its physical-asset Relation`
|
|
434
|
+
}));
|
|
435
|
+
}
|
|
181
436
|
/** Validates one complete set of entities and its authoritative Relation rows. */
|
|
182
437
|
function validateEntityRelationSet(entityRefs, index, options) {
|
|
183
438
|
const { entities, issues } = collectRelatedEntities(entityRefs, index);
|
|
184
439
|
const entityIds = new Set(entities.map((entity) => entity.entityId));
|
|
440
|
+
const rows = {
|
|
441
|
+
entities: entities.map((entity) => entityToRow(entity.current())),
|
|
442
|
+
relations: []
|
|
443
|
+
};
|
|
444
|
+
for (const entity of entities) try {
|
|
445
|
+
assembleEntityContent(rows, entity.entityId);
|
|
446
|
+
} catch (error) {
|
|
447
|
+
issues.push({
|
|
448
|
+
code: "invalid_variant_composition",
|
|
449
|
+
entityId: entity.entityId,
|
|
450
|
+
message: error instanceof Error ? error.message : "Invalid variant composition"
|
|
451
|
+
});
|
|
452
|
+
}
|
|
185
453
|
for (const entity of entities) {
|
|
186
454
|
const current = entity.current();
|
|
187
455
|
const entityIssues = validateEntity(entity, entityIds);
|
|
188
456
|
issues.push(...entityIssues);
|
|
457
|
+
if (isMediaAssetVariantKind(current.entityKind)) issues.push(...validateMediaAssetIdentity(entity, index));
|
|
189
458
|
if (entityIssues.some((issue) => issue.code === "invalid_entity_payload")) continue;
|
|
190
459
|
if (current.entityKind === "sequence-marker") {
|
|
191
460
|
const marker = entity;
|
|
@@ -201,7 +470,8 @@ function validateEntityRelationSet(entityRefs, index, options) {
|
|
|
201
470
|
issues.push(...validateClipPlacement(entity, index));
|
|
202
471
|
}
|
|
203
472
|
if (current.entityKind === "axvideo") issues.push(...validateAXVideoAdmission(entity, index));
|
|
204
|
-
if (current.entityKind === "caption") issues.push(...
|
|
473
|
+
if (current.entityKind === "caption") issues.push(...validateCaptionComposition(entity, index, entities));
|
|
474
|
+
if (current.entityKind === "phonetic-script") issues.push(...validatePhoneticComposition(entity, index, entities));
|
|
205
475
|
issues.push(...validateSequenceComposition(entity));
|
|
206
476
|
}
|
|
207
477
|
issues.push(...validateClipAnchorCycles(entities, index));
|
|
@@ -213,7 +483,39 @@ function validateMarkerUse(marker, index) {
|
|
|
213
483
|
const axVideoEdges = ofKind(relations, "axvideo-marker");
|
|
214
484
|
const contentEdges = ofKind(relations, "marker-content");
|
|
215
485
|
const timelineEdges = ofKind(relations, "marker-timeline");
|
|
486
|
+
const scriptEdges = ofKind(relations, "audio-script-marker");
|
|
216
487
|
const issues = [];
|
|
488
|
+
if (scriptEdges.length > 0) {
|
|
489
|
+
if (clipEdges.length + axVideoEdges.length + contentEdges.length + timelineEdges.length > 0) issues.push({
|
|
490
|
+
code: "marker_container_xor",
|
|
491
|
+
entityId: marker.entityId,
|
|
492
|
+
message: "An AudioScript annotation Marker must not carry Clip, AXVideo, content, or Timeline relations"
|
|
493
|
+
});
|
|
494
|
+
const ranges = marker.current().segmentRanges;
|
|
495
|
+
if (!ranges?.length) issues.push({
|
|
496
|
+
code: "invalid_entity_payload",
|
|
497
|
+
entityId: marker.entityId,
|
|
498
|
+
message: "An AudioScript annotation Marker requires segmentRanges"
|
|
499
|
+
});
|
|
500
|
+
for (const relation of scriptEdges) {
|
|
501
|
+
const script = relation.other(marker)?.deref()?.current();
|
|
502
|
+
if (script?.entityKind !== "audio-script") continue;
|
|
503
|
+
const segments = script.segments;
|
|
504
|
+
if (!Array.isArray(segments)) continue;
|
|
505
|
+
const segmentIds = new Set(segments.map((segment) => segment.segmentId));
|
|
506
|
+
if (ranges?.some((range) => !segmentIds.has(range.segmentId))) issues.push({
|
|
507
|
+
code: "invalid_entity_payload",
|
|
508
|
+
entityId: marker.entityId,
|
|
509
|
+
message: "Annotation segmentRanges must name segments of the related AudioScript"
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
return issues;
|
|
513
|
+
}
|
|
514
|
+
if (marker.current().segmentRanges !== void 0) issues.push({
|
|
515
|
+
code: "invalid_entity_payload",
|
|
516
|
+
entityId: marker.entityId,
|
|
517
|
+
message: "segmentRanges belongs to an AudioScript annotation Marker, not a display Marker"
|
|
518
|
+
});
|
|
217
519
|
if (clipEdges.length + axVideoEdges.length !== 1) issues.push({
|
|
218
520
|
code: "marker_container_xor",
|
|
219
521
|
entityId: marker.entityId,
|
|
@@ -272,25 +574,35 @@ function validateAXVideoAdmission(axVideo, index) {
|
|
|
272
574
|
message: "AXVideo must have exactly one live AXVideo-Marker Relation"
|
|
273
575
|
}];
|
|
274
576
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
577
|
+
/** Validate a Caption against the complete entity set, including its directly held bases. */
|
|
578
|
+
function validateCaptionComposition(caption, index, entities = []) {
|
|
579
|
+
return validateVariantComposition(caption, index, entities, "caption");
|
|
580
|
+
}
|
|
581
|
+
function validatePhoneticComposition(phonetic, index, entities = []) {
|
|
582
|
+
return validateVariantComposition(phonetic, index, entities, "phonetic-script");
|
|
583
|
+
}
|
|
584
|
+
function validateVariantComposition(variant, index, entities, kind) {
|
|
585
|
+
const issues = [];
|
|
586
|
+
if (kind === "caption" && ofKind([...index.relationsOf(variant)], "physical-asset").length > 0) issues.push({
|
|
587
|
+
code: "caption_physical_asset_forbidden",
|
|
588
|
+
entityId: variant.entityId,
|
|
589
|
+
message: "Caption text is composed from its AudioScript; Caption must not bind a Physical Asset"
|
|
590
|
+
});
|
|
591
|
+
const rows = {
|
|
592
|
+
entities: collectRelatedEntities([...entities, variant], index).entities.map((ref) => entityToRow(ref.current())),
|
|
593
|
+
relations: []
|
|
594
|
+
};
|
|
595
|
+
try {
|
|
596
|
+
if (kind === "caption") assembleCaptionContent(rows, variant.entityId);
|
|
597
|
+
else assemblePhoneticScriptContent(rows, variant.entityId);
|
|
598
|
+
} catch (error) {
|
|
599
|
+
issues.push({
|
|
600
|
+
code: kind === "caption" ? "caption_composition_required" : "phonetic_script_composition_required",
|
|
601
|
+
entityId: variant.entityId,
|
|
602
|
+
message: error instanceof Error ? error.message : "Invalid variant composition"
|
|
603
|
+
});
|
|
288
604
|
}
|
|
289
|
-
return
|
|
290
|
-
code: "caption_asset_required",
|
|
291
|
-
entityId: caption.entityId,
|
|
292
|
-
message: "Caption must have a Physical Asset Relation"
|
|
293
|
-
}];
|
|
605
|
+
return issues;
|
|
294
606
|
}
|
|
295
607
|
function validateClipPlacement(clip, index) {
|
|
296
608
|
const marker = ofKind([...index.relationsOf(clip)], "clip-marker")[0]?.other(clip)?.deref();
|
|
@@ -316,10 +628,10 @@ function validateClipPlacement(clip, index) {
|
|
|
316
628
|
const content = ofKind([...index.relationsOf(marker)], "marker-content")[0]?.other(marker)?.deref()?.current();
|
|
317
629
|
const track = ofKind([...index.relationsOf(clip)], "track-clip")[0]?.other(clip)?.deref()?.current();
|
|
318
630
|
const trackRole = track?.entityKind === "track" ? track.role : void 0;
|
|
319
|
-
if (content?.entityKind !== "audio" || trackRole !== "bgm" || !hasOrder) issues.push({
|
|
631
|
+
if (content?.entityKind !== "audio" || trackRole !== "bgm" || !hasOrder && !hasTarget) issues.push({
|
|
320
632
|
code: "timeline_duration_policy_invalid",
|
|
321
633
|
entityId: marker.entityId,
|
|
322
|
-
message: "Marker durationPolicy \"timeline\" is only valid for ordered Audio Clips on the bgm Track"
|
|
634
|
+
message: "Marker durationPolicy \"timeline\" is only valid for ordered or absolutely placed Audio Clips on the bgm Track"
|
|
323
635
|
});
|
|
324
636
|
}
|
|
325
637
|
return issues;
|
|
@@ -418,6 +730,18 @@ function validateKnownEntityPayload(entity) {
|
|
|
418
730
|
const value = entity;
|
|
419
731
|
const problems = [];
|
|
420
732
|
if (value.lifecycle !== void 0 && !isRecord(value.lifecycle)) problems.push("lifecycle must be an object when present");
|
|
733
|
+
if (entity.entityKind === "audio-script" || entity.entityKind === "phonetic-script") {
|
|
734
|
+
for (const key of [
|
|
735
|
+
"extent",
|
|
736
|
+
"sampling",
|
|
737
|
+
"coordinateSpace",
|
|
738
|
+
"sourceRange",
|
|
739
|
+
"targetRange",
|
|
740
|
+
"duration",
|
|
741
|
+
"startMs",
|
|
742
|
+
"endMs"
|
|
743
|
+
]) if (Object.hasOwn(value, key)) problems.push(`${key} is not intrinsic script data; assign an external annotation Marker`);
|
|
744
|
+
}
|
|
421
745
|
switch (entity.entityKind) {
|
|
422
746
|
case "track":
|
|
423
747
|
validateOptionalField(value, "hidden", "boolean", problems);
|
|
@@ -429,11 +753,13 @@ function validateKnownEntityPayload(entity) {
|
|
|
429
753
|
case "voice":
|
|
430
754
|
case "caption":
|
|
431
755
|
validateSequencePayload(value, "bounded", "native", problems);
|
|
756
|
+
if (entity.entityKind !== "caption") validateMediaAssetFields(value, problems);
|
|
432
757
|
if (entity.entityKind === "voice") validateVoicePayload(value, problems);
|
|
433
758
|
if (entity.entityKind === "caption") validateCaptionPayload(value, problems);
|
|
434
759
|
break;
|
|
435
760
|
case "image":
|
|
436
761
|
validateSequencePayload(value, "unbounded", "constant", problems);
|
|
762
|
+
validateMediaAssetFields(value, problems);
|
|
437
763
|
break;
|
|
438
764
|
case "axvideo":
|
|
439
765
|
validateSequencePayload(value, "bounded", "derived", problems);
|
|
@@ -442,9 +768,11 @@ function validateKnownEntityPayload(entity) {
|
|
|
442
768
|
validateMarkerPayload(value, problems);
|
|
443
769
|
break;
|
|
444
770
|
case "audio-script":
|
|
445
|
-
case "phonetic-script":
|
|
446
771
|
validateScriptPayload(value, problems);
|
|
447
772
|
break;
|
|
773
|
+
case "phonetic-script":
|
|
774
|
+
validatePhoneticScriptPayload(value, problems);
|
|
775
|
+
break;
|
|
448
776
|
case "timeline":
|
|
449
777
|
case "viewport": break;
|
|
450
778
|
case "clip":
|
|
@@ -481,22 +809,58 @@ function validateMarkerPayload(value, problems) {
|
|
|
481
809
|
} else if (duration.mode !== "from-source") problems.push("duration.mode must be \"from-source\" or \"fixed\"");
|
|
482
810
|
if (value.anchorOffset === void 0 && Object.hasOwn(value, "anchorOffset")) problems.push("anchorOffset cannot be undefined when present");
|
|
483
811
|
if (value.durationPolicy !== void 0 && value.durationPolicy !== "timeline") problems.push("durationPolicy must be \"timeline\" when present");
|
|
812
|
+
if (value.segmentRanges !== void 0) validateSegmentRanges(value.segmentRanges, problems);
|
|
813
|
+
}
|
|
814
|
+
/** Annotation Marker time values are directly assigned facts; no cross-references are allowed. */
|
|
815
|
+
function validateSegmentRanges(value, problems) {
|
|
816
|
+
if (!Array.isArray(value)) {
|
|
817
|
+
problems.push("segmentRanges must be an array when present");
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
const seen = /* @__PURE__ */ new Set();
|
|
821
|
+
for (const [index, entry] of value.entries()) {
|
|
822
|
+
if (!isRecord(entry)) {
|
|
823
|
+
problems.push(`segmentRanges[${index}] must be an object`);
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
if (typeof entry.segmentId !== "string") problems.push(`segmentRanges[${index}].segmentId must be a string`);
|
|
827
|
+
else if (seen.has(entry.segmentId)) problems.push(`segmentRanges[${index}].segmentId must be unique within the Marker`);
|
|
828
|
+
else seen.add(entry.segmentId);
|
|
829
|
+
for (const key of ["startMs", "endMs"]) {
|
|
830
|
+
const point = entry[key];
|
|
831
|
+
if (typeof point !== "number" || !Number.isFinite(point)) problems.push(`segmentRanges[${index}].${key} must be a finite number`);
|
|
832
|
+
}
|
|
833
|
+
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`);
|
|
834
|
+
}
|
|
484
835
|
}
|
|
485
836
|
function validateAssetPayload(value, problems) {
|
|
837
|
+
validateExternalLocator(value, problems);
|
|
838
|
+
validateStorageKey(value, problems);
|
|
839
|
+
if (value.inline !== void 0) problems.push("inline is not a physical location; text is domain content owned by its AudioScript");
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Media variants own their Asset locator directly: `external` is required and
|
|
843
|
+
* textual domain content is owned by AudioScript; the physical-location
|
|
844
|
+
* vocabularies cannot mix.
|
|
845
|
+
*/
|
|
846
|
+
function validateMediaAssetFields(value, problems) {
|
|
847
|
+
if (value.external === void 0) problems.push("external is required; media variants own their Asset identity");
|
|
848
|
+
else validateExternalLocator(value, problems);
|
|
849
|
+
validateStorageKey(value, problems);
|
|
850
|
+
if (value.inline !== void 0) problems.push("media variants use external locators, not inline domain text");
|
|
851
|
+
}
|
|
852
|
+
function validateExternalLocator(value, problems) {
|
|
486
853
|
const external = value.external;
|
|
487
|
-
if (external
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
854
|
+
if (external === void 0) return;
|
|
855
|
+
if (!isRecord(external)) {
|
|
856
|
+
problems.push("external must be an object when present");
|
|
857
|
+
return;
|
|
491
858
|
}
|
|
859
|
+
if (external.system !== "memota" && external.system !== "memota-speech") problems.push("external.system must be \"memota\" or \"memota-speech\"");
|
|
860
|
+
if (typeof external.key !== "string" || external.key.trim() === "") problems.push("external.key must be a non-empty string");
|
|
861
|
+
}
|
|
862
|
+
function validateStorageKey(value, problems) {
|
|
492
863
|
if (value.storageKey !== void 0 && (typeof value.storageKey !== "string" || value.storageKey.trim() === "")) problems.push("storageKey must be a non-empty string when present");
|
|
493
|
-
const inline = value.inline;
|
|
494
|
-
if (inline !== void 0) if (!isRecord(inline)) problems.push("inline must be an object when present");
|
|
495
|
-
else {
|
|
496
|
-
if (inline.mediaType !== "text/plain") problems.push("inline.mediaType must be \"text/plain\"");
|
|
497
|
-
if (typeof inline.text !== "string") problems.push("inline.text must be a string");
|
|
498
|
-
}
|
|
499
|
-
if (external !== void 0 && inline !== void 0) problems.push("Asset must not combine external and inline physical locations");
|
|
500
864
|
}
|
|
501
865
|
function validateVoicePayload(value, problems) {
|
|
502
866
|
const voice = value.voice;
|
|
@@ -510,7 +874,26 @@ function validateVoicePayload(value, problems) {
|
|
|
510
874
|
if (voice.name !== void 0 && typeof voice.name !== "string") problems.push("voice.name must be a string when present");
|
|
511
875
|
}
|
|
512
876
|
function validateCaptionPayload(value, problems) {
|
|
513
|
-
|
|
877
|
+
const selections = value.selections;
|
|
878
|
+
if (!Array.isArray(selections)) problems.push("selections must be a non-empty array of AudioScript segment selections");
|
|
879
|
+
else {
|
|
880
|
+
if (selections.length === 0) problems.push("selections must name at least one AudioScript segment");
|
|
881
|
+
const seen = /* @__PURE__ */ new Set();
|
|
882
|
+
for (const [index, selection] of selections.entries()) {
|
|
883
|
+
if (!isRecord(selection)) {
|
|
884
|
+
problems.push(`selections[${index}] must be an object`);
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
if (typeof selection.segmentId !== "string" || selection.segmentId.trim() === "") problems.push(`selections[${index}].segmentId must be a string`);
|
|
888
|
+
else if (seen.has(selection.segmentId)) problems.push(`selections[${index}].segmentId must be unique within the Caption`);
|
|
889
|
+
else seen.add(selection.segmentId);
|
|
890
|
+
if (Object.keys(selection).some((key) => key !== "segmentId" && key !== "textRange")) problems.push(`selections[${index}] may only contain segmentId and textRange`);
|
|
891
|
+
if (selection.textRange !== void 0) {
|
|
892
|
+
const range = selection.textRange;
|
|
893
|
+
if (!isRecord(range) || !Number.isSafeInteger(range.start) || !Number.isSafeInteger(range.end) || range.start < 0 || range.end <= range.start || Object.keys(range).some((key) => key !== "start" && key !== "end")) problems.push(`selections[${index}].textRange must be a non-empty half-open code-point range`);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
514
897
|
const style = value.style;
|
|
515
898
|
if (style === void 0) return;
|
|
516
899
|
if (!isRecord(style)) {
|
|
@@ -545,6 +928,12 @@ function validateRange(value, path, required, problems) {
|
|
|
545
928
|
if (!Object.hasOwn(value, "start") || value.start === void 0) problems.push(`${path}.start is required`);
|
|
546
929
|
if (!Object.hasOwn(value, "end") || value.end === void 0) problems.push(`${path}.end is required`);
|
|
547
930
|
}
|
|
931
|
+
/** Phonetic variants store pronunciation fields only; base text stays in the AudioScript. */
|
|
932
|
+
function validatePhoneticScriptPayload(value, problems) {
|
|
933
|
+
if (value.segments !== void 0) validateScriptPayload(value, problems);
|
|
934
|
+
if (value.phonemeScript !== void 0 && (typeof value.phonemeScript !== "string" || value.phonemeScript.trim() === "")) problems.push("phonemeScript must be a non-empty string when present");
|
|
935
|
+
if (value.prosody !== void 0 && !isRecord(value.prosody)) problems.push("prosody must be an object when present");
|
|
936
|
+
}
|
|
548
937
|
function validateScriptPayload(value, problems) {
|
|
549
938
|
if (!Array.isArray(value.segments)) {
|
|
550
939
|
problems.push("segments must be an array");
|
|
@@ -555,6 +944,15 @@ function validateScriptPayload(value, problems) {
|
|
|
555
944
|
problems.push(`segments[${index}] must be an object`);
|
|
556
945
|
continue;
|
|
557
946
|
}
|
|
947
|
+
for (const key of [
|
|
948
|
+
"startMs",
|
|
949
|
+
"endMs",
|
|
950
|
+
"start_ms",
|
|
951
|
+
"end_ms",
|
|
952
|
+
"sourceRange",
|
|
953
|
+
"targetRange",
|
|
954
|
+
"duration"
|
|
955
|
+
]) if (Object.hasOwn(segment, key)) problems.push(`segments[${index}].${key} is timing; attach an AudioScript annotation Marker instead`);
|
|
558
956
|
if (typeof segment.segmentId !== "string") problems.push(`segments[${index}].segmentId must be a string`);
|
|
559
957
|
if (typeof segment.text !== "string") problems.push(`segments[${index}].text must be a string`);
|
|
560
958
|
if (segment.language !== void 0 && typeof segment.language !== "string") problems.push(`segments[${index}].language must be a string when present`);
|
|
@@ -578,6 +976,7 @@ function visitPeerEntityIdPaths(value, entityKind, parentPath, ancestors, paths)
|
|
|
578
976
|
if (Array.isArray(value)) for (const [index, item] of value.entries()) visitPeerEntityIdPaths(item, entityKind, `${parentPath}[${index}]`, ancestors, paths);
|
|
579
977
|
else for (const [key, child] of Object.entries(value)) {
|
|
580
978
|
const path = parentPath.length === 0 ? key : `${parentPath}.${key}`;
|
|
979
|
+
if (parentPath.length === 0 && key === "baseEntityIds") continue;
|
|
581
980
|
if (!(parentPath.length === 0 && key === "entityId") && !isOwnedLocalIdPath(entityKind, path) && isEntityIdFieldName(key)) paths.push(path);
|
|
582
981
|
visitPeerEntityIdPaths(child, entityKind, path, ancestors, paths);
|
|
583
982
|
}
|
|
@@ -597,7 +996,7 @@ function visitPeerEntityValues(value, entityKind, ownEntityId, entityIds, path,
|
|
|
597
996
|
ancestors.add(value);
|
|
598
997
|
if (Array.isArray(value)) for (const [index, item] of value.entries()) visitPeerEntityValues(item, entityKind, ownEntityId, entityIds, `${path}[${index}]`, ancestors, paths);
|
|
599
998
|
else for (const [key, child] of Object.entries(value)) {
|
|
600
|
-
if (path.length === 0 && (key === "entityId" || key === "entityKind")) continue;
|
|
999
|
+
if (path.length === 0 && (key === "entityId" || key === "entityKind" || key === "baseEntityIds")) continue;
|
|
601
1000
|
visitPeerEntityValues(child, entityKind, ownEntityId, entityIds, path.length === 0 ? key : `${path}.${key}`, ancestors, paths);
|
|
602
1001
|
}
|
|
603
1002
|
ancestors.delete(value);
|
|
@@ -613,15 +1012,27 @@ function isOwnedLocalEntityValuePath(entityKind, path) {
|
|
|
613
1012
|
"axvideo"
|
|
614
1013
|
].includes(entityKind) && /^(?:sampling|coordinateSpace|coordinateSpace\.unit|extent\.kind)$/.test(path)) return true;
|
|
615
1014
|
if (entityKind === "sequence-marker" && /^(?:durationPolicy|duration\.mode|timeRemapping\.(?:kind|mode))$/.test(path)) return true;
|
|
616
|
-
if (entityKind === "
|
|
1015
|
+
if (entityKind === "sequence-marker" && /^segmentRanges\[\d+\]\.segmentId$/.test(path)) return true;
|
|
1016
|
+
if (entityKind === "asset" && /^(?:external\.(?:system|key)|storageKey)$/.test(path)) return true;
|
|
1017
|
+
if (isMediaAssetVariantKind(entityKind) && /^(?:external\.(?:system|key)|storageKey)$/.test(path)) return true;
|
|
617
1018
|
if (entityKind === "voice" && /^voice\.(?:system|key|name)$/.test(path)) return true;
|
|
618
|
-
if (entityKind === "caption" && (path
|
|
619
|
-
if (
|
|
1019
|
+
if (entityKind === "caption" && (path.startsWith("style.") || /^selections\[\d+\]\.segmentId$/.test(path))) return true;
|
|
1020
|
+
if ([
|
|
1021
|
+
"audio-script",
|
|
1022
|
+
"caption",
|
|
1023
|
+
"phonetic-script"
|
|
1024
|
+
].includes(entityKind) && /^segments\[\d+\]\.(?:segmentId|text|language)$/.test(path)) return true;
|
|
620
1025
|
return false;
|
|
621
1026
|
}
|
|
622
1027
|
function isOwnedLocalIdPath(entityKind, path) {
|
|
623
1028
|
if (path === "lifecycle.actorId") return true;
|
|
624
|
-
if (
|
|
1029
|
+
if ([
|
|
1030
|
+
"audio-script",
|
|
1031
|
+
"caption",
|
|
1032
|
+
"phonetic-script"
|
|
1033
|
+
].includes(entityKind) && /^segments\[\d+\]\.segmentId$/.test(path)) return true;
|
|
1034
|
+
if (entityKind === "caption" && /^selections\[\d+\]\.segmentId$/.test(path)) return true;
|
|
1035
|
+
if (entityKind === "sequence-marker" && /^segmentRanges\[\d+\]\.segmentId$/.test(path)) return true;
|
|
625
1036
|
if (entityKind === "asset" && /^(?:tracks\[\d+\]\.trackId|renditions\[\d+\]\.renditionId)$/.test(path)) return true;
|
|
626
1037
|
return false;
|
|
627
1038
|
}
|
|
@@ -676,35 +1087,6 @@ function ofKind(relations, kind) {
|
|
|
676
1087
|
return relations.filter((relation) => relation.kind === kind);
|
|
677
1088
|
}
|
|
678
1089
|
//#endregion
|
|
679
|
-
//#region ../medeo-dsl/src/json-values.ts
|
|
680
|
-
const nativeObjectConstructorSource = Function.prototype.toString.call(Object);
|
|
681
|
-
function isJsonObject(value) {
|
|
682
|
-
return isJsonValue(value, /* @__PURE__ */ new Set()) && !Array.isArray(value) && value !== null;
|
|
683
|
-
}
|
|
684
|
-
function isJsonValue(value, ancestors) {
|
|
685
|
-
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
686
|
-
if (typeof value === "number") return Number.isFinite(value);
|
|
687
|
-
if (typeof value !== "object") return false;
|
|
688
|
-
if (!Array.isArray(value) && !isPlainObject(value)) return false;
|
|
689
|
-
if (ancestors.has(value)) return false;
|
|
690
|
-
ancestors.add(value);
|
|
691
|
-
const valid = Array.isArray(value) ? value.every((item) => isJsonValue(item, ancestors)) : Object.values(value).every((item) => isJsonValue(item, ancestors));
|
|
692
|
-
ancestors.delete(value);
|
|
693
|
-
return valid;
|
|
694
|
-
}
|
|
695
|
-
/** Recognize an ordinary object from any VM realm without admitting class instances. */
|
|
696
|
-
function isPlainObject(value) {
|
|
697
|
-
try {
|
|
698
|
-
const prototype = Object.getPrototypeOf(value);
|
|
699
|
-
if (prototype === null) return true;
|
|
700
|
-
if (Object.getPrototypeOf(prototype) !== null) return false;
|
|
701
|
-
const constructor = Object.getOwnPropertyDescriptor(prototype, "constructor")?.value;
|
|
702
|
-
return typeof constructor === "function" && constructor.prototype === prototype && Function.prototype.toString.call(constructor) === nativeObjectConstructorSource;
|
|
703
|
-
} catch {
|
|
704
|
-
return false;
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
//#endregion
|
|
708
1090
|
//#region ../medeo-dsl/src/relation-specs.ts
|
|
709
1091
|
const timelineTrackRelationSpec = emptySpec("timeline-track", "timeline", "track");
|
|
710
1092
|
const trackClipRelationSpec = emptySpec("track-clip", "track", "clip");
|
|
@@ -727,8 +1109,6 @@ const generatedRelationSpec = Object.freeze({
|
|
|
727
1109
|
validateEndpoints: (endpoints) => endpoints.every((endpoint) => isGeneratedMedia(endpoint.current())),
|
|
728
1110
|
validateMetadata: isEmptyMetadata
|
|
729
1111
|
});
|
|
730
|
-
const phoneticScriptProvenanceRelationSpec = metadataSpec("phonetic-script-provenance", "phonetic-script", "audio-script", isSegmentAlignmentMetadata);
|
|
731
|
-
const captionProvenanceRelationSpec = metadataSpec("caption-provenance", "caption", "audio-script", isSegmentAlignmentMetadata);
|
|
732
1112
|
const captionAlignmentRelationSpec = Object.freeze({
|
|
733
1113
|
kind: "caption-alignment",
|
|
734
1114
|
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["caption"]), new Set(["audio", "voice"])),
|
|
@@ -740,13 +1120,31 @@ const clipAnchorRelationSpec = Object.freeze({
|
|
|
740
1120
|
validateEndpoints: (endpoints) => endpoints[0].current().entityKind === "clip" && endpoints[1].current().entityKind === "clip",
|
|
741
1121
|
validateMetadata: isEmptyMetadata
|
|
742
1122
|
});
|
|
743
|
-
/**
|
|
744
|
-
const
|
|
745
|
-
kind: "
|
|
746
|
-
validateEndpoints: (endpoints) =>
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
1123
|
+
/** Voice is generated from PhoneticScript; kinds determine roles regardless of endpoint positions. */
|
|
1124
|
+
const phoneticScriptRenderRelationSpec = Object.freeze({
|
|
1125
|
+
kind: "phonetic-script-render",
|
|
1126
|
+
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["voice"]), new Set(["phonetic-script"])),
|
|
1127
|
+
validateMetadata: isEmptyMetadata
|
|
1128
|
+
});
|
|
1129
|
+
/** AudioScript was transcribed from Audio, Video, or recorded Voice; kinds determine roles. */
|
|
1130
|
+
const audioScriptSourceRelationSpec = Object.freeze({
|
|
1131
|
+
kind: "audio-script-source",
|
|
1132
|
+
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["audio-script"]), new Set([
|
|
1133
|
+
"audio",
|
|
1134
|
+
"video",
|
|
1135
|
+
"voice"
|
|
1136
|
+
])),
|
|
1137
|
+
validateMetadata: isEmptyMetadata
|
|
1138
|
+
});
|
|
1139
|
+
/**
|
|
1140
|
+
* `audio-script-marker(script, marker)` attaches an annotation Sequence Marker
|
|
1141
|
+
* whose `segmentRanges` hold directly assigned per-Segment time values. The
|
|
1142
|
+
* script keeps no Sequence and no Clip admission; annotation Markers cannot
|
|
1143
|
+
* enter Clip/AXVideo use chains (enforced by the marker-use invariant).
|
|
1144
|
+
*/
|
|
1145
|
+
const audioScriptMarkerRelationSpec = Object.freeze({
|
|
1146
|
+
kind: "audio-script-marker",
|
|
1147
|
+
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["audio-script"]), new Set(["sequence-marker"])),
|
|
750
1148
|
validateMetadata: isEmptyMetadata
|
|
751
1149
|
});
|
|
752
1150
|
/** Built-in kinds are reserved; callers may add specs only under new names. */
|
|
@@ -759,11 +1157,11 @@ const builtInRelationSpecs = Object.freeze([
|
|
|
759
1157
|
markerTimelineRelationSpec,
|
|
760
1158
|
physicalAssetRelationSpec,
|
|
761
1159
|
generatedRelationSpec,
|
|
762
|
-
phoneticScriptProvenanceRelationSpec,
|
|
763
|
-
captionProvenanceRelationSpec,
|
|
764
1160
|
captionAlignmentRelationSpec,
|
|
765
1161
|
clipAnchorRelationSpec,
|
|
766
|
-
|
|
1162
|
+
phoneticScriptRenderRelationSpec,
|
|
1163
|
+
audioScriptSourceRelationSpec,
|
|
1164
|
+
audioScriptMarkerRelationSpec
|
|
767
1165
|
]);
|
|
768
1166
|
function emptySpec(kind, a, b) {
|
|
769
1167
|
return metadataSpec(kind, a, b, isEmptyMetadata);
|
|
@@ -801,9 +1199,6 @@ function isAssetBinding(value) {
|
|
|
801
1199
|
const orderingKey = /(order|ordinal|position|rank|index|z[_-]?index)/i;
|
|
802
1200
|
return Object.keys(value).every((key) => !orderingKey.test(key));
|
|
803
1201
|
}
|
|
804
|
-
function isSegmentAlignmentMetadata(value) {
|
|
805
|
-
return isJsonObject(value) && Object.hasOwn(value, "segmentAlignment");
|
|
806
|
-
}
|
|
807
1202
|
function isCaptionAlignmentMetadata(value) {
|
|
808
1203
|
return isJsonObject(value) && Object.hasOwn(value, "alignment");
|
|
809
1204
|
}
|
|
@@ -860,7 +1255,7 @@ var BiRelationIndex = class {
|
|
|
860
1255
|
byRelationId = /* @__PURE__ */ new Map();
|
|
861
1256
|
link(input) {
|
|
862
1257
|
if (input.spec.kind === "generated") throw new Error("Author generated Relations with linkGenerated({ output, input })");
|
|
863
|
-
if (input.spec.kind === "clip-anchor" || input.spec.kind === "
|
|
1258
|
+
if (input.spec.kind === "clip-anchor" || input.spec.kind === "phonetic-script-render" || input.spec.kind === "audio-script-source") throw new Error(`Author ordered ${input.spec.kind} Relations with the dedicated role-named method`);
|
|
864
1259
|
return this.linkValidated(input);
|
|
865
1260
|
}
|
|
866
1261
|
/** Author `generated(output, input)` without exposing positional arguments. */
|
|
@@ -883,12 +1278,22 @@ var BiRelationIndex = class {
|
|
|
883
1278
|
trace: input.trace
|
|
884
1279
|
});
|
|
885
1280
|
}
|
|
886
|
-
/** Author `
|
|
887
|
-
|
|
1281
|
+
/** Author `phonetic-script-render(output, phoneticScript)` without exposing positional arguments. */
|
|
1282
|
+
linkPhoneticScriptRender(input) {
|
|
888
1283
|
return this.linkValidated({
|
|
889
1284
|
relationId: input.relationId,
|
|
890
|
-
spec:
|
|
891
|
-
endpoints: [input.output, input.
|
|
1285
|
+
spec: phoneticScriptRenderRelationSpec,
|
|
1286
|
+
endpoints: [input.output, input.phoneticScript],
|
|
1287
|
+
metadata: {},
|
|
1288
|
+
trace: input.trace
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
/** Author `audio-script-source(script, source)` without exposing positional arguments. */
|
|
1292
|
+
linkAudioScriptSource(input) {
|
|
1293
|
+
return this.linkValidated({
|
|
1294
|
+
relationId: input.relationId,
|
|
1295
|
+
spec: audioScriptSourceRelationSpec,
|
|
1296
|
+
endpoints: [input.script, input.source],
|
|
892
1297
|
metadata: {},
|
|
893
1298
|
trace: input.trace
|
|
894
1299
|
});
|
|
@@ -979,7 +1384,13 @@ function decodeEntityRelationRows(rows, options) {
|
|
|
979
1384
|
const entitiesById = /* @__PURE__ */ new Map();
|
|
980
1385
|
const extensionEntityKinds = new Set(options.entityKinds ?? []);
|
|
981
1386
|
for (const row of rows.entities) {
|
|
982
|
-
|
|
1387
|
+
let entity = decodeEntityRow(row, extensionEntityKinds, issues);
|
|
1388
|
+
if (entity != null) try {
|
|
1389
|
+
entity = decodeEntityRow(assembleEntityContent(rows, row.entityId), extensionEntityKinds, issues);
|
|
1390
|
+
} catch (error) {
|
|
1391
|
+
issues.push(errorMessage(error));
|
|
1392
|
+
continue;
|
|
1393
|
+
}
|
|
983
1394
|
if (entity == null) continue;
|
|
984
1395
|
const ref = createEntityRef(entity);
|
|
985
1396
|
if (entitiesById.has(ref.entityId)) {
|
|
@@ -1112,17 +1523,6 @@ function errorMessage(error) {
|
|
|
1112
1523
|
return error instanceof Error ? error.message : String(error);
|
|
1113
1524
|
}
|
|
1114
1525
|
//#endregion
|
|
1115
|
-
//#region ../medeo-dsl/src/rows.ts
|
|
1116
|
-
function entityToRow(entity) {
|
|
1117
|
-
const { entityId, entityKind, ...payload } = entity;
|
|
1118
|
-
if (!isJsonObject(payload)) throw new Error(`Entity "${entityId}" payload must contain only JSON values`);
|
|
1119
|
-
return {
|
|
1120
|
-
entityId,
|
|
1121
|
-
entityKind,
|
|
1122
|
-
payload
|
|
1123
|
-
};
|
|
1124
|
-
}
|
|
1125
|
-
//#endregion
|
|
1126
1526
|
//#region src/entity/entity-sandbox.ts
|
|
1127
1527
|
const ASSET_SOURCE_KEY = "external";
|
|
1128
1528
|
const MEMOTA_SYSTEM = "memota";
|
|
@@ -1139,6 +1539,7 @@ var EntitySandbox = class {
|
|
|
1139
1539
|
constructor(options) {
|
|
1140
1540
|
this.original = cloneSnapshot(options.state ?? {
|
|
1141
1541
|
revision: 0,
|
|
1542
|
+
audioScriptEntityId: null,
|
|
1142
1543
|
entities: [],
|
|
1143
1544
|
relations: []
|
|
1144
1545
|
});
|
|
@@ -1155,6 +1556,20 @@ var EntitySandbox = class {
|
|
|
1155
1556
|
getCommands() {
|
|
1156
1557
|
return this.commands;
|
|
1157
1558
|
}
|
|
1559
|
+
/** Host-owned fixed structure is journaled through the same CAS graph as model edits. */
|
|
1560
|
+
ensureFoundation(timelinePayload = {}) {
|
|
1561
|
+
const foundation = ensureEditorFoundation(toDslRows(this.state), this.idFactory, timelinePayload);
|
|
1562
|
+
this.appendResourceRows(foundation.rows);
|
|
1563
|
+
}
|
|
1564
|
+
get audioScriptEntityId() {
|
|
1565
|
+
return this.state.audioScriptEntityId;
|
|
1566
|
+
}
|
|
1567
|
+
setDocumentAudioScript(audioScriptEntityId) {
|
|
1568
|
+
this.record({
|
|
1569
|
+
kind: "set-document-audio-script",
|
|
1570
|
+
audioScriptEntityId
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1158
1573
|
rollbackTo(index) {
|
|
1159
1574
|
if (!Number.isInteger(index) || index < 0 || index > this.commands.length) throw new Error(`rollbackTo: entity checkpoint index ${index} is past journal length ${this.commands.length}`);
|
|
1160
1575
|
const prefix = this.commands.slice(0, index);
|
|
@@ -1165,7 +1580,13 @@ var EntitySandbox = class {
|
|
|
1165
1580
|
this.onTruncate?.(index);
|
|
1166
1581
|
}
|
|
1167
1582
|
buildPlan() {
|
|
1168
|
-
|
|
1583
|
+
const rows = toDslRows(this.state);
|
|
1584
|
+
decodeEntityRelationRows(rows, numericMarkerComparators);
|
|
1585
|
+
assertCanonicalEditorResources(rows);
|
|
1586
|
+
readDocumentAudioScript({
|
|
1587
|
+
rows,
|
|
1588
|
+
audioScriptEntityId: this.state.audioScriptEntityId
|
|
1589
|
+
});
|
|
1169
1590
|
const currentEntityIds = new Set(this.state.entities.map((entity) => entity.entity_id));
|
|
1170
1591
|
const currentRelationIds = new Set(this.state.relations.map((relation) => relation.relation_id));
|
|
1171
1592
|
return {
|
|
@@ -1179,6 +1600,9 @@ var EntitySandbox = class {
|
|
|
1179
1600
|
renderPreview() {
|
|
1180
1601
|
const lines = [`Entity plan: base_revision=${this.original.revision} commands=${this.commands.length} entities=${this.state.entities.length} relations=${this.state.relations.length}`];
|
|
1181
1602
|
for (const command of this.commands) switch (command.kind) {
|
|
1603
|
+
case "set-document-audio-script":
|
|
1604
|
+
lines.push(`~ document audioScriptEntityId=${command.audioScriptEntityId}`);
|
|
1605
|
+
break;
|
|
1182
1606
|
case "create-entity":
|
|
1183
1607
|
lines.push(`+ entity ${command.entity.entity_id} kind=${command.entity.entity_kind}`);
|
|
1184
1608
|
break;
|
|
@@ -1198,21 +1622,67 @@ var EntitySandbox = class {
|
|
|
1198
1622
|
}
|
|
1199
1623
|
return lines.join("\n");
|
|
1200
1624
|
}
|
|
1625
|
+
assembledEntity(entity) {
|
|
1626
|
+
const assembled = assembleEntityContent(toDslRows(this.state), createEntityId(entity.entity_id));
|
|
1627
|
+
return {
|
|
1628
|
+
...clone(entity),
|
|
1629
|
+
payload: assembled.payload
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1201
1632
|
buildEntityFacade() {
|
|
1202
1633
|
return {
|
|
1203
|
-
list: () =>
|
|
1634
|
+
list: () => this.state.entities.map((entity) => this.assembledEntity(entity)),
|
|
1204
1635
|
get: (entityId) => {
|
|
1205
1636
|
const entity = this.state.entities.find((candidate) => candidate.entity_id === entityId);
|
|
1206
|
-
return entity == null ? null :
|
|
1637
|
+
return entity == null ? null : this.assembledEntity(entity);
|
|
1207
1638
|
},
|
|
1208
1639
|
findByAssetId: (assetId) => {
|
|
1209
1640
|
assertTrimmed(assetId, "assetId");
|
|
1210
|
-
return clone(this.state.entities.filter((entity) => entity.entity_kind
|
|
1641
|
+
return clone(this.state.entities.map((entity) => this.assembledEntity(entity)).filter((entity) => isMediaAssetVariantKind(entity.entity_kind) && isImportedMemotaAsset(entity.payload, assetId)));
|
|
1642
|
+
},
|
|
1643
|
+
readCaptionContent: (entityId) => {
|
|
1644
|
+
const assembled = assembleCaptionContent(toDslRows(this.state), createEntityId(entityId));
|
|
1645
|
+
return clone({
|
|
1646
|
+
audio_script_entity_id: assembled.audioScript.entityId,
|
|
1647
|
+
text: assembled.text,
|
|
1648
|
+
segments: assembled.segments.map((segment) => ({ ...segment }))
|
|
1649
|
+
});
|
|
1650
|
+
},
|
|
1651
|
+
readPhoneticScriptContent: (entityId) => {
|
|
1652
|
+
const assembled = assemblePhoneticScriptContent(toDslRows(this.state), createEntityId(entityId));
|
|
1653
|
+
const own = assembled.phoneticScript.payload;
|
|
1654
|
+
return clone({
|
|
1655
|
+
audio_script_entity_id: assembled.audioScript.entityId,
|
|
1656
|
+
text: assembled.text,
|
|
1657
|
+
segments: assembled.segments.map((segment) => ({ ...segment })),
|
|
1658
|
+
...typeof own.phonemeScript === "string" ? { phonemeScript: own.phonemeScript } : {},
|
|
1659
|
+
...isJsonObject(own.prosody) ? { prosody: clone(own.prosody) } : {}
|
|
1660
|
+
});
|
|
1211
1661
|
},
|
|
1212
1662
|
create: (input) => this.createEntity(input),
|
|
1213
|
-
update: (input) =>
|
|
1663
|
+
update: (input) => {
|
|
1664
|
+
assertTrimmed(input.entity_id, "entity_id");
|
|
1665
|
+
if (!this.state.entities.some((entity) => entity.entity_id === input.entity_id)) throw new Error(`Entity id "${input.entity_id}" does not exist`);
|
|
1666
|
+
const updates = updateEntityFields(toDslRows(this.state), createEntityId(input.entity_id), input.payload);
|
|
1667
|
+
for (const row of updates) this.replaceOwnedPayload({
|
|
1668
|
+
entity_id: row.entityId,
|
|
1669
|
+
payload: row.payload
|
|
1670
|
+
});
|
|
1671
|
+
},
|
|
1672
|
+
declareFields: (input) => {
|
|
1673
|
+
const current = this.state.entities.find((entity) => entity.entity_id === input.entity_id);
|
|
1674
|
+
if (current === void 0) throw new Error(`Unknown entity "${input.entity_id}"`);
|
|
1675
|
+
assembleEntityContent(toDslRows(this.state), createEntityId(input.entity_id));
|
|
1676
|
+
this.replaceOwnedPayload({
|
|
1677
|
+
entity_id: input.entity_id,
|
|
1678
|
+
payload: {
|
|
1679
|
+
...current.payload,
|
|
1680
|
+
...input.payload
|
|
1681
|
+
}
|
|
1682
|
+
});
|
|
1683
|
+
},
|
|
1214
1684
|
delete: (input) => this.deleteEntity(input),
|
|
1215
|
-
|
|
1685
|
+
ensureMedia: (fact) => this.ensureMedia(fact)
|
|
1216
1686
|
};
|
|
1217
1687
|
}
|
|
1218
1688
|
buildRelationFacade() {
|
|
@@ -1226,7 +1696,8 @@ var EntitySandbox = class {
|
|
|
1226
1696
|
link: (input) => this.link(input),
|
|
1227
1697
|
linkGenerated: (input) => this.linkGenerated(input),
|
|
1228
1698
|
linkClipAnchor: (input) => this.linkClipAnchor(input),
|
|
1229
|
-
|
|
1699
|
+
linkPhoneticScriptRender: (input) => this.linkPhoneticScriptRender(input),
|
|
1700
|
+
linkAudioScriptSource: (input) => this.linkAudioScriptSource(input),
|
|
1230
1701
|
unlink: (input) => this.unlinkRelation(input)
|
|
1231
1702
|
};
|
|
1232
1703
|
}
|
|
@@ -1234,6 +1705,21 @@ var EntitySandbox = class {
|
|
|
1234
1705
|
if (!isKnownEntityKind(input.entity_kind)) throw new Error(`Unknown or extension Entity kind "${String(input.entity_kind)}"`);
|
|
1235
1706
|
const payload = clone(input.payload);
|
|
1236
1707
|
if (!isJsonObject(payload)) throw new Error("Entity payload must contain only JSON values");
|
|
1708
|
+
if (input.entity_kind === "timeline" || input.entity_kind === "track") {
|
|
1709
|
+
const matches = this.state.entities.filter((entity) => entity.entity_kind === input.entity_kind && (input.entity_kind === "timeline" || entity.payload.role === payload.role));
|
|
1710
|
+
if (matches.length > 1) throw new Error(`Ambiguous editor ${input.entity_kind}; resolve existing identities`);
|
|
1711
|
+
if (matches[0] !== void 0) return this.reuseEntity(matches[0], input, payload);
|
|
1712
|
+
}
|
|
1713
|
+
const external = payload.external;
|
|
1714
|
+
if (isMediaAssetVariantKind(input.entity_kind) && isJsonObject(external) && (external.system === "memota" || external.system === "memota-speech") && typeof external.key === "string") {
|
|
1715
|
+
const matches = this.state.entities.filter((entity) => isMediaAssetVariantKind(entity.entity_kind) && isJsonObject(entity.payload.external) && entity.payload.external.system === external.system && entity.payload.external.key === external.key);
|
|
1716
|
+
if (matches.length > 1) throw new Error(`Ambiguous Asset bindings for ${external.key}`);
|
|
1717
|
+
const existing = matches[0];
|
|
1718
|
+
if (existing !== void 0) {
|
|
1719
|
+
if (existing.entity_kind !== input.entity_kind) throw new Error(`Asset kind conflicts for ${external.key}; cannot change ${existing.entity_kind} to ${input.entity_kind}`);
|
|
1720
|
+
return this.reuseEntity(existing, input, payload);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1237
1723
|
const entityId = input.entity_id ?? this.idFactory("entity");
|
|
1238
1724
|
assertTrimmed(entityId, "entity_id");
|
|
1239
1725
|
const entity = {
|
|
@@ -1252,7 +1738,21 @@ var EntitySandbox = class {
|
|
|
1252
1738
|
});
|
|
1253
1739
|
return entityId;
|
|
1254
1740
|
}
|
|
1255
|
-
|
|
1741
|
+
reuseEntity(existing, input, payload) {
|
|
1742
|
+
if (input.entity_id !== void 0 && input.entity_id !== existing.entity_id) throw new Error(`Entity already exists; reuse ${existing.entity_id}`);
|
|
1743
|
+
for (const [key, value] of Object.entries(payload)) if (existing.payload[key] !== void 0 && !sameJson(existing.payload[key], value)) throw new Error(`Resource facts conflict for ${existing.entity_id}: ${key}`);
|
|
1744
|
+
const merged = {
|
|
1745
|
+
...existing.payload,
|
|
1746
|
+
...payload
|
|
1747
|
+
};
|
|
1748
|
+
if (!sameJson(existing.payload, merged)) this.replaceOwnedPayload({
|
|
1749
|
+
entity_id: existing.entity_id,
|
|
1750
|
+
payload: merged
|
|
1751
|
+
});
|
|
1752
|
+
return existing.entity_id;
|
|
1753
|
+
}
|
|
1754
|
+
/** Host persistence adapter; never exposed as the DSL field update operation. */
|
|
1755
|
+
replaceOwnedPayload(input) {
|
|
1256
1756
|
assertTrimmed(input.entity_id, "entity_id");
|
|
1257
1757
|
const payload = clone(input.payload);
|
|
1258
1758
|
if (!isJsonObject(payload)) throw new Error("Entity payload must contain only JSON values");
|
|
@@ -1269,21 +1769,42 @@ var EntitySandbox = class {
|
|
|
1269
1769
|
entity_id: input.entity_id
|
|
1270
1770
|
});
|
|
1271
1771
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1772
|
+
ensureMedia(fact) {
|
|
1773
|
+
const checkpoint = this.commandCount;
|
|
1774
|
+
try {
|
|
1775
|
+
const imported = importMediaAsset(toDslRows(this.state), fact, this.idFactory);
|
|
1776
|
+
this.appendResourceRows(imported.rows);
|
|
1777
|
+
return { contentEntityId: imported.contentEntityId };
|
|
1778
|
+
} catch (error) {
|
|
1779
|
+
this.rollbackTo(checkpoint);
|
|
1780
|
+
throw error;
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
appendResourceRows(rows) {
|
|
1784
|
+
for (const row of rows.entities) {
|
|
1785
|
+
const existing = this.state.entities.find((entity) => entity.entity_id === row.entityId);
|
|
1786
|
+
if (existing === void 0) this.createEntity({
|
|
1787
|
+
entity_id: row.entityId,
|
|
1788
|
+
entity_kind: row.entityKind,
|
|
1789
|
+
payload: row.payload
|
|
1790
|
+
});
|
|
1791
|
+
else if (!sameJson(existing.payload, row.payload)) this.replaceOwnedPayload({
|
|
1792
|
+
entity_id: row.entityId,
|
|
1793
|
+
payload: clone(row.payload)
|
|
1794
|
+
});
|
|
1795
|
+
}
|
|
1796
|
+
for (const row of rows.relations) {
|
|
1797
|
+
if (this.state.relations.some((relation) => relation.relation_id === row.relationId)) continue;
|
|
1798
|
+
if (row.relationKind !== "timeline-track") throw new Error(`Unexpected resource Relation ${row.relationKind}`);
|
|
1799
|
+
this.link({
|
|
1800
|
+
relation_id: row.relationId,
|
|
1801
|
+
relation_kind: row.relationKind,
|
|
1802
|
+
endpoint_0_entity_id: row.endpoint0EntityId,
|
|
1803
|
+
endpoint_1_entity_id: row.endpoint1EntityId,
|
|
1804
|
+
metadata: clone(row.metadata),
|
|
1805
|
+
trace: clone(row.trace)
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1287
1808
|
}
|
|
1288
1809
|
link(input) {
|
|
1289
1810
|
if (input.relation_kind === "generated") throw new Error("Author generated Relations with relations.linkGenerated({ output_entity_id, input_entity_id })");
|
|
@@ -1346,19 +1867,40 @@ var EntitySandbox = class {
|
|
|
1346
1867
|
});
|
|
1347
1868
|
return relation.relation_id;
|
|
1348
1869
|
}
|
|
1349
|
-
|
|
1870
|
+
linkPhoneticScriptRender(input) {
|
|
1350
1871
|
const relation = this.relationFromInput({
|
|
1351
1872
|
...input.relation_id !== void 0 ? { relation_id: input.relation_id } : {},
|
|
1352
1873
|
endpoint_0_entity_id: input.output_entity_id,
|
|
1353
|
-
endpoint_1_entity_id: input.
|
|
1874
|
+
endpoint_1_entity_id: input.phonetic_script_entity_id,
|
|
1354
1875
|
metadata: {},
|
|
1355
1876
|
...input.trace !== void 0 ? { trace: input.trace } : {}
|
|
1356
|
-
}, "
|
|
1877
|
+
}, "phonetic-script-render");
|
|
1357
1878
|
const [output, script] = this.refsFor(relation);
|
|
1358
|
-
new BiRelationIndex().
|
|
1879
|
+
new BiRelationIndex().linkPhoneticScriptRender({
|
|
1359
1880
|
relationId: createRelationId(relation.relation_id),
|
|
1360
1881
|
output,
|
|
1882
|
+
phoneticScript: script,
|
|
1883
|
+
trace: relation.trace
|
|
1884
|
+
});
|
|
1885
|
+
this.record({
|
|
1886
|
+
kind: "link-relation",
|
|
1887
|
+
relation
|
|
1888
|
+
});
|
|
1889
|
+
return relation.relation_id;
|
|
1890
|
+
}
|
|
1891
|
+
linkAudioScriptSource(input) {
|
|
1892
|
+
const relation = this.relationFromInput({
|
|
1893
|
+
...input.relation_id !== void 0 ? { relation_id: input.relation_id } : {},
|
|
1894
|
+
endpoint_0_entity_id: input.script_entity_id,
|
|
1895
|
+
endpoint_1_entity_id: input.source_entity_id,
|
|
1896
|
+
metadata: {},
|
|
1897
|
+
...input.trace !== void 0 ? { trace: input.trace } : {}
|
|
1898
|
+
}, "audio-script-source");
|
|
1899
|
+
const [script, source] = this.refsFor(relation);
|
|
1900
|
+
new BiRelationIndex().linkAudioScriptSource({
|
|
1901
|
+
relationId: createRelationId(relation.relation_id),
|
|
1361
1902
|
script,
|
|
1903
|
+
source,
|
|
1362
1904
|
trace: relation.trace
|
|
1363
1905
|
});
|
|
1364
1906
|
this.record({
|
|
@@ -1407,6 +1949,13 @@ var EntitySandbox = class {
|
|
|
1407
1949
|
}
|
|
1408
1950
|
apply(command, enforceIdentity) {
|
|
1409
1951
|
switch (command.kind) {
|
|
1952
|
+
case "set-document-audio-script":
|
|
1953
|
+
readDocumentAudioScript({
|
|
1954
|
+
rows: toDslRows(this.state),
|
|
1955
|
+
audioScriptEntityId: command.audioScriptEntityId
|
|
1956
|
+
});
|
|
1957
|
+
this.state.audioScriptEntityId = command.audioScriptEntityId;
|
|
1958
|
+
return;
|
|
1410
1959
|
case "create-entity": {
|
|
1411
1960
|
if (enforceIdentity && this.state.entities.some((entity) => entity.entity_id === command.entity.entity_id)) throw new Error(`Entity id "${command.entity.entity_id}" already exists`);
|
|
1412
1961
|
const original = this.original.entities.find((entity) => entity.entity_id === command.entity.entity_id);
|
|
@@ -1426,6 +1975,7 @@ var EntitySandbox = class {
|
|
|
1426
1975
|
return;
|
|
1427
1976
|
}
|
|
1428
1977
|
case "delete-entity": {
|
|
1978
|
+
if (this.state.audioScriptEntityId === command.entity_id) throw new Error("Clear or switch the document AudioScript association before deleting its entity");
|
|
1429
1979
|
const index = this.state.entities.findIndex((entity) => entity.entity_id === command.entity_id);
|
|
1430
1980
|
if (index < 0) throw new Error(`Entity id "${command.entity_id}" does not exist`);
|
|
1431
1981
|
const incidentRelationIds = this.state.relations.filter((relation) => relation.endpoint_0_entity_id === command.entity_id || relation.endpoint_1_entity_id === command.entity_id).map((relation) => relation.relation_id).sort();
|
|
@@ -1452,7 +2002,7 @@ var EntitySandbox = class {
|
|
|
1452
2002
|
};
|
|
1453
2003
|
function isImportedMemotaAsset(payload, assetId) {
|
|
1454
2004
|
const external = payload[ASSET_SOURCE_KEY];
|
|
1455
|
-
return external != null && !Array.isArray(external) && typeof external === "object" && external.system === MEMOTA_SYSTEM && external.key === assetId;
|
|
2005
|
+
return external != null && !Array.isArray(external) && typeof external === "object" && (external.system === MEMOTA_SYSTEM || external.system === "memota-speech") && external.key === assetId;
|
|
1456
2006
|
}
|
|
1457
2007
|
function toDslEntity(entity) {
|
|
1458
2008
|
return {
|
|
@@ -1484,6 +2034,13 @@ function cloneSnapshot(state) {
|
|
|
1484
2034
|
function clone(value) {
|
|
1485
2035
|
return structuredClone(value);
|
|
1486
2036
|
}
|
|
2037
|
+
function sameJson(left, right) {
|
|
2038
|
+
if (left === right) return true;
|
|
2039
|
+
if (Array.isArray(left) && Array.isArray(right)) return left.length === right.length && left.every((value, index) => sameJson(value, right[index]));
|
|
2040
|
+
if (!isJsonObject(left) || !isJsonObject(right)) return false;
|
|
2041
|
+
const keys = Object.keys(left);
|
|
2042
|
+
return keys.length === Object.keys(right).length && keys.every((key) => Object.hasOwn(right, key) && sameJson(left[key], right[key]));
|
|
2043
|
+
}
|
|
1487
2044
|
function assertTrimmed(value, label) {
|
|
1488
2045
|
if (typeof value !== "string" || value.length === 0 || value.trim() !== value) throw new Error(`${label} must be a non-empty trimmed string`);
|
|
1489
2046
|
}
|
|
@@ -1836,6 +2393,6 @@ function replayJournalSync(adapter, journal) {
|
|
|
1836
2393
|
}
|
|
1837
2394
|
}
|
|
1838
2395
|
//#endregion
|
|
1839
|
-
export {
|
|
2396
|
+
export { createRelationId as a, renderPreview as c, createEntityId as i, renderCompactProjection as l, EntitySandbox as n, isMediaAssetVariantKind as o, toDslRows as r, collectAffectedPartIds as s, EditSandboxSession as t };
|
|
1840
2397
|
|
|
1841
|
-
//# sourceMappingURL=script-session-
|
|
2398
|
+
//# sourceMappingURL=script-session-C2uQHYt4.mjs.map
|