@mengine/medeo-tool 1.4.1-alpha.2 → 1.4.1-alpha.4

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/index.mjs CHANGED
@@ -1,7 +1,141 @@
1
- import { a as createEntityId, c as collectAffectedPartIds, i as businessState, l as renderPreview, n as EntitySandbox, o as createRelationId, s as isMediaAssetVariantKind, t as EditSandboxSession, u as renderCompactProjection } from "./script-session-lXpqmupK.mjs";
2
- import { LoroEntityDocument, ManualSyncDoc, MengineHttpClient, MengineHttpRequestError, ValidationError, base64ToBytes, bytesToBase64, compileEntityRows, createMirrorVideoDocument, createPlainMemoryAdapter, decodeDocVersionMark, encodeDocVersionMark, ensureEditorFoundation, replayJournal, toVideoDocument } from "@mengine/medeo-client";
1
+ import { a as createEntityId, i as businessState, o as createRelationId, s as isMediaAssetVariantKind, t as EntitySandbox } from "./entity-sandbox-OArq9NSH.mjs";
2
+ import { LoroEntityDocument, ManualSyncDoc, MengineHttpClient, MengineHttpRequestError, ValidationError, base64ToBytes, bytesToBase64, compileEntityRows, createMirrorVideoDocument, createPlainMemoryAdapter, decodeDocVersionMark, effectiveVideoClipDurationMs, encodeDocVersionMark, ensureEditorFoundation, replayJournal, solveVideoDocument, speedOf, toVideoDocument } from "@mengine/medeo-client";
3
3
  import { Worker } from "node:worker_threads";
4
4
  import { createHash, randomUUID } from "node:crypto";
5
+ //#region src/document/compact-projection.ts
6
+ const DEFAULT_TEXT_PREVIEW_LENGTH = 24;
7
+ /** Kind tag shown in the first column (`video_clip` → `clip`). */
8
+ function kindTag(kind) {
9
+ return kind === "video_clip" ? "clip" : kind;
10
+ }
11
+ /** Lane label: `video_clip` tracks display as `main`, otherwise `parts_kind`. */
12
+ function laneLabel(partsKind) {
13
+ return partsKind === "video_clip" ? "main" : partsKind;
14
+ }
15
+ /** Speed token: absent → `1`; linear → numeric multiplier; anything else → `custom`. */
16
+ function speedToken(speedShift) {
17
+ if (speedShift == null) return "1";
18
+ if (speedShift.category === "linear") return String(speedOf(speedShift));
19
+ return "custom";
20
+ }
21
+ function effectiveDurationMs(part, timelineDurationMs) {
22
+ if (part.video_clip != null) return effectiveVideoClipDurationMs(part.video_clip);
23
+ if (part.speech != null) return part.speech.media_duration_ms ?? 0;
24
+ if (part.caption != null) return part.caption.initial_duration_ms ?? 0;
25
+ if (part.bgm != null) return timelineDurationMs;
26
+ return 0;
27
+ }
28
+ function truncateText(text, budget) {
29
+ if (text.length <= budget) return text;
30
+ return `${text.slice(0, budget)}…`;
31
+ }
32
+ function anchorToken(timePosition) {
33
+ if (timePosition.mode === "anchored") return `anchor=${timePosition.anchorPartId}+${timePosition.offsetMs}`;
34
+ if (timePosition.mode === "absolute") return "anchor=abs";
35
+ return "anchor=abs";
36
+ }
37
+ function clipAttrs(clip) {
38
+ const playIn = clip.play_in ?? 0;
39
+ const playOut = clip.play_out ?? 0;
40
+ return `media=${clip.origin_media_id ?? ""} trim=${playIn}-${playOut} speed=${speedToken(clip.speed_shift)} vol=${clip.volume ?? 0}`;
41
+ }
42
+ function partAttrs(part, item, textPreviewLength) {
43
+ if (part.video_clip != null) return clipAttrs(part.video_clip);
44
+ if (part.speech != null) return `${anchorToken(item.time_position)} dur=${part.speech.media_duration_ms ?? 0}`;
45
+ if (part.caption != null) {
46
+ const preview = truncateText(part.caption.text ?? "", textPreviewLength);
47
+ return `${anchorToken(item.time_position)} text="${preview}"`;
48
+ }
49
+ if (part.bgm != null) return `vol=${part.bgm.volume ?? 0}`;
50
+ return "";
51
+ }
52
+ /**
53
+ * Render a `VideoDocument` as compact text: one header line plus one row per
54
+ * timeline part (optionally filtered by `onlyPartIds`). Deterministic and
55
+ * side-effect free — same document always yields the same string.
56
+ */
57
+ function renderCompactProjection(document, options) {
58
+ const onlyPartIds = options?.onlyPartIds;
59
+ const textPreviewLength = options?.textPreviewLength ?? DEFAULT_TEXT_PREVIEW_LENGTH;
60
+ const solved = solveVideoDocument(document);
61
+ const library = document.part_library ?? {};
62
+ const tracks = document.tracks ?? [];
63
+ let totalParts = 0;
64
+ const rows = [];
65
+ for (const track of tracks) {
66
+ const partsKind = track.parts_kind;
67
+ if (partsKind == null) continue;
68
+ const lane = laneLabel(partsKind);
69
+ const tag = kindTag(partsKind);
70
+ for (const item of track.items ?? []) {
71
+ totalParts += 1;
72
+ const partId = item.part_id;
73
+ if (onlyPartIds != null && !onlyPartIds.has(partId)) continue;
74
+ const part = library[partId];
75
+ if (part == null) continue;
76
+ const abs = solved.absByPartId.get(partId) ?? 0;
77
+ const dur = effectiveDurationMs(part, solved.durationMs);
78
+ const attrs = partAttrs(part, item, textPreviewLength);
79
+ rows.push(`${tag} ${partId} ${lane} [${abs},${abs + dur}) ${attrs}`);
80
+ }
81
+ }
82
+ return [`# draft=${document.meta.draft_id ?? ""} v=${document.meta.version ?? 0} duration=${solved.durationMs} parts=${totalParts} shown=${rows.length}`, ...rows].join("\n");
83
+ }
84
+ //#endregion
85
+ //#region src/sandbox/preview.ts
86
+ /**
87
+ * Collect part ids referenced by a journal for compact preview filtering.
88
+ * Walks known id-shaped payload keys (宁多勿少) and unions `generated_ids`.
89
+ */
90
+ const PART_ID_KEYS = new Set([
91
+ "clip_id",
92
+ "clip_ids",
93
+ "before_clip_id",
94
+ "after_clip_id",
95
+ "speech_id",
96
+ "speech_ids",
97
+ "speech_part_id",
98
+ "caption_id",
99
+ "caption_ids",
100
+ "bgm_id",
101
+ "anchor_part_id",
102
+ "part_id",
103
+ "body_part_id"
104
+ ]);
105
+ /** Extract every part id a journal entry touches (payload refs + minted ids). */
106
+ function collectAffectedPartIds(journal) {
107
+ const ids = /* @__PURE__ */ new Set();
108
+ for (const entry of journal) {
109
+ for (const generated of entry.generated_ids ?? []) if (generated.length > 0) ids.add(generated);
110
+ collectFromValue(entry.payload, ids);
111
+ }
112
+ return ids;
113
+ }
114
+ function collectFromValue(value, ids) {
115
+ if (value == null) return;
116
+ if (Array.isArray(value)) {
117
+ for (const item of value) collectFromValue(item, ids);
118
+ return;
119
+ }
120
+ if (typeof value !== "object") return;
121
+ for (const [key, child] of Object.entries(value)) {
122
+ if (PART_ID_KEYS.has(key)) {
123
+ if (typeof child === "string" && child.length > 0) ids.add(child);
124
+ else if (Array.isArray(child)) {
125
+ for (const item of child) if (typeof item === "string" && item.length > 0) ids.add(item);
126
+ }
127
+ }
128
+ collectFromValue(child, ids);
129
+ }
130
+ }
131
+ /**
132
+ * Render a ChangePlan preview: header + rows for journal-affected parts only.
133
+ * Empty journal → empty `onlyPartIds` (header alone), matching the T2 contract.
134
+ */
135
+ function renderPreview(document, journal) {
136
+ return renderCompactProjection(document, { onlyPartIds: journal.length === 0 ? /* @__PURE__ */ new Set() : collectAffectedPartIds(journal) });
137
+ }
138
+ //#endregion
5
139
  //#region src/sandbox/node-host.ts
