@bendyline/squisq 2.13.5 → 2.13.7

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.
Files changed (31) hide show
  1. package/dist/{Doc-BQUVDooL.d.ts → Doc-BCrrYAuK.d.ts} +1 -1
  2. package/dist/{ImageEditDoc-glA5UUau.d.ts → ImageEditDoc-CSuevovH.d.ts} +1 -1
  3. package/dist/{chunk-F5WTRHWL.js → chunk-CJYN3MTX.js} +151 -11
  4. package/dist/{chunk-FSZ6HB7S.js → chunk-DOKSRBW2.js} +331 -1
  5. package/dist/{chunk-KVEOD57Z.js → chunk-L3KSFLTP.js} +0 -29
  6. package/dist/{chunk-K75GTKSN.js → chunk-MWJSEKM3.js} +5 -5
  7. package/dist/{chunk-CZHWFQ7C.js → chunk-NGQ5TJVX.js} +2 -1
  8. package/dist/{chunk-FJNTG3VZ.js → chunk-S76W65BB.js} +2 -2
  9. package/dist/{chunk-ZF4NWRZU.js → chunk-TTJZXUQA.js} +35 -10
  10. package/dist/{chunk-CPD3MFX7.js → chunk-XDOSE5M3.js} +1 -1
  11. package/dist/doc/index.d.ts +5 -5
  12. package/dist/doc/index.js +6 -6
  13. package/dist/fence/index.d.ts +1 -1
  14. package/dist/generate/index.d.ts +1 -1
  15. package/dist/imageEdit/index.d.ts +3 -3
  16. package/dist/index.d.ts +6 -6
  17. package/dist/index.js +14 -8
  18. package/dist/jsonForm/index.d.ts +1 -1
  19. package/dist/markdown/index.d.ts +7 -3
  20. package/dist/markdown/index.js +3 -3
  21. package/dist/{materializePageSection-_ftXlwvB.d.ts → materializePageSection-FT1B5NOo.d.ts} +1 -1
  22. package/dist/mediaEdit/index.d.ts +2 -2
  23. package/dist/narration/index.d.ts +60 -3
  24. package/dist/narration/index.js +8 -4
  25. package/dist/recommend/index.js +2 -2
  26. package/dist/schemas/index.d.ts +4 -4
  27. package/dist/{themeLibrary-D6GK29l3.d.ts → themeLibrary-BWvtBQ42.d.ts} +1 -1
  28. package/dist/timing/index.d.ts +1 -1
  29. package/dist/transform/index.d.ts +25 -4
  30. package/dist/transform/index.js +7 -5
  31. package/package.json +2 -2
@@ -3026,7 +3026,7 @@ interface VideoLayer extends BaseLayer {
3026
3026
  };
3027
3027
  }
3028
3028
  /**
3029
- * Hosted video layer — the YouTube / Vimeo / Loom / Dailymotion / Wistia
3029
+ * Hosted video layer — the YouTube / Vimeo / Loom / Wistia
3030
3030
  * player for a paragraph that is only a link to the video's page.
3031
3031
  *
3032
3032
  * Carries the page URL, never a player URL: renderers re-derive the iframe
@@ -1,4 +1,4 @@
1
- import { a3 as ImageLayer, by as TextLayer, bj as ShapeLayer, aZ as PathLayer } from './Doc-BQUVDooL.js';
1
+ import { a3 as ImageLayer, by as TextLayer, bj as ShapeLayer, aZ as PathLayer } from './Doc-BCrrYAuK.js';
2
2
 
3
3
  /**
4
4
  * ImageEditDoc — JSON document describing a layered, editable image.
@@ -4,15 +4,21 @@ import {
4
4
  flattenRenderableBlocks,
5
5
  getPinnedBlockMeta,
6
6
  markdownToDoc
7
- } from "./chunk-ZF4NWRZU.js";
7
+ } from "./chunk-TTJZXUQA.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-NGQ5TJVX.js";
16
22
  import {
17
23
  extractContent,
18
24
  mapElementToBlock,
@@ -20,19 +26,22 @@ 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 {
26
33
  parseMarkdown
27
- } from "./chunk-CPD3MFX7.js";
34
+ } from "./chunk-XDOSE5M3.js";
28
35
  import {
29
36
  isContainerTemplate
30
37
  } from "./chunk-DOOUVVOM.js";
31
38
  import {
32
39
  extractPlainText,
40
+ findBlockVideoEmbed,
33
41
  getChildren,
42
+ parseTimeSeconds,
34
43
  stringifyHtmlNodes
35
- } from "./chunk-KVEOD57Z.js";
44
+ } from "./chunk-L3KSFLTP.js";
36
45
  import {
37
46
  parseBuildSpec
38
47
  } from "./chunk-P53Z4RYC.js";
@@ -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-TTJZXUQA.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,
@@ -933,7 +933,6 @@ var VIDEO_EMBED_PROVIDERS = [
933
933
  name: "Loom",
934
934
  example: "https://www.loom.com/share/0281766fa2d04bb788eaf19e65135184"
935
935
  },
936
- { id: "dailymotion", name: "Dailymotion", example: "https://www.dailymotion.com/video/x7tgad0" },
937
936
  { id: "wistia", name: "Wistia", example: "https://home.wistia.com/medias/e4a27b971d" }
938
937
  ];
939
938
  var PROVIDER_NAMES = Object.fromEntries(
@@ -1121,33 +1120,6 @@ function parseLoom(url) {
1121
1120
  watchUrl: `https://www.loom.com/share/${id}`
1122
1121
  });
1123
1122
  }
1124
- var DAILYMOTION_ID_RE = /^[A-Za-z0-9]{5,20}$/;
1125
- function parseDailymotion(url) {
1126
- const host = baseHost(url);
1127
- const segments = pathSegments(url);
1128
- let raw = null;
1129
- if (host === "dai.ly") raw = segments[0] ?? null;
1130
- else if (host === "dailymotion.com") {
1131
- if (segments[0] === "video") raw = segments[1] ?? null;
1132
- else if (segments[0] === "embed" && segments[1] === "video") raw = segments[2] ?? null;
1133
- else return null;
1134
- } else if (host === "geo.dailymotion.com") {
1135
- raw = url.searchParams.get("video");
1136
- } else {
1137
- return null;
1138
- }
1139
- const id = raw?.split("_")[0];
1140
- if (!id || !DAILYMOTION_ID_RE.test(id)) return null;
1141
- const startSeconds = parseVideoStartTime(url.searchParams.get("start"));
1142
- const start = startSeconds ? String(startSeconds) : void 0;
1143
- return build("dailymotion", {
1144
- id,
1145
- startSeconds,
1146
- embedUrl: withQuery(`https://www.dailymotion.com/embed/video/${id}`, [["start", start]]),
1147
- watchUrl: withQuery(`https://www.dailymotion.com/video/${id}`, [["start", start]]),
1148
- thumbnailUrl: `https://www.dailymotion.com/thumbnail/video/${id}`
1149
- });
1150
- }
1151
1123
  var WISTIA_ID_RE = /^[a-z0-9]{10}$/;
1152
1124
  var WISTIA_ACCOUNT_HOST_RE = /^[a-z0-9-]{1,63}\.wistia\.com$/;
1153
1125
  function parseWistia(url) {
@@ -1175,7 +1147,6 @@ var PARSERS = [
1175
1147
  parseYouTube,
1176
1148
  parseVimeo,
1177
1149
  parseLoom,
1178
- parseDailymotion,
1179
1150
  parseWistia
1180
1151
  ];
1181
1152
  function parseVideoEmbedUrl(input) {