@bendyline/squisq 2.13.4 → 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.
- package/dist/{Doc-DnRFE0EI.d.ts → Doc-BQUVDooL.d.ts} +9 -0
- package/dist/{ImageEditDoc-CadNz387.d.ts → ImageEditDoc-glA5UUau.d.ts} +1 -1
- package/dist/{chunk-DRHJD6U4.js → chunk-4O26IMVA.js} +151 -11
- package/dist/{chunk-FIPRGGXD.js → chunk-B2MOGSWY.js} +331 -1
- package/dist/{chunk-D3JQMZUT.js → chunk-CPD3MFX7.js} +1 -1
- package/dist/{chunk-POZCKPCO.js → chunk-CSR6VCSJ.js} +2 -1
- package/dist/{chunk-BITSNRJH.js → chunk-FGN4OZVK.js} +5 -5
- package/dist/{chunk-JFJZ4NS6.js → chunk-FJNTG3VZ.js} +2 -2
- package/dist/{chunk-6NWVLLMI.js → chunk-KVEOD57Z.js} +5 -0
- package/dist/{chunk-ISVJ42I3.js → chunk-SG3HXPMF.js} +3 -0
- package/dist/{chunk-J2NFT7GH.js → chunk-VQXOX3YN.js} +174 -39
- package/dist/doc/index.d.ts +10 -6
- package/dist/doc/index.js +6 -6
- package/dist/fence/index.d.ts +1 -1
- package/dist/generate/index.d.ts +1 -1
- package/dist/imageEdit/index.d.ts +3 -3
- package/dist/index.d.ts +7 -7
- package/dist/index.js +17 -9
- package/dist/jsonForm/index.d.ts +1 -1
- package/dist/jsonForm/index.js +1 -1
- package/dist/markdown/index.d.ts +6 -1
- package/dist/markdown/index.js +5 -3
- package/dist/{materializePageSection-D84uA6Vk.d.ts → materializePageSection-_ftXlwvB.d.ts} +1 -1
- package/dist/mediaEdit/index.d.ts +2 -2
- package/dist/narration/index.d.ts +60 -3
- package/dist/narration/index.js +8 -4
- package/dist/recommend/index.js +2 -2
- package/dist/schemas/index.d.ts +4 -4
- package/dist/schemas/index.js +1 -1
- package/dist/{themeLibrary-in0fafp4.d.ts → themeLibrary-D6GK29l3.d.ts} +1 -1
- package/dist/timing/index.d.ts +1 -1
- package/dist/transform/index.d.ts +25 -4
- package/dist/transform/index.js +7 -5
- package/package.json +1 -1
|
@@ -2348,6 +2348,15 @@ interface StartBlockConfig {
|
|
|
2348
2348
|
* (so a square crop, such as a social link card, shows all of it).
|
|
2349
2349
|
*/
|
|
2350
2350
|
coverStyle?: 'scrim' | 'frosted';
|
|
2351
|
+
/**
|
|
2352
|
+
* Where a frosted cover puts its subtitle. `'stacked'` (default): under the title, on the
|
|
2353
|
+
* title's plate. `'split'`: the title on a plate near the top of the frame and the
|
|
2354
|
+
* subtitle, set as a display deck, on a plate of its own near the bottom — a vertical
|
|
2355
|
+
* cover's name and its hook. Both plates stay inside the middle 3:4 that feeds crop a
|
|
2356
|
+
* 9:16 frame to, with even room above the title and below the subtitle. A split that would crowd the frame (two plates closer than half
|
|
2357
|
+
* the title's size) stacks instead. No effect without a subtitle or with the scrim style.
|
|
2358
|
+
*/
|
|
2359
|
+
coverLayout?: 'stacked' | 'split';
|
|
2351
2360
|
}
|
|
2352
2361
|
/**
|
|
2353
2362
|
* A structural problem detected while converting or validating a document.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a3 as ImageLayer, by as TextLayer, bj as ShapeLayer, aZ as PathLayer } from './Doc-
|
|
1
|
+
import { a3 as ImageLayer, by as TextLayer, bj as ShapeLayer, aZ as PathLayer } from './Doc-BQUVDooL.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-
|
|
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-
|
|
21
|
+
} from "./chunk-CSR6VCSJ.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-
|
|
34
|
+
} from "./chunk-CPD3MFX7.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-
|
|
44
|
+
} from "./chunk-KVEOD57Z.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 =
|
|
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 {
|
|
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-
|
|
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,
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
splitKeyValueToken,
|
|
17
17
|
tokenizeAttrTokens,
|
|
18
18
|
walkMarkdownTree
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-KVEOD57Z.js";
|
|
20
20
|
|
|
21
21
|
// src/doc/asciiDiagram/chars.ts
|
|
22
22
|
function toCells(s) {
|
|
@@ -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,
|