6
140
  const DEFAULT_TIMEOUT_MS = 2e3;
7
141
  const DEFAULT_MEMORY_MB = 256;
@@ -35,8 +169,7 @@ function runEditScript(options) {
35
169
  script: options.script,
36
170
  inputs: options.inputs,
37
171
  entityState: options.entityState,
38
- idLabel: options.idLabel,
39
- entityOnly: options.entityOnly
172
+ idLabel: options.idLabel
40
173
  },
41
174
  execArgv: resolveRegisterUrl.pathname.endsWith(".ts") ? [
42
175
  "--experimental-transform-types",
@@ -624,12 +757,10 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
624
757
  " sampling: 'native';",
625
758
  " coordinateSpace: JsonValue;",
626
759
  "}",
627
- "/** Business editing surface. Infrastructure Assets are assembled by the host. */",
760
+ "/** Business editing surface. Reads are assembled snapshots; writes preserve native operation intent. */",
628
761
  "export interface BusinessEntityFacade {",
629
762
  " list(): SandboxEntity[];",
630
763
  " get(entityId: string): SandboxEntity | null;",
631
- " readCaptionContent(entityId: string): ComposedScriptContent;",
632
- " readPhoneticScriptContent(entityId: string): ComposedPhoneticContent;",
633
764
  " create(",
634
765
  " input: Exclude<",
635
766
  " CreateEntityInput,",
@@ -638,66 +769,18 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
638
769
  " }",
639
770
  " >,",
640
771
  " ): string;",
641
- " update(input: UpdateEntityInput): void;",
772
+ " update(input: EntityUpdateInput): void;",
642
773
  " declareFields(input: UpdateEntityInput): void;",
643
774
  " delete(input: DeleteEntityInput): void;",
644
775
  "}",
645
- "/** Physical Asset bindings are maintained outside the sandbox. */",
776
+ "/** Endpoint order is retained; each relation kind defines its endpoint semantics. */",
646
777
  "export interface BusinessRelationFacade {",
647
778
  " list(): SandboxRelation[];",
648
779
  " of(entityId: string, relationKind?: KnownRelationKind): SandboxRelation[];",
649
- " link(",
650
- " input: Exclude<",
651
- " LinkRelationInput,",
652
- " {",
653
- " relation_kind: 'physical-asset';",
654
- " }",
655
- " >,",
656
- " ): string;",
657
- " linkGenerated(input: LinkGeneratedRelationInput): string;",
658
- " linkClipAnchor(input: LinkClipAnchorRelationInput): string;",
659
- " linkPhoneticScriptRender(input: LinkPhoneticScriptRenderRelationInput): string;",
660
- " linkAudioScriptSource(input: LinkAudioScriptSourceRelationInput): string;",
780
+ " link(input: LinkRelationInput): string;",
781
+ " update(input: RelationUpdateInput): void;",
661
782
  " unlink(input: UnlinkRelationInput): void;",
662
783
  "}",
663
- "export interface CaptionFontDescriptor {",
664
- " readonly system: 'font-library';",
665
- " readonly key: string;",
666
- "}",
667
- "/**",
668
- " * One ordered entry of the Caption's segment selection. `segmentId` quotes the",
669
- " * composed AudioScript's own stable segment identity — a local id quoted by the",
670
- " * variant, never a peer Entity reference. Text itself is never copied here;",
671
- " * complete Caption content is assembled through its direct baseEntityIds.",
672
- " * The optional `textRange` narrows one Segment to an intra-Segment sub-span",
673
- " * (intra-segment re-segmentation); without it the whole Segment text is selected.",
674
- " */",
675
- "export type CaptionSegmentSelection = JsonObject & {",
676
- " readonly segmentId: string;",
677
- " readonly textRange?: CaptionTextRange;",
678
- "};",
679
- "export interface CaptionStyleFields {",
680
- " readonly font?: CaptionFontDescriptor;",
681
- " readonly fontSize?: number;",
682
- " readonly fontColor?: string;",
683
- " readonly fontWeight?: number;",
684
- " readonly entranceAnimation?: string;",
685
- " readonly entranceAnimationDurationMs?: number;",
686
- " readonly strokeColor?: string;",
687
- " readonly strokeWidth?: number;",
688
- " readonly positionX?: number;",
689
- " readonly positionY?: number;",
690
- "}",
691
- "/**",
692
- " * Half-open `[start, end)` position window inside one Segment's text, counted",
693
- " * in Unicode code points (not UTF-16 code units), so a boundary never splits a",
694
- " * surrogate pair. Positions are non-negative safe integers with `start < end`;",
695
- " * `end` must not exceed the Segment's code-point length.",
696
- " */",
697
- "export interface CaptionTextRange extends JsonObject {",
698
- " readonly start: number;",
699
- " readonly end: number;",
700
- "}",
701
784
  "export type CaptionTextSelection = JsonObject & {",
702
785
  " segmentId: string;",
703
786
  " /** Half-open Unicode code-point range within the selected source segment. */",
@@ -706,31 +789,6 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
706
789
  " end: number;",
707
790
  " };",
708
791
  "};",
709
- "export type ClipEntityId = EntityId;",
710
- "export type ClipPlacement =",
711
- " | {",
712
- " readonly kind: 'sequential';",
713
- " readonly order: number;",
714
- " }",
715
- " | {",
716
- " readonly kind: 'absolute';",
717
- " readonly targetRange: SequenceRange<number>;",
718
- " }",
719
- " | {",
720
- " readonly kind: 'anchored';",
721
- " readonly hostClipEntityId: string;",
722
- " readonly anchorOffset: number;",
723
- " };",
724
- "export interface ComposedPhoneticContent extends ComposedScriptContent {",
725
- " phonemeScript?: string;",
726
- " prosody?: JsonObject;",
727
- "}",
728
- "/** Read result only: base text is assembled from the real AudioScript row. */",
729
- "export interface ComposedScriptContent {",
730
- " audio_script_entity_id: string;",
731
- " text: string;",
732
- " segments: ScriptTextSegment[];",
733
- "}",
734
792
  "export type CreateEntityInput = {",
735
793
  " [K in KnownEntityKind]: {",
736
794
  " entity_id?: string;",
@@ -738,36 +796,14 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
738
796
  " payload: StoredEntityPayload<K>;",
739
797
  " };",
740
798
  "}[KnownEntityKind];",
741
- "export interface DeleteBgmInput {",
742
- " readonly timelineEntityId: string;",
743
- "}",
744
- "export interface DeleteClipInput {",
745
- " readonly clipEntityId: string;",
746
- "}",
747
- "export interface DeleteClipTreeInput {",
748
- " readonly clipEntityIds: readonly string[];",
749
- " readonly onAnchored: 'cascade' | 'detach';",
750
- "}",
751
799
  "export interface DeleteEntityInput {",
752
800
  " entity_id: string;",
753
801
  "}",
754
- "export interface DeleteVoiceoverInput {",
755
- " readonly voiceoverClipEntityIds: readonly string[];",
756
- "}",
757
- "export type EmptyRelationKind =",
758
- " | 'timeline-track'",
759
- " | 'track-clip'",
760
- " | 'clip-marker'",
761
- " | 'marker-content'",
762
- " | 'axvideo-marker'",
763
- " | 'marker-timeline'",
764
- " | 'audio-script-marker';",
765
802
  "/** Immutable resource content resolved by the host for a document Entity. */",
766
803
  "export interface EntityAssetContent {",
767
804
  " assetId: string;",
768
805
  " content: JsonValue;",
769
806
  "}",
770
- "export type EntityId = string;",
771
807
  "export interface EntityPayloadByKind {",
772
808
  " axvideo: BoundedDerivedSequencePayload;",
773
809
  " timeline: JsonObject;",
@@ -827,49 +863,55 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
827
863
  " }[];",
828
864
  " };",
829
865
  "}",
830
- "export interface EntityStoreSnapshot {",
831
- " /** Causal compiler baseline; required for publishing edits. */",
832
- " loroSnapshot?: string;",
833
- " revision: number;",
834
- " /** Current AudioScript version attached to the project; initialized projects always attach a script, possibly empty. */",
835
- " audioScriptEntityId: string | null;",
836
- " entities: SandboxEntity[];",
837
- " relations: SandboxRelation[];",
838
- "}",
839
- "export interface InsertCaptionClipInput {",
840
- " readonly timelineEntityId: string;",
841
- " /** Existing generation identity for newly materialized Caption content, distinct from its Clip. */",
842
- " readonly captionEntityId?: string;",
843
- " /** Stable placed caption identity, distinct from the Caption content identity. */",
844
- " readonly captionClipEntityId?: string;",
845
- " /** Existing bases composed by this variant; includes an AudioScript text owner. */",
846
- " readonly baseEntityIds: readonly string[];",
847
- " /** Ordered selection of the AudioScript segments this Caption displays. */",
848
- " readonly selections: readonly CaptionSegmentSelection[];",
849
- " /** Intrinsic cue length of the Caption entity itself; display comes from the placement. */",
850
- " readonly durationMs: number;",
851
- " readonly style?: CaptionStyleFields;",
852
- " readonly placement: ClipPlacement;",
866
+ "/** Tokens have no script-authored data; only the creating session can validate them. */",
867
+ "export interface EntitySandboxCheckpoint {",
868
+ " readonly __checkpoint: unique symbol;",
853
869
  "}",
854
- "export interface InsertClipInput {",
855
- " readonly trackEntityId: string;",
856
- " /** Existing Sequence media Entity id. Asset ids and URLs are not content ids. */",
857
- " readonly contentEntityId: string;",
858
- " readonly sourceRange: SequenceRange<number>;",
859
- " readonly duration: SequenceDuration<number>;",
860
- " readonly targetRange?: SequenceRange<number>;",
861
- " readonly clipPayload?: JsonObject;",
862
- "}",
863
- "export interface InsertPlacedClipInput {",
864
- " readonly trackEntityId: string;",
865
- " readonly contentEntityId: string;",
866
- " readonly sourceRange: SequenceRange<number>;",
867
- " readonly duration: SequenceDuration<number>;",
868
- " readonly placement: ClipPlacement;",
869
- " readonly clipPayload?: JsonObject;",
870
- " /** Stable caller-owned placement identity, when one already exists outside the graph. */",
871
- " readonly clipEntityId?: string;",
870
+ "export interface EntityUpdateInput {",
871
+ " entity_id: string;",
872
+ " changes: FieldChange[];",
872
873
  "}",
874
+ "export type FieldChange =",
875
+ " | {",
876
+ " op: 'set';",
877
+ " path: FieldPath;",
878
+ " value: JsonValue;",
879
+ " }",
880
+ " | {",
881
+ " op: 'unset';",
882
+ " path: FieldPath;",
883
+ " }",
884
+ " | {",
885
+ " op: 'text.splice';",
886
+ " path: FieldPath;",
887
+ " index: number;",
888
+ " deleteCount: number;",
889
+ " text: string;",
890
+ " }",
891
+ " | {",
892
+ " op: 'list.insert';",
893
+ " path: FieldPath;",
894
+ " value: JsonValue;",
895
+ " beforeElementId: string | null;",
896
+ " }",
897
+ " | {",
898
+ " op: 'list.move';",
899
+ " path: FieldPath;",
900
+ " elementId: string;",
901
+ " beforeElementId: string | null;",
902
+ " }",
903
+ " | {",
904
+ " op: 'list.remove';",
905
+ " path: FieldPath;",
906
+ " elementId: string;",
907
+ " };",
908
+ "/** List members are addressed by identity, never a transient array offset. */",
909
+ "export type FieldPath = readonly (",
910
+ " | string",
911
+ " | {",
912
+ " elementId: string;",
913
+ " }",
914
+ ")[];",
873
915
  "export interface JsonObject {",
874
916
  " [key: string]: JsonValue;",
875
917
  "}",
@@ -902,99 +944,25 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
902
944
  " | 'phonetic-script-render'",
903
945
  " | 'audio-script-source'",
904
946
  " | 'audio-script-marker';",
905
- "export interface LinearClipSpeed {",
906
- " readonly kind: 'linear';",
907
- " readonly rate: number;",
908
- " readonly mode?: string;",
909
- "}",
910
- "/** `audio-script-source(script, source)`; the script was transcribed from the source media. */",
911
- "export interface LinkAudioScriptSourceRelationInput {",
912
- " relation_id?: string;",
913
- " script_entity_id: string;",
914
- " source_entity_id: string;",
915
- " trace?: JsonObject;",
916
- "}",
917
- "export interface LinkClipAnchorRelationInput {",
918
- " relation_id?: string;",
919
- " child_clip_entity_id: string;",
920
- " host_clip_entity_id: string;",
921
- " trace?: JsonObject;",
922
- "}",
923
- "export interface LinkGeneratedRelationInput {",
924
- " relation_id?: string;",
925
- " output_entity_id: string;",
926
- " input_entity_id: string;",
927
- " trace?: JsonObject;",
928
- "}",
929
- "export interface LinkPhoneticScriptRenderRelationInput {",
930
- " relation_id?: string;",
931
- " output_entity_id: string;",
932
- " phonetic_script_entity_id: string;",
933
- " trace?: JsonObject;",
934
- "}",
935
947
  "interface LinkRelationBase {",
936
948
  " relation_id?: string;",
937
949
  " endpoint_0_entity_id: string;",
938
950
  " endpoint_1_entity_id: string;",
939
951
  " trace?: JsonObject;",
940
952
  "}",
941
- "export type LinkRelationInput =",
942
- " | (LinkRelationBase & {",
943
- " relation_kind: EmptyRelationKind;",
944
- " metadata?: {",
945
- " [key: string]: never;",
946
- " };",
947
- " })",
948
- " | (LinkRelationBase & {",
949
- " relation_kind: 'physical-asset';",
950
- " metadata?: JsonObject;",
951
- " })",
952
- " | (LinkRelationBase & {",
953
- " relation_kind: 'caption-alignment';",
954
- " metadata: JsonObject & {",
955
- " alignment: JsonValue;",
956
- " };",
957
- " });",
953
+ "export interface LinkRelationInput extends LinkRelationBase {",
954
+ " relation_kind: KnownRelationKind;",
955
+ " metadata?: JsonObject;",
956
+ "}",
958
957
  "export type MediaAssetPayload = JsonObject & {",
959
958
  " external: {",
960
959
  " system: 'memota' | 'memota-speech';",
961
960
  " key: string;",
962
961
  " };",
963
962
  "};",
964
- "export interface MoveClipInput {",
965
- " readonly clipEntityId: string;",
966
- " readonly trackEntityId: string;",
967
- "}",
968
- "export interface MoveClipsToStartsInput {",
969
- " readonly moves: readonly {",
970
- " readonly clipEntityId: string;",
971
- " readonly newStartMs: number;",
972
- " }[];",
973
- " /** Absolute-time drags preserve every voiceover's current visible landing. */",
974
- " readonly onAnchored: 'keepAbsolute';",
975
- "}",
976
- "export interface MoveSequentialClipsInput {",
977
- " readonly clipEntityIds: readonly string[];",
978
- " readonly anchor: SequentialClipAnchor;",
979
- " readonly onAnchored: 'follow' | 'keepAbsolute';",
980
- "}",
981
- "export interface MoveVoiceoverInput {",
982
- " readonly voiceoverClipEntityId: string;",
983
- " /** Absolute requested timeline start; MEngine resolves and persists the host relation. */",
984
- " readonly newStartMs: number;",
985
- "}",
986
- "export interface PatchCaptionStyleInput {",
987
- " readonly timelineEntityId: string;",
988
- " readonly style: CaptionStyleFields;",
989
- "}",
990
- "export interface ReplaceClipContentInput {",
991
- " readonly clipEntityId: string;",
992
- " /** Existing Sequence media Entity id. Asset ids and URLs are not content ids. */",
993
- " readonly contentEntityId: string;",
994
- " readonly sourceRange: SequenceRange<number>;",
995
- " readonly duration: SequenceDuration<number>;",
996
- " readonly targetRange?: SequenceRange<number>;",
997
- " readonly timeRemapping?: JsonValue;",
963
+ "export interface RelationUpdateInput {",
964
+ " relation_id: string;",
965
+ " changes: FieldChange[];",
998
966
  "}",
999
967
  "export interface SandboxEntity<K extends KnownEntityKind = KnownEntityKind> {",
1000
968
  " entity_id: string;",
@@ -1014,43 +982,6 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
1014
982
  " text: string;",
1015
983
  " language?: string;",
1016
984
  "};",
1017
- "export type SequenceDuration<Span = unknown> =",
1018
- " | {",
1019
- " readonly mode: 'from-source';",
1020
- " }",
1021
- " | {",
1022
- " readonly mode: 'fixed';",
1023
- " readonly value: Span;",
1024
- " };",
1025
- "export interface SequenceRange<Point = unknown> {",
1026
- " readonly start: Point;",
1027
- " readonly end: Point;",
1028
- "}",
1029
- "export type SequentialClipAnchor =",
1030
- " | {",
1031
- " readonly position: 'before' | 'after';",
1032
- " readonly clipEntityId: string;",
1033
- " }",
1034
- " | {",
1035
- " readonly position: 'trackStart';",
1036
- " };",
1037
- "export interface SetCaptionVisibilityInput {",
1038
- " readonly timelineEntityId: string;",
1039
- " readonly hidden: boolean;",
1040
- "}",
1041
- "export interface SetClipPlacementInput {",
1042
- " readonly clipEntityId: string;",
1043
- " readonly placement: ClipPlacement;",
1044
- "}",
1045
- "export interface SetClipSpeedInput {",
1046
- " readonly clipEntityId: string;",
1047
- " readonly timeRemapping: LinearClipSpeed | null;",
1048
- "}",
1049
- "export interface SetClipVolumeInput {",
1050
- " readonly clipEntityId: string;",
1051
- " /** Playback gain in decibels. */",
1052
- " readonly volume: number;",
1053
- "}",
1054
985
  "/** Stored own fields; a variant may obtain required content fields from its declared bases. */",
1055
986
  "export type StoredEntityPayload<K extends KnownEntityKind> =",
1056
987
  " | EntityPayloadByKind[K]",
@@ -1059,10 +990,6 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
1059
990
  " Partial<EntityPayloadByKind[K]> & {",
1060
991
  " baseEntityIds: string[];",
1061
992
  " });",
1062
- "export interface TrimClipInput {",
1063
- " readonly clipEntityId: string;",
1064
- " readonly sourceRange: SequenceRange<number>;",
1065
- "}",
1066
993
  "export interface UnboundedConstantSequencePayload extends JsonObject {",
1067
994
  " extent: {",
1068
995
  " kind: 'unbounded';",
@@ -1074,96 +1001,60 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
1074
1001
  "export interface UnlinkRelationInput {",
1075
1002
  " relation_id: string;",
1076
1003
  "}",
1077
- "export interface UpdateClipInput {",
1078
- " readonly clipEntityId: string;",
1079
- " /** Complete replacement for the Clip-owned payload. */",
1080
- " readonly payload: JsonObject;",
1081
- "}",
1082
- "export interface UpdateClipMarkerInput {",
1083
- " readonly clipEntityId: string;",
1084
- " readonly sourceRange?: SequenceRange<number>;",
1085
- " /** Passing `undefined` explicitly removes the optional target range. */",
1086
- " readonly targetRange?: SequenceRange<number> | undefined;",
1087
- " readonly duration?: SequenceDuration<number>;",
1088
- " /** Passing `undefined` explicitly removes the optional remapping value. */",
1089
- " readonly timeRemapping?: JsonValue | undefined;",
1090
- "}",
1091
1004
  "/** Patch supplied fields on the assembled entity; omitted fields remain unchanged. */",
1092
1005
  "export interface UpdateEntityInput {",
1093
1006
  " entity_id: string;",
1094
1007
  " payload: JsonObject;",
1095
1008
  "}",
1096
- "/** Timeline writes accept existing media Entity ids, never Memota asset ids or URLs. */",
1097
- "export interface EditApi {",
1098
- " insertClip(input: InsertClipInput): ClipEntityId;",
1099
- " insertPlacedClip(input: InsertPlacedClipInput): ClipEntityId;",
1100
- " updateClipMarker(input: UpdateClipMarkerInput): void;",
1101
- " setClipPlacement(input: SetClipPlacementInput): void;",
1102
- " moveSequentialClips(input: MoveSequentialClipsInput): void;",
1103
- " moveClip(input: MoveClipInput): void;",
1104
- " replaceClipContent(input: ReplaceClipContentInput): void;",
1105
- " setClipVolume(input: SetClipVolumeInput): void;",
1106
- " setClipSpeed(input: SetClipSpeedInput): void;",
1107
- " trimClip(input: TrimClipInput): void;",
1108
- " deleteClip(input: DeleteClipInput): void;",
1109
- " deleteClipTree(input: DeleteClipTreeInput): void;",
1110
- " updateClip(input: UpdateClipInput): void;",
1111
- " moveVoiceover(input: MoveVoiceoverInput): void;",
1112
- " moveClipsToStarts(input: MoveClipsToStartsInput): void;",
1113
- " deleteVoiceover(input: DeleteVoiceoverInput): void;",
1114
- " deleteBgm(input: DeleteBgmInput): void;",
1115
- " setCaptionVisibility(input: SetCaptionVisibilityInput): void;",
1116
- " patchCaptionStyle(input: PatchCaptionStyleInput): void;",
1117
- " insertCaptionClip(input: InsertCaptionClipInput): ClipEntityId;",
1118
- "}",
1119
- "export interface TimelineApi {",
1120
- " snapshot(): EntityStoreSnapshot & {",
1121
- " audioScriptEntityId: string;",
1122
- " };",
1123
- "}",
1124
- "export interface SandboxCheckpoint {",
1125
- " readonly index: number;",
1126
- "}",
1127
- "export declare const edit: EditApi;",
1128
- "export declare const timeline: TimelineApi;",
1129
1009
  "export declare const entities: BusinessEntityFacade;",
1130
1010
  "export declare const relations: BusinessRelationFacade;",
1131
- "/** Resolve the resource attached to an Entity through the host; await before using a new Caption. */",
1011
+ "/** Resolve this Entity's attached resource through the host; await before using an uninitialized Caption. */",
1132
1012
  "export declare function rgetAssetFromEntity(entityId: string): Promise<EntityAssetContent>;",
1133
- "export declare function checkpoint(): SandboxCheckpoint;",
1134
- "export declare function rollbackTo(cp: SandboxCheckpoint): void;",
1013
+ "export declare function checkpoint(): EntitySandboxCheckpoint;",
1014
+ "export declare function rollbackTo(cp: EntitySandboxCheckpoint): void;",
1135
1015
  "export declare const inputs: Readonly<Record<string, unknown>>;",
1016
+ "export declare const console: {",
1017
+ " log(...values: unknown[]): void;",
1018
+ " info(...values: unknown[]): void;",
1019
+ " warn(...values: unknown[]): void;",
1020
+ " error(...values: unknown[]): void;",
1021
+ "};",
1136
1022
  ""
1137
1023
  ].join("\n");
