@mengine/medeo-client 1.2.1-alpha.1 → 1.2.1-alpha.2

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.
@@ -322,6 +322,71 @@ function syncAggregatedClipsTimePosition(doc) {
322
322
  }
323
323
  }
324
324
  /**
325
+ * Resolve authoritative `time_position` edges without inventing parents from
326
+ * geometric overlap. Roots stay absolute/sequential; every anchored descendant
327
+ * follows its declared parent, regardless of whether that parent is video, BGM,
328
+ * or speech.
329
+ */
330
+ function resolveAnchoredClipsTimePosition(doc) {
331
+ const items = [
332
+ ...doc.main_track,
333
+ ...doc.bgm_track,
334
+ ...doc.speech_track,
335
+ ...doc.caption_track
336
+ ];
337
+ const byPart = indexByPart(items);
338
+ const resolved = /* @__PURE__ */ new Set();
339
+ const visiting = /* @__PURE__ */ new Set();
340
+ const resolve = (item) => {
341
+ if (resolved.has(item.part_id)) return item.abs_time_position;
342
+ if (visiting.has(item.part_id)) throw new Error(`anchored position cycle at ${item.part_id}`);
343
+ visiting.add(item.part_id);
344
+ const position = item.time_position;
345
+ if (position.mode === "absolute") item.abs_time_position = Math.round(position.offsetMs);
346
+ else if (position.mode === "anchored") {
347
+ const parent = byPart.get(position.anchorPartId);
348
+ if (parent != null) item.abs_time_position = resolve(parent) + Math.round(position.offsetMs);
349
+ }
350
+ visiting.delete(item.part_id);
351
+ resolved.add(item.part_id);
352
+ return item.abs_time_position;
353
+ };
354
+ for (const item of items) resolve(item);
355
+ }
356
+ /** Preserve the historical orphan fallback without re-parenting valid roots. */
357
+ function recoverOrphanedSpeechAnchorsByTime(doc) {
358
+ const placedPartIds = new Set([
359
+ ...doc.main_track,
360
+ ...doc.bgm_track,
361
+ ...doc.speech_track,
362
+ ...doc.caption_track
363
+ ].map((item) => item.part_id));
364
+ const ranges = doc.main_track.map((item) => ({
365
+ part_id: item.part_id,
366
+ start: item.abs_time_position,
367
+ end: item.abs_time_position + partDurationMs(doc, item.part_id)
368
+ }));
369
+ if (ranges.length === 0) return;
370
+ for (const speech of doc.speech_track) {
371
+ const position = speech.time_position;
372
+ if (position.mode !== "anchored" || placedPartIds.has(position.anchorPartId)) continue;
373
+ const range = ranges.find((candidate) => candidate.start <= speech.abs_time_position && speech.abs_time_position < candidate.end) ?? (speech.abs_time_position < ranges[0].start ? ranges[0] : ranges[ranges.length - 1]);
374
+ const relative = Math.max(0, Math.round(speech.abs_time_position - range.start));
375
+ let aggregation = doc.aggregations.find((candidate) => candidate.body_part_id === range.part_id);
376
+ if (aggregation == null) {
377
+ aggregation = {
378
+ body_part_id: range.part_id,
379
+ attachments: []
380
+ };
381
+ doc.aggregations.push(aggregation);
382
+ }
383
+ aggregation.attachments.push({
384
+ part_id: speech.part_id,
385
+ relative_time_position: relative
386
+ });
387
+ }
388
+ }
389
+ /**
325
390
  * Reassign each speech to the video clip whose time range contains its start,
326
391
  * rebuilding `aggregations`. A speech before the first clip or after the last
327
392
  * falls back to the first / last clip respectively (FE: see §4 note — FE falls
@@ -524,14 +589,14 @@ function hostVideoOf(doc, speechPartId) {
524
589
  //#endregion
525
590
  //#region src/timeline-core/entrypoints.ts
526
591
  /**
527
- * The full solve pipeline: arrange → sync → reassign → resolve-overlap →
528
- * fill-gaps → recalc. The single cascade the read-side projection runs; ops
592
+ * The full solve pipeline: arrange → resolve declared parent edges →
593
+ * resolve-overlap → fill-gaps → recalc. The single cascade the read-side projection runs; ops
529
594
  * never call it (they write only facts — RFC 02 §7/§10).
530
595
  */
