@bendyline/squisq 2.13.5 → 2.13.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,15 +4,21 @@ import {
4
4
  flattenRenderableBlocks,
5
5
  getPinnedBlockMeta,
6
6
  markdownToDoc
7
- } from "./chunk-ZF4NWRZU.js";
7
+ } from "./chunk-VQXOX3YN.js";
8
8
  import {
9
9
  deriveTemplateInputs,
10
+ extractEmbeddedVideos,
10
11
  extractImages,
11
12
  isAsciiDiagramFence,
12
13
  isAsciiTimelineFence,
14
+ isExplicitDiagramLang,
15
+ isExplicitTimelineLang,
16
+ isExplicitTreeLang,
13
17
  isPictureFence,
18
+ isPictureParagraph,
19
+ isStandaloneHtmlVideo,
14
20
  isTreeFence
15
- } from "./chunk-CZHWFQ7C.js";
21
+ } from "./chunk-CSR6VCSJ.js";
16
22
  import {
17
23
  extractContent,
18
24
  mapElementToBlock,
@@ -20,6 +26,7 @@ import {
20
26
  } from "./chunk-SVN7T6BK.js";
21
27
  import {
22
28
  cloneAndFreezeData,
29
+ cloneData,
23
30
  isTemplateBlock
24
31
  } from "./chunk-WNEH3BZP.js";
25
32
  import {
@@ -30,7 +37,9 @@ import {
30
37
  } from "./chunk-DOOUVVOM.js";
31
38
  import {
32
39
  extractPlainText,
40
+ findBlockVideoEmbed,
33
41
  getChildren,
42
+ parseTimeSeconds,
34
43
  stringifyHtmlNodes
35
44
  } from "./chunk-KVEOD57Z.js";
36
45
  import {
@@ -57,7 +66,8 @@ var PRESENTATION_LAYOUTS = [
57
66
  var PRESENTATION_LIMITS = {
58
67
  sourceCharacters: 4e4,
59
68
  beats: 80,
60
- serializedCharacters: 16e4
69
+ serializedCharacters: 16e4,
70
+ mediaPerBeat: 4
61
71
  };
62
72
  function record(value) {
63
73
  return !!value && typeof value === "object" && !Array.isArray(value);
@@ -90,13 +100,15 @@ function parsePresentationPlan(input) {
90
100
  if (!record(beat) || !keys(
91
101
  beat,
92
102
  ["id", "sourceStart", "sourceEnd", "layout", "headline", "points"],
93
- ["imageSrc"]
103
+ ["imageSrc", "media"]
94
104
  ) || !text(beat.id, 80) || ids.has(beat.id) || !Number.isInteger(beat.sourceStart) || !Number.isInteger(beat.sourceEnd) || typeof beat.sourceStart !== "number" || typeof beat.sourceEnd !== "number" || beat.sourceStart < end || beat.sourceEnd <= beat.sourceStart || beat.sourceEnd > value.sourceText.length || value.sourceText.slice(end, beat.sourceStart).trim() || !PRESENTATION_LAYOUTS.includes(beat.layout) || !text(beat.headline, 110) || !Array.isArray(beat.points) || beat.points.length > 4 || !beat.points.every((point) => text(point, 180)))
95
105
  return null;
96
106
  if (["comparison", "steps", "list"].includes(String(beat.layout)) && beat.points.length < 2)
97
107
  return null;
98
108
  if (beat.layout === "comparison" && beat.points.length !== 2) return null;
99
109
  if (beat.layout === "image" ? !text(beat.imageSrc, 2048) : "imageSrc" in beat) return null;
110
+ if ("media" in beat && (!Array.isArray(beat.media) || !beat.media.length || beat.media.length > PRESENTATION_LIMITS.mediaPerBeat || new Set(beat.media).size !== beat.media.length || !beat.media.every((ref) => text(ref, 1100))))
111
+ return null;
100
112
  ids.add(beat.id);
101
113
  end = beat.sourceEnd;
102
114
  beats.push({
@@ -106,7 +118,8 @@ function parsePresentationPlan(input) {
106
118
  layout: beat.layout,
107
119
  headline: beat.headline,
108
120
  points: [...beat.points],
109
- ...beat.layout === "image" ? { imageSrc: beat.imageSrc } : {}
121
+ ...beat.layout === "image" ? { imageSrc: beat.imageSrc } : {},
122
+ ..."media" in beat ? { media: [...beat.media] } : {}
110
123
  });
111
124
  }
112
125
  if (value.sourceText.slice(end).trim()) return null;
@@ -1638,6 +1651,55 @@ function videoBlock(video, index, segmentId, duration) {
1638
1651
  };
1639
1652
  }
1640
1653
 
1654
+ // src/transform/presentationMedia.ts
1655
+ function mediaKind(node) {
1656
+ if (node.type === "code")
1657
+ return isPictureFence(node) || isExplicitDiagramLang(node.lang) || isExplicitTreeLang(node.lang) || isExplicitTimelineLang(node.lang) ? { kind: "diagram", continuous: true } : null;
1658
+ if (isStandaloneHtmlVideo(node)) return { kind: "video", continuous: true };
1659
+ if (findBlockVideoEmbed(node) || isPictureParagraph(node) && extractEmbeddedVideos([node]).length > 0)
1660
+ return { kind: "video", continuous: false };
1661
+ return null;
1662
+ }
1663
+ function collectPresentationMedia(doc, sectionIds) {
1664
+ const sections = new Set(sectionIds);
1665
+ const result = /* @__PURE__ */ new Map();
1666
+ let owner = sectionIds[0];
1667
+ for (const block of flattenRenderableBlocks(doc.blocks)) {
1668
+ if (sections.has(block.id)) owner = block.id;
1669
+ if (!owner) continue;
1670
+ for (const node of block.contents ?? []) {
1671
+ const kind = mediaKind(node);
1672
+ if (!kind) continue;
1673
+ const items = result.get(owner) ?? [];
1674
+ items.push({ ...kind, ref: `${owner}#${items.length}`, node });
1675
+ result.set(owner, items);
1676
+ }
1677
+ }
1678
+ return result;
1679
+ }
1680
+ function videoElements(node, found = []) {
1681
+ if (!node || typeof node !== "object") return found;
1682
+ const value = node;
1683
+ if (value.type === "htmlElement" && value.tagName === "video") found.push(value);
1684
+ for (const key of ["children", "htmlChildren"])
1685
+ if (Array.isArray(value[key])) for (const child of value[key]) videoElements(child, found);
1686
+ return found;
1687
+ }
1688
+ function presentationMediaNode(item, elapsed) {
1689
+ const node = cloneData(item.node);
1690
+ if (item.kind !== "video" || elapsed <= 0) return node;
1691
+ for (const element of videoElements(node)) {
1692
+ const attributes = element.attributes ?? {};
1693
+ const seconds = (name) => attributes[name] ? parseTimeSeconds(attributes[name]) : null;
1694
+ const from = (seconds("data-squisq-video-clip-start") ?? 0) + Math.max(0, elapsed - (seconds("data-squisq-video-start-at") ?? 0));
1695
+ const until = seconds("data-squisq-video-clip-end");
1696
+ if (until !== null && from >= until) return null;
1697
+ delete attributes["data-squisq-video-start-at"];
1698
+ element.attributes = { ...attributes, "data-squisq-video-clip-start": String(from) };
1699
+ }
1700
+ return node;
1701
+ }
1702
+
1641
1703
  // src/transform/presentationPlanner.ts
1642
1704
  var SENTENCE_END = /[.!?][”"')\]]?$/u;
1643
1705
  var SENTENCE_START = /^[\p{Lu}\p{N}"“‘'([]/u;
@@ -1847,6 +1909,27 @@ function createPresentationPlan(doc, hints = defaultPresentationHints()) {
1847
1909
  throw new Error(
1848
1910
  "This document has too many sections for one presentation. Split it into shorter documents."
1849
1911
  );
1912
+ const sectionMedia = collectPresentationMedia(
1913
+ doc,
1914
+ script.blocks.map((block) => block.blockId)
1915
+ );
1916
+ for (const block of script.blocks) {
1917
+ const items = sectionMedia.get(block.blockId);
1918
+ if (!items?.length) continue;
1919
+ const own = beats.filter(
1920
+ (beat) => beat.sourceStart >= block.charStart && beat.sourceStart < block.charEnd
1921
+ );
1922
+ const open = own.filter((beat) => beat !== beats[0] && beat.layout !== "image");
1923
+ const slots = open.length ? open : own;
1924
+ const shown = /* @__PURE__ */ new Set();
1925
+ slots.forEach((beat, slot) => {
1926
+ const from = Math.floor(slot * items.length / slots.length);
1927
+ const to = Math.max(from + 1, Math.floor((slot + 1) * items.length / slots.length));
1928
+ const refs = items.slice(from, to).filter((item) => item.continuous || !shown.has(item.ref)).slice(0, PRESENTATION_LIMITS.mediaPerBeat).map((item) => item.ref);
1929
+ for (const ref of refs) shown.add(ref);
1930
+ if (refs.length) beat.media = refs;
1931
+ });
1932
+ }
1850
1933
  const closing = beats[beats.length - 1];
1851
1934
  if (beats.length > 2 && closing?.layout === "statement" && !hints.blocks.some(
1852
1935
  (hint) => hint.layout && script.blocks.some(
@@ -1875,6 +1958,7 @@ function createPresentationPlan(doc, hints = defaultPresentationHints()) {
1875
1958
  beat.headline = hint.wording.headline ?? beat.headline;
1876
1959
  beat.points = hint.wording.points ?? beat.points;
1877
1960
  }
1961
+ if (beat.media && beat.layout === "steps" && hint?.layout !== "steps") beat.layout = "list";
1878
1962
  if (["steps", "list", "comparison"].includes(beat.layout) && (beat.points.length < 2 || beat.layout === "comparison" && beat.points.length !== 2))
1879
1963
  beat.layout = "statement";
1880
1964
  if (beat.layout !== "image") delete beat.imageSrc;
@@ -1913,6 +1997,18 @@ function compilePresentationPlan(doc, plan) {
1913
1997
  throw new Error(
1914
1998
  "A presentation image is no longer in the document. Make the presentation again."
1915
1999
  );
2000
+ const media = new Map(
2001
+ [
2002
+ ...collectPresentationMedia(
2003
+ doc,
2004
+ script.blocks.map((block) => block.blockId)
2005
+ ).values()
2006
+ ].flat().map((item) => [item.ref, item])
2007
+ );
2008
+ if (checked.beats.some((beat) => beat.media?.some((ref) => !media.has(ref))))
2009
+ throw new Error(
2010
+ "A presentation video or diagram is no longer in the document. Make the presentation again."
2011
+ );
1916
2012
  const duration = narration ? narration.startTime + narration.duration : Math.max(4, script.tokens.length / 2.5);
1917
2013
  const bookmarks = (narration?.bookmarks ?? []).filter(
1918
2014
  (word) => Number.isFinite(word.time) && word.time >= 0 && word.time <= duration && Number.isInteger(word.charOffset) && word.charOffset >= 0 && word.charOffset < script.sourceText.length
@@ -1932,17 +2028,53 @@ function compilePresentationPlan(doc, plan) {
1932
2028
  });
1933
2029
  const authoredBlocks = flattenRenderableBlocks(doc.blocks);
1934
2030
  const buildHints = parsePresentationHints(doc.frontmatter?.[PRESENTATION_HINTS_KEY]);
2031
+ const sourceOf = (beat) => script.blocks.find(
2032
+ (block) => beat.sourceStart >= block.charStart && beat.sourceStart < block.charEnd
2033
+ );
2034
+ const sectionEnds = /* @__PURE__ */ new Map();
2035
+ checked.beats.forEach((beat, index) => {
2036
+ const section = sourceOf(beat)?.blockId;
2037
+ if (section) sectionEnds.set(section, starts[index + 1] ?? duration);
2038
+ });
2039
+ const clipsPlaced = /* @__PURE__ */ new Set();
2040
+ const mediaStarts = /* @__PURE__ */ new Map();
1935
2041
  const blocks = [];
1936
2042
  checked.beats.forEach((beat, index) => {
1937
2043
  const start = starts[index];
1938
2044
  const end = starts[index + 1] ?? duration;
1939
2045
  if (end <= start) return;
1940
- const source = script.blocks.find(
1941
- (block2) => beat.sourceStart >= block2.charStart && beat.sourceStart < block2.charEnd
1942
- );
2046
+ const source = sourceOf(beat);
1943
2047
  const build = buildHints?.blocks.find((hint) => hint.blockId === source?.blockId)?.build ?? authoredBlocks.find((block2) => block2.id === source?.blockId)?.build;
2048
+ const contents = (beat.media ?? []).flatMap((ref) => {
2049
+ const first = mediaStarts.get(ref) ?? start;
2050
+ mediaStarts.set(ref, first);
2051
+ const node = presentationMediaNode(media.get(ref), start - first);
2052
+ return node ? [node] : [];
2053
+ });
2054
+ let clips = [];
2055
+ if (source && !clipsPlaced.has(source.blockId)) {
2056
+ clipsPlaced.add(source.blockId);
2057
+ const sectionEnd = sectionEnds.get(source.blockId) ?? end;
2058
+ clips = (authoredBlocks.find((block2) => block2.id === source.blockId)?.media ?? []).flatMap(
2059
+ (clip2) => {
2060
+ if (clip2.spillover) return [clip2];
2061
+ const room = sectionEnd - start - clip2.startAt;
2062
+ if (room <= 0) return [];
2063
+ const clipStart = clip2.clipStart ?? 0;
2064
+ return [
2065
+ {
2066
+ ...clip2,
2067
+ spillover: true,
2068
+ clipEnd: Math.min(clip2.clipEnd ?? Infinity, clipStart + room)
2069
+ }
2070
+ ];
2071
+ }
2072
+ );
2073
+ }
1944
2074
  const base = {
1945
2075
  ...build ? { build } : {},
2076
+ ...contents.length ? { contents } : {},
2077
+ ...clips.length ? { media: clips } : {},
1946
2078
  id: beat.id,
1947
2079
  duration: end - start,
1948
2080
  startTime: start,
@@ -1980,7 +2112,14 @@ function visualBlock(beat, base, index) {
1980
2112
  explanation: beat.points.join("\n\n")
1981
2113
  };
1982
2114
  case "list":
1983
- return { ...base, template: "list", title: beat.headline, items: beat.points, colorScheme };
2115
+ return {
2116
+ ...base,
2117
+ template: "list",
2118
+ title: beat.headline,
2119
+ items: beat.points,
2120
+ ordered: false,
2121
+ colorScheme
2122
+ };
1984
2123
  case "comparison":
1985
2124
  return {
1986
2125
  ...base,
@@ -2069,6 +2208,7 @@ export {
2069
2208
  allocateTiming,
2070
2209
  applyTransform,
2071
2210
  transformNarratedSegment,
2211
+ collectPresentationMedia,
2072
2212
  createPresentationPlan,
2073
2213
  compilePresentationPlan,
2074
2214
  applySavedPresentation
@@ -5,10 +5,11 @@ import {
5
5
  } from "./chunk-NEWNNLWN.js";
6
6
  import {
7
7
  buildNarrationScript,
8
+ estimateSyllables,
8
9
  expectedSyllablesAt,
9
10
  flattenRenderableBlocks,
10
11
  wordPosAtExpectedSyllables
11
- } from "./chunk-ZF4NWRZU.js";
12
+ } from "./chunk-VQXOX3YN.js";
12
13
 
13
14
  // src/narration/types.ts
14
15
  var DEFAULT_FEATURE_CONFIG = Object.freeze({
@@ -839,6 +840,333 @@ function proportionalAlignment(script, effectiveDuration, takeDuration, c, detec
839
840
  return { words, blocks, detectedSyllables, cost: 0 };
840
841
  }
841
842
 
843
+ // src/narration/transcriptAlign.ts
844
+ var MAX_DP_CELLS = 1e6;
845
+ var ANCHOR_GRAM = 3;
846
+ var EXACT_SCORE = 2;
847
+ var NEAR_SCORE = 1;
848
+ var MISMATCH_SCORE = -1;
849
+ var GAP_SCORE = -1;
850
+ var NEAR_SIMILARITY = 0.75;
851
+ var ISOLATED_MATCH_MAX_LENGTH = 3;
852
+ var NON_SPEECH = /^\s*[[(].*[\])]\s*$/u;
853
+ function transcriptWordKeys(text) {
854
+ return text.normalize("NFKD").replace(/\p{M}+/gu, "").toLowerCase().replace(/['’]/gu, "").split(/[^\p{L}\p{N}]+/u).filter(Boolean);
855
+ }
856
+ function clampTime(value, durationSec) {
857
+ return Number.isFinite(value) ? Math.min(Math.max(0, value), durationSec) : 0;
858
+ }
859
+ function heardFromSegments(segments, durationSec) {
860
+ const heard = [];
861
+ const units = [];
862
+ const add = (text, startSec, endSec) => {
863
+ if (NON_SPEECH.test(text)) return;
864
+ const keys = transcriptWordKeys(text);
865
+ if (!keys.length) return;
866
+ for (const key of keys) units.push({ key, index: heard.length });
867
+ heard.push({ startSec, endSec: Math.max(startSec, endSec) });
868
+ };
869
+ const ordered = [...segments].sort((a, b) => a.startSec - b.startSec);
870
+ for (const segment of ordered) {
871
+ const start = clampTime(segment.startSec, durationSec);
872
+ const end = Math.max(start, clampTime(segment.endSec, durationSec));
873
+ if (segment.words?.length) {
874
+ for (const word of segment.words) {
875
+ const wordStart = clampTime(word.startSec, durationSec);
876
+ add(word.text, wordStart, clampTime(word.endSec, durationSec));
877
+ }
878
+ continue;
879
+ }
880
+ const pieces = segment.text.split(/\s+/u).filter((piece) => piece && !NON_SPEECH.test(piece));
881
+ const weights = pieces.map((piece) => estimateSyllables(piece));
882
+ const total = weights.reduce((sum, weight) => sum + weight, 0) || 1;
883
+ let before = 0;
884
+ pieces.forEach((piece, i) => {
885
+ const from = start + before / total * (end - start);
886
+ before += weights[i];
887
+ add(piece, from, start + before / total * (end - start));
888
+ });
889
+ }
890
+ let running = 0;
891
+ for (const word of heard) {
892
+ word.startSec = Math.max(running, word.startSec);
893
+ word.endSec = Math.max(word.startSec, word.endSec);
894
+ running = word.startSec;
895
+ }
896
+ return { heard, units };
897
+ }
898
+ function scriptUnits(script) {
899
+ const units = [];
900
+ script.tokens.forEach((token, index) => {
901
+ if (!token.spoken) return;
902
+ for (const key of transcriptWordKeys(token.text)) units.push({ key, index });
903
+ });
904
+ return units;
905
+ }
906
+ function uniqueGrams(units) {
907
+ const grams = /* @__PURE__ */ new Map();
908
+ for (let i = 0; i + ANCHOR_GRAM <= units.length; i++) {
909
+ const gram = units.slice(i, i + ANCHOR_GRAM).map((unit) => unit.key).join(" ");
910
+ grams.set(gram, grams.has(gram) ? -1 : i);
911
+ }
912
+ return grams;
913
+ }
914
+ function anchorChain(script, heard) {
915
+ const heardGrams = uniqueGrams(heard);
916
+ const pairs = [];
917
+ for (const [gram, s] of uniqueGrams(script)) {
918
+ const t = heardGrams.get(gram);
919
+ if (s >= 0 && t !== void 0 && t >= 0) pairs.push([s, t]);
920
+ }
921
+ pairs.sort((a, b) => a[0] - b[0]);
922
+ const tails = [];
923
+ const previous = new Int32Array(pairs.length).fill(-1);
924
+ for (let i = 0; i < pairs.length; i++) {
925
+ let lo = 0;
926
+ let hi = tails.length;
927
+ while (lo < hi) {
928
+ const mid = lo + hi >> 1;
929
+ if (pairs[tails[mid]][1] < pairs[i][1]) lo = mid + 1;
930
+ else hi = mid;
931
+ }
932
+ if (lo > 0) previous[i] = tails[lo - 1];
933
+ tails[lo] = i;
934
+ }
935
+ const chain = [];
936
+ for (let i = tails.length ? tails[tails.length - 1] : -1; i >= 0; i = previous[i]) {
937
+ chain.push(pairs[i]);
938
+ }
939
+ return chain.reverse();
940
+ }
941
+ function editDistance(a, b) {
942
+ let row = Array.from({ length: b.length + 1 }, (_, j) => j);
943
+ for (let i = 1; i <= a.length; i++) {
944
+ const next = [i];
945
+ for (let j = 1; j <= b.length; j++) {
946
+ next[j] = Math.min(
947
+ row[j] + 1,
948
+ next[j - 1] + 1,
949
+ row[j - 1] + (a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1)
950
+ );
951
+ }
952
+ row = next;
953
+ }
954
+ return row[b.length];
955
+ }
956
+ function createScorer() {
957
+ const near = /* @__PURE__ */ new Map();
958
+ return (a, b) => {
959
+ if (a === b) return EXACT_SCORE;
960
+ if (a.length < 4 || b.length < 4 || a[0] !== b[0] || Math.abs(a.length - b.length) > 3)
961
+ return MISMATCH_SCORE;
962
+ const key = a < b ? `${a} ${b}` : `${b} ${a}`;
963
+ let similar = near.get(key);
964
+ if (similar === void 0) {
965
+ similar = 1 - editDistance(a, b) / Math.max(a.length, b.length) >= NEAR_SIMILARITY;
966
+ near.set(key, similar);
967
+ }
968
+ return similar ? NEAR_SCORE : MISMATCH_SCORE;
969
+ };
970
+ }
971
+ function alignStretch(script, sFrom, sTo, heard, tFrom, tTo, score) {
972
+ const rows = sTo - sFrom;
973
+ const cols = tTo - tFrom;
974
+ if (rows <= 0 || cols <= 0 || rows * cols > MAX_DP_CELLS) return [];
975
+ const width = cols + 1;
976
+ const moves = new Uint8Array((rows + 1) * width);
977
+ let prev = new Float64Array(width);
978
+ for (let j2 = 1; j2 <= cols; j2++) {
979
+ prev[j2] = j2 * GAP_SCORE;
980
+ moves[j2] = 2;
981
+ }
982
+ for (let i2 = 1; i2 <= rows; i2++) {
983
+ const row = new Float64Array(width);
984
+ row[0] = i2 * GAP_SCORE;
985
+ moves[i2 * width] = 1;
986
+ const key = script[sFrom + i2 - 1].key;
987
+ for (let j2 = 1; j2 <= cols; j2++) {
988
+ const diagonal = prev[j2 - 1] + score(key, heard[tFrom + j2 - 1].key);
989
+ const up = prev[j2] + GAP_SCORE;
990
+ const left = row[j2 - 1] + GAP_SCORE;
991
+ if (diagonal >= up && diagonal >= left) {
992
+ row[j2] = diagonal;
993
+ moves[i2 * width + j2] = 0;
994
+ } else if (up >= left) {
995
+ row[j2] = up;
996
+ moves[i2 * width + j2] = 1;
997
+ } else {
998
+ row[j2] = left;
999
+ moves[i2 * width + j2] = 2;
1000
+ }
1001
+ }
1002
+ prev = row;
1003
+ }
1004
+ const pairs = [];
1005
+ let i = rows;
1006
+ let j = cols;
1007
+ while (i > 0 || j > 0) {
1008
+ const move = moves[i * width + j];
1009
+ if (move === 0) {
1010
+ const s = sFrom + i - 1;
1011
+ const t = tFrom + j - 1;
1012
+ if (score(script[s].key, heard[t].key) > 0) pairs.push([s, t]);
1013
+ i--;
1014
+ j--;
1015
+ } else if (move === 1) i--;
1016
+ else j--;
1017
+ }
1018
+ return pairs.reverse();
1019
+ }
1020
+ function matchUnits(script, heard) {
1021
+ const anchored = [];
1022
+ let lastS = -1;
1023
+ let lastT = -1;
1024
+ for (const [s, t] of anchorChain(script, heard)) {
1025
+ for (let k = 0; k < ANCHOR_GRAM; k++) {
1026
+ if (s + k > lastS && t + k > lastT) {
1027
+ anchored.push([s + k, t + k]);
1028
+ lastS = s + k;
1029
+ lastT = t + k;
1030
+ }
1031
+ }
1032
+ }
1033
+ const score = createScorer();
1034
+ const pairs = [];
1035
+ let from = [-1, -1];
1036
+ for (const to of [...anchored, [script.length, heard.length]]) {
1037
+ pairs.push(...alignStretch(script, from[0] + 1, to[0], heard, from[1] + 1, to[1], score));
1038
+ if (to[0] < script.length) pairs.push(to);
1039
+ from = to;
1040
+ }
1041
+ return pairs.filter(([s, t], i) => {
1042
+ if (script[s].key.length > ISOLATED_MATCH_MAX_LENGTH) return true;
1043
+ const before = pairs[i - 1];
1044
+ const after = pairs[i + 1];
1045
+ return before !== void 0 && before[0] === s - 1 && before[1] === t - 1 || after !== void 0 && after[0] === s + 1 && after[1] === t + 1;
1046
+ });
1047
+ }
1048
+ function placeWords(script, matched, heard, speechEnd, durationSec, gapMinSec) {
1049
+ const n = script.tokens.length;
1050
+ const cumulative = script.cumulativeSyllables;
1051
+ if (!heard.length) {
1052
+ const total = Math.max(1, script.totalSyllables);
1053
+ return script.tokens.map((_, k) => ({
1054
+ tokenIndex: k,
1055
+ tSec: cumulative[k] / total * durationSec,
1056
+ interpolated: true
1057
+ }));
1058
+ }
1059
+ const startOf = (h) => h < heard.length ? heard[h].startSec : speechEnd;
1060
+ const words = new Array(n);
1061
+ let leftToken = -1;
1062
+ let leftHeard = -1;
1063
+ const fill = (rightToken, rightHeard) => {
1064
+ const from = leftToken + 1;
1065
+ const between = rightHeard - leftHeard - 1;
1066
+ const leftBlock = leftToken >= 0 ? script.tokens[leftToken].blockIndex : -1;
1067
+ let leftCount = 0;
1068
+ let rightCount = 0;
1069
+ for (let u = from; u < rightToken; u++) {
1070
+ if (!script.tokens[u].spoken) continue;
1071
+ if (script.tokens[u].blockIndex === leftBlock) leftCount++;
1072
+ else rightCount++;
1073
+ }
1074
+ if (between > 0 && between >= leftCount + rightCount) {
1075
+ let rightFloor = rightHeard - rightCount;
1076
+ for (let h = rightHeard; h > rightFloor && h > 0; h--) {
1077
+ if (startOf(h) - heard[h - 1].endSec >= gapMinSec) {
1078
+ rightFloor = h;
1079
+ break;
1080
+ }
1081
+ }
1082
+ let leftRank = 0;
1083
+ let rightRank = 0;
1084
+ for (let u = from; u < rightToken; u++) {
1085
+ const token = script.tokens[u];
1086
+ const left = token.blockIndex === leftBlock;
1087
+ const position = left ? leftHeard + 1 + leftRank : Math.max(rightFloor, rightHeard - rightCount + rightRank);
1088
+ words[u] = { tokenIndex: u, tSec: startOf(position), interpolated: true };
1089
+ if (token.spoken && left) leftRank++;
1090
+ else if (token.spoken) rightRank++;
1091
+ }
1092
+ return;
1093
+ }
1094
+ const base = cumulative[from] ?? 0;
1095
+ const span = (rightToken < n ? cumulative[rightToken] : script.totalSyllables) - base;
1096
+ for (let u = from; u < rightToken; u++) {
1097
+ let tSec = startOf(rightHeard);
1098
+ if (between > 0) {
1099
+ const fraction = span > 0 ? (cumulative[u] - base) / span : (u - from) / (rightToken - from);
1100
+ const position = leftHeard + 1 + fraction * between;
1101
+ const index = Math.floor(position);
1102
+ tSec = startOf(index) + (position - index) * (startOf(index + 1) - startOf(index));
1103
+ }
1104
+ words[u] = { tokenIndex: u, tSec, interpolated: true };
1105
+ }
1106
+ };
1107
+ for (let k = 0; k < n; k++) {
1108
+ const h = matched[k];
1109
+ if (h === void 0) continue;
1110
+ fill(k, h);
1111
+ words[k] = { tokenIndex: k, tSec: heard[h].startSec, interpolated: false };
1112
+ leftToken = k;
1113
+ leftHeard = h;
1114
+ }
1115
+ fill(n, heard.length);
1116
+ let running = 0;
1117
+ for (const word of words) {
1118
+ running = Math.max(running, word.tSec);
1119
+ word.tSec = running;
1120
+ }
1121
+ return words;
1122
+ }
1123
+ function alignTranscript(input, config) {
1124
+ const c = { ...DEFAULT_ALIGN_CONFIG, ...config };
1125
+ const { script } = input;
1126
+ const durationSec = Number.isFinite(input.durationSec) && input.durationSec > 0 ? input.durationSec : 0;
1127
+ if (script.tokens.length > c.maxScriptTokens) {
1128
+ throw new RangeError(`Narration script exceeds the ${c.maxScriptTokens}-token safety limit`);
1129
+ }
1130
+ const spokenWords = script.tokens.filter((token) => token.spoken).length;
1131
+ const { heard, units: heardUnits } = heardFromSegments(input.segments, durationSec);
1132
+ if (script.tokens.length === 0 || durationSec === 0) {
1133
+ return { words: [], blocks: [], detectedSyllables: 0, cost: 0, matchedWords: 0, spokenWords };
1134
+ }
1135
+ const units = scriptUnits(script);
1136
+ const matched = new Array(script.tokens.length);
1137
+ for (const [s, t] of matchUnits(units, heardUnits)) {
1138
+ const token = units[s].index;
1139
+ matched[token] ?? (matched[token] = heardUnits[t].index);
1140
+ }
1141
+ const matchedWords = matched.filter((heardIndex) => heardIndex !== void 0).length;
1142
+ const lastSpeechEnd = heard.reduce((end, word) => Math.max(end, word.endSec), 0);
1143
+ const words = placeWords(
1144
+ script,
1145
+ matched,
1146
+ heard,
1147
+ lastSpeechEnd || durationSec,
1148
+ durationSec,
1149
+ c.gapMinSec
1150
+ );
1151
+ const gaps = [];
1152
+ for (let i = 1; i < heard.length; i++) {
1153
+ const startSec = heard[i - 1].endSec;
1154
+ const endSec = heard[i].startSec;
1155
+ if (endSec - startSec >= c.gapMinSec) {
1156
+ gaps.push({ kind: "gap", tSec: (startSec + endSec) / 2, startSec, endSec });
1157
+ }
1158
+ }
1159
+ const blocks = deriveBlockRanges(script, words, gaps, durationSec, lastSpeechEnd, c);
1160
+ return {
1161
+ words,
1162
+ blocks,
1163
+ detectedSyllables: heard.length,
1164
+ cost: 0,
1165
+ matchedWords,
1166
+ spokenWords
1167
+ };
1168
+ }
1169
+
842
1170
  // src/narration/advanceTiming.ts
843
1171
  var EMPTY_ADVANCE_LOG = Object.freeze([]);
844
1172
  function recordSlideShown(log, blockId, atMs) {
@@ -963,6 +1291,8 @@ export {
963
1291
  traceWordPosAt,
964
1292
  downsampleTrace,
965
1293
  alignNarration,
1294
+ transcriptWordKeys,
1295
+ alignTranscript,
966
1296
  EMPTY_ADVANCE_LOG,
967
1297
  recordSlideShown,
968
1298
  buildAdvanceTimingJson,
@@ -3977,6 +3977,7 @@ export {
3977
3977
  parseAnnotationInner,
3978
3978
  parseStandaloneAnnotation,
3979
3979
  isMediaAnnotationName,
3980
+ isStandaloneHtmlVideo,
3980
3981
  extractMediaFromContents,
3981
3982
  toCells,
3982
3983
  cellWidth,
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-FTN6SGW5.js";
4
4
  import {
5
5
  applySavedPresentation
6
- } from "./chunk-F5WTRHWL.js";
6
+ } from "./chunk-4O26IMVA.js";
7
7
  import {
8
8
  SHAPE_NAMES,
9
9
  buildChartData,
@@ -31,7 +31,7 @@ import {
31
31
  templateRegistry,
32
32
  writeCustomTemplatesToFrontmatter,
33
33
  writeCustomThemesToFrontmatter
34
- } from "./chunk-ZF4NWRZU.js";
34
+ } from "./chunk-VQXOX3YN.js";
35
35
  import {
36
36
  ASCII_TREE_VOCAB,
37
37
  ASCII_VOCAB,
@@ -54,7 +54,7 @@ import {
54
54
  toCells,
55
55
  toGrid,
56
56
  traceBoxes
57
- } from "./chunk-CZHWFQ7C.js";
57
+ } from "./chunk-CSR6VCSJ.js";
58
58
  import {
59
59
  iconMarker
60
60
  } from "./chunk-7ZAAICW4.js";
@@ -32,7 +32,7 @@ import {
32
32
  profileBlockContents,
33
33
  renderMarkdownBlocksHtml,
34
34
  treeToTemplateData
35
- } from "./chunk-CZHWFQ7C.js";
35
+ } from "./chunk-CSR6VCSJ.js";
36
36
  import {
37
37
  anchorPoint,
38
38
  base64ToUtf8,
@@ -4508,6 +4508,17 @@ function dataTable(input, context) {
4508
4508
 
4509
4509
  // src/doc/templates/chart/parse.ts
4510
4510
  var NUMERIC_COLUMN_THRESHOLD = 0.6;
4511
+ function cellText(cell) {
4512
+ if (typeof cell === "string") return cell;
4513
+ if (cell === null || cell === void 0) return "";
4514
+ if (typeof cell === "number") return Number.isFinite(cell) ? String(cell) : "";
4515
+ return typeof cell === "object" ? "" : String(cell);
4516
+ }
4517
+ function normalizeChartTable(table) {
4518
+ const headers = Array.isArray(table.headers) ? table.headers.map(cellText) : [];
4519
+ const rows = Array.isArray(table.rows) ? table.rows.filter(Array.isArray).map((row) => row.map(cellText)) : [];
4520
+ return { headers, rows };
4521
+ }
4511
4522
  function parseCellNumber(raw) {
4512
4523
  if (typeof raw !== "string") return null;
4513
4524
  let text = raw.trim();
@@ -4553,8 +4564,7 @@ function resolveColumnRef(ref, headers) {
4553
4564
  return null;
4554
4565
  }
4555
4566
  function buildChartData(table, options = {}) {
4556
- const headers = Array.isArray(table.headers) ? table.headers : [];
4557
- const rows = Array.isArray(table.rows) ? table.rows : [];
4567
+ const { headers, rows } = normalizeChartTable(table);
4558
4568
  if (headers.length === 0 || rows.length === 0) return null;
4559
4569
  const warnings = [];
4560
4570
  let labelIndex = 0;
@@ -4972,7 +4982,7 @@ function renderChart(kind, input, context) {
4972
4982
  }
4973
4983
  function resolveTable(input, context) {
4974
4984
  if (Array.isArray(input.headers) && input.headers.length > 0 && Array.isArray(input.rows) && input.rows.length > 0) {
4975
- return { headers: input.headers, rows: input.rows };
4985
+ return normalizeChartTable(input);
4976
4986
  }
4977
4987
  const contents = input.contents ?? context.block?.contents;
4978
4988
  return extractTableFromContents(contents);
@@ -6276,14 +6286,15 @@ function mapForegroundLayers(layers, contentRect, viewport, fontScale, template)
6276
6286
  if (isFullViewportBackground(layer, viewport)) return layer;
6277
6287
  const position = mapPosition(layer.position, contentRect, viewport);
6278
6288
  if (layer.type === "text") {
6289
+ const fontSize = Math.max(12, Math.round(layer.content.style.fontSize * fontScale));
6279
6290
  return {
6280
6291
  ...layer,
6281
- position,
6292
+ position: layer.content.flow?.role === "marker" ? markerPosition(layer, position, fontSize / layer.content.style.fontSize, viewport) : position,
6282
6293
  content: {
6283
6294
  ...layer.content,
6284
6295
  style: {
6285
6296
  ...layer.content.style,
6286
- fontSize: Math.max(12, Math.round(layer.content.style.fontSize * fontScale)),
6297
+ fontSize,
6287
6298
  ...layer.content.style.padding !== void 0 ? { padding: Math.round(layer.content.style.padding * fontScale) } : {}
6288
6299
  }
6289
6300
  }
@@ -6293,6 +6304,20 @@ function mapForegroundLayers(layers, contentRect, viewport, fontScale, template)
6293
6304
  });
6294
6305
  return arrangeTitleFamily(mapped, template, contentRect, viewport);
6295
6306
  }
6307
+ function markerPosition(layer, mapped, textScale, viewport) {
6308
+ if (mapped.width === void 0 || layer.position.width === void 0) return mapped;
6309
+ const width = resolvePositionValue(mapped.width, viewport.width);
6310
+ const needed = Math.ceil(resolvePositionValue(layer.position.width, viewport.width) * textScale);
6311
+ if (needed <= width) return mapped;
6312
+ const grow = needed - width;
6313
+ const anchor = mapped.anchor ?? "top-left";
6314
+ const shift = layer.content.style.textAlign !== "right" || anchor.includes("right") ? 0 : anchor === "center" ? grow / 2 : grow;
6315
+ return {
6316
+ ...mapped,
6317
+ x: resolvePositionValue(mapped.x, viewport.width) - shift,
6318
+ width: needed
6319
+ };
6320
+ }
6296
6321
  function arrangeTitleFamily(layers, template, contentRect, viewport) {
6297
6322
  const centerX = contentRect.x + contentRect.width / 2;
6298
6323
  const centerY = contentRect.y + contentRect.height / 2;
package/dist/doc/index.js CHANGED
@@ -75,9 +75,9 @@ import {
75
75
  validateMarkdownSource,
76
76
  wordRevealDelay,
77
77
  writeDashboardLayoutsToFrontmatter
78
- } from "../chunk-K75GTKSN.js";
78
+ } from "../chunk-FGN4OZVK.js";
79
79
  import "../chunk-FTN6SGW5.js";
80
- import "../chunk-F5WTRHWL.js";
80
+ import "../chunk-4O26IMVA.js";
81
81
  import {
82
82
  BASE_INPUT_DESCRIPTORS,
83
83
  BLOCK_MEDIA_LAYOUT_POLICIES,
@@ -195,7 +195,7 @@ import {
195
195
  wrapWithPersistentLayers,
196
196
  writeCustomTemplatesToFrontmatter,
197
197
  writeCustomThemesToFrontmatter
198
- } from "../chunk-ZF4NWRZU.js";
198
+ } from "../chunk-VQXOX3YN.js";
199
199
  import {
200
200
  ASCII_CHAR_H,
201
201
  ASCII_CHAR_W,
@@ -248,7 +248,7 @@ import {
248
248
  treeFromMarkdownList,
249
249
  treeFromTemplateData,
250
250
  treeToTemplateData
251
- } from "../chunk-CZHWFQ7C.js";
251
+ } from "../chunk-CSR6VCSJ.js";
252
252
  import "../chunk-SVN7T6BK.js";
253
253
  import {
254
254
  PATH_SHAPE_KINDS,
package/dist/index.d.ts CHANGED
@@ -16,7 +16,7 @@ export { BuildCaptionTrackOptions, CaptionSegmentSource, DEFAULT_WORDS_PER_SECON
16
16
  export { SeededRandom, hashString } from './random/index.js';
17
17
  export { C as ComparisonData, D as DateData, a as DefinitionData, E as ExtractedElement, b as ExtractionOptions, c as ExtractionResult, d as ExtractionType, F as FactData, I as ImpactLineData, L as ListData, Q as QuoteData, S as StatData, e as extractContent, s as stripMarkdown } from './contentExtractor-BNfVJV2U.js';
18
18
  export { AffinityAssignment, AffinityMatchResult, AffinityTarget, ClipSelectionOptions, ClipSource, KeywordExtractionOptions, MapOptions, SelectedClip, TranscriptSegmentSource, calculateKeywordAffinity, extractKeywords, mapElementToBlock, matchByKeywordAffinity, selectSafeZoneClips, selectTranscriptClips } from './generate/index.js';
19
- export { AnalyzedBlock, BlockImage, DEFAULT_TRANSFORM_STYLE_ID, DYNAMIC_PRESENTATION_STYLE, NarratedSegmentInput, NarratedSegmentResult, NarratedSegmentTransformOptions, NarratedSegmentVideo, PRESENTATION_HINTS_KEY, PRESENTATION_KEY, PRESENTATION_LAYOUTS, PRESENTATION_LIMITS, PresentationBeat, PresentationBlockHint, PresentationHints, PresentationLayout, PresentationPlan, TransformImage, TransformOptions, TransformResult, TransformStyleConfig, TransformStyleId, TransformStyleInput, TransformStyleRegistry, TransformStyleSummary, allocateTiming, analyzeBlocks, applySavedPresentation, applyTransform, compilePresentationPlan, createPresentationPlan, createTransformStyleRegistry, defaultPresentationHints, extractDocImages, getTransformStyleIds, getTransformStyleSummaries, parsePresentationHints, parsePresentationPlan, presentationSections, resolveTransformStyle, serializePresentationHints, serializePresentationPlan, transformNarratedSegment, usesDynamicPresentation } from './transform/index.js';
19
+ export { AnalyzedBlock, BlockImage, DEFAULT_TRANSFORM_STYLE_ID, DYNAMIC_PRESENTATION_STYLE, NarratedSegmentInput, NarratedSegmentResult, NarratedSegmentTransformOptions, NarratedSegmentVideo, PRESENTATION_HINTS_KEY, PRESENTATION_KEY, PRESENTATION_LAYOUTS, PRESENTATION_LIMITS, PresentationBeat, PresentationBlockHint, PresentationHints, PresentationLayout, PresentationMediaItem, PresentationPlan, TransformImage, TransformOptions, TransformResult, TransformStyleConfig, TransformStyleId, TransformStyleInput, TransformStyleRegistry, TransformStyleSummary, allocateTiming, analyzeBlocks, applySavedPresentation, applyTransform, collectPresentationMedia, compilePresentationPlan, createPresentationPlan, createTransformStyleRegistry, defaultPresentationHints, extractDocImages, getTransformStyleIds, getTransformStyleSummaries, parsePresentationHints, parsePresentationPlan, presentationSections, resolveTransformStyle, serializePresentationHints, serializePresentationPlan, transformNarratedSegment, usesDynamicPresentation } from './transform/index.js';
20
20
  export { C as CoalesceOptions, P as PrunePolicy, R as RevertFailureReason, a as RevertOptions, b as RevertResult, S as SaveVersionOptions, c as SaveVersionResult, V as Version } from './types-8QjefM9J.js';
21
21
  export { DATA_BACKUP_PREFIX, DocumentVersionManager, DocumentVersionManagerOptions, VERSIONS_PREFIX, VERSION_TIMESTAMP_PATTERN, buildDataBackupPath, buildVersionPath, coalesceVersions, formatVersionTimestamp, getDocBasename, listVersions, parseVersionPath, parseVersionTimestamp, pruneVersions, readVersion, revertToVersion, saveVersion } from './versions/index.js';
22
22
  export { BuildJsonFormTokensOptions, ControlKind, InferSchemaOptions, JsonFormValidationError, JsonFormValidator, SquisqAnnotatedSchema, SquisqHints, SquisqWhen, appendPointer, arrayItemKind, buildJsonFormTokens, chooseControl, escapePointerSegment, evaluateWhen, getByPointer, inferSchema, isSafeConditionRegex, pointerSegments, primaryType, resolveFlag, resolveJsonFormTheme, resolveRef, setByPointer, toPointer } from './jsonForm/index.js';
@@ -24,7 +24,7 @@ export { AnimatedImageFormat, AnimatedImageInfo, IMAGE_EDIT_ASSETS_PREFIX, IMAGE
24
24
  export { I as ICONS, a as IconEntry, b as IconFamily, c as IconTextRun, h as hasIconMarker, i as iconClass, d as iconMarker, s as splitIconMarkers, e as stripIconMarkers } from './inlineIconMarker-BGjv-W4j.js';
25
25
  export { IconSuggestion, canonicalIconToken, iconGlyph, looksLikeIconToken, resolveIcon, suggestIcons } from './icons/index.js';
26
26
  export { BlockContentProfile, RecommendationResult, profileBlockContents, recommendTemplatesForBlock } from './recommend/index.js';
27
- export { AdvanceCoverage, AlignConfig, AlignInput, BandpassState, BuildAdvanceTimingOptions, BuildNarrationTimingOptions, BuildScriptOptions, DEFAULT_ALIGN_CONFIG, DEFAULT_FEATURE_CONFIG, DEFAULT_NUCLEI_CONFIG, DEFAULT_PACING_CONFIG, DEFAULT_VAD_CONFIG, EMPTY_ADVANCE_LOG, FeatureConfig, FeatureState, FrameFeatures, NarrationAlignment, NarrationBlockRange, NarrationScript, NarrationSessionConfig, NarrationSessionState, NarrationTimingBlock, NarrationTimingGenerator, NarrationTimingJsonV3, NarrationTimingMethod, NarrationTrace, NucleiConfig, NucleiState, PacingConfig, PacingState, PacingTick, ScriptBlockRange, ScriptToken, SlideAdvance, SlideAdvanceLog, TraceSample, VadConfig, VadState, WordTiming, advanceCoverage, alignNarration, bandpassRun, buildAdvanceTimingJson, buildNarrationScript, buildNarrationTimingJson, createBandpass, createFeatureState, createNarrationSession, createNucleiState, createPacingState, createVadState, detectSyllableOnsets, downsampleTrace, estimateSyllables, expectedSyllablesAt, extractFrameFeatures, featureStep, narrationSessionStep, nucleiStep, pacingStep, parseNarrationTimingJson, reanchorPacing, reanchorSession, recordSlideShown, resolveNarrationTimingGenerator, traceWordPosAt, vadStep, wordIndexAtChar, wordIndexAtTime, wordPosAtExpectedSyllables } from './narration/index.js';
27
+ export { AdvanceCoverage, AlignConfig, AlignInput, BandpassState, BuildAdvanceTimingOptions, BuildNarrationTimingOptions, BuildScriptOptions, DEFAULT_ALIGN_CONFIG, DEFAULT_FEATURE_CONFIG, DEFAULT_NUCLEI_CONFIG, DEFAULT_PACING_CONFIG, DEFAULT_VAD_CONFIG, EMPTY_ADVANCE_LOG, FeatureConfig, FeatureState, FrameFeatures, NarrationAlignment, NarrationBlockRange, NarrationScript, NarrationSessionConfig, NarrationSessionState, NarrationTimingBlock, NarrationTimingGenerator, NarrationTimingJsonV3, NarrationTimingMethod, NarrationTrace, NucleiConfig, NucleiState, PacingConfig, PacingState, PacingTick, ScriptBlockRange, ScriptToken, SlideAdvance, SlideAdvanceLog, TraceSample, TranscriptAlignInput, TranscriptAlignment, TranscriptSegment, TranscriptWord, VadConfig, VadState, WordTiming, advanceCoverage, alignNarration, alignTranscript, bandpassRun, buildAdvanceTimingJson, buildNarrationScript, buildNarrationTimingJson, createBandpass, createFeatureState, createNarrationSession, createNucleiState, createPacingState, createVadState, detectSyllableOnsets, downsampleTrace, estimateSyllables, expectedSyllablesAt, extractFrameFeatures, featureStep, narrationSessionStep, nucleiStep, pacingStep, parseNarrationTimingJson, reanchorPacing, reanchorSession, recordSlideShown, resolveNarrationTimingGenerator, traceWordPosAt, transcriptWordKeys, vadStep, wordIndexAtChar, wordIndexAtTime, wordPosAtExpectedSyllables } from './narration/index.js';
28
28
  export { FenceRenderContext, FenceRenderer, FenceRendererMap, fenceRendererLangs } from './fence/index.js';
29
29
  export { P as PROOF_DIALECTS, a as ProofCategory, b as ProofDialect, c as ProofFinding, d as ProofRange, e as ProofSuggestion, f as ProofSuggestionKind } from './types-BQWu0gzi.js';
30
30
  export { BlankProtectedSpansOptions, BlankedText, DEFAULT_PROOF_SETTINGS, JoinedSegments, LINT_KIND_CATEGORIES, PROOF_FRONTMATTER_KEYS, PROOF_JOIN_SEPARATOR, ProofingSettings, SegmentSpan, blankProtectedSpans, buildJoinedText, categorizeLintKind, dropMaskedFindings, formatProofDictionary, mapJoinedSpanToSegment, parseProofDictionary, readProofingSettings, resolveProofDialect } from './proof/index.js';
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  EMPTY_ADVANCE_LOG,
8
8
  advanceCoverage,
9
9
  alignNarration,
10
+ alignTranscript,
10
11
  bandpassRun,
11
12
  buildAdvanceTimingJson,
12
13
  createBandpass,
@@ -26,8 +27,9 @@ import {
26
27
  reanchorSession,
27
28
  recordSlideShown,
28
29
  traceWordPosAt,
30
+ transcriptWordKeys,
29
31
  vadStep
30
- } from "./chunk-FSZ6HB7S.js";
32
+ } from "./chunk-B2MOGSWY.js";
31
33
  import {
32
34
  fenceRendererLangs
33
35
  } from "./chunk-PGI7HSWE.js";
@@ -230,7 +232,7 @@ import {
230
232
  validateMarkdownSource,
231
233
  wordRevealDelay,
232
234
  writeDashboardLayoutsToFrontmatter
233
- } from "./chunk-K75GTKSN.js";
235
+ } from "./chunk-FGN4OZVK.js";
234
236
  import {
235
237
  buildNarrationTimingJson,
236
238
  parseNarrationTimingJson,
@@ -247,6 +249,7 @@ import {
247
249
  analyzeBlocks,
248
250
  applySavedPresentation,
249
251
  applyTransform,
252
+ collectPresentationMedia,
250
253
  compilePresentationPlan,
251
254
  createPresentationPlan,
252
255
  createTransformStyleRegistry,
@@ -262,7 +265,7 @@ import {
262
265
  serializePresentationPlan,
263
266
  transformNarratedSegment,
264
267
  usesDynamicPresentation
265
- } from "./chunk-F5WTRHWL.js";
268
+ } from "./chunk-4O26IMVA.js";
266
269
  import {
267
270
  BASE_INPUT_DESCRIPTORS,
268
271
  BLOCK_MEDIA_LAYOUT_POLICIES,
@@ -386,7 +389,7 @@ import {
386
389
  wrapWithPersistentLayers,
387
390
  writeCustomTemplatesToFrontmatter,
388
391
  writeCustomThemesToFrontmatter
389
- } from "./chunk-ZF4NWRZU.js";
392
+ } from "./chunk-VQXOX3YN.js";
390
393
  import {
391
394
  ASCII_CHAR_H,
392
395
  ASCII_CHAR_W,
@@ -441,7 +444,7 @@ import {
441
444
  treeFromMarkdownList,
442
445
  treeFromTemplateData,
443
446
  treeToTemplateData
444
- } from "./chunk-CZHWFQ7C.js";
447
+ } from "./chunk-CSR6VCSJ.js";
445
448
  import {
446
449
  extractContent,
447
450
  mapElementToBlock,
@@ -830,6 +833,7 @@ export {
830
833
  adjustY,
831
834
  advanceCoverage,
832
835
  alignNarration,
836
+ alignTranscript,
833
837
  allocateTiming,
834
838
  analyzeBlocks,
835
839
  anchorPoint,
@@ -894,6 +898,7 @@ export {
894
898
  coalesceImageEditVersions,
895
899
  coalesceVersions,
896
900
  coerceTemplateParams,
901
+ collectPresentationMedia,
897
902
  comparisonBar,
898
903
  compilePresentationPlan,
899
904
  compileTheme,
@@ -1298,6 +1303,7 @@ export {
1298
1303
  topFeature,
1299
1304
  touch,
1300
1305
  traceWordPosAt,
1306
+ transcriptWordKeys,
1301
1307
  transformNarratedSegment,
1302
1308
  transformOriginFor,
1303
1309
  transposeCells,
@@ -568,6 +568,60 @@ interface AlignInput {
568
568
  }
569
569
  declare function alignNarration(input: AlignInput, config?: Partial<AlignConfig>): NarrationAlignment;
570
570
 
571
+ /**
572
+ * Transcript alignment: word/block timestamps for a recorded take from a
573
+ * speech recognizer's timed transcript.
574
+ *
575
+ * `alignNarration` infers timing from the take's acoustics against the
576
+ * script's expected syllables, so it needs a reader who follows the script
577
+ * closely. This matches the WORDS a recognizer heard to the script's words
578
+ * instead, so it tolerates paraphrase, ad-libs, skipped passages and long
579
+ * silent demos.
580
+ *
581
+ * Pipeline: both sides become normalized word units → word trigrams that
582
+ * occur exactly once on each side anchor a monotone chain (longest increasing
583
+ * subsequence) → the short stretches between anchors are aligned with a small
584
+ * global DP that also accepts near-miss spellings → script words left without
585
+ * a match are placed on the unmatched words heard between their matched
586
+ * neighbors, so a passage the speaker skipped collapses onto the next thing
587
+ * they said, and a long pause or an off-script aside stays with the slide
588
+ * already showing. Block
589
+ * ranges come from the same contiguous-range builder the acoustic aligner
590
+ * uses, with the silences between heard words as its breath gaps.
591
+ *
592
+ * Pure and deterministic, like the rest of the narration engine.
593
+ */
594
+
595
+ /** One recognized word, in seconds on the take's timeline. */
596
+ interface TranscriptWord {
597
+ text: string;
598
+ startSec: number;
599
+ endSec: number;
600
+ }
601
+ /** One recognized phrase. Without `words`, its time is shared out by syllables. */
602
+ interface TranscriptSegment {
603
+ text: string;
604
+ startSec: number;
605
+ endSec: number;
606
+ words?: readonly TranscriptWord[];
607
+ }
608
+ interface TranscriptAlignInput {
609
+ script: NarrationScript;
610
+ /** Recognized segments on the take's timeline (seconds from its start). */
611
+ segments: readonly TranscriptSegment[];
612
+ /** Take length in seconds (decoded-PCM length). */
613
+ durationSec: number;
614
+ }
615
+ interface TranscriptAlignment extends NarrationAlignment {
616
+ /** Spoken script words whose time came from a matched recognized word. */
617
+ matchedWords: number;
618
+ /** Spoken script words in total. */
619
+ spokenWords: number;
620
+ }
621
+ /** Lowercased letter/digit runs; apostrophes join ("you're" → "youre"). */
622
+ declare function transcriptWordKeys(text: string): string[];
623
+ declare function alignTranscript(input: TranscriptAlignInput, config?: Partial<AlignConfig>): TranscriptAlignment;
624
+
571
625
  /** Narration time range of one doc block at record time. */
572
626
  interface NarrationTimingBlock {
573
627
  /** Doc block id at record time (primary match key; ids can drift on heading edits). */
@@ -614,8 +668,11 @@ interface NarrationTimingJsonV3 {
614
668
  * - `'tts'` — a host synthesized the narration (text-to-speech). Block and
615
669
  * sentence boundaries are exact because each was synthesized separately;
616
670
  * word times are interpolated.
671
+ * - `'transcript-align'` — a speech recognizer transcribed the take and its
672
+ * timed words were matched to the script's words. Paraphrase and skipped
673
+ * passages are tolerated; unmatched words are interpolated.
617
674
  */
618
- type NarrationTimingMethod = 'dsp-align' | 'presenter-advance' | 'tts';
675
+ type NarrationTimingMethod = 'dsp-align' | 'presenter-advance' | 'tts' | 'transcript-align';
619
676
  /** Provenance recorded in a sidecar's `generator` field. */
620
677
  interface NarrationTimingGenerator {
621
678
  /** Producer id, e.g. `'squisq-teleprompter'` or a host's TTS engine name. */
@@ -747,4 +804,4 @@ interface AdvanceCoverage {
747
804
  /** Summarize an advance log against a doc. Pure; drives the review-panel warning. */
748
805
  declare function advanceCoverage(doc: Doc, advances: SlideAdvanceLog): AdvanceCoverage;
749
806
 
750
- export { type AdvanceCoverage, type AlignConfig, type AlignInput, type BandpassState, type BuildAdvanceTimingOptions, type BuildNarrationTimingOptions, type BuildScriptOptions, DEFAULT_ALIGN_CONFIG, DEFAULT_FEATURE_CONFIG, DEFAULT_NUCLEI_CONFIG, DEFAULT_PACING_CONFIG, DEFAULT_VAD_CONFIG, EMPTY_ADVANCE_LOG, type FeatureConfig, type FeatureState, type FrameFeatures, type NarrationAlignment, type NarrationBlockRange, type NarrationScript, type NarrationSessionConfig, type NarrationSessionState, type NarrationTimingBlock, type NarrationTimingGenerator, type NarrationTimingJsonV3, type NarrationTimingMethod, type NarrationTrace, type NucleiConfig, type NucleiState, type PacingConfig, type PacingState, type PacingTick, type ScriptBlockRange, type ScriptToken, type SlideAdvance, type SlideAdvanceLog, type TraceSample, type VadConfig, type VadState, type WordTiming, advanceCoverage, alignNarration, bandpassRun, buildAdvanceTimingJson, buildNarrationScript, buildNarrationTimingJson, createBandpass, createFeatureState, createNarrationSession, createNucleiState, createPacingState, createVadState, detectSyllableOnsets, downsampleTrace, estimateSyllables, expectedSyllablesAt, extractFrameFeatures, featureStep, narrationSessionStep, nucleiStep, pacingStep, parseNarrationTimingJson, reanchorPacing, reanchorSession, recordSlideShown, resolveNarrationTimingGenerator, traceWordPosAt, vadStep, wordIndexAtChar, wordIndexAtTime, wordPosAtExpectedSyllables };
807
+ export { type AdvanceCoverage, type AlignConfig, type AlignInput, type BandpassState, type BuildAdvanceTimingOptions, type BuildNarrationTimingOptions, type BuildScriptOptions, DEFAULT_ALIGN_CONFIG, DEFAULT_FEATURE_CONFIG, DEFAULT_NUCLEI_CONFIG, DEFAULT_PACING_CONFIG, DEFAULT_VAD_CONFIG, EMPTY_ADVANCE_LOG, type FeatureConfig, type FeatureState, type FrameFeatures, type NarrationAlignment, type NarrationBlockRange, type NarrationScript, type NarrationSessionConfig, type NarrationSessionState, type NarrationTimingBlock, type NarrationTimingGenerator, type NarrationTimingJsonV3, type NarrationTimingMethod, type NarrationTrace, type NucleiConfig, type NucleiState, type PacingConfig, type PacingState, type PacingTick, type ScriptBlockRange, type ScriptToken, type SlideAdvance, type SlideAdvanceLog, type TraceSample, type TranscriptAlignInput, type TranscriptAlignment, type TranscriptSegment, type TranscriptWord, type VadConfig, type VadState, type WordTiming, advanceCoverage, alignNarration, alignTranscript, bandpassRun, buildAdvanceTimingJson, buildNarrationScript, buildNarrationTimingJson, createBandpass, createFeatureState, createNarrationSession, createNucleiState, createPacingState, createVadState, detectSyllableOnsets, downsampleTrace, estimateSyllables, expectedSyllablesAt, extractFrameFeatures, featureStep, narrationSessionStep, nucleiStep, pacingStep, parseNarrationTimingJson, reanchorPacing, reanchorSession, recordSlideShown, resolveNarrationTimingGenerator, traceWordPosAt, transcriptWordKeys, vadStep, wordIndexAtChar, wordIndexAtTime, wordPosAtExpectedSyllables };
@@ -7,6 +7,7 @@ import {
7
7
  EMPTY_ADVANCE_LOG,
8
8
  advanceCoverage,
9
9
  alignNarration,
10
+ alignTranscript,
10
11
  bandpassRun,
11
12
  buildAdvanceTimingJson,
12
13
  createBandpass,
@@ -26,8 +27,9 @@ import {
26
27
  reanchorSession,
27
28
  recordSlideShown,
28
29
  traceWordPosAt,
30
+ transcriptWordKeys,
29
31
  vadStep
30
- } from "../chunk-FSZ6HB7S.js";
32
+ } from "../chunk-B2MOGSWY.js";
31
33
  import "../chunk-NEWNNLWN.js";
32
34
  import {
33
35
  buildNarrationTimingJson,
@@ -41,8 +43,8 @@ import {
41
43
  wordIndexAtChar,
42
44
  wordIndexAtTime,
43
45
  wordPosAtExpectedSyllables
44
- } from "../chunk-ZF4NWRZU.js";
45
- import "../chunk-CZHWFQ7C.js";
46
+ } from "../chunk-VQXOX3YN.js";
47
+ import "../chunk-CSR6VCSJ.js";
46
48
  import "../chunk-FUBMT6SJ.js";
47
49
  import "../chunk-WNEH3BZP.js";
48
50
  import "../chunk-L7QZORLB.js";
@@ -60,6 +62,7 @@ export {
60
62
  EMPTY_ADVANCE_LOG,
61
63
  advanceCoverage,
62
64
  alignNarration,
65
+ alignTranscript,
63
66
  bandpassRun,
64
67
  buildAdvanceTimingJson,
65
68
  buildNarrationScript,
@@ -85,6 +88,7 @@ export {
85
88
  recordSlideShown,
86
89
  resolveNarrationTimingGenerator,
87
90
  traceWordPosAt,
91
+ transcriptWordKeys,
88
92
  vadStep,
89
93
  wordIndexAtChar,
90
94
  wordIndexAtTime,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  profileBlockContents,
3
3
  recommendTemplatesForBlock
4
- } from "../chunk-CZHWFQ7C.js";
4
+ } from "../chunk-CSR6VCSJ.js";
5
5
  import "../chunk-L7QZORLB.js";
6
6
  import "../chunk-KVEOD57Z.js";
7
7
  import "../chunk-P53Z4RYC.js";
@@ -1,7 +1,7 @@
1
1
  import { R as Doc, s as ColorScheme, k as Block, bt as TemplateBlock } from '../Doc-BQUVDooL.js';
2
2
  import { i as PageTransformHints } from '../materializePageSection-_ftXlwvB.js';
3
3
  import { d as ExtractionType, E as ExtractedElement, b as ExtractionOptions } from '../contentExtractor-BNfVJV2U.js';
4
- import { f as BuildSpec } from '../types-Zk8qYuaT.js';
4
+ import { f as BuildSpec, M as MarkdownBlockNode } from '../types-Zk8qYuaT.js';
5
5
 
6
6
  /** Identifier for a built-in or caller-registered transform style. */
7
7
  type TransformStyleId = string;
@@ -336,6 +336,8 @@ interface PresentationBeat {
336
336
  headline: string;
337
337
  points: string[];
338
338
  imageSrc?: string;
339
+ /** The section's videos and diagrams shown beside this slide's text (`presentationMedia`). */
340
+ media?: string[];
339
341
  }
340
342
  interface PresentationPlan {
341
343
  version: 1;
@@ -347,6 +349,7 @@ declare const PRESENTATION_LIMITS: {
347
349
  readonly sourceCharacters: 40000;
348
350
  readonly beats: 80;
349
351
  readonly serializedCharacters: 160000;
352
+ readonly mediaPerBeat: 4;
350
353
  };
351
354
  declare function parsePresentationPlan(input: unknown): PresentationPlan | null;
352
355
  declare function serializePresentationPlan(plan: PresentationPlan): string;
@@ -400,4 +403,22 @@ declare function createPresentationPlan(doc: Doc, hints?: PresentationHints): Pr
400
403
  declare function compilePresentationPlan(doc: Doc, plan: PresentationPlan): Doc;
401
404
  declare function applySavedPresentation(doc: Doc): Doc;
402
405
 
403
- export { type AnalyzedBlock, type BlockImage, DEFAULT_TRANSFORM_STYLE_ID, DYNAMIC_PRESENTATION_STYLE, type NarratedSegmentInput, type NarratedSegmentResult, type NarratedSegmentTransformOptions, type NarratedSegmentVideo, PRESENTATION_HINTS_KEY, PRESENTATION_KEY, PRESENTATION_LAYOUTS, PRESENTATION_LIMITS, type PresentationBeat, type PresentationBlockHint, type PresentationHints, type PresentationLayout, type PresentationPlan, type TransformImage, type TransformOptions, type TransformResult, type TransformStyleConfig, type TransformStyleId, type TransformStyleInput, type TransformStyleRegistry, type TransformStyleSummary, allocateTiming, analyzeBlocks, applySavedPresentation, applyTransform, compilePresentationPlan, createPresentationPlan, createTransformStyleRegistry, defaultPresentationHints, extractDocImages, getTransformStyleIds, getTransformStyleSummaries, parsePresentationHints, parsePresentationPlan, presentationSections, resolveTransformStyle, serializePresentationHints, serializePresentationPlan, transformNarratedSegment, usesDynamicPresentation };
406
+ /** The videos and diagrams a narrated section embeds, which dynamic slides show
407
+ * beside its summaries. Images keep their own full-bleed slides; these are the
408
+ * visuals rich-media layout places next to slide text (`appendRichContentLayers`).
409
+ */
410
+
411
+ interface PresentationMediaItem {
412
+ /** `<section block id>#<index>`, stable while the section's visuals are unchanged. */
413
+ ref: string;
414
+ kind: 'video' | 'diagram';
415
+ /** The body node exactly as authored; slides render it through rich-media layout. */
416
+ node: MarkdownBlockNode;
417
+ /** A diagram, or an HTML video that can resume mid-clip on a following slide. */
418
+ continuous: boolean;
419
+ }
420
+ /** Each section's visuals in source order, claimed like images: a nested
421
+ * block's visuals belong to the nearest narrated section at or before it. */
422
+ declare function collectPresentationMedia(doc: Doc, sectionIds: string[]): Map<string, PresentationMediaItem[]>;
423
+
424
+ export { type AnalyzedBlock, type BlockImage, DEFAULT_TRANSFORM_STYLE_ID, DYNAMIC_PRESENTATION_STYLE, type NarratedSegmentInput, type NarratedSegmentResult, type NarratedSegmentTransformOptions, type NarratedSegmentVideo, PRESENTATION_HINTS_KEY, PRESENTATION_KEY, PRESENTATION_LAYOUTS, PRESENTATION_LIMITS, type PresentationBeat, type PresentationBlockHint, type PresentationHints, type PresentationLayout, type PresentationMediaItem, type PresentationPlan, type TransformImage, type TransformOptions, type TransformResult, type TransformStyleConfig, type TransformStyleId, type TransformStyleInput, type TransformStyleRegistry, type TransformStyleSummary, allocateTiming, analyzeBlocks, applySavedPresentation, applyTransform, collectPresentationMedia, compilePresentationPlan, createPresentationPlan, createTransformStyleRegistry, defaultPresentationHints, extractDocImages, getTransformStyleIds, getTransformStyleSummaries, parsePresentationHints, parsePresentationPlan, presentationSections, resolveTransformStyle, serializePresentationHints, serializePresentationPlan, transformNarratedSegment, usesDynamicPresentation };
@@ -9,6 +9,7 @@ import {
9
9
  analyzeBlocks,
10
10
  applySavedPresentation,
11
11
  applyTransform,
12
+ collectPresentationMedia,
12
13
  compilePresentationPlan,
13
14
  createPresentationPlan,
14
15
  createTransformStyleRegistry,
@@ -24,9 +25,9 @@ import {
24
25
  serializePresentationPlan,
25
26
  transformNarratedSegment,
26
27
  usesDynamicPresentation
27
- } from "../chunk-F5WTRHWL.js";
28
- import "../chunk-ZF4NWRZU.js";
29
- import "../chunk-CZHWFQ7C.js";
28
+ } from "../chunk-4O26IMVA.js";
29
+ import "../chunk-VQXOX3YN.js";
30
+ import "../chunk-CSR6VCSJ.js";
30
31
  import "../chunk-SVN7T6BK.js";
31
32
  import "../chunk-FUBMT6SJ.js";
32
33
  import "../chunk-WNEH3BZP.js";
@@ -49,6 +50,7 @@ export {
49
50
  analyzeBlocks,
50
51
  applySavedPresentation,
51
52
  applyTransform,
53
+ collectPresentationMedia,
52
54
  compilePresentationPlan,
53
55
  createPresentationPlan,
54
56
  createTransformStyleRegistry,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bendyline/squisq",
3
- "version": "2.13.5",
3
+ "version": "2.13.6",
4
4
  "description": "Headless utilities for doc/block rendering, spatial math, Markdown, and storage",
5
5
  "license": "MIT",
6
6
  "author": "Bendyline",