1138
1024
  //#endregion
1139
1025
  //#region src/prompt.ts
1140
1026
  const MEDEO_TOOL_DESCRIPTION = `
1141
- Edit the authoritative Medeo Entity/Relation graph through a deterministic, side-effect-free JavaScript sandbox. Timeline objects and edit targets are Entities, not Memota assets or legacy parts.
1027
+ Edit the authoritative Medeo Entity/Relation graph in an isolated JavaScript sandbox.
1142
1028
 
1143
1029
  Operations:
1144
- - snapshot: read the current Entity/Relation view, project attachments and causal Loro baseline. Reading does not initialize or mutate domain data.
1145
- - run-edit-script: inspect timeline.snapshot(), entities.*, and relations.*; edit.* operates existing Entity ids and creates the required Clip/SequenceMarker structural graph. The sandbox has no direct network, storage or generation access. rgetAssetFromEntity(entityId) delegates an attached-resource read to the host. Use assembled entity fields; the host manages resource storage and generation provenance. A successful run returns preview, logs, base revision and plan_id.
1146
- - commit-plan: publish the native Loro update compiled against the plans causal baseline. Concurrent independent edits merge through Loro. The timeline and AudioScript panel read the merged entity state. A failed transport is unconfirmed; retry the same plan_id so operation identities are preserved.
1030
+ - snapshot: read assembled entities, relations, project attachments and the current document version. Reading never initializes or changes the document.
1031
+ - run-edit-script: use entities and relations to inspect and edit the causal working view. rgetAssetFromEntity(entityId) delegates an attached-resource read to the host. The script has no network, generation or storage access. A successful run returns preview, logs and plan_id.
1032
+ - commit-plan: publish the original native Loro operations compiled against the plan's causal baseline. Independent concurrent edits merge; failed transport is unconfirmed, so retry the same plan_id to preserve operation identity.
1147
1033
 
1148
- Default flow: snapshot → run-edit-script with auto_commit=false → inspect preview → commit-plan. Use auto_commit=true only for low-risk edits when the host does not need user confirmation. Concurrent edits do not require replaying the script against a newer snapshot. If a domain conflict is reported, inspect the merged state and resolve it explicitly; never replace the complete document to force the edit through.
1149
-
1150
- Generation tools return Asset references, not domain Entity ids. An Asset id may be passed as an entity resource field: external: { system: "memota", key: assetId }. Create or update the domain Entity using entities.*, then place its Entity id with edit.insertClip. Asset ids are not Entity ids, baseEntityIds or Relation endpoints. The sandbox exposes no Asset creation, lookup, reading or storage API; the host resolves resource references. Each placement has its own Clip and SequenceMarker. Generation lineage is host-synced; relations.of(entityId) is endpoint-agnostic.
1034
+ Default flow: snapshot → run-edit-script with auto_commit=false → inspect preview → commit-plan. Use auto_commit=true for low-risk edits that need no host confirmation. A domain conflict requires inspecting and explicitly resolving the merged state; never overwrite the document or silently replay an old plan on a new snapshot.
1151
1035
  `.trim();
