@mengine/medeo-tool 1.2.1-alpha.9 → 1.3.1-alpha.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -14
- package/dist/{entity-contract-DycLxdQ5.d.mts → entity-contract-DHasvrhq.d.mts} +89 -33
- package/dist/index.d.mts +51 -178
- package/dist/index.mjs +300 -497
- package/dist/index.mjs.map +1 -1
- package/dist/sandbox-api.d.mts +149 -44
- package/dist/{script-session-CHyIUBkO.mjs → script-session-DLBhoA1R.mjs} +682 -132
- package/dist/script-session-DLBhoA1R.mjs.map +1 -0
- package/dist/worker-entry.d.mts +1 -1
- package/dist/worker-entry.mjs +39 -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 typeof value === "object" && value !== null && !Array.isArray(value) && isJsonValue(value, /* @__PURE__ */ new Set());
|
|
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,15 @@ 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
|
+
if (this.state.audioScriptEntityId === null) this.state.audioScriptEntityId = foundation.audioScriptEntityId;
|
|
1564
|
+
}
|
|
1565
|
+
get audioScriptEntityId() {
|
|
1566
|
+
return this.state.audioScriptEntityId;
|
|
1567
|
+
}
|
|
1158
1568
|
rollbackTo(index) {
|
|
1159
1569
|
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
1570
|
const prefix = this.commands.slice(0, index);
|
|
@@ -1165,7 +1575,15 @@ var EntitySandbox = class {
|
|
|
1165
1575
|
this.onTruncate?.(index);
|
|
1166
1576
|
}
|
|
1167
1577
|
buildPlan() {
|
|
1168
|
-
|
|
1578
|
+
const rows = toDslRows(this.state);
|
|
1579
|
+
decodeEntityRelationRows(rows, numericMarkerComparators);
|
|
1580
|
+
if (this.state.revision !== 0 || rows.entities.length !== 0 || this.commands.length !== 0) {
|
|
1581
|
+
assertCanonicalEditorResources(rows);
|
|
1582
|
+
readDocumentAudioScript({
|
|
1583
|
+
rows,
|
|
1584
|
+
audioScriptEntityId: this.state.audioScriptEntityId
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1169
1587
|
const currentEntityIds = new Set(this.state.entities.map((entity) => entity.entity_id));
|
|
1170
1588
|
const currentRelationIds = new Set(this.state.relations.map((relation) => relation.relation_id));
|
|
1171
1589
|
return {
|
|
@@ -1198,21 +1616,67 @@ var EntitySandbox = class {
|
|
|
1198
1616
|
}
|
|
1199
1617
|
return lines.join("\n");
|
|
1200
1618
|
}
|
|
1619
|
+
assembledEntity(entity) {
|
|
1620
|
+
const assembled = assembleEntityContent(toDslRows(this.state), createEntityId(entity.entity_id));
|
|
1621
|
+
return {
|
|
1622
|
+
...clone(entity),
|
|
1623
|
+
payload: assembled.payload
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1201
1626
|
buildEntityFacade() {
|
|
1202
1627
|
return {
|
|
1203
|
-
list: () =>
|
|
1628
|
+
list: () => this.state.entities.map((entity) => this.assembledEntity(entity)),
|
|
1204
1629
|
get: (entityId) => {
|
|
1205
1630
|
const entity = this.state.entities.find((candidate) => candidate.entity_id === entityId);
|
|
1206
|
-
return entity == null ? null :
|
|
1631
|
+
return entity == null ? null : this.assembledEntity(entity);
|
|
1207
1632
|
},
|
|
1208
1633
|
findByAssetId: (assetId) => {
|
|
1209
1634
|
assertTrimmed(assetId, "assetId");
|
|
1210
|
-
return clone(this.state.entities.filter((entity) => entity.entity_kind
|
|
1635
|
+
return clone(this.state.entities.map((entity) => this.assembledEntity(entity)).filter((entity) => isMediaAssetVariantKind(entity.entity_kind) && isImportedMemotaAsset(entity.payload, assetId)));
|
|
1636
|
+
},
|
|
1637
|
+
readCaptionContent: (entityId) => {
|
|
1638
|
+
const assembled = assembleCaptionContent(toDslRows(this.state), createEntityId(entityId));
|
|
1639
|
+
return clone({
|
|
1640
|
+
audio_script_entity_id: assembled.audioScript.entityId,
|
|
1641
|
+
text: assembled.text,
|
|
1642
|
+
segments: assembled.segments.map((segment) => ({ ...segment }))
|
|
1643
|
+
});
|
|
1644
|
+
},
|
|
1645
|
+
readPhoneticScriptContent: (entityId) => {
|
|
1646
|
+
const assembled = assemblePhoneticScriptContent(toDslRows(this.state), createEntityId(entityId));
|
|
1647
|
+
const own = assembled.phoneticScript.payload;
|
|
1648
|
+
return clone({
|
|
1649
|
+
audio_script_entity_id: assembled.audioScript.entityId,
|
|
1650
|
+
text: assembled.text,
|
|
1651
|
+
segments: assembled.segments.map((segment) => ({ ...segment })),
|
|
1652
|
+
...typeof own.phonemeScript === "string" ? { phonemeScript: own.phonemeScript } : {},
|
|
1653
|
+
...isJsonObject(own.prosody) ? { prosody: clone(own.prosody) } : {}
|
|
1654
|
+
});
|
|
1211
1655
|
},
|
|
1212
1656
|
create: (input) => this.createEntity(input),
|
|
1213
|
-
update: (input) =>
|
|
1657
|
+
update: (input) => {
|
|
1658
|
+
assertTrimmed(input.entity_id, "entity_id");
|
|
1659
|
+
if (!this.state.entities.some((entity) => entity.entity_id === input.entity_id)) throw new Error(`Entity id "${input.entity_id}" does not exist`);
|
|
1660
|
+
const updates = updateEntityFields(toDslRows(this.state), createEntityId(input.entity_id), input.payload);
|
|
1661
|
+
for (const row of updates) this.replaceOwnedPayload({
|
|
1662
|
+
entity_id: row.entityId,
|
|
1663
|
+
payload: row.payload
|
|
1664
|
+
});
|
|
1665
|
+
},
|
|
1666
|
+
declareFields: (input) => {
|
|
1667
|
+
const current = this.state.entities.find((entity) => entity.entity_id === input.entity_id);
|
|
1668
|
+
if (current === void 0) throw new Error(`Unknown entity "${input.entity_id}"`);
|
|
1669
|
+
assembleEntityContent(toDslRows(this.state), createEntityId(input.entity_id));
|
|
1670
|
+
this.replaceOwnedPayload({
|
|
1671
|
+
entity_id: input.entity_id,
|
|
1672
|
+
payload: {
|
|
1673
|
+
...current.payload,
|
|
1674
|
+
...input.payload
|
|
1675
|
+
}
|
|
1676
|
+
});
|
|
1677
|
+
},
|
|
1214
1678
|
delete: (input) => this.deleteEntity(input),
|
|
1215
|
-
|
|
1679
|
+
ensureMedia: (fact) => this.ensureMedia(fact)
|
|
1216
1680
|
};
|
|
1217
1681
|
}
|
|
1218
1682
|
buildRelationFacade() {
|
|
@@ -1226,7 +1690,8 @@ var EntitySandbox = class {
|
|
|
1226
1690
|
link: (input) => this.link(input),
|
|
1227
1691
|
linkGenerated: (input) => this.linkGenerated(input),
|
|
1228
1692
|
linkClipAnchor: (input) => this.linkClipAnchor(input),
|
|
1229
|
-
|
|
1693
|
+
linkPhoneticScriptRender: (input) => this.linkPhoneticScriptRender(input),
|
|
1694
|
+
linkAudioScriptSource: (input) => this.linkAudioScriptSource(input),
|
|
1230
1695
|
unlink: (input) => this.unlinkRelation(input)
|
|
1231
1696
|
};
|
|
1232
1697
|
}
|
|
@@ -1234,8 +1699,27 @@ var EntitySandbox = class {
|
|
|
1234
1699
|
if (!isKnownEntityKind(input.entity_kind)) throw new Error(`Unknown or extension Entity kind "${String(input.entity_kind)}"`);
|
|
1235
1700
|
const payload = clone(input.payload);
|
|
1236
1701
|
if (!isJsonObject(payload)) throw new Error("Entity payload must contain only JSON values");
|
|
1702
|
+
if (input.entity_kind === "timeline" || input.entity_kind === "track") {
|
|
1703
|
+
const matches = this.state.entities.filter((entity) => entity.entity_kind === input.entity_kind && (input.entity_kind === "timeline" || entity.payload.role === payload.role));
|
|
1704
|
+
if (matches.length > 1) throw new Error(`Ambiguous editor ${input.entity_kind}; resolve existing identities`);
|
|
1705
|
+
if (matches[0] !== void 0) return this.reuseEntity(matches[0], input, payload);
|
|
1706
|
+
}
|
|
1707
|
+
const external = payload.external;
|
|
1708
|
+
if (isMediaAssetVariantKind(input.entity_kind) && isJsonObject(external) && (external.system === "memota" || external.system === "memota-speech") && typeof external.key === "string") {
|
|
1709
|
+
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);
|
|
1710
|
+
if (matches.length > 1) throw new Error(`Ambiguous Asset bindings for ${external.key}`);
|
|
1711
|
+
const existing = matches[0];
|
|
1712
|
+
if (existing !== void 0) {
|
|
1713
|
+
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}`);
|
|
1714
|
+
return this.reuseEntity(existing, input, payload);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1237
1717
|
const entityId = input.entity_id ?? this.idFactory("entity");
|
|
1238
1718
|
assertTrimmed(entityId, "entity_id");
|
|
1719
|
+
if (input.entity_kind === "audio-script") {
|
|
1720
|
+
const existing = this.state.entities.find((entity) => entity.entity_kind === "audio-script");
|
|
1721
|
+
if (existing !== void 0 && existing.entity_id !== entityId) throw new Error(`Editor requires exactly one AudioScript; edit ${existing.entity_id} instead of creating ${entityId}`);
|
|
1722
|
+
}
|
|
1239
1723
|
const entity = {
|
|
1240
1724
|
entity_id: entityId,
|
|
1241
1725
|
entity_kind: input.entity_kind,
|
|
@@ -1252,7 +1736,21 @@ var EntitySandbox = class {
|
|
|
1252
1736
|
});
|
|
1253
1737
|
return entityId;
|
|
1254
1738
|
}
|
|
1255
|
-
|
|
1739
|
+
reuseEntity(existing, input, payload) {
|
|
1740
|
+
if (input.entity_id !== void 0 && input.entity_id !== existing.entity_id) throw new Error(`Entity already exists; reuse ${existing.entity_id}`);
|
|
1741
|
+
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}`);
|
|
1742
|
+
const merged = {
|
|
1743
|
+
...existing.payload,
|
|
1744
|
+
...payload
|
|
1745
|
+
};
|
|
1746
|
+
if (!sameJson(existing.payload, merged)) this.replaceOwnedPayload({
|
|
1747
|
+
entity_id: existing.entity_id,
|
|
1748
|
+
payload: merged
|
|
1749
|
+
});
|
|
1750
|
+
return existing.entity_id;
|
|
1751
|
+
}
|
|
1752
|
+
/** Host persistence adapter; never exposed as the DSL field update operation. */
|
|
1753
|
+
replaceOwnedPayload(input) {
|
|
1256
1754
|
assertTrimmed(input.entity_id, "entity_id");
|
|
1257
1755
|
const payload = clone(input.payload);
|
|
1258
1756
|
if (!isJsonObject(payload)) throw new Error("Entity payload must contain only JSON values");
|
|
@@ -1269,21 +1767,42 @@ var EntitySandbox = class {
|
|
|
1269
1767
|
entity_id: input.entity_id
|
|
1270
1768
|
});
|
|
1271
1769
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1770
|
+
ensureMedia(fact) {
|
|
1771
|
+
const checkpoint = this.commandCount;
|
|
1772
|
+
try {
|
|
1773
|
+
const imported = importMediaAsset(toDslRows(this.state), fact, this.idFactory);
|
|
1774
|
+
this.appendResourceRows(imported.rows);
|
|
1775
|
+
return { contentEntityId: imported.contentEntityId };
|
|
1776
|
+
} catch (error) {
|
|
1777
|
+
this.rollbackTo(checkpoint);
|
|
1778
|
+
throw error;
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
appendResourceRows(rows) {
|
|
1782
|
+
for (const row of rows.entities) {
|
|
1783
|
+
const existing = this.state.entities.find((entity) => entity.entity_id === row.entityId);
|
|
1784
|
+
if (existing === void 0) this.createEntity({
|
|
1785
|
+
entity_id: row.entityId,
|
|
1786
|
+
entity_kind: row.entityKind,
|
|
1787
|
+
payload: row.payload
|
|
1788
|
+
});
|
|
1789
|
+
else if (!sameJson(existing.payload, row.payload)) this.replaceOwnedPayload({
|
|
1790
|
+
entity_id: row.entityId,
|
|
1791
|
+
payload: clone(row.payload)
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
for (const row of rows.relations) {
|
|
1795
|
+
if (this.state.relations.some((relation) => relation.relation_id === row.relationId)) continue;
|
|
1796
|
+
if (row.relationKind !== "timeline-track") throw new Error(`Unexpected resource Relation ${row.relationKind}`);
|
|
1797
|
+
this.link({
|
|
1798
|
+
relation_id: row.relationId,
|
|
1799
|
+
relation_kind: row.relationKind,
|
|
1800
|
+
endpoint_0_entity_id: row.endpoint0EntityId,
|
|
1801
|
+
endpoint_1_entity_id: row.endpoint1EntityId,
|
|
1802
|
+
metadata: clone(row.metadata),
|
|
1803
|
+
trace: clone(row.trace)
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1287
1806
|
}
|
|
1288
1807
|
link(input) {
|
|
1289
1808
|
if (input.relation_kind === "generated") throw new Error("Author generated Relations with relations.linkGenerated({ output_entity_id, input_entity_id })");
|
|
@@ -1346,19 +1865,40 @@ var EntitySandbox = class {
|
|
|
1346
1865
|
});
|
|
1347
1866
|
return relation.relation_id;
|
|
1348
1867
|
}
|
|
1349
|
-
|
|
1868
|
+
linkPhoneticScriptRender(input) {
|
|
1350
1869
|
const relation = this.relationFromInput({
|
|
1351
1870
|
...input.relation_id !== void 0 ? { relation_id: input.relation_id } : {},
|
|
1352
1871
|
endpoint_0_entity_id: input.output_entity_id,
|
|
1353
|
-
endpoint_1_entity_id: input.
|
|
1872
|
+
endpoint_1_entity_id: input.phonetic_script_entity_id,
|
|
1354
1873
|
metadata: {},
|
|
1355
1874
|
...input.trace !== void 0 ? { trace: input.trace } : {}
|
|
1356
|
-
}, "
|
|
1875
|
+
}, "phonetic-script-render");
|
|
1357
1876
|
const [output, script] = this.refsFor(relation);
|
|
1358
|
-
new BiRelationIndex().
|
|
1877
|
+
new BiRelationIndex().linkPhoneticScriptRender({
|
|
1359
1878
|
relationId: createRelationId(relation.relation_id),
|
|
1360
1879
|
output,
|
|
1880
|
+
phoneticScript: script,
|
|
1881
|
+
trace: relation.trace
|
|
1882
|
+
});
|
|
1883
|
+
this.record({
|
|
1884
|
+
kind: "link-relation",
|
|
1885
|
+
relation
|
|
1886
|
+
});
|
|
1887
|
+
return relation.relation_id;
|
|
1888
|
+
}
|
|
1889
|
+
linkAudioScriptSource(input) {
|
|
1890
|
+
const relation = this.relationFromInput({
|
|
1891
|
+
...input.relation_id !== void 0 ? { relation_id: input.relation_id } : {},
|
|
1892
|
+
endpoint_0_entity_id: input.script_entity_id,
|
|
1893
|
+
endpoint_1_entity_id: input.source_entity_id,
|
|
1894
|
+
metadata: {},
|
|
1895
|
+
...input.trace !== void 0 ? { trace: input.trace } : {}
|
|
1896
|
+
}, "audio-script-source");
|
|
1897
|
+
const [script, source] = this.refsFor(relation);
|
|
1898
|
+
new BiRelationIndex().linkAudioScriptSource({
|
|
1899
|
+
relationId: createRelationId(relation.relation_id),
|
|
1361
1900
|
script,
|
|
1901
|
+
source,
|
|
1362
1902
|
trace: relation.trace
|
|
1363
1903
|
});
|
|
1364
1904
|
this.record({
|
|
@@ -1408,10 +1948,12 @@ var EntitySandbox = class {
|
|
|
1408
1948
|
apply(command, enforceIdentity) {
|
|
1409
1949
|
switch (command.kind) {
|
|
1410
1950
|
case "create-entity": {
|
|
1951
|
+
if (command.entity.entity_kind === "audio-script" && this.state.entities.some((entity) => entity.entity_kind === "audio-script")) throw new Error("The project AudioScript already exists; edit its segments instead");
|
|
1411
1952
|
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
1953
|
const original = this.original.entities.find((entity) => entity.entity_id === command.entity.entity_id);
|
|
1413
1954
|
if (enforceIdentity && original != null && original.entity_kind !== command.entity.entity_kind) throw new Error(`Entity id "${command.entity.entity_id}" was originally kind "${original.entity_kind}" and cannot be recreated as "${command.entity.entity_kind}"`);
|
|
1414
1955
|
this.state.entities.push(clone(command.entity));
|
|
1956
|
+
if (command.entity.entity_kind === "audio-script" && this.state.audioScriptEntityId === null) this.state.audioScriptEntityId = command.entity.entity_id;
|
|
1415
1957
|
return;
|
|
1416
1958
|
}
|
|
1417
1959
|
case "update-entity": {
|
|
@@ -1426,6 +1968,7 @@ var EntitySandbox = class {
|
|
|
1426
1968
|
return;
|
|
1427
1969
|
}
|
|
1428
1970
|
case "delete-entity": {
|
|
1971
|
+
if (this.state.entities.find((entity) => entity.entity_id === command.entity_id)?.entity_kind === "audio-script") throw new Error("The document AudioScript is a fixed project identity and cannot be deleted");
|
|
1429
1972
|
const index = this.state.entities.findIndex((entity) => entity.entity_id === command.entity_id);
|
|
1430
1973
|
if (index < 0) throw new Error(`Entity id "${command.entity_id}" does not exist`);
|
|
1431
1974
|
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 +1995,7 @@ var EntitySandbox = class {
|
|
|
1452
1995
|
};
|
|
1453
1996
|
function isImportedMemotaAsset(payload, assetId) {
|
|
1454
1997
|
const external = payload[ASSET_SOURCE_KEY];
|
|
1455
|
-
return external != null && !Array.isArray(external) && typeof external === "object" && external.system === MEMOTA_SYSTEM && external.key === assetId;
|
|
1998
|
+
return external != null && !Array.isArray(external) && typeof external === "object" && (external.system === MEMOTA_SYSTEM || external.system === "memota-speech") && external.key === assetId;
|
|
1456
1999
|
}
|
|
1457
2000
|
function toDslEntity(entity) {
|
|
1458
2001
|
return {
|
|
@@ -1484,6 +2027,13 @@ function cloneSnapshot(state) {
|
|
|
1484
2027
|
function clone(value) {
|
|
1485
2028
|
return structuredClone(value);
|
|
1486
2029
|
}
|
|
2030
|
+
function sameJson(left, right) {
|
|
2031
|
+
if (left === right) return true;
|
|
2032
|
+
if (Array.isArray(left) && Array.isArray(right)) return left.length === right.length && left.every((value, index) => sameJson(value, right[index]));
|
|
2033
|
+
if (!isJsonObject(left) || !isJsonObject(right)) return false;
|
|
2034
|
+
const keys = Object.keys(left);
|
|
2035
|
+
return keys.length === Object.keys(right).length && keys.every((key) => Object.hasOwn(right, key) && sameJson(left[key], right[key]));
|
|
2036
|
+
}
|
|
1487
2037
|
function assertTrimmed(value, label) {
|
|
1488
2038
|
if (typeof value !== "string" || value.length === 0 || value.trim() !== value) throw new Error(`${label} must be a non-empty trimmed string`);
|
|
1489
2039
|
}
|
|
@@ -1836,6 +2386,6 @@ function replayJournalSync(adapter, journal) {
|
|
|
1836
2386
|
}
|
|
1837
2387
|
}
|
|
1838
2388
|
//#endregion
|
|
1839
|
-
export {
|
|
2389
|
+
export { isMediaAssetVariantKind as a, renderCompactProjection as c, createRelationId as i, EntitySandbox as n, collectAffectedPartIds as o, createEntityId as r, renderPreview as s, EditSandboxSession as t };
|
|
1840
2390
|
|
|
1841
|
-
//# sourceMappingURL=script-session-
|
|
2391
|
+
//# sourceMappingURL=script-session-DLBhoA1R.mjs.map
|