531
596
  function cascadeAfterVideoClipChanges(doc, makeEmptyPart) {
532
597
  arrangeMainTrackSeamlessly(doc);
533
- syncAggregatedClipsTimePosition(doc);
534
- reassignSpeechesToVideoClipsByTime(doc);
598
+ resolveAnchoredClipsTimePosition(doc);
599
+ recoverOrphanedSpeechAnchorsByTime(doc);
535
600
  resolveAllSpeechOverlaps(doc);
536
601
  fillMainTrackTimeGaps(doc, makeEmptyPart);
537
602
  recalculateTimelineDuration(doc);
@@ -605,7 +670,7 @@ function videoDocumentToTimelineDoc(document) {
605
670
  caption_track: captionItems,
606
671
  bgm_track: bgmItems,
607
672
  part_library: partLibrary,
608
- aggregations: seedAggregations(mainItems, speechItems),
673
+ aggregations: seedAggregations([...mainItems, ...bgmItems], speechItems),
609
674
  timeline: {
610
675
  duration_ms: 0,
611
676
  unit_time_ms: document.timeline?.unit_time_ms ?? 0
@@ -629,19 +694,18 @@ function seedAbs(position, fallbackAbsMs) {
629
694
  }
630
695
  }
631
696
  /**
632
- * Build the cascade `aggregations` parent-map from anchored items: an anchored
633
- * item's `anchorPartId` is the host's `part_id` (= `body_part_id`), `offsetMs`
634
- * is `relative_time_position`. Only main-track (video) hosts form aggregations;
635
- * caption→speech relations are recomputed by the cascade from `caption.start_ms`.
697
+ * Build the read-view `aggregations` parent-map from anchored speech items.
698
+ * Video and BGM roots are both valid hosts; absolute speech roots have no entry.
699
+ * Caption→speech ownership remains in `speech_part_id` / `caption_ids`.
636
700
  */
637
- function seedAggregations(mainItems, speechItems) {
638
- const mainPartIds = new Set(mainItems.map((item) => item.part_id));
701
+ function seedAggregations(hostItems, speechItems) {
702
+ const hostPartIds = new Set(hostItems.map((item) => item.part_id));
639
703
  const byHost = /* @__PURE__ */ new Map();
640
704
  const order = [];
641
705
  for (const speech of speechItems) {
642
706
  if (speech.time_position.mode !== "anchored") continue;
643
707
  const host = speech.time_position.anchorPartId;
644
- if (!mainPartIds.has(host)) continue;
708
+ if (!hostPartIds.has(host)) continue;
645
709
  let aggregation = byHost.get(host);
646
710
  if (aggregation == null) {
647
711
  aggregation = {
@@ -1663,4 +1727,4 @@ function toTrackRow(track) {
1663
1727
  };
1664
1728
  }
1665
1729
  //#endregion
1666
- export { partDurationMs as A, reassignSpeechesToVideoClipsByTime as C, syncAggregatedClipsTimePosition as D, resolveSpeechOverlapByShiftingVideos as E, partUnionToDraft as F, recordEntries as I, videoDocumentMirrorSchema as L, DEFAULT_UNIT_TIME_MS as M, VIDEO_DOCUMENT_SCHEMA_VERSION as N, TIMELINE_SKELETON_DURATION_MS as O, effectiveVideoClipDurationMs as P, base64ToBytes as R, fillMainTrackTimeGaps as S, resolveAllSpeechOverlaps as T, findLaneTrack as _, buildInitialVideoDocument as a, cascadeAfterVideoClipChanges as b, fromVideoDocument as c, assertValidVideoDocument as d, validateVideoDocument as f, ensureLaneTrack as g, LANE_KINDS_IN_STACK_ORDER as h, readVideoDocumentFromDraft as i, safeDurationMs as j, isEmptyVideoClip as k, toVideoDocument as l, videoDocumentSchema as m, createMirrorVideoDocument as n, buildSpeechHostMap as o, partUnionSchema as p, createMirrorVideoDocumentAdapter as r, derivePositionFromAbs as s, MirrorVideoDocumentAdapter as t, VideoDocumentValidationError as u, laneTrackId as v, recalculateTimelineDuration as w, arrangeMainTrackSeamlessly as x, solveVideoDocument as y, bytesToBase64 as z };
1730
+ export { TIMELINE_SKELETON_DURATION_MS as A, base64ToBytes as B, reassignSpeechesToVideoClipsByTime as C, resolveAnchoredClipsTimePosition as D, resolveAllSpeechOverlaps as E, VIDEO_DOCUMENT_SCHEMA_VERSION as F, effectiveVideoClipDurationMs as I, partUnionToDraft as L, partDurationMs as M, safeDurationMs as N, resolveSpeechOverlapByShiftingVideos as O, DEFAULT_UNIT_TIME_MS as P, recordEntries as R, fillMainTrackTimeGaps as S, recoverOrphanedSpeechAnchorsByTime as T, bytesToBase64 as V, findLaneTrack as _, buildInitialVideoDocument as a, cascadeAfterVideoClipChanges as b, fromVideoDocument as c, assertValidVideoDocument as d, validateVideoDocument as f, ensureLaneTrack as g, LANE_KINDS_IN_STACK_ORDER as h, readVideoDocumentFromDraft as i, isEmptyVideoClip as j, syncAggregatedClipsTimePosition as k, toVideoDocument as l, videoDocumentSchema as m, createMirrorVideoDocument as n, buildSpeechHostMap as o, partUnionSchema as p, createMirrorVideoDocumentAdapter as r, derivePositionFromAbs as s, MirrorVideoDocumentAdapter as t, VideoDocumentValidationError as u, laneTrackId as v, recalculateTimelineDuration as w, arrangeMainTrackSeamlessly as x, solveVideoDocument as y, videoDocumentMirrorSchema as z };
@@ -948,7 +948,7 @@ type TrackItemDraft = NonNullable<NonNullable<TrackDraft['items']>[number]>;
948
948
  declare const addSpeechesInputSchema: z.ZodObject<{
949
949
  speeches: z.ZodArray<z.ZodObject<{
950
950
  speech_id: z.ZodString;
951
- anchor_part_id: z.ZodString;
951
+ anchor_part_id: z.ZodNullable<z.ZodString>;
952
952
  offset_ms: z.ZodNumber;
953
953
  audio_storage_key: z.ZodString;
954
954
  duration_ms: z.ZodNumber;
@@ -1053,7 +1053,7 @@ type AdjustVideoClipVolumeInput = z.infer<typeof adjustVideoClipVolumeInputSchem
1053
1053
  declare const changeSpeechScriptInputSchema: z.ZodObject<{
1054
1054
  speeches: z.ZodArray<z.ZodObject<{
1055
1055
  speech_id: z.ZodString;
1056
- anchor_part_id: z.ZodString;
1056
+ anchor_part_id: z.ZodNullable<z.ZodString>;
1057
1057
  offset_ms: z.ZodNumber;
1058
1058
  audio_storage_key: z.ZodString;
1059
1059
  duration_ms: z.ZodNumber;
@@ -1077,7 +1077,7 @@ declare const changeSpeechScriptInputSchema: z.ZodObject<{
1077
1077
  declare const changeSpeechVoiceInputSchema: z.ZodObject<{
1078
1078
  speeches: z.ZodArray<z.ZodObject<{
1079
1079
  speech_id: z.ZodString;
1080
- anchor_part_id: z.ZodString;
1080
+ anchor_part_id: z.ZodNullable<z.ZodString>;
1081
1081
  offset_ms: z.ZodNumber;
1082
1082
  audio_storage_key: z.ZodString;
1083
1083
  duration_ms: z.ZodNumber;
@@ -1156,6 +1156,12 @@ declare const moveSpeechesInputSchema: z.ZodObject<{
1156
1156
  }, z.core.$strip>;
1157
1157
  type MoveSpeechesInput = z.infer<typeof moveSpeechesInputSchema>;
1158
1158
  //#endregion
1159
+ //#region src/editor/schemas/move-bgm.d.ts
1160
+ declare const moveBgmInputSchema: z.ZodObject<{
1161
+ new_start_ms: z.ZodNumber;
1162
+ }, z.core.$strip>;
1163
+ type MoveBgmInput = z.infer<typeof moveBgmInputSchema>;
1164
+ //#endregion
1159
1165
  //#region src/editor/schemas/move-video-clips-by-anchor.d.ts
1160
1166
  /**
1161
1167
  * Where the moved block lands on the main track.
@@ -1490,16 +1496,14 @@ declare const voiceSchema: z.ZodObject<{
1490
1496
  * stable speech + caption parts and writes them as authoritative facts. No
1491
1497
  * cascade runs on write — the projection derives absolute positions on read.
1492
1498
  *
1493
- * Each speech carries the anchoring fact directly (RFC 02 §4): the host video
1494
- * clip `anchor_part_id` and the `offset_ms` within it. The upstream caller
1495
- * already knows which clip a speech attaches to, so the op writes
1496
- * `{ mode:'anchored', anchorPartId, offsetMs }` verbatim — no write-time
1497
- * host-picking. Captions anchor to their speech via the caption part's
1498
- * `start_ms` (offset within the speech).
1499
+ * `anchor_part_id` states the root relationship explicitly: a video/BGM id
1500
+ * creates an anchored speech; `null` creates an absolute audio root.
1501
+ * `offset_ms` is relative to the anchor or absolute for a root. Captions always
1502
+ * anchor to their speech via `start_ms`.
1499
1503
  */
1500
1504
  declare const speechAssetSchema: z.ZodObject<{
1501
1505
  speech_id: z.ZodString;
1502
- anchor_part_id: z.ZodString;
1506
+ anchor_part_id: z.ZodNullable<z.ZodString>;
1503
1507
  offset_ms: z.ZodNumber;
1504
1508
  audio_storage_key: z.ZodString;
1505
1509
  duration_ms: z.ZodNumber;
@@ -1523,7 +1527,7 @@ declare const captionAssetSchema: z.ZodObject<{
1523
1527
  declare const speechAssetsSchema: z.ZodObject<{
1524
1528
  speeches: z.ZodArray<z.ZodObject<{
1525
1529
  speech_id: z.ZodString;
1526
- anchor_part_id: z.ZodString;
1530
+ anchor_part_id: z.ZodNullable<z.ZodString>;
1527
1531
  offset_ms: z.ZodNumber;
1528
1532
  audio_storage_key: z.ZodString;
1529
1533
  duration_ms: z.ZodNumber;
@@ -1548,7 +1552,7 @@ type SpeechAsset = z.infer<typeof speechAssetSchema>;
1548
1552
  type CaptionAsset = z.infer<typeof captionAssetSchema>;
1549
1553
  type SpeechAssets = z.infer<typeof speechAssetsSchema>;
1550
1554
  declare namespace index_d_exports {
1551
- export { AddSpeechesInput, AddVideoClipsInput, AdjustBgmVolumeInput, AdjustSpeechVolumeInput, AdjustVideoClipDurationInput, AdjustVideoClipVolumeInput, AnchoredDeletePolicy, AnchoredReplacePolicy, CaptionAsset, ChangeSpeechScriptInput, ChangeSpeechVoiceInput, DeleteBgmInput, DeleteSpeechesInput, DeleteVideoClipsInput, MoveAnchor, MoveSpeechesInput, MoveVideoClipsByAnchorInput, MoveVideoClipsInput, MovedClipAnchoredPolicy, ReplaceVideoClipContentInput, ReplaceVideoClipSequenceInput, ResizeCaptionInput, SetBgmInput, SetCaptionStyleInput, SetCaptionVisibilityInput, SetVideoClipSpeedShiftInput, SpeechAsset, SpeechAssets, addSpeechesInputSchema, addVideoClipsInputSchema, adjustBgmVolumeInputSchema, adjustSpeechVolumeInputSchema, adjustVideoClipDurationInputSchema, adjustVideoClipVolumeInputSchema, anchoredDeletePolicySchema, anchoredReplacePolicySchema, changeSpeechScriptInputSchema, changeSpeechVoiceInputSchema, clipIdSchema, clipIdsSchema, deleteBgmInputSchema, deleteSpeechesInputSchema, deleteVideoClipsInputSchema, mediaIdSchema, moveAnchorSchema, moveSpeechesInputSchema, moveVideoClipsByAnchorInputSchema, moveVideoClipsInputSchema, movedClipAnchoredPolicySchema, positiveMsSchema, replaceVideoClipContentInputSchema, replaceVideoClipSequenceInputSchema, resizeCaptionInputSchema, setBgmInputSchema, setCaptionStyleInputSchema, setCaptionVisibilityInputSchema, setVideoClipSpeedShiftInputSchema, speechAssetsSchema, speechIdSchema, speechIdsSchema, speedShiftSchema, timelineMsSchema, voiceSchema, volumeSchema };
1555
+ export { AddSpeechesInput, AddVideoClipsInput, AdjustBgmVolumeInput, AdjustSpeechVolumeInput, AdjustVideoClipDurationInput, AdjustVideoClipVolumeInput, AnchoredDeletePolicy, AnchoredReplacePolicy, CaptionAsset, ChangeSpeechScriptInput, ChangeSpeechVoiceInput, DeleteBgmInput, DeleteSpeechesInput, DeleteVideoClipsInput, MoveAnchor, MoveBgmInput, MoveSpeechesInput, MoveVideoClipsByAnchorInput, MoveVideoClipsInput, MovedClipAnchoredPolicy, ReplaceVideoClipContentInput, ReplaceVideoClipSequenceInput, ResizeCaptionInput, SetBgmInput, SetCaptionStyleInput, SetCaptionVisibilityInput, SetVideoClipSpeedShiftInput, SpeechAsset, SpeechAssets, addSpeechesInputSchema, addVideoClipsInputSchema, adjustBgmVolumeInputSchema, adjustSpeechVolumeInputSchema, adjustVideoClipDurationInputSchema, adjustVideoClipVolumeInputSchema, anchoredDeletePolicySchema, anchoredReplacePolicySchema, changeSpeechScriptInputSchema, changeSpeechVoiceInputSchema, clipIdSchema, clipIdsSchema, deleteBgmInputSchema, deleteSpeechesInputSchema, deleteVideoClipsInputSchema, mediaIdSchema, moveAnchorSchema, moveBgmInputSchema, moveSpeechesInputSchema, moveVideoClipsByAnchorInputSchema, moveVideoClipsInputSchema, movedClipAnchoredPolicySchema, positiveMsSchema, replaceVideoClipContentInputSchema, replaceVideoClipSequenceInputSchema, resizeCaptionInputSchema, setBgmInputSchema, setCaptionStyleInputSchema, setCaptionVisibilityInputSchema, setVideoClipSpeedShiftInputSchema, speechAssetsSchema, speechIdSchema, speechIdsSchema, speedShiftSchema, timelineMsSchema, voiceSchema, volumeSchema };
1552
1556
  }
1553
1557
  //#endregion
1554
1558
  //#region src/editor/schema-validator.d.ts
@@ -1597,6 +1601,7 @@ declare class SchemaValidator {
1597
1601
  validateChangeSpeechVoice(input: ChangeSpeechVoiceInput, doc: SnapshotReadable): void;
1598
1602
  validateDeleteSpeeches(input: DeleteSpeechesInput, doc: SnapshotReadable): void;
1599
1603
  validateMoveSpeeches(input: MoveSpeechesInput, doc: SnapshotReadable): void;
1604
+ validateMoveBgm(input: MoveBgmInput, doc: SnapshotReadable): void;
1600
1605
  validateSetBgm(input: SetBgmInput, _doc: SnapshotReadable): void;
1601
1606
  validateDeleteBgm(input: DeleteBgmInput, _doc: SnapshotReadable): void;
1602
1607
  validateSetCaptionVisibility(input: SetCaptionVisibilityInput, _doc: SnapshotReadable): void;
@@ -1609,12 +1614,8 @@ declare class SchemaValidator {
1609
1614
  /** Each regenerated speech (re-TTS) must already exist in the document. */
1610
1615
  private assertSpeechesExist;
1611
1616
  /**
1612
- * Each speech's `anchor_part_id` must point at a video clip that already
1613
- * exists. A relative speech anchored to a missing clip cannot be positioned
1614
- * by the projection and has no host to recover to (RFC 02 §4/§11.1), so the
1615
- * write must be rejected rather than landing a dangling reference. (Restores
1616
- * the dangling-reference guard the old `validateMaterializedPatch` carried;
1617
- * ADR 0009.)
1617
+ * Anchored speeches may follow a video or BGM root. A null anchor is an
1618
+ * explicit absolute speech root. Missing and wrong-kind parents are rejected.
1618
1619
  */
1619
1620
  private assertAnchorsExist;
1620
1621
  private assertPartKind;
@@ -1649,7 +1650,7 @@ declare class SchemaValidator {
1649
1650
  * side-effect payload contract in
1650
1651
  * `docs/projects/medeo-integration/results/phase-4-side-effect-payload-contract.md`.
1651
1652
  */
1652
- type ImplementedSemanticOpKind = 'MoveVideoClips' | 'MoveVideoClipsByAnchor' | 'DeleteVideoClips' | 'AddVideoClips' | 'AdjustVideoClipVolume' | 'SetVideoClipSpeedShift' | 'ReplaceVideoClipContent' | 'ReplaceVideoClipSequence' | 'AdjustVideoClipDuration' | 'AddSpeeches' | 'DeleteSpeeches' | 'MoveSpeeches' | 'ChangeSpeechScript' | 'ChangeSpeechVoice' | 'AdjustSpeechVolume' | 'ResizeCaption' | 'SetCaptionVisibility' | 'SetCaptionStyle' | 'SetBgm' | 'DeleteBgm' | 'AdjustBgmVolume';
1653
+ type ImplementedSemanticOpKind = 'MoveVideoClips' | 'MoveVideoClipsByAnchor' | 'DeleteVideoClips' | 'AddVideoClips' | 'AdjustVideoClipVolume' | 'SetVideoClipSpeedShift' | 'ReplaceVideoClipContent' | 'ReplaceVideoClipSequence' | 'AdjustVideoClipDuration' | 'AddSpeeches' | 'DeleteSpeeches' | 'MoveSpeeches' | 'MoveBgm' | 'ChangeSpeechScript' | 'ChangeSpeechVoice' | 'AdjustSpeechVolume' | 'ResizeCaption' | 'SetCaptionVisibility' | 'SetCaptionStyle' | 'SetBgm' | 'DeleteBgm' | 'AdjustBgmVolume';
1653
1654
  /**
1654
1655
  * Operations on the roadmap but NOT yet implemented by `SemanticEditor`. Empty:
1655
1656
  * Phase 4 covers every op with a real entry point. Kept as a named type so the
@@ -1711,7 +1712,7 @@ interface CommitOptions {
1711
1712
  /** Author of this op; omitted for writes with no acting user (bootstrap, repair). */
1712
1713
  actor?: OpActor;
1713
1714
  }
1714
- type SemanticOpInput = MoveVideoClipsInput | MoveVideoClipsByAnchorInput | DeleteVideoClipsInput | AddVideoClipsInput | AdjustVideoClipVolumeInput | AdjustSpeechVolumeInput | AdjustBgmVolumeInput | SetVideoClipSpeedShiftInput | ReplaceVideoClipContentInput | ReplaceVideoClipSequenceInput | AdjustVideoClipDurationInput | AddSpeechesInput | DeleteSpeechesInput | MoveSpeechesInput | ResizeCaptionInput | SetCaptionVisibilityInput | SetCaptionStyleInput | SetBgmInput | DeleteBgmInput;
1715
+ type SemanticOpInput = MoveVideoClipsInput | MoveVideoClipsByAnchorInput | DeleteVideoClipsInput | AddVideoClipsInput | AdjustVideoClipVolumeInput | AdjustSpeechVolumeInput | AdjustBgmVolumeInput | SetVideoClipSpeedShiftInput | ReplaceVideoClipContentInput | ReplaceVideoClipSequenceInput | AdjustVideoClipDurationInput | AddSpeechesInput | DeleteSpeechesInput | MoveSpeechesInput | MoveBgmInput | ResizeCaptionInput | SetCaptionVisibilityInput | SetCaptionStyleInput | SetBgmInput | DeleteBgmInput;
1715
1716
  type SemanticOpName = ImplementedSemanticOpKind;
1716
1717
  /** Audit metadata committed alongside an op's writes. */
1717
1718
  interface TransactAudit {
@@ -1827,6 +1828,8 @@ declare class SemanticEditor {
1827
1828
  * derives absolute positions on read.
1828
1829
  */
1829
1830
  moveSpeeches(input: MoveSpeechesInput, options?: CommitOptions): Promise<void>;
1831
+ /** Move the BGM root; any speech/caption descendants follow through their anchors. */
1832
+ moveBgm(input: MoveBgmInput, options?: CommitOptions): Promise<void>;
1830
1833
  /** Change a speech's script. Re-TTS runs upstream; the regenerated parts arrive materialized (no cascade). */
1831
1834
  changeSpeechScript(input: ChangeSpeechScriptInput, options?: CommitOptions): Promise<void>;
1832
1835
  /** Change a speech's voice. Re-TTS runs upstream; the regenerated parts arrive materialized (no cascade). */
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as VideoDocumentSchemaVersion, A as assertValidVideoDocument, B as BgmPart, C as index_d_exports, D as VideoDocumentMirrorSchema, E as VideoDocumentDraft, F as SpeechHostMap, G as SpeechPart, H as DEFAULT_UNIT_TIME_MS, I as buildSpeechHostMap, J as TrackItem, K as Timeline, L as derivePositionFromAbs, M as InitialDocumentFacts, N as buildInitialVideoDocument, O as videoDocumentMirrorSchema, P as DerivedItemPosition, Q as VideoDocument, R as fromVideoDocument, S as ValidationError, T as TrackItemDraft, U as PartKind, V as CaptionPart, W as PartUnion, X as VIDEO_DOCUMENT_SCHEMA_VERSION, Y as TrackItemTimePosition, Z as VideoClipPart, _ as PlannedSemanticOpKind, a as MirrorVideoDocumentOptions, at as CaptionPart$1, b as SchemaValidator, c as CommitOptions, ct as SpeedShift, d as SemanticEditor, dt as TrackItem$1, et as VideoDocumentValidationIssue, f as SemanticOpInput, g as ImplementedSemanticOpKind, h as IMPLEMENTED_SEMANTIC_OP_KINDS, i as MirrorVideoDocumentAdapter, it as Attachment, j as validateVideoDocument, k as VideoDocumentValidationError, l as OpActor, lt as Timeline$1, m as TransactAudit, n as videoDocumentSchema, nt as VideoDraft, o as createMirrorVideoDocument, ot as CaptionStyle, p as SemanticOpName, q as Track, r as readVideoDocumentFromDraft, rt as VideoDraftPartUnion, s as createMirrorVideoDocumentAdapter, st as PartAggregation, t as partUnionSchema, tt as VideoDocumentValidationIssueCode, u as SemanticDocumentAdapter, ut as Track$1, v as SemanticOpKind, w as TrackDraft, x as SnapshotReadable, y as isImplementedSemanticOpKind, z as toVideoDocument } from "./index-CZlTzzrm.js";
1
+ import { $ as VideoDocumentSchemaVersion, A as assertValidVideoDocument, B as BgmPart, C as index_d_exports, D as VideoDocumentMirrorSchema, E as VideoDocumentDraft, F as SpeechHostMap, G as SpeechPart, H as DEFAULT_UNIT_TIME_MS, I as buildSpeechHostMap, J as TrackItem, K as Timeline, L as derivePositionFromAbs, M as InitialDocumentFacts, N as buildInitialVideoDocument, O as videoDocumentMirrorSchema, P as DerivedItemPosition, Q as VideoDocument, R as fromVideoDocument, S as ValidationError, T as TrackItemDraft, U as PartKind, V as CaptionPart, W as PartUnion, X as VIDEO_DOCUMENT_SCHEMA_VERSION, Y as TrackItemTimePosition, Z as VideoClipPart, _ as PlannedSemanticOpKind, a as MirrorVideoDocumentOptions, at as CaptionPart$1, b as SchemaValidator, c as CommitOptions, ct as SpeedShift, d as SemanticEditor, dt as TrackItem$1, et as VideoDocumentValidationIssue, f as SemanticOpInput, g as ImplementedSemanticOpKind, h as IMPLEMENTED_SEMANTIC_OP_KINDS, i as MirrorVideoDocumentAdapter, it as Attachment, j as validateVideoDocument, k as VideoDocumentValidationError, l as OpActor, lt as Timeline$1, m as TransactAudit, n as videoDocumentSchema, nt as VideoDraft, o as createMirrorVideoDocument, ot as CaptionStyle, p as SemanticOpName, q as Track, r as readVideoDocumentFromDraft, rt as VideoDraftPartUnion, s as createMirrorVideoDocumentAdapter, st as PartAggregation, t as partUnionSchema, tt as VideoDocumentValidationIssueCode, u as SemanticDocumentAdapter, ut as Track$1, v as SemanticOpKind, w as TrackDraft, x as SnapshotReadable, y as isImplementedSemanticOpKind, z as toVideoDocument } from "./index-DrEHTguy.js";
2
2
  import { PeerID } from "loro-crdt";
3
3
  import { DocState } from "@mengine/sync";
4
4
  import { BaseDocStorage, Connection, DocDiff, DocPushReceipt, DocSnapshotRecord, DocStorage, DocStorageOptions, DocUpdate, DocUpdateRecord } from "@mengine/storage";
@@ -853,6 +853,15 @@ declare function arrangeMainTrackSeamlessly(doc: TimelineDoc): void;
853
853
  * caption.abs = speech.abs + caption.start_ms
854
854
  */
855
855
  declare function syncAggregatedClipsTimePosition(doc: TimelineDoc): void;
856
+ /**
857
+ * Resolve authoritative `time_position` edges without inventing parents from
858
+ * geometric overlap. Roots stay absolute/sequential; every anchored descendant
859
+ * follows its declared parent, regardless of whether that parent is video, BGM,
860
+ * or speech.
861
+ */
862
+ declare function resolveAnchoredClipsTimePosition(doc: TimelineDoc): void;
863
+ /** Preserve the historical orphan fallback without re-parenting valid roots. */
864
+ declare function recoverOrphanedSpeechAnchorsByTime(doc: TimelineDoc): void;
856
865
  /**
857
866
  * Reassign each speech to the video clip whose time range contains its start,
858
867
  * rebuilding `aggregations`. A speech before the first clip or after the last
@@ -918,8 +927,8 @@ type MakeEmptyPart = (durationMs: number) => {
918
927
  partId: string;
919
928
  };
920
929
  /**
921
- * The full solve pipeline: arrange → sync → reassign → resolve-overlap →
922
- * fill-gaps → recalc. The single cascade the read-side projection runs; ops
930
+ * The full solve pipeline: arrange → resolve declared parent edges →
931
+ * resolve-overlap → fill-gaps → recalc. The single cascade the read-side projection runs; ops
923
932
  * never call it (they write only facts — RFC 02 §7/§10).
924
933
  */
925
934
  declare function cascadeAfterVideoClipChanges(doc: TimelineDoc, makeEmptyPart: MakeEmptyPart): void;
@@ -1064,4 +1073,4 @@ declare function hostForAbsMs(ranges: MainClipRange[], absMs: number): MainClipR
1064
1073
  */
1065
1074
  declare function relativePositionForAbs(ranges: MainClipRange[], absMs: number): TrackItemTimePosition;
1066
1075
  //#endregion
1067
- export { type Aggregation, type Attachment, type BgmPart, type CaptionPart, type CaptionStyle, type CommitOptions, DEFAULT_UNIT_TIME_MS, type DerivedItemPosition, type DocStorageLike, type DocVersionMark, IMPLEMENTED_SEMANTIC_OP_KINDS, type ImplementedSemanticOpKind, type InitialDocumentFacts, LANE_KINDS_IN_STACK_ORDER, type LaneKind, type MainClipRange, type MakeEmptyPart, ManualSyncDoc, type ManualSyncDocOptions, MedeoHttpDocStorage, type MedeoHttpDocStorageOptions, MemoryDocStorage, MengineAckFailedError, type MengineAuditEntry, type MengineAuditResponse, MengineDocSession, type MengineDocSessionOptions, type MengineDocSessionUpdateEvent, type MengineDocumentVersion, type MengineEventStreamOptions, MengineHttpClient, type MengineHttpClientOptions, MengineHttpRequestError, MenginePushRejectedError, type MenginePushResponse, type MenginePushUpdateResponse, type MengineRejectedResponse, type MengineSnapshotResponse, type MengineSseUpdateEvent, type MengineSyncResponse, type MengineUpdateMeta, MirrorVideoDocumentAdapter, type MirrorVideoDocumentOptions, type OpActor, type PartAggregation, type PartKind, type PartUnion, type PlannedSemanticOpKind, type PullFailureReason, type PullResult, type PushOutcome, type PushOutcomeKind, type PushResult, type PushResultKind, SchemaValidator, type SemanticDocumentAdapter, SemanticEditor, type SemanticOpInput, type SemanticOpKind, type SemanticOpName, type SnapshotReadable, type SolvedVideoDocument, type SpeechHostMap, type SpeechPart, type SpeedShift, TIMELINE_SKELETON_DURATION_MS, type Timeline, type TimelineDoc, type TimelineItem, type Track, type TrackDraft, type TrackItem, type TrackItemDraft, type TrackItemTimePosition, type TransactAudit, VIDEO_DOCUMENT_SCHEMA_VERSION, ValidationError, type VideoClipPart, type VideoDocument, type VideoDocumentDraft, type VideoDocumentMirrorSchema, type VideoDocumentSchemaVersion, VideoDocumentValidationError, type VideoDocumentValidationIssue, type VideoDocumentValidationIssueCode, type VideoDraft, type CaptionPart$1 as VideoDraftCaptionPart, type VideoDraftPartUnion, type Timeline$1 as VideoDraftTimeline, type Track$1 as VideoDraftTrack, type TrackItem$1 as VideoDraftTrackItem, type WaitForServerAckOptions, arrangeMainTrackSeamlessly, assertValidVideoDocument, base64ToBytes, buildInitialVideoDocument, buildSpeechHostMap, bytesToBase64, cascadeAfterVideoClipChanges, createMirrorVideoDocument, createMirrorVideoDocumentAdapter, decodeDocVersionMark, derivePositionFromAbs, encodeDocVersionMark, ensureLaneTrack, fillMainTrackTimeGaps, findLaneTrack, fromVideoDocument, generatePartId, getAt, hostForAbsMs, isEmptyVideoClip, isImplementedSemanticOpKind, isMap, laneTrackId, mainTrackRanges, partDurationMs, partUnionSchema, readMainTrackItems, readMengineEventStream, readPart, readPartDurationMs, readVideoDocumentFromDraft, reassignSpeechesToVideoClipsByTime, recalculateTimelineDuration, relativePositionForAbs, resolveAllSpeechOverlaps, resolveSpeechOverlapByShiftingVideos, safeDurationMs, index_d_exports as schemas, snapshotToPlain, solveVideoDocument, syncAggregatedClipsTimePosition, toVideoDocument, validateVideoDocument, videoDocumentMirrorSchema, videoDocumentSchema };
1076
+ export { type Aggregation, type Attachment, type BgmPart, type CaptionPart, type CaptionStyle, type CommitOptions, DEFAULT_UNIT_TIME_MS, type DerivedItemPosition, type DocStorageLike, type DocVersionMark, IMPLEMENTED_SEMANTIC_OP_KINDS, type ImplementedSemanticOpKind, type InitialDocumentFacts, LANE_KINDS_IN_STACK_ORDER, type LaneKind, type MainClipRange, type MakeEmptyPart, ManualSyncDoc, type ManualSyncDocOptions, MedeoHttpDocStorage, type MedeoHttpDocStorageOptions, MemoryDocStorage, MengineAckFailedError, type MengineAuditEntry, type MengineAuditResponse, MengineDocSession, type MengineDocSessionOptions, type MengineDocSessionUpdateEvent, type MengineDocumentVersion, type MengineEventStreamOptions, MengineHttpClient, type MengineHttpClientOptions, MengineHttpRequestError, MenginePushRejectedError, type MenginePushResponse, type MenginePushUpdateResponse, type MengineRejectedResponse, type MengineSnapshotResponse, type MengineSseUpdateEvent, type MengineSyncResponse, type MengineUpdateMeta, MirrorVideoDocumentAdapter, type MirrorVideoDocumentOptions, type OpActor, type PartAggregation, type PartKind, type PartUnion, type PlannedSemanticOpKind, type PullFailureReason, type PullResult, type PushOutcome, type PushOutcomeKind, type PushResult, type PushResultKind, SchemaValidator, type SemanticDocumentAdapter, SemanticEditor, type SemanticOpInput, type SemanticOpKind, type SemanticOpName, type SnapshotReadable, type SolvedVideoDocument, type SpeechHostMap, type SpeechPart, type SpeedShift, TIMELINE_SKELETON_DURATION_MS, type Timeline, type TimelineDoc, type TimelineItem, type Track, type TrackDraft, type TrackItem, type TrackItemDraft, type TrackItemTimePosition, type TransactAudit, VIDEO_DOCUMENT_SCHEMA_VERSION, ValidationError, type VideoClipPart, type VideoDocument, type VideoDocumentDraft, type VideoDocumentMirrorSchema, type VideoDocumentSchemaVersion, VideoDocumentValidationError, type VideoDocumentValidationIssue, type VideoDocumentValidationIssueCode, type VideoDraft, type CaptionPart$1 as VideoDraftCaptionPart, type VideoDraftPartUnion, type Timeline$1 as VideoDraftTimeline, type Track$1 as VideoDraftTrack, type TrackItem$1 as VideoDraftTrackItem, type WaitForServerAckOptions, arrangeMainTrackSeamlessly, assertValidVideoDocument, base64ToBytes, buildInitialVideoDocument, buildSpeechHostMap, bytesToBase64, cascadeAfterVideoClipChanges, createMirrorVideoDocument, createMirrorVideoDocumentAdapter, decodeDocVersionMark, derivePositionFromAbs, encodeDocVersionMark, ensureLaneTrack, fillMainTrackTimeGaps, findLaneTrack, fromVideoDocument, generatePartId, getAt, hostForAbsMs, isEmptyVideoClip, isImplementedSemanticOpKind, isMap, laneTrackId, mainTrackRanges, partDurationMs, partUnionSchema, readMainTrackItems, readMengineEventStream, readPart, readPartDurationMs, readVideoDocumentFromDraft, reassignSpeechesToVideoClipsByTime, recalculateTimelineDuration, recoverOrphanedSpeechAnchorsByTime, relativePositionForAbs, resolveAllSpeechOverlaps, resolveAnchoredClipsTimePosition, resolveSpeechOverlapByShiftingVideos, safeDurationMs, index_d_exports as schemas, snapshotToPlain, solveVideoDocument, syncAggregatedClipsTimePosition, toVideoDocument, validateVideoDocument, videoDocumentMirrorSchema, videoDocumentSchema };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./chunk-D7D4PA-g.js";
2
- import { A as partDurationMs, C as reassignSpeechesToVideoClipsByTime, D as syncAggregatedClipsTimePosition, E as resolveSpeechOverlapByShiftingVideos, F as partUnionToDraft, I as recordEntries, L as videoDocumentMirrorSchema, M as DEFAULT_UNIT_TIME_MS, N as VIDEO_DOCUMENT_SCHEMA_VERSION, O as TIMELINE_SKELETON_DURATION_MS, P as effectiveVideoClipDurationMs, R as base64ToBytes, S as fillMainTrackTimeGaps, T as resolveAllSpeechOverlaps, _ as findLaneTrack, a as buildInitialVideoDocument, b as cascadeAfterVideoClipChanges, c as fromVideoDocument, d as assertValidVideoDocument, f as validateVideoDocument, g as ensureLaneTrack, h as LANE_KINDS_IN_STACK_ORDER, i as readVideoDocumentFromDraft, j as safeDurationMs, k as isEmptyVideoClip, l as toVideoDocument, m as videoDocumentSchema, n as createMirrorVideoDocument, o as buildSpeechHostMap, p as partUnionSchema, r as createMirrorVideoDocumentAdapter, s as derivePositionFromAbs, t as MirrorVideoDocumentAdapter, u as VideoDocumentValidationError, v as laneTrackId, w as recalculateTimelineDuration, x as arrangeMainTrackSeamlessly, y as solveVideoDocument, z as bytesToBase64 } from "./document-Dcu87api.js";
2
+ import { A as TIMELINE_SKELETON_DURATION_MS, B as base64ToBytes, C as reassignSpeechesToVideoClipsByTime, D as resolveAnchoredClipsTimePosition, E as resolveAllSpeechOverlaps, F as VIDEO_DOCUMENT_SCHEMA_VERSION, I as effectiveVideoClipDurationMs, L as partUnionToDraft, M as partDurationMs, N as safeDurationMs, O as resolveSpeechOverlapByShiftingVideos, P as DEFAULT_UNIT_TIME_MS, R as recordEntries, S as fillMainTrackTimeGaps, T as recoverOrphanedSpeechAnchorsByTime, V as bytesToBase64, _ as findLaneTrack, a as buildInitialVideoDocument, b as cascadeAfterVideoClipChanges, c as fromVideoDocument, d as assertValidVideoDocument, f as validateVideoDocument, g as ensureLaneTrack, h as LANE_KINDS_IN_STACK_ORDER, i as readVideoDocumentFromDraft, j as isEmptyVideoClip, k as syncAggregatedClipsTimePosition, l as toVideoDocument, m as videoDocumentSchema, n as createMirrorVideoDocument, o as buildSpeechHostMap, p as partUnionSchema, r as createMirrorVideoDocumentAdapter, s as derivePositionFromAbs, t as MirrorVideoDocumentAdapter, u as VideoDocumentValidationError, v as laneTrackId, w as recalculateTimelineDuration, x as arrangeMainTrackSeamlessly, y as solveVideoDocument, z as videoDocumentMirrorSchema } from "./document-C5KDvMMR.js";
3
3
  import { z } from "zod";
4
4
  import { LoroDoc, VersionVector } from "loro-crdt";
5
5
  import { ClientServerSynchronizer, DocManager } from "@mengine/sync";
@@ -308,17 +308,15 @@ const voiceSchema = z.object({
308
308
  * stable speech + caption parts and writes them as authoritative facts. No
309
309
  * cascade runs on write — the projection derives absolute positions on read.
310
310
  *
311
- * Each speech carries the anchoring fact directly (RFC 02 §4): the host video
312
- * clip `anchor_part_id` and the `offset_ms` within it. The upstream caller
313
- * already knows which clip a speech attaches to, so the op writes
314
- * `{ mode:'anchored', anchorPartId, offsetMs }` verbatim — no write-time
315
- * host-picking. Captions anchor to their speech via the caption part's
316
- * `start_ms` (offset within the speech).
311
+ * `anchor_part_id` states the root relationship explicitly: a video/BGM id
312
+ * creates an anchored speech; `null` creates an absolute audio root.
313
+ * `offset_ms` is relative to the anchor or absolute for a root. Captions always
314
+ * anchor to their speech via `start_ms`.
317
315
  */
318
316
  const speechAssetSchema = z.object({
319
317
  speech_id: speechIdSchema.describe("The speech part ID (= side-effect speech_parts[].id)"),
320
- anchor_part_id: clipIdSchema.describe("Host video clip part ID the speech anchors to (RFC 02 §4)"),
321
- offset_ms: timelineMsSchema.describe("Offset within the host clip (speech.abs = host.abs + offset_ms)"),
318
+ anchor_part_id: z.string().min(1).nullable().describe("Video/BGM parent part ID, or null when the speech itself is the root"),
319
+ offset_ms: timelineMsSchema.describe("Relative offset from the parent, or absolute start for a root speech"),
322
320
  audio_storage_key: z.string().min(1),
323
321
  duration_ms: positiveMsSchema,
324
322
  audio_script: z.string(),
@@ -509,6 +507,9 @@ const moveSpeechesInputSchema = z.object({ speeches: z.array(z.object({
509
507
  new_start_ms: timelineMsSchema.describe("New absolute start time on the timeline")
510
508
  })).min(1).describe("Speeches to move to new positions") });
511
509
  //#endregion
510
+ //#region src/editor/schemas/move-bgm.ts
511
+ const moveBgmInputSchema = z.object({ new_start_ms: timelineMsSchema.describe("New absolute BGM start on the timeline") });
512
+ //#endregion
512
513
  //#region src/editor/schemas/move-video-clips-by-anchor.ts
513
514
  /**
514
515
  * Where the moved block lands on the main track.
@@ -765,6 +766,7 @@ var schemas_exports = /* @__PURE__ */ __exportAll({
765
766
  deleteVideoClipsInputSchema: () => deleteVideoClipsInputSchema,
766
767
  mediaIdSchema: () => mediaIdSchema,
767
768
  moveAnchorSchema: () => moveAnchorSchema,
769
+ moveBgmInputSchema: () => moveBgmInputSchema,
768
770
  moveSpeechesInputSchema: () => moveSpeechesInputSchema,
769
771
  moveVideoClipsByAnchorInputSchema: () => moveVideoClipsByAnchorInputSchema,
770
772
  moveVideoClipsInputSchema: () => moveVideoClipsInputSchema,
@@ -1014,6 +1016,10 @@ var SchemaValidator = class {
1014
1016
  const snapshot = doc.snapshot();
1015
1017
  for (const s of parsed.speeches) this.assertPartKind(snapshot, s.speech_id, "speech", "move_speeches");
1016
1018
  }
1019
+ validateMoveBgm(input, doc) {
1020
+ this.parse(moveBgmInputSchema, input, "move_bgm_invalid_input");
1021
+ if (doc.snapshot().tracks?.find((track) => track.parts_kind === "bgm")?.items?.[0] == null) throw new ValidationError("move_bgm_not_found", "document has no BGM item");
1022
+ }
1017
1023
  validateSetBgm(input, _doc) {
1018
1024
  this.parse(setBgmInputSchema, input, "set_bgm_invalid_input");
1019
1025
  }
@@ -1061,16 +1067,20 @@ var SchemaValidator = class {
1061
1067
  for (const speech of assets.speeches) this.assertPartKind(snapshot, speech.speech_id, "speech", opName);
1062
1068
  }
1063
1069
  /**
1064
- * Each speech's `anchor_part_id` must point at a video clip that already
1065
- * exists. A relative speech anchored to a missing clip cannot be positioned
1066
- * by the projection and has no host to recover to (RFC 02 §4/§11.1), so the
1067
- * write must be rejected rather than landing a dangling reference. (Restores
1068
- * the dangling-reference guard the old `validateMaterializedPatch` carried;
1069
- * ADR 0009.)
1070
+ * Anchored speeches may follow a video or BGM root. A null anchor is an
1071
+ * explicit absolute speech root. Missing and wrong-kind parents are rejected.
1070
1072
  */
1071
1073
  assertAnchorsExist(assets, doc, opName) {
1072
1074
  const snapshot = doc.snapshot();
1073
- for (const speech of assets.speeches) this.assertPartKind(snapshot, speech.anchor_part_id, "video_clip", `${opName}_anchor`);
1075
+ for (const speech of assets.speeches) {
1076
+ if (speech.anchor_part_id == null) continue;
1077
+ const part = readPart(snapshot, speech.anchor_part_id);
1078
+ if (part == null) throw new ValidationError(`${opName}_anchor_part_not_found`, `part_library has no parent`, { part_id: speech.anchor_part_id });
1079
+ if (part.part_kind !== "video_clip" && part.part_kind !== "bgm") throw new ValidationError(`${opName}_anchor_wrong_part_kind`, `speech parent must be video_clip or bgm`, {
1080
+ part_id: speech.anchor_part_id,
1081
+ part_kind: part.part_kind
1082
+ });
1083
+ }
1074
1084
  }
1075
1085
  assertPartKind(snapshot, partId, kind, opName) {
1076
1086
  const part = readPart(snapshot, partId);
@@ -1419,8 +1429,9 @@ var SemanticEditor = class {
1419
1429
  */
1420
1430
  async addSpeeches(input, options) {
1421
1431
  this.validator.validateAddSpeeches(input, this.doc);
1432
+ const absByPartId = solveVideoDocument(this.doc.snapshot()).absByPartId;
1422
1433
  this.doc.transact((draft) => {
1423
- writeSpeechAssets(draft, input);
1434
+ writeSpeechAssets(draft, input, absByPartId);
1424
1435
  }, audit("AddSpeeches", input, options));
1425
1436
  }
1426
1437
  /** Delete speeches with their captions; the subtree is removed (§9.2). Surviving lanes keep their facts. */
@@ -1438,29 +1449,60 @@ var SemanticEditor = class {
1438
1449
  */
1439
1450
  async moveSpeeches(input, options) {
1440
1451
  this.validator.validateMoveSpeeches(input, this.doc);
1452
+ const snapshot = this.doc.snapshot();
1453
+ const absByPartId = solveVideoDocument(snapshot).absByPartId;
1441
1454
  this.doc.transact((draft) => {
1442
- const ranges = mainTrackRanges(draft);
1443
1455
  const track = findLaneTrack(draft, "speech");
1456
+ const ranges = mainTrackRanges(draft);
1444
1457
  for (const move of input.speeches) {
1445
1458
  const item = (track?.items ?? []).find((it) => it?.part_id === move.speech_id);
1446
1459
  if (item == null) continue;
1447
- item.time_position = relativePositionForAbs(ranges, move.new_start_ms);
1460
+ const current = item.time_position;
1461
+ if (current?.mode === "anchored") {
1462
+ const parentStartMs = absByPartId.get(current.anchorPartId);
1463
+ item.time_position = readPart(snapshot, current.anchorPartId)?.part_kind === "video_clip" ? relativePositionForAbs(ranges, move.new_start_ms) : parentStartMs == null ? {
1464
+ mode: "absolute",
1465
+ offsetMs: move.new_start_ms
1466
+ } : {
1467
+ mode: "anchored",
1468
+ anchorPartId: current.anchorPartId,
1469
+ offsetMs: Math.max(0, move.new_start_ms - parentStartMs)
1470
+ };
1471
+ } else item.time_position = {
1472
+ mode: "absolute",
1473
+ offsetMs: move.new_start_ms
1474
+ };
1448
1475
  item.fallback_abs_ms = move.new_start_ms;
1449
1476
  }
1450
1477
  }, audit("MoveSpeeches", input, options));
1451
1478
  }
1479
+ /** Move the BGM root; any speech/caption descendants follow through their anchors. */
1480
+ async moveBgm(input, options) {
1481
+ this.validator.validateMoveBgm(input, this.doc);
1482
+ this.doc.transact((draft) => {
1483
+ const item = findLaneTrack(draft, "bgm")?.items?.[0];
1484
+ if (item == null) return;
1485
+ item.time_position = {
1486
+ mode: "absolute",
1487
+ offsetMs: input.new_start_ms
1488
+ };
1489
+ item.fallback_abs_ms = void 0;
1490
+ }, audit("MoveBgm", input, options));
1491
+ }
1452
1492
  /** Change a speech's script. Re-TTS runs upstream; the regenerated parts arrive materialized (no cascade). */
1453
1493
  async changeSpeechScript(input, options) {
1454
1494
  this.validator.validateChangeSpeechScript(input, this.doc);
1495
+ const absByPartId = solveVideoDocument(this.doc.snapshot()).absByPartId;
1455
1496
  this.doc.transact((draft) => {
1456
- writeSpeechAssets(draft, input);
1497
+ writeSpeechAssets(draft, input, absByPartId);
1457
1498
  }, audit("ChangeSpeechScript", input, options));
1458
1499
  }
1459
1500
  /** Change a speech's voice. Re-TTS runs upstream; the regenerated parts arrive materialized (no cascade). */
1460
1501
  async changeSpeechVoice(input, options) {
1461
1502
  this.validator.validateChangeSpeechVoice(input, this.doc);
1503
+ const absByPartId = solveVideoDocument(this.doc.snapshot()).absByPartId;
1462
1504
  this.doc.transact((draft) => {
1463
- writeSpeechAssets(draft, input);
1505
+ writeSpeechAssets(draft, input, absByPartId);
1464
1506
  }, audit("ChangeSpeechVoice", input, options));
1465
1507
  }
1466
1508
  /**
@@ -1699,7 +1741,7 @@ function normalizeSpeedShift(input) {
1699
1741
  * projection derives it on read. On re-TTS the speech id is preserved, so an
1700
1742
  * existing item is re-positioned rather than duplicated.
1701
1743
  */
1702
- function writeSpeechAssets(draft, assets) {
1744
+ function writeSpeechAssets(draft, assets, absByPartId) {
1703
1745
  const captionById = new Map(assets.captions.map((c) => [c.caption_id, c]));
1704
1746
  const speechTrack = ensureLaneTrack(draft, "speech");
1705
1747
  speechTrack.items ??= [];
@@ -1712,8 +1754,6 @@ function writeSpeechAssets(draft, assets) {
1712
1754
  }
1713
1755
  return captionItems;
1714
1756
  };
1715
- const ranges = mainTrackRanges(draft);
1716
- const hostStartMs = (anchorPartId) => ranges.find((r) => r.partId === anchorPartId)?.startMs;
1717
1757
  for (const speech of assets.speeches) {
1718
1758
  const priorCaptionIds = draft.part_library?.[speech.speech_id]?.speech?.caption_ids ?? [];
1719
1759
  const nextCaptionIds = new Set(speech.caption_ids);
@@ -1737,9 +1777,10 @@ function writeSpeechAssets(draft, assets) {
1737
1777
  voice: speech.voice,
1738
1778
  caption_ids: speech.caption_ids
1739
1779
  } });
1740
- const hostStart = hostStartMs(speech.anchor_part_id);
1741
- const speechAbs = hostStart == null ? void 0 : hostStart + speech.offset_ms;
1742
- placeRelative(speechTrack.items, speech.speech_id, speech.anchor_part_id, speech.offset_ms, speechAbs);
1780
+ const hostStart = speech.anchor_part_id == null ? void 0 : absByPartId.get(speech.anchor_part_id);
1781
+ const speechAbs = speech.anchor_part_id == null ? speech.offset_ms : hostStart == null ? void 0 : hostStart + speech.offset_ms;
1782
+ if (speech.anchor_part_id == null) placeAbsolute(speechTrack.items, speech.speech_id, speech.offset_ms);
1783
+ else placeRelative(speechTrack.items, speech.speech_id, speech.anchor_part_id, speech.offset_ms, speechAbs);
1743
1784
  for (const captionId of speech.caption_ids) {
1744
1785
  const caption = captionById.get(captionId);
1745
1786
  if (caption == null) continue;
@@ -1785,6 +1826,24 @@ function placeRelative(items, partId, anchorPartId, offsetMs, fallbackAbsMs) {
1785
1826
  duration_override_ms: void 0
1786
1827
  });
1787
1828
  }
1829
+ function placeAbsolute(items, partId, startMs) {
1830
+ const timePosition = {
1831
+ mode: "absolute",
1832
+ offsetMs: Math.max(0, Math.round(startMs))
1833
+ };
1834
+ const existing = items.find((item) => item?.part_id === partId);
1835
+ if (existing != null) {
1836
+ existing.time_position = timePosition;
1837
+ existing.fallback_abs_ms = void 0;
1838
+ return;
1839
+ }
1840
+ items.push({
1841
+ part_id: partId,
1842
+ time_position: timePosition,
1843
+ fallback_abs_ms: void 0,
1844
+ duration_override_ms: void 0
1845
+ });
1846
+ }
1788
1847
  /**
1789
1848
  * After the main-track flow order changes (move / reorder), re-parent each
1790
1849
  * anchored speech to the clip its *preserved* absolute landing now falls in
@@ -1923,6 +1982,7 @@ const IMPLEMENTED_SEMANTIC_OP_KINDS = [
1923
1982
  "AddSpeeches",
1924
1983
  "DeleteSpeeches",
1925
1984
  "MoveSpeeches",
1985
+ "MoveBgm",
1926
1986
  "ChangeSpeechScript",
1927
1987
  "ChangeSpeechVoice",
1928
1988
  "AdjustSpeechVolume",
@@ -2812,4 +2872,4 @@ function serverCovers(serverVV, target) {
2812
2872
  return covers(server, target);
2813
2873
  }
2814
2874
  //#endregion
2815
- export { DEFAULT_UNIT_TIME_MS, IMPLEMENTED_SEMANTIC_OP_KINDS, LANE_KINDS_IN_STACK_ORDER, ManualSyncDoc, MedeoHttpDocStorage, MemoryDocStorage, MengineAckFailedError, MengineDocSession, MengineHttpClient, MengineHttpRequestError, MenginePushRejectedError, MirrorVideoDocumentAdapter, SchemaValidator, SemanticEditor, TIMELINE_SKELETON_DURATION_MS, VIDEO_DOCUMENT_SCHEMA_VERSION, ValidationError, VideoDocumentValidationError, arrangeMainTrackSeamlessly, assertValidVideoDocument, base64ToBytes, buildInitialVideoDocument, buildSpeechHostMap, bytesToBase64, cascadeAfterVideoClipChanges, createMirrorVideoDocument, createMirrorVideoDocumentAdapter, decodeDocVersionMark, derivePositionFromAbs, encodeDocVersionMark, ensureLaneTrack, fillMainTrackTimeGaps, findLaneTrack, fromVideoDocument, generatePartId, getAt, hostForAbsMs, isEmptyVideoClip, isImplementedSemanticOpKind, isMap, laneTrackId, mainTrackRanges, partDurationMs, partUnionSchema, readMainTrackItems, readMengineEventStream, readPart, readPartDurationMs, readVideoDocumentFromDraft, reassignSpeechesToVideoClipsByTime, recalculateTimelineDuration, relativePositionForAbs, resolveAllSpeechOverlaps, resolveSpeechOverlapByShiftingVideos, safeDurationMs, schemas_exports as schemas, snapshotToPlain, solveVideoDocument, syncAggregatedClipsTimePosition, toVideoDocument, validateVideoDocument, videoDocumentMirrorSchema, videoDocumentSchema };
2875
+ export { DEFAULT_UNIT_TIME_MS, IMPLEMENTED_SEMANTIC_OP_KINDS, LANE_KINDS_IN_STACK_ORDER, ManualSyncDoc, MedeoHttpDocStorage, MemoryDocStorage, MengineAckFailedError, MengineDocSession, MengineHttpClient, MengineHttpRequestError, MenginePushRejectedError, MirrorVideoDocumentAdapter, SchemaValidator, SemanticEditor, TIMELINE_SKELETON_DURATION_MS, VIDEO_DOCUMENT_SCHEMA_VERSION, ValidationError, VideoDocumentValidationError, arrangeMainTrackSeamlessly, assertValidVideoDocument, base64ToBytes, buildInitialVideoDocument, buildSpeechHostMap, bytesToBase64, cascadeAfterVideoClipChanges, createMirrorVideoDocument, createMirrorVideoDocumentAdapter, decodeDocVersionMark, derivePositionFromAbs, encodeDocVersionMark, ensureLaneTrack, fillMainTrackTimeGaps, findLaneTrack, fromVideoDocument, generatePartId, getAt, hostForAbsMs, isEmptyVideoClip, isImplementedSemanticOpKind, isMap, laneTrackId, mainTrackRanges, partDurationMs, partUnionSchema, readMainTrackItems, readMengineEventStream, readPart, readPartDurationMs, readVideoDocumentFromDraft, reassignSpeechesToVideoClipsByTime, recalculateTimelineDuration, recoverOrphanedSpeechAnchorsByTime, relativePositionForAbs, resolveAllSpeechOverlaps, resolveAnchoredClipsTimePosition, resolveSpeechOverlapByShiftingVideos, safeDurationMs, schemas_exports as schemas, snapshotToPlain, solveVideoDocument, syncAggregatedClipsTimePosition, toVideoDocument, validateVideoDocument, videoDocumentMirrorSchema, videoDocumentSchema };
package/dist/testing.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { i as MirrorVideoDocumentAdapter, nt as VideoDraft } from "./index-CZlTzzrm.js";
1
+ import { i as MirrorVideoDocumentAdapter, nt as VideoDraft } from "./index-DrEHTguy.js";
2
2
 
3
3
  //#region src/testing/in-memory-mengine-server.d.ts
4
4
  /**
package/dist/testing.js CHANGED
@@ -1,4 +1,4 @@
1
- import { N as VIDEO_DOCUMENT_SCHEMA_VERSION, R as base64ToBytes, r as createMirrorVideoDocumentAdapter, z as bytesToBase64 } from "./document-Dcu87api.js";
1
+ import { B as base64ToBytes, F as VIDEO_DOCUMENT_SCHEMA_VERSION, V as bytesToBase64, r as createMirrorVideoDocumentAdapter } from "./document-C5KDvMMR.js";
2
2
  import { t as classifyUpdate } from "./loro-relay-doc-ssdYpuef.js";
3
3
  import { LoroDoc, VersionVector, encodeFrontiers } from "loro-crdt";
4
4
  //#region src/testing/in-memory-mengine-server.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mengine/medeo-client",
3
- "version": "1.2.1-alpha.1",
3
+ "version": "1.2.1-alpha.2",
4
4
  "license": "UNLICENSED",
5
5
  "repository": {
6
6
  "type": "git",
@@ -25,9 +25,9 @@
25
25
  "dependencies": {
26
26
  "loro-mirror": "^2.2.0",
27
27
  "zod": "^4.4.3",
28
- "@mengine/storage": "1.2.1-alpha.1",
29
- "@mengine/sync": "1.2.1-alpha.1",
30
- "@mengine/utils": "1.2.1-alpha.1"
28
+ "@mengine/storage": "1.2.1-alpha.2",
29
+ "@mengine/sync": "1.2.1-alpha.2",
30
+ "@mengine/utils": "1.2.1-alpha.2"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^25.9.1",