1152
1036
  const MEDEO_TOOL_EXECUTION_RULES = `
1153
- The host supplies the current document. Do not ask for, invent, or pass a document id.
1154
- timeline.snapshot() returns the Entity/Relation graph with its revision, not a legacy VideoDraft. Inspect Timeline, Track, Clip, SequenceMarker and their relations to plan edits.
1155
- Inspect existing Image/Video/Audio/Voice Entities and their factual extents before placing them. For an uploaded or generated resource, create its media Entity with the returned Asset id in external.key and the factual media extent. The host resolves its physical resource. Replace a Clip's content using another content Entity id. Never fabricate a duration.
1156
- For a Caption Asset, create a Caption with payload:{external:{system:'memota',key:assetId}}, then await rgetAssetFromEntity(captionEntityId). This initializes its intrinsic segmentRanges/extent and the project AudioScript text in the same plan. The result {assetId,content} is the original resource content. entities.get/readCaptionContent then expose editable assembled text. Call the getter before placing a newly resource-backed Caption. Caption resource initialization also runs before final plan validation; failure aborts the plan. Do not transcribe repeatedly or infer speech absence from visual descriptions when an output_caption Asset already exists. Existing text edits are preserved on repeated reads. No Asset ID can be used as the getter argument.
1157
- Caption composes AudioScript text. Its optional external field may carry the Caption Asset id; the host resolves that resource. A Caption created from AudioScript may have no physical resource. Read and edit assembled fields through entities; do not create Asset entities or physical-asset Relations.
1158
- The editor projection supports the existing four Track roles: video_clip (Image/Video), speech (Voice), caption (Caption), and bgm (Audio), one of each. Clip.volume is decibels (-60 to 20, 0 = original). Marker.sourceRange is the selected source interval; Marker.duration is effective display/playback duration. Coordinates and duration are whole milliseconds for this reader, not a global DSL restriction. Placement is exactly one of Clip.order, Marker.targetRange, or clip-anchor(child,host) plus Marker.anchorOffset. Use the native move/delete/voiceover helpers so placement and cascade decisions are in the same entity plan. Reading the graph never rebinds anchors or invents empty clips. Linear timeRemapping is {kind:'linear',rate:2,mode:'constant'} and agrees with rounded source span divided by rate. Image remains unbounded/constant; an explicit linear rate scales its display window, not an invented media extent. Nonlinear speed and multiple visual overlay tracks are unsupported.
1159
- Entities own fields; ordinary Relations express associations; variants directly hold baseEntityIds and assemble the referenced entities. These foundations are fixed: implementation must follow them, never redefine them. Any entity may compose multiple bases. Equal field names from multiple bases (even equal values) are errors, even when the variant declares that field itself. After validating all base fields are unambiguous, explicitly declared own fields may override base fields without mutating the bases. Base ordering never resolves conflicts. AudioScript owns segmented text. Caption and PhoneticScript persist baseEntityIds including their AudioScript, plus their own fields; no composition Relation exists. Create the real bases before reading or committing a variant. Inside the DSL sandbox, entities.get/list expose complete assembled fields. Consumers read fields without inspecting base IDs or merging bases. entities.update patches supplied fields and routes inherited fields to their declaring entity; omitted fields remain unchanged. entities.declareFields explicitly declares own overrides and is distinct from an ordinary field edit. Persistence keeps owned fields only. entities.readCaptionContent(id) and entities.readPhoneticScriptContent(id) return assembled text. Missing/cyclic bases and field conflicts fail before persistence.
1160
- Use edit.insertCaptionClip with baseEntityIds and selections, plus captionEntityId when generation returned a Caption identity; each voiceover caption also supplies baseEntityIds. A selection names segmentId and may use a half-open Unicode code-point textRange to split a segment for the screen without rewriting AudioScript. Generate Voice from an existing PhoneticScript, then use phoneticScriptEntityId in the voiceover helper or relations.linkPhoneticScriptRender({output_entity_id,phonetic_script_entity_id}) for its render relation. Caption and Voice have their own Clips; display anchoring is explicit and independent of composition/alignment.
1161
- Move or stretch only the Clip's display Marker; preserve Caption intrinsic Sequence, AudioScript text and its annotation Markers. AudioScript cannot enter a Clip and has no intrinsic time. audio-script-source links its ASR source Audio/Video/Voice; audio-script-marker attaches annotation Markers with directly assigned segmentRanges:{segmentId,startMs,endMs} in whole milliseconds. Annotation Markers have no Clip/AXVideo/content/Timeline relations and never refer to other Markers for time. BGM keeps factual source duration with durationPolicy:'timeline'. Never introduce a speech entity kind.
1162
- Create only the known entity kinds. Project creation initializes one current Timeline, four Tracks and an attached AudioScript with segments:[]. Read the current AudioScript ID from timeline.snapshot(); the panel displays this attachment and preserves its segments. Normal edits operate this script, not an unrelated newly created script.
1163
- Immutable updates apply to every entity: editing an owned field creates a new content version ID; changing only a variant's base ID preserves the variant ID. Editing a base through a variant updates the owner, and the compiler advances the affected base links and project attachment. A variant-owned edit creates a new variant version and retains its unchanged bases. Do not manually clone entities or duplicate inherited fields to implement versioning. Versions preserve native text and list editing identities so independent concurrent edits survive. Missing facts, unsupported layouts and composition conflicts are explicit errors; there is no legacy migration or whole-state overwrite path.
1164
- Use only the globals and methods declared by the following TypeScript interface. Values not declared here are unavailable.
1037
+ The host supplies the current document. Do not invent or request a document id. The sandbox exposes only the API below: entity operations, relation operations, entity-bound resource reading and checkpoints. All business edits are expressed using entity fields and relation kinds.
1038
+
1039
+ Entity reads return assembled snapshots. Mutating returned objects has no persistence effect. Create declares own fields. Update requires a nonempty changes array and atomically applies its ordered operations to assembled fields, routing each to its declaring owner. DeclareFields explicitly establishes own fields or an override; it is not the normal way to edit inherited content. Deletion requires explicitly handling incident relations, variant references and protected project attachments; it never deletes external resources or cascades business entities.
1040
+
1041
+ FieldChange paths start at payload for an entity, and at metadata or trace for a relation. Use strings for fields and {elementId:'...'} for list members, never numeric indices. Segment identity is segmentId; reference identity is the referenced id. set writes scalars or schema-declared atomic values, including complete sourceRange, targetRange, extent, duration, timeRemapping, textRange and external. It cannot replace collaborative text, lists or maps. Edit text with text.splice (Unicode code-point index and deleteCount), and segments/selections/segmentRanges with list.insert, list.move or list.remove. beforeElementId:null means append; moving before itself, duplicate identity or an absent anchor is an error. Unset removes optional fields only. Map fields are edited through leaf paths; initialize a new own map via declareFields. baseEntityIds is unordered composition membership: insert/remove are supported, moving bases cannot resolve field conflicts. Independent fields and list-item text edits retain native Loro identities.
1042
+
1043
+ Entities own data. Relations associate two existing entities; endpoint_0 and endpoint_1 retain their declared positions. Each relation kind defines direction and meaning; neither relation listing order nor endpoint storage order implies a global business order. Relations.update changes metadata/trace fields only; kind/endpoints require explicit unlink plus link with a new identity. Generated relations use generated(output,input). Ordinary associations do not implement composition.
1044
+
1045
+ Variants directly hold baseEntityIds. Assembly validates every base before applying explicit own overrides: two bases supplying the same field are an error even if values match or the variant declares that field. Missing/cyclic bases fail. Consumers operate assembled fields without copying inherited content or resolving owners themselves. An owned-field edit creates a new immutable entity version. Base changes automatically advance variant base references and project attachments without versioning an unchanged variant. A variant-owned change versions the variant and retains unchanged bases. Do not manually clone entities for versioning. Use the current plan's ids throughout that execution; refresh after commit.
1046
+
1047
+ Generation tools return resource references. An asset id may be passed in an entity's external:{system:'memota',key:assetId}; it is never an entity id, baseEntityId or relation endpoint. Physical resources are host infrastructure. The sandbox has no direct resource lookup/CRUD by asset id. rgetAssetFromEntity takes only an existing business entity id and returns {assetId,content} for its attached resource. For output_caption, create a Caption with payload:{external:{system:'memota',key:assetId}}, then await rgetAssetFromEntity(captionId). The host initializes Caption intrinsic timing and its composed AudioScript text in the same causal plan; Without explicit bases, the host creates an independent AudioScript for that resource. With bases, it resolves their unique AudioScript text owner and preserves other nonconflicting bases. It never routes text through the panel selection. Read the editable fields with entities.get afterwards. Repeated reads preserve edited text and return the original immutable resource content. Unawaited initialization is completed before validation; failure prevents publication. Resource-free Caption composition from AudioScript is also valid.
1048
+
1049
+ Project initialization creates one current Timeline, the four standard Tracks and one attached AudioScript with segments:[]. A project may contain multiple independent complete AudioScripts (for example a generated script and an uploaded video transcript). The project AudioScript attachment selects only the panel view; it is not a singleton or membership constraint. Locate scripts with entities.list and choose the appropriate source. AudioScript owns independent multi-segment text; each can have multiple Caption variants. Caption and PhoneticScript compose it through baseEntityIds. Caption owns selections ({segmentId,textRange?}), intrinsic segmentRanges and style; segmentId is a stable script-local identity, not an entity reference or transient array index; selection textRange is a half-open code-point range and can split display text without rewriting the script. PhoneticScript owns pronunciation/prosody; Voice is generated from it and related by phonetic-script-render(voice,phoneticScript). Speech and caption occupy separate Clips. Subtitles normally accompany sound; use source media or generate Voice from PhoneticScript when the request requires narration. Do not fabricate a sound resource.
1050
+
1051
+ AudioScript has no intrinsic time and cannot be Clip content. ASR input Audio/Video/Voice associates via audio-script-source(script,source). Its external annotation SequenceMarker associates via audio-script-marker(script,marker) and receives assigned segmentRanges, not references to another marker. Caption intrinsic timing and its Clip's display SequenceMarker are distinct. Move/stretch display by editing only the placement marker.
1052
+
1053
+ To place content, create a Clip and a SequenceMarker, then link track-clip(track,clip), clip-marker(clip,marker) and marker-content(marker,content). Existing track membership uses timeline-track(timeline,track). The editor has one track of each role: video_clip for Image/Video, speech for Voice, caption for Caption, bgm for Audio. Inspect factual media extents; never invent duration. Each placement is exactly one of Clip.order (sequential), Marker.targetRange (absolute) or clip-anchor(childClip,hostClip) plus Marker.anchorOffset. Marker.sourceRange selects source, duration describes playback/display. Whole milliseconds are required by this editor projection. Clip.volume is decibels (-60..20, 0 original). Image is unbounded/constant. Linear timeRemapping is {kind:'linear',rate:2,mode:'constant'}, with duration matching the rounded source span/rate. BGM can use durationPolicy:'timeline'. Nonlinear speed and multiple overlay tracks are unsupported. Caption visibility is the caption Track's hidden field. Caption style is its style map. Handle structural links explicitly when moving/deleting; there is no implicit cascade.
1054
+
1055
+ checkpoint returns an opaque token valid only in this execution. rollbackTo restores that point and invalidates later checkpoints. Returned read snapshots and inputs are not writable document authorities. Use only the declarations below.
1165
1056
  `.trim();
