@mengine/medeo-client 2.0.1-alpha.27 → 2.0.1-alpha.29
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/dist/{document-BGYPKtJ3.js → document-B3RLmTzA.js} +297 -32
- package/dist/{index-CcIoMxs1.d.ts → index-DaabF2vA.d.ts} +342 -76
- package/dist/index.d.ts +88 -17
- package/dist/index.js +422 -32
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/package.json +4 -4
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { u as volumeSchema } from "./shared-iWnU2osE.js";
|
|
2
2
|
import { Mirror, schema, validateSchema } from "loro-mirror";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import { LoroDoc, LoroMap, LoroMovableList, LoroText, UndoManager } from "loro-crdt";
|
|
5
4
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
6
|
-
import { bytesToHex } from "@noble/hashes/utils.js";
|
|
5
|
+
import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils.js";
|
|
6
|
+
import { LoroDoc, LoroMap, LoroMovableList, LoroText, UndoManager } from "loro-crdt";
|
|
7
7
|
import { produce } from "immer";
|
|
8
8
|
//#region src/client/base64.ts
|
|
9
9
|
function bytesToBase64(bytes) {
|
|
@@ -1508,6 +1508,7 @@ const ENTITY_KINDS = Object.freeze({
|
|
|
1508
1508
|
]),
|
|
1509
1509
|
voice: Object.freeze(["Container", "IPBound"]),
|
|
1510
1510
|
"sequence-marker": Object.freeze([]),
|
|
1511
|
+
script: Object.freeze(["SegmentText"]),
|
|
1511
1512
|
"audio-script": Object.freeze(["SegmentText", "MediaFile"]),
|
|
1512
1513
|
"phonetic-script": Object.freeze(["SegmentText", "MediaFile"]),
|
|
1513
1514
|
caption: Object.freeze([
|
|
@@ -1809,6 +1810,32 @@ function entityToRow(entity) {
|
|
|
1809
1810
|
};
|
|
1810
1811
|
}
|
|
1811
1812
|
//#endregion
|
|
1813
|
+
//#region ../medeo-dsl/src/script-content.ts
|
|
1814
|
+
/** Canonical text used to identify one immutable professional Script version. */
|
|
1815
|
+
function normalizeScriptText(text) {
|
|
1816
|
+
return text.normalize("NFC").replaceAll("\r\n", "\n").replaceAll("\r", "\n").split("\n").map((line) => line.trimEnd()).join("\n");
|
|
1817
|
+
}
|
|
1818
|
+
/** Preserve segment identity, boundaries and order while removing textual encoding noise. */
|
|
1819
|
+
function normalizeScriptSegments(segments) {
|
|
1820
|
+
return segments.map(({ segmentId, text, language }) => ({
|
|
1821
|
+
segmentId,
|
|
1822
|
+
text: normalizeScriptText(text),
|
|
1823
|
+
...language === void 0 ? {} : { language: language.normalize("NFC").trim() }
|
|
1824
|
+
}));
|
|
1825
|
+
}
|
|
1826
|
+
/** Content identity: ordered canonical text with array boundaries, never local ids or language metadata. */
|
|
1827
|
+
function scriptContentHash(segments) {
|
|
1828
|
+
return `sha256:${bytesToHex(sha256(utf8ToBytes(JSON.stringify(normalizeScriptSegments(segments).map((segment) => segment.text)))))}`;
|
|
1829
|
+
}
|
|
1830
|
+
//#endregion
|
|
1831
|
+
//#region ../medeo-dsl/src/script-revision.ts
|
|
1832
|
+
const HASH = /^sha256:[0-9a-f]{64}$/;
|
|
1833
|
+
function readScriptRevisionTrace(value) {
|
|
1834
|
+
if (!isJsonObject(value)) return null;
|
|
1835
|
+
if (Object.keys(value).length !== 4 || value.command !== "revise-script" || typeof value.editId !== "string" || value.editId.trim() !== value.editId || value.editId === "" || typeof value.sourceContentHash !== "string" || !HASH.test(value.sourceContentHash) || typeof value.resultContentHash !== "string" || !HASH.test(value.resultContentHash)) return null;
|
|
1836
|
+
return value;
|
|
1837
|
+
}
|
|
1838
|
+
//#endregion
|
|
1812
1839
|
//#region ../medeo-dsl/src/invariants.ts
|
|
1813
1840
|
/** An entity is made from at most one Asset; two would leave its bytes ambiguous. */
|
|
1814
1841
|
/** Validates one complete set of entities and its authoritative Relation rows. */
|
|
@@ -1848,6 +1875,38 @@ function validateEntityRelationSet(entityRefs, index) {
|
|
|
1848
1875
|
issues.push(...validateSequenceComposition(entity));
|
|
1849
1876
|
}
|
|
1850
1877
|
issues.push(...validateClipAnchorCycles(entities, index));
|
|
1878
|
+
issues.push(...validateScriptRevisionRelations(entities, index));
|
|
1879
|
+
return issues;
|
|
1880
|
+
}
|
|
1881
|
+
function validateScriptRevisionRelations(entities, index) {
|
|
1882
|
+
const byId = new Map(entities.map((entity) => [entity.entityId, entity.current()]));
|
|
1883
|
+
const seenRelations = /* @__PURE__ */ new Set();
|
|
1884
|
+
const editIds = /* @__PURE__ */ new Set();
|
|
1885
|
+
const parentCounts = /* @__PURE__ */ new Map();
|
|
1886
|
+
const issues = [];
|
|
1887
|
+
for (const entity of entities) for (const relation of index.relationsOf(entity)) {
|
|
1888
|
+
const row = relation.toRow();
|
|
1889
|
+
if (row == null || seenRelations.has(row.relationId)) continue;
|
|
1890
|
+
seenRelations.add(row.relationId);
|
|
1891
|
+
const result = byId.get(row.endpoint0EntityId);
|
|
1892
|
+
const source = byId.get(row.endpoint1EntityId);
|
|
1893
|
+
if (row.relationKind !== "derived-from" || result?.entityKind !== "script" || source?.entityKind !== "script") continue;
|
|
1894
|
+
const trace = readScriptRevisionTrace(row.trace);
|
|
1895
|
+
const invalid = trace == null || trace.resultContentHash !== result.contentHash || trace.sourceContentHash !== source.contentHash || trace.resultContentHash === trace.sourceContentHash || trace != null && editIds.has(trace.editId);
|
|
1896
|
+
if (trace != null) editIds.add(trace.editId);
|
|
1897
|
+
const parentCount = (parentCounts.get(row.endpoint0EntityId) ?? 0) + 1;
|
|
1898
|
+
parentCounts.set(row.endpoint0EntityId, parentCount);
|
|
1899
|
+
if (invalid) issues.push({
|
|
1900
|
+
code: "invalid_script_revision",
|
|
1901
|
+
entityId: row.endpoint0EntityId,
|
|
1902
|
+
message: "Script revision requires one canonical revise-script trace matching distinct result and source text"
|
|
1903
|
+
});
|
|
1904
|
+
if (parentCount > 1) issues.push({
|
|
1905
|
+
code: "invalid_script_revision",
|
|
1906
|
+
entityId: row.endpoint0EntityId,
|
|
1907
|
+
message: "A Script revision may name exactly one source Script"
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1851
1910
|
return issues;
|
|
1852
1911
|
}
|
|
1853
1912
|
function validateMarkerUse(marker, index) {
|
|
@@ -2068,7 +2127,7 @@ function validateKnownEntityPayload(entity) {
|
|
|
2068
2127
|
const value = entity;
|
|
2069
2128
|
const problems = [];
|
|
2070
2129
|
if (value.lifecycle !== void 0 && !isRecord(value.lifecycle)) problems.push("lifecycle must be an object when present");
|
|
2071
|
-
if (entity.entityKind === "audio-script" || entity.entityKind === "phonetic-script") {
|
|
2130
|
+
if (entity.entityKind === "script" || entity.entityKind === "audio-script" || entity.entityKind === "phonetic-script") {
|
|
2072
2131
|
for (const key of [
|
|
2073
2132
|
"durationMs",
|
|
2074
2133
|
"extent",
|
|
@@ -2109,6 +2168,9 @@ function validateKnownEntityPayload(entity) {
|
|
|
2109
2168
|
case "audio-script":
|
|
2110
2169
|
validateScriptPayload(value, problems);
|
|
2111
2170
|
break;
|
|
2171
|
+
case "script":
|
|
2172
|
+
validateProfessionalScriptPayload(value, problems);
|
|
2173
|
+
break;
|
|
2112
2174
|
case "phonetic-script":
|
|
2113
2175
|
validatePhoneticScriptPayload(value, problems);
|
|
2114
2176
|
break;
|
|
@@ -2284,6 +2346,24 @@ function validateScriptPayload(value, problems) {
|
|
|
2284
2346
|
if (segment.language !== void 0 && typeof segment.language !== "string") problems.push(`segments[${index}].language must be a string when present`);
|
|
2285
2347
|
}
|
|
2286
2348
|
}
|
|
2349
|
+
function validateProfessionalScriptPayload(value, problems) {
|
|
2350
|
+
validateScriptPayload(value, problems);
|
|
2351
|
+
if (!Array.isArray(value.segments) || !value.segments.every(isCompleteScriptSegment)) return;
|
|
2352
|
+
const segments = value.segments;
|
|
2353
|
+
if (segments.length === 0) problems.push("segments must contain at least one screenplay segment");
|
|
2354
|
+
const ids = segments.map((segment) => segment.segmentId);
|
|
2355
|
+
if (ids.some((id) => id.trim() === "")) problems.push("segmentId must be a non-empty trimmed string");
|
|
2356
|
+
if (ids.some((id) => id.trim() !== id)) problems.push("segmentId must be a non-empty trimmed string");
|
|
2357
|
+
if (new Set(ids).size !== ids.length) problems.push("segmentId must be unique within the Script");
|
|
2358
|
+
const normalized = normalizeScriptSegments(segments);
|
|
2359
|
+
if (normalized.some((segment) => segment.text === "")) problems.push("segment text must be non-empty after normalization");
|
|
2360
|
+
if (segments.some((segment, index) => segment.text !== normalized[index].text)) problems.push("segment text must use canonical NFC, LF line endings, and trimmed line endings");
|
|
2361
|
+
if (segments.some((segment, index) => segment.language !== normalized[index].language)) problems.push("segment language must be canonical NFC and trimmed when present");
|
|
2362
|
+
if (typeof value.contentHash !== "string" || value.contentHash !== scriptContentHash(segments)) problems.push("contentHash must equal the SHA-256 of the canonical ordered segment texts");
|
|
2363
|
+
}
|
|
2364
|
+
function isCompleteScriptSegment(value) {
|
|
2365
|
+
return isRecord(value) && typeof value.segmentId === "string" && typeof value.text === "string" && (value.language === void 0 || typeof value.language === "string");
|
|
2366
|
+
}
|
|
2287
2367
|
function validateOptionalField(value, key, expectedType, problems) {
|
|
2288
2368
|
if (value[key] !== void 0 && typeof value[key] !== expectedType) problems.push(`${key} must be a ${expectedType} when present`);
|
|
2289
2369
|
}
|
|
@@ -2342,6 +2422,7 @@ function isOwnedLocalEntityValuePath(entityKind, path) {
|
|
|
2342
2422
|
if (entityKind === "voice" && /^voice\.(?:system|key|name)$/.test(path)) return true;
|
|
2343
2423
|
if (entityKind === "caption" && (path.startsWith("style.") || path === "selection.segmentId")) return true;
|
|
2344
2424
|
if ([
|
|
2425
|
+
"script",
|
|
2345
2426
|
"audio-script",
|
|
2346
2427
|
"caption",
|
|
2347
2428
|
"phonetic-script"
|
|
@@ -2351,6 +2432,7 @@ function isOwnedLocalEntityValuePath(entityKind, path) {
|
|
|
2351
2432
|
function isOwnedLocalIdPath(entityKind, path) {
|
|
2352
2433
|
if (path === "lifecycle.actorId") return true;
|
|
2353
2434
|
if ([
|
|
2435
|
+
"script",
|
|
2354
2436
|
"audio-script",
|
|
2355
2437
|
"caption",
|
|
2356
2438
|
"phonetic-script"
|
|
@@ -2495,7 +2577,7 @@ const toAssetRelationSpec = Object.freeze({
|
|
|
2495
2577
|
validateEndpoints: (endpoints) => hasExactlyOneAsset(endpoints),
|
|
2496
2578
|
validateMetadata: isAssetBinding
|
|
2497
2579
|
});
|
|
2498
|
-
/** Media
|
|
2580
|
+
/** Media lineage, speech inputs, transcription sources, and immutable script revisions. */
|
|
2499
2581
|
const MEDIA_VARIANT_KINDS = [
|
|
2500
2582
|
"video",
|
|
2501
2583
|
"image",
|
|
@@ -2517,7 +2599,7 @@ const derivedFromRelationSpec = Object.freeze({
|
|
|
2517
2599
|
kind: "derived-from",
|
|
2518
2600
|
ordered: true,
|
|
2519
2601
|
sequenced: false,
|
|
2520
|
-
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(MEDIA_VARIANT_KINDS), new Set(MEDIA_VARIANT_KINDS)) || hasKinds(endpoints, new Set(["voiceover-recording"]), new Set(["phonetic-script"])) || hasKinds(endpoints, new Set(["audio-script"]), new Set(MEDIA_AUDIBLE_KINDS)),
|
|
2602
|
+
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(MEDIA_VARIANT_KINDS), new Set(MEDIA_VARIANT_KINDS)) || hasKinds(endpoints, new Set(["voiceover-recording"]), new Set(["phonetic-script"])) || hasKinds(endpoints, new Set(["video"]), new Set(["script"])) || hasKinds(endpoints, new Set(["script"]), new Set(["script"])) || hasKinds(endpoints, new Set(["audio-script"]), new Set(MEDIA_AUDIBLE_KINDS)) || hasKinds(endpoints, new Set(["audio-script"]), new Set(["audio-script"])),
|
|
2521
2603
|
validateMetadata: isEmptyMetadata
|
|
2522
2604
|
});
|
|
2523
2605
|
const captionAlignmentRelationSpec = Object.freeze({
|
|
@@ -2652,6 +2734,7 @@ var BiRelationIndex = class {
|
|
|
2652
2734
|
}
|
|
2653
2735
|
/** Author `derived-from(product, source)` without exposing positional arguments. */
|
|
2654
2736
|
linkDerivedFrom(input) {
|
|
2737
|
+
if (input.product.current().entityKind === "script" && input.source.current().entityKind === "script") throw new Error("Script revision provenance is host-authored by reviseScript");
|
|
2655
2738
|
return this.linkValidated({
|
|
2656
2739
|
relationId: input.relationId,
|
|
2657
2740
|
spec: derivedFromRelationSpec,
|
|
@@ -2887,6 +2970,63 @@ function errorMessage(error) {
|
|
|
2887
2970
|
return error instanceof Error ? error.message : String(error);
|
|
2888
2971
|
}
|
|
2889
2972
|
//#endregion
|
|
2973
|
+
//#region ../medeo-dsl/src/relation-transition.ts
|
|
2974
|
+
var InvalidEntityRelationTransitionError = class extends Error {
|
|
2975
|
+
issues;
|
|
2976
|
+
constructor(issues) {
|
|
2977
|
+
super(`Invalid Medeo relation transition:\n- ${issues.join("\n- ")}`);
|
|
2978
|
+
this.issues = issues;
|
|
2979
|
+
this.name = "InvalidEntityRelationTransitionError";
|
|
2980
|
+
}
|
|
2981
|
+
};
|
|
2982
|
+
/** Enforce relation fact identity while admitting a complete candidate view. */
|
|
2983
|
+
function validateEntityRelationTransition(before, candidate) {
|
|
2984
|
+
const issues = [];
|
|
2985
|
+
const previous = new Map(before.relations.map((row) => [row.relationId, row]));
|
|
2986
|
+
const beforeEntities = new Map(before.entities.map((row) => [row.entityId, row]));
|
|
2987
|
+
const candidateEntities = new Map(candidate.entities.map((row) => [row.entityId, row]));
|
|
2988
|
+
const revisionEditIds = new Set(before.relations.filter((relation) => isScriptRevision(relation, beforeEntities)).map((relation) => readScriptRevisionTrace(relation.trace)?.editId).filter((editId) => editId !== void 0));
|
|
2989
|
+
const revisionParents = /* @__PURE__ */ new Map();
|
|
2990
|
+
for (const entity of before.entities) if (entity.entityKind === "script" && !candidateEntities.has(entity.entityId)) issues.push(`Script entity "${entity.entityId}" is immutable history and cannot be deleted or replaced`);
|
|
2991
|
+
for (const relation of candidate.relations) {
|
|
2992
|
+
if (isScriptRevision(relation, candidateEntities)) {
|
|
2993
|
+
const count = (revisionParents.get(relation.endpoint0EntityId) ?? 0) + 1;
|
|
2994
|
+
revisionParents.set(relation.endpoint0EntityId, count);
|
|
2995
|
+
if (count > 1) issues.push(`Script "${relation.endpoint0EntityId}" may name only one revision source`);
|
|
2996
|
+
}
|
|
2997
|
+
const original = previous.get(relation.relationId);
|
|
2998
|
+
if (original == null) {
|
|
2999
|
+
if (relation.relationKind === "caption-alignment") issues.push(`new caption-alignment relation "${relation.relationId}" is forbidden`);
|
|
3000
|
+
if (isScriptRevision(relation, candidateEntities)) {
|
|
3001
|
+
const trace = readScriptRevisionTrace(relation.trace);
|
|
3002
|
+
const result = candidateEntities.get(relation.endpoint0EntityId);
|
|
3003
|
+
const source = candidateEntities.get(relation.endpoint1EntityId);
|
|
3004
|
+
if (trace == null || beforeEntities.has(relation.endpoint0EntityId) || !beforeEntities.has(relation.endpoint1EntityId) || trace.resultContentHash !== result.payload.contentHash || trace.sourceContentHash !== source.payload.contentHash || trace.resultContentHash === trace.sourceContentHash || revisionEditIds.has(trace.editId)) issues.push(`Script revision relation "${relation.relationId}" must be one canonical new-result to existing-source edit`);
|
|
3005
|
+
else revisionEditIds.add(trace.editId);
|
|
3006
|
+
}
|
|
3007
|
+
continue;
|
|
3008
|
+
}
|
|
3009
|
+
if (original.relationKind !== relation.relationKind || original.endpoint0EntityId !== relation.endpoint0EntityId || original.endpoint1EntityId !== relation.endpoint1EntityId) {
|
|
3010
|
+
issues.push(`relation "${relation.relationId}" changed its kind or ordered endpoints`);
|
|
3011
|
+
continue;
|
|
3012
|
+
}
|
|
3013
|
+
if (!sameJson(original.metadata, relation.metadata)) issues.push(`relation "${relation.relationId}" changed semantic metadata without a new identity`);
|
|
3014
|
+
if (relation.relationKind === "caption-alignment" && !sameJson(original.trace, relation.trace)) issues.push(`legacy caption-alignment relation "${relation.relationId}" may only be carried unchanged or deleted`);
|
|
3015
|
+
if (isScriptRevision(relation, candidateEntities) && !sameJson(original.trace, relation.trace)) issues.push(`Script revision relation "${relation.relationId}" trace is immutable`);
|
|
3016
|
+
}
|
|
3017
|
+
if (issues.length > 0) throw new InvalidEntityRelationTransitionError(issues);
|
|
3018
|
+
}
|
|
3019
|
+
function isScriptRevision(relation, entities) {
|
|
3020
|
+
return relation.relationKind === "derived-from" && entities.get(relation.endpoint0EntityId)?.entityKind === "script" && entities.get(relation.endpoint1EntityId)?.entityKind === "script";
|
|
3021
|
+
}
|
|
3022
|
+
function sameJson(left, right) {
|
|
3023
|
+
if (left === right) return true;
|
|
3024
|
+
if (Array.isArray(left) && Array.isArray(right)) return left.length === right.length && left.every((value, index) => sameJson(value, right[index]));
|
|
3025
|
+
if (!isJsonObject(left) || !isJsonObject(right)) return false;
|
|
3026
|
+
const leftKeys = Object.keys(left);
|
|
3027
|
+
return leftKeys.length === Object.keys(right).length && leftKeys.every((key) => Object.hasOwn(right, key) && sameJson(left[key], right[key]));
|
|
3028
|
+
}
|
|
3029
|
+
//#endregion
|
|
2890
3030
|
//#region src/document/entity-projection-values.ts
|
|
2891
3031
|
/** A graph cannot be represented faithfully by the compatibility reader. */
|
|
2892
3032
|
var EntityTimelineProjectionError = class extends Error {
|
|
@@ -3003,7 +3143,7 @@ function resolveGraphLayout(graph) {
|
|
|
3003
3143
|
const held = graph.holds(clip.entityId);
|
|
3004
3144
|
if (held.length !== 1) fail(`Clip ${clip.entityId} must contain exactly one content entity`);
|
|
3005
3145
|
const content = held[0];
|
|
3006
|
-
const expected = typedRole === "video_clip" ? ["image", "video"] : typedRole === "speech" ? ["voiceover-recording"] : typedRole === "caption" ? ["caption"] : ["audio"];
|
|
3146
|
+
const expected = typedRole === "video_clip" ? ["image", "video"] : typedRole === "speech" ? ["voiceover-recording", "audio"] : typedRole === "caption" ? ["caption"] : ["audio"];
|
|
3007
3147
|
if (!expected.includes(content.entityKind)) fail(`${typedRole} Track accepts ${expected.join(" or ")}, got ${content.entityKind}`);
|
|
3008
3148
|
const inMs = milliseconds(marker.payload.inMs, "inMs");
|
|
3009
3149
|
const outMs = milliseconds(marker.payload.outMs, "outMs");
|
|
@@ -3153,12 +3293,12 @@ function projectEntityTimeline(rows) {
|
|
|
3153
3293
|
} else if (track.role === "speech") {
|
|
3154
3294
|
const asset = mediaAssetFacts(graph, content);
|
|
3155
3295
|
requireUntrimmedAudio(content, marker, placement);
|
|
3156
|
-
const
|
|
3157
|
-
const
|
|
3296
|
+
const synthesized = content.entityKind === "voiceover-recording";
|
|
3297
|
+
const external = externalLocator(asset, synthesized ? "memota-speech" : "memota");
|
|
3158
3298
|
const timbreEdges = graph.adjacent(content.entityId, "voice-timbre");
|
|
3159
|
-
const assembled =
|
|
3299
|
+
const assembled = synthesized ? assemblePhoneticScriptContent(rows, synthesizedPhoneticScript(graph, content).entityId) : void 0;
|
|
3300
|
+
const transcript = synthesized ? void 0 : transcribedAudioScript(graph, content);
|
|
3160
3301
|
if (timbreEdges.length > 1) fail("A voiceover resolves at most one Voice identity");
|
|
3161
|
-
const transcripts = graph.madeInto(content.entityId, "derived-from");
|
|
3162
3302
|
const voice = timbreEdges.length === 0 ? void 0 : graph.one(content.entityId, "voice-timbre", "voice").payload.voice;
|
|
3163
3303
|
if (voice !== void 0 && (!object(voice) || voice.system !== "voice-library" || typeof voice.key !== "string")) fail("Voice must use an explicit voice-library locator");
|
|
3164
3304
|
const captionIds = layout.tracks.flatMap((candidate) => candidate.role === "caption" ? candidate.clips.filter((caption) => caption.anchorClipEntityId === clip.entityId).map((caption) => caption.clipEntityId) : []);
|
|
@@ -3168,7 +3308,8 @@ function projectEntityTimeline(rows) {
|
|
|
3168
3308
|
media_duration_ms: placement.duration,
|
|
3169
3309
|
audio_storage_key: storageKey(asset),
|
|
3170
3310
|
origin_speech_id: external.key,
|
|
3171
|
-
|
|
3311
|
+
...synthesized ? {} : { origin_asset_system: "memota" },
|
|
3312
|
+
audio_script: assembled?.text ?? (transcript === void 0 ? "" : assembleScriptText(transcript)),
|
|
3172
3313
|
volume: volume.data,
|
|
3173
3314
|
voice: object(voice) ? {
|
|
3174
3315
|
id: typeof voice.key === "string" ? voice.key : void 0,
|
|
@@ -3287,7 +3428,17 @@ function captionStyle(value) {
|
|
|
3287
3428
|
}
|
|
3288
3429
|
function requireUntrimmedAudio(content, marker, placement) {
|
|
3289
3430
|
const source = markerWindow(marker);
|
|
3290
|
-
if (source.start !== 0 || content.payload.durationMs !== source.end || placement.duration !== source.end || marker.payload.rate !== void 0) fail("
|
|
3431
|
+
if (source.start !== 0 || content.payload.durationMs !== source.end || placement.duration !== source.end || marker.payload.rate !== void 0) fail("Speech compatibility requires the complete, unremapped take");
|
|
3432
|
+
}
|
|
3433
|
+
function synthesizedPhoneticScript(graph, content) {
|
|
3434
|
+
const scripts = graph.rows.relations.filter((row) => row.relationKind === "derived-from" && row.endpoint0EntityId === content.entityId).map((row) => graph.entity(row.endpoint1EntityId)).filter((row) => row.entityKind === "phonetic-script");
|
|
3435
|
+
if (scripts.length !== 1) fail("Synthesized Speech requires exactly one ordered PhoneticScript input");
|
|
3436
|
+
return scripts[0];
|
|
3437
|
+
}
|
|
3438
|
+
function transcribedAudioScript(graph, content) {
|
|
3439
|
+
const scripts = graph.rows.relations.filter((row) => row.relationKind === "derived-from" && row.endpoint1EntityId === content.entityId).map((row) => graph.entity(row.endpoint0EntityId)).filter((row) => row.entityKind === "audio-script");
|
|
3440
|
+
if (scripts.length > 1) fail("Recorded Speech has ambiguous AudioScript transcripts");
|
|
3441
|
+
return scripts[0];
|
|
3291
3442
|
}
|
|
3292
3443
|
/** The Marker's window, in the {start,end} shape this compatibility reader speaks. */
|
|
3293
3444
|
function markerWindow(marker) {
|
|
@@ -3696,6 +3847,19 @@ function writeEntityOrder(doc, group, roots, token) {
|
|
|
3696
3847
|
//#region src/entity-crdt/entity-document.ts
|
|
3697
3848
|
const LORO_ENTITY_SCHEMA = "medeo.entities.loro.v1";
|
|
3698
3849
|
const MATERIALIZE_ORIGIN = "medeo.materialize";
|
|
3850
|
+
/** Read persisted relation roots before current entity versions are projected. */
|
|
3851
|
+
function readEntityRelationRecords(doc) {
|
|
3852
|
+
if (root(doc).get("schema") !== "medeo.entities.loro.v1") throw new Error("Document has no Loro entity state");
|
|
3853
|
+
return relationRecords(doc).map(({ rootId, identity, map, deleted }) => ({
|
|
3854
|
+
relationId: createRelationId(rootId),
|
|
3855
|
+
relationKind: identity.kind,
|
|
3856
|
+
endpoint0EntityId: createEntityId(identity.endpoints[0]),
|
|
3857
|
+
endpoint1EntityId: createEntityId(identity.endpoints[1]),
|
|
3858
|
+
metadata: requireMap(map, "metadata").toJSON(),
|
|
3859
|
+
trace: requireMap(map, "trace").toJSON(),
|
|
3860
|
+
deleted
|
|
3861
|
+
}));
|
|
3862
|
+
}
|
|
3699
3863
|
function hash(value) {
|
|
3700
3864
|
return bytesToHex(sha256(new TextEncoder().encode(canonicalJson(value))));
|
|
3701
3865
|
}
|
|
@@ -3782,35 +3946,31 @@ function readEntityDocument(doc, allowSelectionConflicts = false) {
|
|
|
3782
3946
|
}
|
|
3783
3947
|
};
|
|
3784
3948
|
});
|
|
3785
|
-
const relations =
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
if (original.endpoints.some((id) => !ids.has(family(doc, id)))) continue;
|
|
3791
|
-
const endpoints = original.endpoints.map((id) => currentId(doc, id, ids));
|
|
3792
|
-
const relationId = equalJson(endpoints, original.endpoints) ? key : `relation_${hash([key, endpoints])}`;
|
|
3793
|
-
relations.push({
|
|
3794
|
-
relationId: createRelationId(relationId),
|
|
3795
|
-
relationKind: original.kind,
|
|
3949
|
+
const relations = activeRelationRecords(doc, ids).map(({ rootId, identity, map }) => {
|
|
3950
|
+
const endpoints = identity.endpoints.map((id) => currentId(doc, id, ids));
|
|
3951
|
+
return {
|
|
3952
|
+
relationId: createRelationId(equalJson(endpoints, identity.endpoints) ? rootId : `relation_${hash([rootId, endpoints])}`),
|
|
3953
|
+
relationKind: identity.kind,
|
|
3796
3954
|
endpoint0EntityId: createEntityId(endpoints[0]),
|
|
3797
3955
|
endpoint1EntityId: createEntityId(endpoints[1]),
|
|
3798
3956
|
metadata: requireMap(map, "metadata").toJSON(),
|
|
3799
3957
|
trace: requireMap(map, "trace").toJSON()
|
|
3800
|
-
}
|
|
3801
|
-
}
|
|
3958
|
+
};
|
|
3959
|
+
});
|
|
3802
3960
|
sortSequencedRelations(doc, relations);
|
|
3803
3961
|
const project = {};
|
|
3804
3962
|
for (const [key, value] of Object.entries(section(doc, "attachments").toJSON())) {
|
|
3805
3963
|
if (typeof value !== "string") throw new Error(`Invalid project attachment ${key}`);
|
|
3806
3964
|
project[key] = currentId(doc, value, ids);
|
|
3807
3965
|
}
|
|
3966
|
+
const phoneticScriptSelections = readPhoneticScriptSelections(doc, ids);
|
|
3808
3967
|
const state = {
|
|
3809
3968
|
rows: {
|
|
3810
3969
|
entities,
|
|
3811
3970
|
relations
|
|
3812
3971
|
},
|
|
3813
|
-
project
|
|
3972
|
+
project,
|
|
3973
|
+
...phoneticScriptSelections === void 0 ? {} : { phoneticScriptSelections }
|
|
3814
3974
|
};
|
|
3815
3975
|
if (!allowSelectionConflicts) for (const [key, id] of ids) {
|
|
3816
3976
|
const fingerprint = selectionFingerprint(state.rows, id);
|
|
@@ -3818,6 +3978,54 @@ function readEntityDocument(doc, allowSelectionConflicts = false) {
|
|
|
3818
3978
|
}
|
|
3819
3979
|
return state;
|
|
3820
3980
|
}
|
|
3981
|
+
function readPhoneticScriptSelections(doc, ids) {
|
|
3982
|
+
const native = root(doc).get("phoneticScriptSelections");
|
|
3983
|
+
if (native === void 0) return void 0;
|
|
3984
|
+
if (!(native instanceof LoroMap)) throw new Error("Invalid phonetic-script selector");
|
|
3985
|
+
const selections = {};
|
|
3986
|
+
for (const clipRoot of native.keys().sort((a, b) => a < b ? -1 : a > b ? 1 : 0)) {
|
|
3987
|
+
const phoneticScriptRoot = native.get(clipRoot);
|
|
3988
|
+
if (typeof phoneticScriptRoot !== "string") throw new Error("Invalid phonetic-script selection");
|
|
3989
|
+
const clipId = ids.get(clipRoot);
|
|
3990
|
+
const phoneticScriptId = ids.get(phoneticScriptRoot);
|
|
3991
|
+
if (clipId === void 0 || phoneticScriptId === void 0) continue;
|
|
3992
|
+
if (workspace(doc, clipRoot).get("kind") !== "clip") throw new Error("Phonetic-script selection target must be a Clip");
|
|
3993
|
+
if (workspace(doc, phoneticScriptRoot).get("kind") !== "phonetic-script") throw new Error("Phonetic-script selection value must be a PhoneticScript");
|
|
3994
|
+
selections[clipRoot] = phoneticScriptId;
|
|
3995
|
+
}
|
|
3996
|
+
return Object.keys(selections).length === 0 ? void 0 : selections;
|
|
3997
|
+
}
|
|
3998
|
+
function relationRecords(doc) {
|
|
3999
|
+
const records = [];
|
|
4000
|
+
for (const rootId of section(doc, "relations").keys().sort((a, b) => a < b ? -1 : a > b ? 1 : 0)) {
|
|
4001
|
+
const map = section(doc, "relations").get(rootId);
|
|
4002
|
+
if (!(map instanceof LoroMap)) throw new Error(`Invalid relation record ${rootId}`);
|
|
4003
|
+
const value = map.get("identity");
|
|
4004
|
+
if (!isJsonObject(value) || typeof value.kind !== "string" || !Array.isArray(value.endpoints) || value.endpoints.length !== 2 || !value.endpoints.every((endpoint) => typeof endpoint === "string")) throw new Error(`Invalid relation identity ${rootId}`);
|
|
4005
|
+
records.push({
|
|
4006
|
+
rootId,
|
|
4007
|
+
identity: {
|
|
4008
|
+
kind: value.kind,
|
|
4009
|
+
endpoints: [value.endpoints[0], value.endpoints[1]]
|
|
4010
|
+
},
|
|
4011
|
+
map,
|
|
4012
|
+
deleted: map.get("deleted") === true
|
|
4013
|
+
});
|
|
4014
|
+
}
|
|
4015
|
+
return records;
|
|
4016
|
+
}
|
|
4017
|
+
function activeRelationRecords(doc, ids) {
|
|
4018
|
+
const selector = root(doc).get("clipContentSelections");
|
|
4019
|
+
if (selector !== void 0 && !(selector instanceof LoroMap)) throw new Error("Invalid clip-content selector");
|
|
4020
|
+
return relationRecords(doc).filter(({ rootId, identity, deleted }) => {
|
|
4021
|
+
if (deleted) return false;
|
|
4022
|
+
if (!identity.endpoints.every((id) => ids.has(family(doc, id)))) return false;
|
|
4023
|
+
if (identity.kind !== "contain" || selector === void 0 || workspace(doc, family(doc, identity.endpoints[0])).get("kind") !== "clip") return true;
|
|
4024
|
+
const selected = selector.get(family(doc, identity.endpoints[0]));
|
|
4025
|
+
if (selected !== void 0 && typeof selected !== "string") throw new Error("Invalid clip-content selection");
|
|
4026
|
+
return selected === void 0 || selected === rootId;
|
|
4027
|
+
});
|
|
4028
|
+
}
|
|
3821
4029
|
/** Native editing containers survive domain version changes and merge independently. */
|
|
3822
4030
|
var LoroEntityDocument = class LoroEntityDocument {
|
|
3823
4031
|
doc;
|
|
@@ -3849,7 +4057,9 @@ var LoroEntityDocument = class LoroEntityDocument {
|
|
|
3849
4057
|
"relations",
|
|
3850
4058
|
"project",
|
|
3851
4059
|
"attachments",
|
|
3852
|
-
"ordering"
|
|
4060
|
+
"ordering",
|
|
4061
|
+
"clipContentSelections",
|
|
4062
|
+
"phoneticScriptSelections"
|
|
3853
4063
|
]) root(doc).ensureMergeableMap(name);
|
|
3854
4064
|
for (const row of rows.entities) apply(doc, {
|
|
3855
4065
|
type: "create",
|
|
@@ -3900,11 +4110,13 @@ var LoroEntityDocument = class LoroEntityDocument {
|
|
|
3900
4110
|
}
|
|
3901
4111
|
/** The callback and every domain validation execute on a causally complete fork first. */
|
|
3902
4112
|
transact(edit) {
|
|
4113
|
+
const before = readEntityDocument(this.doc, true);
|
|
3903
4114
|
const probe = this.doc.fork();
|
|
3904
4115
|
const draft = new LoroEntityDraft(probe);
|
|
3905
4116
|
edit(draft);
|
|
3906
4117
|
draft.captureSelectionBaselines();
|
|
3907
4118
|
const state = readEntityDocument(probe);
|
|
4119
|
+
validateEntityRelationTransition(before.rows, state.rows);
|
|
3908
4120
|
assertState(state);
|
|
3909
4121
|
this.validate?.(state);
|
|
3910
4122
|
materialize(probe);
|
|
@@ -3918,8 +4130,15 @@ var LoroEntityDocument = class LoroEntityDocument {
|
|
|
3918
4130
|
from
|
|
3919
4131
|
});
|
|
3920
4132
|
}
|
|
3921
|
-
/**
|
|
4133
|
+
/** Validate remote operations on a fork before importing their native identities. */
|
|
3922
4134
|
import(update) {
|
|
4135
|
+
const before = readEntityDocument(this.doc, true);
|
|
4136
|
+
const probe = this.doc.fork();
|
|
4137
|
+
probe.import(update);
|
|
4138
|
+
const candidate = readEntityDocument(probe, true);
|
|
4139
|
+
validateEntityRelationTransition(before.rows, candidate.rows);
|
|
4140
|
+
assertState(candidate);
|
|
4141
|
+
this.validate?.(candidate);
|
|
3923
4142
|
this.doc.import(update);
|
|
3924
4143
|
const state = this.snapshot();
|
|
3925
4144
|
assertState(state);
|
|
@@ -4035,6 +4254,7 @@ var LoroEntityDraft = class {
|
|
|
4035
4254
|
});
|
|
4036
4255
|
}
|
|
4037
4256
|
delete(entityId) {
|
|
4257
|
+
if (workspace(this.doc, family(this.doc, entityId)).get("kind") === "script") throw new Error("Script entities are immutable history and cannot be deleted");
|
|
4038
4258
|
this.record({
|
|
4039
4259
|
type: "delete",
|
|
4040
4260
|
root: family(this.doc, entityId)
|
|
@@ -4053,11 +4273,14 @@ var LoroEntityDraft = class {
|
|
|
4053
4273
|
row: clone(row)
|
|
4054
4274
|
});
|
|
4055
4275
|
}
|
|
4056
|
-
|
|
4276
|
+
patchRelationTrace(relationId, trace) {
|
|
4277
|
+
const relation = this.snapshot().rows.relations.find((row) => row.relationId === relationId);
|
|
4278
|
+
if (relation == null) throw new Error(`Unknown relation ${relationId}`);
|
|
4279
|
+
if (relation.relationKind === "caption-alignment") throw new Error("caption-alignment Relations are read-only legacy facts");
|
|
4057
4280
|
this.record({
|
|
4058
4281
|
type: "relation-fields",
|
|
4059
4282
|
root: this.relationRoot(relationId),
|
|
4060
|
-
metadata: clone(metadata),
|
|
4283
|
+
metadata: clone(relation.metadata),
|
|
4061
4284
|
trace: clone(trace)
|
|
4062
4285
|
});
|
|
4063
4286
|
}
|
|
@@ -4067,6 +4290,35 @@ var LoroEntityDraft = class {
|
|
|
4067
4290
|
root: this.relationRoot(relationId)
|
|
4068
4291
|
});
|
|
4069
4292
|
}
|
|
4293
|
+
/** Select the one content membership projected for this stable Clip identity. */
|
|
4294
|
+
selectClipContent(relationId) {
|
|
4295
|
+
const relationRoot = this.relationRoot(relationId);
|
|
4296
|
+
const map = section(this.doc, "relations").get(relationRoot);
|
|
4297
|
+
if (!(map instanceof LoroMap)) throw new Error(`Unknown relation ${relationId}`);
|
|
4298
|
+
const identity = map.get("identity");
|
|
4299
|
+
if (!isJsonObject(identity) || identity.kind !== "contain" || !Array.isArray(identity.endpoints) || typeof identity.endpoints[0] !== "string") throw new Error("Clip-content selection requires a contain relation");
|
|
4300
|
+
const clipRoot = family(this.doc, identity.endpoints[0]);
|
|
4301
|
+
if (workspace(this.doc, clipRoot).get("kind") !== "clip") throw new Error("Clip-content selection requires a Clip container");
|
|
4302
|
+
this.record({
|
|
4303
|
+
type: "select-clip-content",
|
|
4304
|
+
clipRoot,
|
|
4305
|
+
relationRoot
|
|
4306
|
+
});
|
|
4307
|
+
}
|
|
4308
|
+
/** Select the pending immutable pronunciation variant for one stable Speech Clip. */
|
|
4309
|
+
selectPhoneticScript(clipEntityId, phoneticScriptEntityId) {
|
|
4310
|
+
const clipRoot = family(this.doc, clipEntityId);
|
|
4311
|
+
const phoneticScriptRoot = family(this.doc, phoneticScriptEntityId);
|
|
4312
|
+
const clip = workspace(this.doc, clipRoot);
|
|
4313
|
+
const phoneticScript = workspace(this.doc, phoneticScriptRoot);
|
|
4314
|
+
if (clip.get("deleted") === true || clip.get("kind") !== "clip") throw new Error("Phonetic-script selection target must be an active Clip");
|
|
4315
|
+
if (phoneticScript.get("deleted") === true || phoneticScript.get("kind") !== "phonetic-script") throw new Error("Phonetic-script selection value must be an active PhoneticScript");
|
|
4316
|
+
this.record({
|
|
4317
|
+
type: "select-phonetic-script",
|
|
4318
|
+
clipRoot,
|
|
4319
|
+
phoneticScriptRoot
|
|
4320
|
+
});
|
|
4321
|
+
}
|
|
4070
4322
|
relationRoot(relationId) {
|
|
4071
4323
|
const alias = this.relationAliases.get(relationId);
|
|
4072
4324
|
if (alias !== void 0) return alias;
|
|
@@ -4104,6 +4356,7 @@ var LoroEntityDraft = class {
|
|
|
4104
4356
|
});
|
|
4105
4357
|
}
|
|
4106
4358
|
replaceOwned(entityId, payload) {
|
|
4359
|
+
this.assertGenericMutationAllowed(entityId);
|
|
4107
4360
|
this.record({
|
|
4108
4361
|
type: "write",
|
|
4109
4362
|
root: family(this.doc, entityId),
|
|
@@ -4114,6 +4367,7 @@ var LoroEntityDraft = class {
|
|
|
4114
4367
|
editText(entityId, path, start, deleteCount, text) {
|
|
4115
4368
|
if (typeof path[0] !== "string") throw new Error("Text path must start with a field");
|
|
4116
4369
|
const owner = resolveEntityFieldOwner(this.snapshot().rows, createEntityId(currentId(this.doc, entityId)), path[0]);
|
|
4370
|
+
this.assertGenericMutationAllowed(owner);
|
|
4117
4371
|
this.record({
|
|
4118
4372
|
type: "text",
|
|
4119
4373
|
root: family(this.doc, owner),
|
|
@@ -4139,6 +4393,7 @@ var LoroEntityDraft = class {
|
|
|
4139
4393
|
const owner = resolveEntityFieldOwner(state.rows, id, change.path[0]);
|
|
4140
4394
|
const row = state.rows.entities.find((row) => row.entityId === owner);
|
|
4141
4395
|
if (equalJson(row.payload, applyFieldChanges(row.payload, [change]))) continue;
|
|
4396
|
+
this.assertGenericMutationAllowed(owner);
|
|
4142
4397
|
this.record({
|
|
4143
4398
|
type: "field",
|
|
4144
4399
|
root: family(this.doc, owner),
|
|
@@ -4148,13 +4403,17 @@ var LoroEntityDraft = class {
|
|
|
4148
4403
|
});
|
|
4149
4404
|
}
|
|
4150
4405
|
}
|
|
4406
|
+
assertGenericMutationAllowed(entityId) {
|
|
4407
|
+
if (workspace(this.doc, family(this.doc, entityId)).get("kind") === "script") throw new Error("Script content is immutable; create a new Script revision");
|
|
4408
|
+
}
|
|
4151
4409
|
changeRelation(relationId, changes) {
|
|
4152
4410
|
assertFieldChanges(changes);
|
|
4153
4411
|
const key = this.relationRoot(relationId);
|
|
4154
4412
|
const map = section(this.doc, "relations").get(key);
|
|
4155
4413
|
if (!(map instanceof LoroMap) || map.get("deleted") === true) throw new Error("Unknown relation");
|
|
4414
|
+
if (map.get("identity").kind === "caption-alignment") throw new Error("caption-alignment Relations are read-only legacy facts");
|
|
4156
4415
|
for (const change of changes) {
|
|
4157
|
-
if (
|
|
4416
|
+
if (change.path[0] !== "trace" || change.path.length < 2) throw new Error("Relation changes may only edit trace fields");
|
|
4158
4417
|
const payload = {
|
|
4159
4418
|
metadata: requireMap(map, "metadata").toJSON(),
|
|
4160
4419
|
trace: requireMap(map, "trace").toJSON()
|
|
@@ -4273,6 +4532,12 @@ function apply(doc, command) {
|
|
|
4273
4532
|
map.set("deleted", true);
|
|
4274
4533
|
break;
|
|
4275
4534
|
}
|
|
4535
|
+
case "select-clip-content":
|
|
4536
|
+
root(doc).ensureMergeableMap("clipContentSelections").set(command.clipRoot, command.relationRoot);
|
|
4537
|
+
break;
|
|
4538
|
+
case "select-phonetic-script":
|
|
4539
|
+
root(doc).ensureMergeableMap("phoneticScriptSelections").set(command.clipRoot, command.phoneticScriptRoot);
|
|
4540
|
+
break;
|
|
4276
4541
|
case "relation-fields": {
|
|
4277
4542
|
const map = section(doc, "relations").get(command.root);
|
|
4278
4543
|
if (!(map instanceof LoroMap)) throw new Error("Unknown relation");
|
|
@@ -4694,4 +4959,4 @@ function createPlainMemoryAdapter(document, options) {
|
|
|
4694
4959
|
return new PlainMemoryAdapter(document, options);
|
|
4695
4960
|
}
|
|
4696
4961
|
//#endregion
|
|
4697
|
-
export {
|
|
4962
|
+
export { fromVideoDocument as $, resolveEntityTimelineLayout as A, base64ToBytes as At, assembleScriptText as B, canonicalJson as C, safeDurationMs as Ct, resolveFieldPath as D, partUnionToDraft as Dt, assertFieldChanges as E, speedOf as Et, relationsTo as F, isJsonObject as G, selectAudioScriptSegment as H, ScriptCompositionError as I, hasSequence as J, createEntityId as K, assembleCaptionContent as L, validateEntityRelationTransition as M, decodeEntityRelationRows as N, projectEntityTimeline as O, recordEntries as Ot, relationsFrom as P, derivePositionFromAbs as Q, assembleEntityContent as R, scriptSourceAssetOf as S, partDurationMs as St, applyFieldChanges as T, effectiveVideoClipDurationMs as Tt, updateEntityFields as U, findComposedAudioScript as V, variantBaseEntityIds as W, buildInitialVideoDocument as X, isMediaAssetVariantKind as Y, buildSpeechHostMap as Z, audioScriptAssetHash as _, resolveAllSpeechOverlaps as _t, createMirrorVideoDocument as a, videoDocumentSchema as at, phoneticScriptAssetHash as b, TIMELINE_SKELETON_DURATION_MS as bt, DocumentMutationGuard as c, findLaneTrack as ct, LoroEntityDraft as d, solveVideoDocument as dt, toVideoDocument as et, readEntityDocument as f, cascadeAfterVideoClipChanges as ft, audioScriptAssetFields as g, recalculateTimelineDuration as gt, audioScriptAssetContent as h, reassignSpeechesToVideoClipsByTime as ht, MirrorVideoDocumentAdapter as i, partUnionSchema as it, EntityTimelineProjectionError as j, bytesToBase64 as jt, EntityProjectionGraph as k, videoDocumentMirrorSchema as kt, LORO_ENTITY_SCHEMA as l, hasResolvedPlacement as lt, entityOrderGroups as m, fillMainTrackTimeGaps as mt, createPlainMemoryAdapter as n, assertValidVideoDocument as nt, createMirrorVideoDocumentAdapter as o, LANE_KINDS_IN_STACK_ORDER as ot, readEntityRelationRecords as p, arrangeMainTrackSeamlessly as pt, createRelationId as q, generatePartId as r, validateVideoDocument as rt, readVideoDocumentFromDraft as s, ensureLaneTrack as st, PlainMemoryAdapter as t, VideoDocumentValidationError as tt, LoroEntityDocument as u, laneTrackId as ut, audioScriptAssetOf as v, resolveSpeechOverlapByShiftingVideos as vt, equalJson as w, DEFAULT_UNIT_TIME_MS as wt, phoneticScriptAssetOf as x, isEmptyVideoClip as xt, phoneticScriptAssetContent as y, syncAggregatedClipsTimePosition as yt, assemblePhoneticScriptContent as z };
|