@mengine/medeo-tool 1.2.1-alpha.9 → 1.3.1-alpha.6
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-hKZbJSRP.d.mts} +85 -33
- package/dist/index.d.mts +76 -76
- package/dist/index.mjs +258 -140
- package/dist/index.mjs.map +1 -1
- package/dist/sandbox-api.d.mts +142 -43
- package/dist/{script-session-CHyIUBkO.mjs → script-session-1eqBGegp.mjs} +664 -132
- package/dist/script-session-1eqBGegp.mjs.map +1 -0
- package/dist/worker-entry.d.mts +1 -1
- package/dist/worker-entry.mjs +28 -8
- 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, 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";
|
|
@@ -1155,6 +1555,11 @@ var EntitySandbox = class {
|
|
|
1155
1555
|
getCommands() {
|
|
1156
1556
|
return this.commands;
|
|
1157
1557
|
}
|
|
1558
|
+
/** Host-owned fixed structure is journaled through the same CAS graph as model edits. */
|
|
1559
|
+
ensureFoundation(timelinePayload = {}) {
|
|
1560
|
+
const foundation = ensureEditorFoundation(toDslRows(this.state), this.idFactory, timelinePayload);
|
|
1561
|
+
this.appendResourceRows(foundation.rows);
|
|
1562
|
+
}
|
|
1158
1563
|
rollbackTo(index) {
|
|
1159
1564
|
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
1565
|
const prefix = this.commands.slice(0, index);
|
|
@@ -1165,7 +1570,9 @@ var EntitySandbox = class {
|
|
|
1165
1570
|
this.onTruncate?.(index);
|
|
1166
1571
|
}
|
|
1167
1572
|
buildPlan() {
|
|
1168
|
-
|
|
1573
|
+
const rows = toDslRows(this.state);
|
|
1574
|
+
decodeEntityRelationRows(rows, numericMarkerComparators);
|
|
1575
|
+
assertCanonicalEditorResources(rows);
|
|
1169
1576
|
const currentEntityIds = new Set(this.state.entities.map((entity) => entity.entity_id));
|
|
1170
1577
|
const currentRelationIds = new Set(this.state.relations.map((relation) => relation.relation_id));
|
|
1171
1578
|
return {
|
|
@@ -1198,21 +1605,67 @@ var EntitySandbox = class {
|
|
|
1198
1605
|
}
|
|
1199
1606
|
return lines.join("\n");
|
|
1200
1607
|
}
|
|
1608
|
+
assembledEntity(entity) {
|
|
1609
|
+
const assembled = assembleEntityContent(toDslRows(this.state), createEntityId(entity.entity_id));
|
|
1610
|
+
return {
|
|
1611
|
+
...clone(entity),
|
|
1612
|
+
payload: assembled.payload
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1201
1615
|
buildEntityFacade() {
|
|
1202
1616
|
return {
|
|
1203
|
-
list: () =>
|
|
1617
|
+
list: () => this.state.entities.map((entity) => this.assembledEntity(entity)),
|
|
1204
1618
|
get: (entityId) => {
|
|
1205
1619
|
const entity = this.state.entities.find((candidate) => candidate.entity_id === entityId);
|
|
1206
|
-
return entity == null ? null :
|
|
1620
|
+
return entity == null ? null : this.assembledEntity(entity);
|
|
1207
1621
|
},
|
|
1208
1622
|
findByAssetId: (assetId) => {
|
|
1209
1623
|
assertTrimmed(assetId, "assetId");
|
|
1210
|
-
return clone(this.state.entities.filter((entity) => entity.entity_kind
|
|
1624
|
+
return clone(this.state.entities.map((entity) => this.assembledEntity(entity)).filter((entity) => isMediaAssetVariantKind(entity.entity_kind) && isImportedMemotaAsset(entity.payload, assetId)));
|
|
1625
|
+
},
|
|
1626
|
+
readCaptionContent: (entityId) => {
|
|
1627
|
+
const assembled = assembleCaptionContent(toDslRows(this.state), createEntityId(entityId));
|
|
1628
|
+
return clone({
|
|
1629
|
+
audio_script_entity_id: assembled.audioScript.entityId,
|
|
1630
|
+
text: assembled.text,
|
|
1631
|
+
segments: assembled.segments.map((segment) => ({ ...segment }))
|
|
1632
|
+
});
|
|
1633
|
+
},
|
|
1634
|
+
readPhoneticScriptContent: (entityId) => {
|
|
1635
|
+
const assembled = assemblePhoneticScriptContent(toDslRows(this.state), createEntityId(entityId));
|
|
1636
|
+
const own = assembled.phoneticScript.payload;
|
|
1637
|
+
return clone({
|
|
1638
|
+
audio_script_entity_id: assembled.audioScript.entityId,
|
|
1639
|
+
text: assembled.text,
|
|
1640
|
+
segments: assembled.segments.map((segment) => ({ ...segment })),
|
|
1641
|
+
...typeof own.phonemeScript === "string" ? { phonemeScript: own.phonemeScript } : {},
|
|
1642
|
+
...isJsonObject(own.prosody) ? { prosody: clone(own.prosody) } : {}
|
|
1643
|
+
});
|
|
1211
1644
|
},
|
|
1212
1645
|
create: (input) => this.createEntity(input),
|
|
1213
|
-
update: (input) =>
|
|
1646
|
+
update: (input) => {
|
|
1647
|
+
assertTrimmed(input.entity_id, "entity_id");
|
|
1648
|
+
if (!this.state.entities.some((entity) => entity.entity_id === input.entity_id)) throw new Error(`Entity id "${input.entity_id}" does not exist`);
|
|
1649
|
+
const updates = updateEntityFields(toDslRows(this.state), createEntityId(input.entity_id), input.payload);
|
|
1650
|
+
for (const row of updates) this.replaceOwnedPayload({
|
|
1651
|
+
entity_id: row.entityId,
|
|
1652
|
+
payload: row.payload
|
|
1653
|
+
});
|
|
1654
|
+
},
|
|
1655
|
+
declareFields: (input) => {
|
|
1656
|
+
const current = this.state.entities.find((entity) => entity.entity_id === input.entity_id);
|
|
1657
|
+
if (current === void 0) throw new Error(`Unknown entity "${input.entity_id}"`);
|
|
1658
|
+
assembleEntityContent(toDslRows(this.state), createEntityId(input.entity_id));
|
|
1659
|
+
this.replaceOwnedPayload({
|
|
1660
|
+
entity_id: input.entity_id,
|
|
1661
|
+
payload: {
|
|
1662
|
+
...current.payload,
|
|
1663
|
+
...input.payload
|
|
1664
|
+
}
|
|
1665
|
+
});
|
|
1666
|
+
},
|
|
1214
1667
|
delete: (input) => this.deleteEntity(input),
|
|
1215
|
-
|
|
1668
|
+
ensureMedia: (fact) => this.ensureMedia(fact)
|
|
1216
1669
|
};
|
|
1217
1670
|
}
|
|
1218
1671
|
buildRelationFacade() {
|
|
@@ -1226,7 +1679,8 @@ var EntitySandbox = class {
|
|
|
1226
1679
|
link: (input) => this.link(input),
|
|
1227
1680
|
linkGenerated: (input) => this.linkGenerated(input),
|
|
1228
1681
|
linkClipAnchor: (input) => this.linkClipAnchor(input),
|
|
1229
|
-
|
|
1682
|
+
linkPhoneticScriptRender: (input) => this.linkPhoneticScriptRender(input),
|
|
1683
|
+
linkAudioScriptSource: (input) => this.linkAudioScriptSource(input),
|
|
1230
1684
|
unlink: (input) => this.unlinkRelation(input)
|
|
1231
1685
|
};
|
|
1232
1686
|
}
|
|
@@ -1234,6 +1688,21 @@ var EntitySandbox = class {
|
|
|
1234
1688
|
if (!isKnownEntityKind(input.entity_kind)) throw new Error(`Unknown or extension Entity kind "${String(input.entity_kind)}"`);
|
|
1235
1689
|
const payload = clone(input.payload);
|
|
1236
1690
|
if (!isJsonObject(payload)) throw new Error("Entity payload must contain only JSON values");
|
|
1691
|
+
if (input.entity_kind === "timeline" || input.entity_kind === "track") {
|
|
1692
|
+
const matches = this.state.entities.filter((entity) => entity.entity_kind === input.entity_kind && (input.entity_kind === "timeline" || entity.payload.role === payload.role));
|
|
1693
|
+
if (matches.length > 1) throw new Error(`Ambiguous editor ${input.entity_kind}; resolve existing identities`);
|
|
1694
|
+
if (matches[0] !== void 0) return this.reuseEntity(matches[0], input, payload);
|
|
1695
|
+
}
|
|
1696
|
+
const external = payload.external;
|
|
1697
|
+
if (isMediaAssetVariantKind(input.entity_kind) && isJsonObject(external) && (external.system === "memota" || external.system === "memota-speech") && typeof external.key === "string") {
|
|
1698
|
+
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);
|
|
1699
|
+
if (matches.length > 1) throw new Error(`Ambiguous Asset bindings for ${external.key}`);
|
|
1700
|
+
const existing = matches[0];
|
|
1701
|
+
if (existing !== void 0) {
|
|
1702
|
+
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}`);
|
|
1703
|
+
return this.reuseEntity(existing, input, payload);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1237
1706
|
const entityId = input.entity_id ?? this.idFactory("entity");
|
|
1238
1707
|
assertTrimmed(entityId, "entity_id");
|
|
1239
1708
|
const entity = {
|
|
@@ -1252,7 +1721,21 @@ var EntitySandbox = class {
|
|
|
1252
1721
|
});
|
|
1253
1722
|
return entityId;
|
|
1254
1723
|
}
|
|
1255
|
-
|
|
1724
|
+
reuseEntity(existing, input, payload) {
|
|
1725
|
+
if (input.entity_id !== void 0 && input.entity_id !== existing.entity_id) throw new Error(`Entity already exists; reuse ${existing.entity_id}`);
|
|
1726
|
+
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}`);
|
|
1727
|
+
const merged = {
|
|
1728
|
+
...existing.payload,
|
|
1729
|
+
...payload
|
|
1730
|
+
};
|
|
1731
|
+
if (!sameJson(existing.payload, merged)) this.replaceOwnedPayload({
|
|
1732
|
+
entity_id: existing.entity_id,
|
|
1733
|
+
payload: merged
|
|
1734
|
+
});
|
|
1735
|
+
return existing.entity_id;
|
|
1736
|
+
}
|
|
1737
|
+
/** Host persistence adapter; never exposed as the DSL field update operation. */
|
|
1738
|
+
replaceOwnedPayload(input) {
|
|
1256
1739
|
assertTrimmed(input.entity_id, "entity_id");
|
|
1257
1740
|
const payload = clone(input.payload);
|
|
1258
1741
|
if (!isJsonObject(payload)) throw new Error("Entity payload must contain only JSON values");
|
|
@@ -1269,21 +1752,42 @@ var EntitySandbox = class {
|
|
|
1269
1752
|
entity_id: input.entity_id
|
|
1270
1753
|
});
|
|
1271
1754
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1755
|
+
ensureMedia(fact) {
|
|
1756
|
+
const checkpoint = this.commandCount;
|
|
1757
|
+
try {
|
|
1758
|
+
const imported = importMediaAsset(toDslRows(this.state), fact, this.idFactory);
|
|
1759
|
+
this.appendResourceRows(imported.rows);
|
|
1760
|
+
return { contentEntityId: imported.contentEntityId };
|
|
1761
|
+
} catch (error) {
|
|
1762
|
+
this.rollbackTo(checkpoint);
|
|
1763
|
+
throw error;
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
appendResourceRows(rows) {
|
|
1767
|
+
for (const row of rows.entities) {
|
|
1768
|
+
const existing = this.state.entities.find((entity) => entity.entity_id === row.entityId);
|
|
1769
|
+
if (existing === void 0) this.createEntity({
|
|
1770
|
+
entity_id: row.entityId,
|
|
1771
|
+
entity_kind: row.entityKind,
|
|
1772
|
+
payload: row.payload
|
|
1773
|
+
});
|
|
1774
|
+
else if (!sameJson(existing.payload, row.payload)) this.replaceOwnedPayload({
|
|
1775
|
+
entity_id: row.entityId,
|
|
1776
|
+
payload: clone(row.payload)
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
for (const row of rows.relations) {
|
|
1780
|
+
if (this.state.relations.some((relation) => relation.relation_id === row.relationId)) continue;
|
|
1781
|
+
if (row.relationKind !== "timeline-track") throw new Error(`Unexpected resource Relation ${row.relationKind}`);
|
|
1782
|
+
this.link({
|
|
1783
|
+
relation_id: row.relationId,
|
|
1784
|
+
relation_kind: row.relationKind,
|
|
1785
|
+
endpoint_0_entity_id: row.endpoint0EntityId,
|
|
1786
|
+
endpoint_1_entity_id: row.endpoint1EntityId,
|
|
1787
|
+
metadata: clone(row.metadata),
|
|
1788
|
+
trace: clone(row.trace)
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1287
1791
|
}
|
|
1288
1792
|
link(input) {
|
|
1289
1793
|
if (input.relation_kind === "generated") throw new Error("Author generated Relations with relations.linkGenerated({ output_entity_id, input_entity_id })");
|
|
@@ -1346,19 +1850,40 @@ var EntitySandbox = class {
|
|
|
1346
1850
|
});
|
|
1347
1851
|
return relation.relation_id;
|
|
1348
1852
|
}
|
|
1349
|
-
|
|
1853
|
+
linkPhoneticScriptRender(input) {
|
|
1350
1854
|
const relation = this.relationFromInput({
|
|
1351
1855
|
...input.relation_id !== void 0 ? { relation_id: input.relation_id } : {},
|
|
1352
1856
|
endpoint_0_entity_id: input.output_entity_id,
|
|
1353
|
-
endpoint_1_entity_id: input.
|
|
1857
|
+
endpoint_1_entity_id: input.phonetic_script_entity_id,
|
|
1354
1858
|
metadata: {},
|
|
1355
1859
|
...input.trace !== void 0 ? { trace: input.trace } : {}
|
|
1356
|
-
}, "
|
|
1860
|
+
}, "phonetic-script-render");
|
|
1357
1861
|
const [output, script] = this.refsFor(relation);
|
|
1358
|
-
new BiRelationIndex().
|
|
1862
|
+
new BiRelationIndex().linkPhoneticScriptRender({
|
|
1359
1863
|
relationId: createRelationId(relation.relation_id),
|
|
1360
1864
|
output,
|
|
1865
|
+
phoneticScript: script,
|
|
1866
|
+
trace: relation.trace
|
|
1867
|
+
});
|
|
1868
|
+
this.record({
|
|
1869
|
+
kind: "link-relation",
|
|
1870
|
+
relation
|
|
1871
|
+
});
|
|
1872
|
+
return relation.relation_id;
|
|
1873
|
+
}
|
|
1874
|
+
linkAudioScriptSource(input) {
|
|
1875
|
+
const relation = this.relationFromInput({
|
|
1876
|
+
...input.relation_id !== void 0 ? { relation_id: input.relation_id } : {},
|
|
1877
|
+
endpoint_0_entity_id: input.script_entity_id,
|
|
1878
|
+
endpoint_1_entity_id: input.source_entity_id,
|
|
1879
|
+
metadata: {},
|
|
1880
|
+
...input.trace !== void 0 ? { trace: input.trace } : {}
|
|
1881
|
+
}, "audio-script-source");
|
|
1882
|
+
const [script, source] = this.refsFor(relation);
|
|
1883
|
+
new BiRelationIndex().linkAudioScriptSource({
|
|
1884
|
+
relationId: createRelationId(relation.relation_id),
|
|
1361
1885
|
script,
|
|
1886
|
+
source,
|
|
1362
1887
|
trace: relation.trace
|
|
1363
1888
|
});
|
|
1364
1889
|
this.record({
|
|
@@ -1452,7 +1977,7 @@ var EntitySandbox = class {
|
|
|
1452
1977
|
};
|
|
1453
1978
|
function isImportedMemotaAsset(payload, assetId) {
|
|
1454
1979
|
const external = payload[ASSET_SOURCE_KEY];
|
|
1455
|
-
return external != null && !Array.isArray(external) && typeof external === "object" && external.system === MEMOTA_SYSTEM && external.key === assetId;
|
|
1980
|
+
return external != null && !Array.isArray(external) && typeof external === "object" && (external.system === MEMOTA_SYSTEM || external.system === "memota-speech") && external.key === assetId;
|
|
1456
1981
|
}
|
|
1457
1982
|
function toDslEntity(entity) {
|
|
1458
1983
|
return {
|
|
@@ -1484,6 +2009,13 @@ function cloneSnapshot(state) {
|
|
|
1484
2009
|
function clone(value) {
|
|
1485
2010
|
return structuredClone(value);
|
|
1486
2011
|
}
|
|
2012
|
+
function sameJson(left, right) {
|
|
2013
|
+
if (left === right) return true;
|
|
2014
|
+
if (Array.isArray(left) && Array.isArray(right)) return left.length === right.length && left.every((value, index) => sameJson(value, right[index]));
|
|
2015
|
+
if (!isJsonObject(left) || !isJsonObject(right)) return false;
|
|
2016
|
+
const keys = Object.keys(left);
|
|
2017
|
+
return keys.length === Object.keys(right).length && keys.every((key) => Object.hasOwn(right, key) && sameJson(left[key], right[key]));
|
|
2018
|
+
}
|
|
1487
2019
|
function assertTrimmed(value, label) {
|
|
1488
2020
|
if (typeof value !== "string" || value.length === 0 || value.trim() !== value) throw new Error(`${label} must be a non-empty trimmed string`);
|
|
1489
2021
|
}
|
|
@@ -1836,6 +2368,6 @@ function replayJournalSync(adapter, journal) {
|
|
|
1836
2368
|
}
|
|
1837
2369
|
}
|
|
1838
2370
|
//#endregion
|
|
1839
|
-
export {
|
|
2371
|
+
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
2372
|
|
|
1841
|
-
//# sourceMappingURL=script-session-
|
|
2373
|
+
//# sourceMappingURL=script-session-1eqBGegp.mjs.map
|