1166
- /** Render the complete MEngine-owned context injected before one model call. */
1057
+ /** MEngine owns API disclosure; Harness only supplies the changing document context. */
1167
1058
  function renderMedeoModelContext(input) {
1168
1059
  const updated = input.updatedSincePreviousModelCall == null ? "unknown (first model call)" : String(input.updatedSincePreviousModelCall);
1169
1060
  return `
@@ -1171,11 +1062,11 @@ ${MEDEO_TOOL_DESCRIPTION}
1171
1062
 
1172
1063
  ${MEDEO_TOOL_EXECUTION_RULES}
1173
1064
 
1174
- Current MEngine document state (sampled dynamically immediately before this model call):
1065
+ Current MEngine document state (sampled immediately before this model call):
1175
1066
  - document_version: ${JSON.stringify(input.documentVersion)}
1176
1067
  - updated_since_previous_model_call: ${updated}
1177
1068
 
1178
- When updated_since_previous_model_call is true, the document changed after the previous model call. The change may have come from this tool or another editor, so take a fresh snapshot before planning further edits.
1069
+ When updated_since_previous_model_call is true, take a fresh snapshot before planning further edits.
1179
1070
 
1180
1071
  Sandbox TypeScript interface:
1181
1072
  \`\`\`ts
@@ -1764,7 +1655,6 @@ function createMedeoTool(options) {
1764
1655
  document,
1765
1656
  baseVersion,
1766
1657
  entityState,
1767
- entityOnly: true,
1768
1658
  loadEntityAsset: options.loadEntityAsset ? (entity) => options.loadEntityAsset(input.doc_id, entity) : void 0,
1769
1659
  script: input.script,
1770
1660
  ...input.inputs !== void 0 ? { inputs: input.inputs } : {},
@@ -1912,13 +1802,10 @@ async function materializeResources(options, resources) {
1912
1802
  ids.push(sandbox.entities.ensureMedia(resource).contentEntityId);
1913
1803
  continue;
1914
1804
  }
1915
- const assetId = stableId("asset", resource.assetId);
1916
- const asset = sandbox.entities.get(assetId);
1917
- if (asset) {
1918
- if (asset.entity_kind !== "asset" || asset.payload.storageKey !== resource.storageKey) throw new Error("Conflicting Caption resource identity");
1919
- const captions = sandbox.relations.of(assetId, "physical-asset").map((edge) => sandbox.entities.get(edge.endpoint_0_entity_id)).filter((entity) => entity?.entity_kind === "caption");
1920
- if (captions.length !== 1) throw new Error("Caption resource must resolve to one materialized Caption");
1921
- ids.push(captions[0].entity_id);
1805
+ const existingCaption = sandbox.entities.list().find((row) => row.entity_kind === "caption" && row.payload.external?.key === resource.assetId);
1806
+ if (existingCaption) {
1807
+ if (existingCaption.payload.storageKey !== resource.storageKey) throw new Error("Conflicting Caption resource identity");
1808
+ ids.push(existingCaption.entity_id);
1922
1809
  continue;
1923
1810
  }
1924
1811
  if (!resource.assetId.trim() || !resource.storageKey.trim() || !resource.segments.length) throw new Error("Caption resource requires a physical locator and timed segments");
@@ -1926,25 +1813,35 @@ async function materializeResources(options, resources) {
1926
1813
  if (typeof segment.text !== "string" || !Number.isFinite(segment.startMs) || !Number.isFinite(segment.endMs) || segment.startMs < 0 || segment.endMs <= segment.startMs) throw new Error("Caption resource has invalid ASR text or timing");
1927
1814
  for (const word of segment.words ?? []) if (typeof word.text !== "string" || !Number.isFinite(word.startMs) || !Number.isFinite(word.endMs) || word.startMs < segment.startMs || word.endMs > segment.endMs || word.endMs < word.startMs) throw new Error("Caption resource has invalid ASR word timing");
1928
1815
  }
1929
- const scriptId = sandbox.audioScriptEntityId;
1930
- if (!scriptId) throw new Error("Document AudioScript is not initialized");
1931
- const script = sandbox.entities.get(scriptId);
1816
+ const scriptId = stableId("entity", "audio-script", resource.assetId);
1932
1817
  const segments = resource.segments.map((segment, index) => ({
1933
1818
  segmentId: stableId("segment", resource.assetId, String(index)),
1934
1819
  text: segment.text
1935
1820
  }));
1936
1821
  const start = Math.min(...resource.segments.map((segment) => segment.startMs));
1937
1822
  const end = Math.max(...resource.segments.map((segment) => segment.endMs));
1938
- sandbox.entities.update({
1823
+ sandbox.entities.create({
1939
1824
  entity_id: scriptId,
1940
- payload: { segments: [...script.payload.segments, ...segments] }
1825
+ entity_kind: "audio-script",
1826
+ payload: { segments }
1941
1827
  });
1828
+ const ranges = resource.segments.map((segment, index) => ({
1829
+ segmentId: segments[index].segmentId,
1830
+ startMs: segment.startMs,
1831
+ endMs: segment.endMs
1832
+ }));
1942
1833
  const captionId = stableId("entity", "caption", resource.assetId);
1943
1834
  sandbox.entities.create({
1944
1835
  entity_id: captionId,
1945
1836
  entity_kind: "caption",
1946
1837
  payload: {
1947
1838
  baseEntityIds: [scriptId],
1839
+ external: {
1840
+ system: "memota",
1841
+ key: resource.assetId
1842
+ },
1843
+ storageKey: resource.storageKey,
1844
+ segmentRanges: ranges,
1948
1845
  selections: segments.map(({ segmentId }) => ({ segmentId })),
1949
1846
  extent: {
1950
1847
  kind: "bounded",
@@ -1981,23 +1878,6 @@ async function materializeResources(options, resources) {
1981
1878
  endpoint_0_entity_id: scriptId,
1982
1879
  endpoint_1_entity_id: markerId
1983
1880
  });
1984
- sandbox.entities.create({
1985
- entity_id: assetId,
1986
- entity_kind: "asset",
1987
- payload: {
1988
- external: {
1989
- system: "memota",
1990
- key: resource.assetId
1991
- },
1992
- storageKey: resource.storageKey
1993
- }
1994
- });
1995
- sandbox.relations.link({
1996
- relation_id: stableId("relation", captionId, assetId),
1997
- relation_kind: "physical-asset",
1998
- endpoint_0_entity_id: captionId,
1999
- endpoint_1_entity_id: assetId
2000
- });
2001
1881
  ids.push(captionId);
2002
1882
  }
2003
1883
  const candidate = sandbox.buildPlan();
@@ -2028,6 +1908,6 @@ function stableId(prefix, ...parts) {
2028
1908
  return `${prefix}_${createHash("sha256").update(JSON.stringify(parts)).digest("hex")}`;
2029
1909
  }
2030
1910
  //#endregion
2031
- export { EditSandboxSession, MEDEO_TOOL_DESCRIPTION, MEDEO_TOOL_NAME, MEDEO_TOOL_PARAMETERS, collectAffectedPartIds, commitPlan, createMedeoTool, materializeResources, renderCompactProjection, renderPreview, runEditScript };
1911
+ export { MEDEO_TOOL_DESCRIPTION, MEDEO_TOOL_NAME, MEDEO_TOOL_PARAMETERS, collectAffectedPartIds, commitPlan, createMedeoTool, materializeResources, renderCompactProjection, renderPreview, runEditScript };
2032
1912
 
2033
1913
  //# sourceMappingURL=index.mjs.map