@bendyline/squisq 2.11.4 → 2.11.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.
Files changed (34) hide show
  1. package/dist/chunk-44BJ42GT.js +165 -0
  2. package/dist/chunk-764WHWKM.js +332 -0
  3. package/dist/{chunk-XV4SF2N5.js → chunk-BUJ5RV2P.js} +1 -1
  4. package/dist/{chunk-PJRVODWW.js → chunk-C3NAY5YP.js} +7 -2
  5. package/dist/{chunk-67L6WRJN.js → chunk-GHOPHGOK.js} +28 -1
  6. package/dist/{chunk-NV7LNKS2.js → chunk-GRGLZF4A.js} +18 -30
  7. package/dist/{chunk-ZSU5DJLX.js → chunk-J2IPSQKP.js} +104 -4
  8. package/dist/{chunk-NKNMGIIN.js → chunk-SVN7T6BK.js} +0 -3
  9. package/dist/{chunk-4UNTPQ4J.js → chunk-UUI35INI.js} +4 -2
  10. package/dist/{chunk-DQRELU4S.js → chunk-VU4T74LI.js} +1 -1
  11. package/dist/{chunk-LI547QHN.js → chunk-WAGJK36Q.js} +19 -346
  12. package/dist/{chunk-RXYKU6EX.js → chunk-Y4D6TDZT.js} +212 -3
  13. package/dist/chunk-ZP5F7LUH.js +223 -0
  14. package/dist/doc/index.js +9 -11
  15. package/dist/generate/index.d.ts +61 -1
  16. package/dist/generate/index.js +14 -1
  17. package/dist/index.d.ts +5 -4
  18. package/dist/index.js +57 -39
  19. package/dist/jsonForm/index.js +3 -4
  20. package/dist/narration/index.js +6 -7
  21. package/dist/proof/index.d.ts +3 -45
  22. package/dist/recommend/index.d.ts +6 -0
  23. package/dist/recommend/index.js +1 -1
  24. package/dist/review/index.d.ts +141 -0
  25. package/dist/review/index.js +29 -0
  26. package/dist/schemas/index.js +17 -19
  27. package/dist/timing/index.d.ts +29 -1
  28. package/dist/timing/index.js +6 -4
  29. package/dist/transform/index.d.ts +63 -2
  30. package/dist/transform/index.js +17 -6
  31. package/dist/types-BQWu0gzi.d.ts +46 -0
  32. package/package.json +6 -1
  33. package/dist/chunk-BAOV476U.js +0 -214
  34. package/dist/chunk-XOFT65EX.js +0 -34
@@ -0,0 +1,165 @@
1
+ import {
2
+ estimateTimeFromText
3
+ } from "./chunk-GHOPHGOK.js";
4
+
5
+ // src/timing/captions.ts
6
+ var DEFAULT_VERSION = 1;
7
+ var DEFAULT_TARGET_WORDS = 10;
8
+ var DEFAULT_MIN_DURATION = 1.5;
9
+ var DEFAULT_MAX_DURATION = 6;
10
+ var DEFAULT_WORDS_PER_SECOND = 2.5;
11
+ function phrasesFromTiming(source, segmentIndex, targetWords) {
12
+ const { segment, timing } = source;
13
+ if (!timing || timing.bookmarks.length === 0) return [];
14
+ const segmentEnd = segment.startTime + segment.duration;
15
+ const phrases = [];
16
+ let words = [];
17
+ let phraseText = "";
18
+ for (let index = 0; index < timing.bookmarks.length; index += 1) {
19
+ const bookmark = timing.bookmarks[index];
20
+ const text = bookmark.textFragment?.trim() ?? "";
21
+ if (!text) continue;
22
+ const next = timing.bookmarks[index + 1];
23
+ const startTime = Math.min(
24
+ segmentEnd,
25
+ Math.max(segment.startTime, segment.startTime + bookmark.time)
26
+ );
27
+ const endTime = Math.min(
28
+ segmentEnd,
29
+ Math.max(startTime, segment.startTime + (next?.time ?? timing.duration))
30
+ );
31
+ const word = {
32
+ text,
33
+ startTime,
34
+ endTime
35
+ };
36
+ words.push(word);
37
+ phraseText += `${phraseText ? " " : ""}${text}`;
38
+ const flush = /[.!?]$/.test(text) && words.length >= 4 || /[,;:]$/.test(text) && words.length >= targetWords - 2 || words.length >= targetWords + 3 || index === timing.bookmarks.length - 1;
39
+ if (flush) {
40
+ phrases.push({
41
+ text: phraseText,
42
+ startTime: words[0].startTime,
43
+ endTime: words[words.length - 1].endTime,
44
+ audioSegment: segmentIndex,
45
+ words
46
+ });
47
+ words = [];
48
+ phraseText = "";
49
+ }
50
+ }
51
+ return phrases;
52
+ }
53
+ function phrasesFromText(source, segmentIndex, options) {
54
+ const { segment } = source;
55
+ const sourceText = source.text?.trim();
56
+ if (!sourceText) return [];
57
+ const prefix = source.spokenPrefix ?? "";
58
+ const prefixWords = prefix.split(/\s+/).filter(Boolean).length;
59
+ const prefixDuration = Math.min(
60
+ segment.duration,
61
+ prefixWords / options.wordsPerSecond + (prefix.match(/\n\n/g)?.length ?? 0) * 0.5
62
+ );
63
+ const contentDuration = Math.max(0, segment.duration - prefixDuration);
64
+ const phrases = [];
65
+ const prefixText = prefix.replace(/\n+/g, " ").trim();
66
+ if (prefixText && prefixDuration > 0.5) {
67
+ phrases.push({
68
+ text: prefixText,
69
+ startTime: segment.startTime,
70
+ endTime: Math.min(segment.startTime + segment.duration, segment.startTime + prefixDuration),
71
+ audioSegment: segmentIndex
72
+ });
73
+ }
74
+ const sentences = sourceText.replace(/\n+/g, " ").split(/(?<=[.!?])\s+/).map((sentence) => sentence.trim()).filter(Boolean);
75
+ let phraseText = "";
76
+ let phraseWordCount = 0;
77
+ let phraseStartOffset = 0;
78
+ let currentOffset = 0;
79
+ const flush = () => {
80
+ if (!phraseText) return;
81
+ const startTime = segment.startTime + prefixDuration + estimateTimeFromText(sourceText, phraseStartOffset, contentDuration);
82
+ const estimatedEnd = segment.startTime + prefixDuration + estimateTimeFromText(sourceText, currentOffset, contentDuration);
83
+ phrases.push({
84
+ text: phraseText,
85
+ startTime,
86
+ endTime: Math.min(
87
+ segment.startTime + segment.duration,
88
+ Math.min(
89
+ Math.max(startTime + options.minPhraseDuration, estimatedEnd),
90
+ startTime + options.maxPhraseDuration
91
+ )
92
+ ),
93
+ audioSegment: segmentIndex
94
+ });
95
+ phraseText = "";
96
+ phraseWordCount = 0;
97
+ phraseStartOffset = currentOffset;
98
+ };
99
+ for (const sentence of sentences) {
100
+ const sentenceWords = sentence.split(/\s+/).filter(Boolean).length;
101
+ if (phraseWordCount > 0 && phraseWordCount + sentenceWords > options.targetWordsPerPhrase * 1.5) {
102
+ flush();
103
+ }
104
+ phraseText += `${phraseText ? " " : ""}${sentence}`;
105
+ phraseWordCount += sentenceWords;
106
+ currentOffset += sentence.length + 1;
107
+ if (phraseWordCount >= options.targetWordsPerPhrase) flush();
108
+ }
109
+ flush();
110
+ return phrases;
111
+ }
112
+ function buildCaptionTrack(sources, options = {}) {
113
+ const targetWordsPerPhrase = options.targetWordsPerPhrase ?? DEFAULT_TARGET_WORDS;
114
+ const textOptions = {
115
+ targetWordsPerPhrase,
116
+ minPhraseDuration: options.minPhraseDuration ?? DEFAULT_MIN_DURATION,
117
+ maxPhraseDuration: options.maxPhraseDuration ?? DEFAULT_MAX_DURATION,
118
+ wordsPerSecond: options.wordsPerSecond ?? DEFAULT_WORDS_PER_SECOND
119
+ };
120
+ if (!Number.isInteger(targetWordsPerPhrase) || targetWordsPerPhrase < 1) {
121
+ throw new Error("targetWordsPerPhrase must be a positive integer");
122
+ }
123
+ if (!Number.isFinite(textOptions.wordsPerSecond) || textOptions.wordsPerSecond <= 0) {
124
+ throw new Error("wordsPerSecond must be a positive finite number");
125
+ }
126
+ if (!Number.isFinite(textOptions.minPhraseDuration) || !Number.isFinite(textOptions.maxPhraseDuration) || textOptions.minPhraseDuration < 0 || textOptions.maxPhraseDuration < textOptions.minPhraseDuration) {
127
+ throw new Error(
128
+ "caption phrase durations must be finite with max greater than or equal to min"
129
+ );
130
+ }
131
+ const phrases = sources.flatMap((source, segmentIndex) => {
132
+ const timed = phrasesFromTiming(source, segmentIndex, targetWordsPerPhrase);
133
+ return timed.length > 0 ? timed : phrasesFromText(source, segmentIndex, textOptions);
134
+ });
135
+ if (options.closePhraseGaps ?? true) {
136
+ for (let index = 0; index < phrases.length - 1; index += 1) {
137
+ if (phrases[index].audioSegment === phrases[index + 1].audioSegment) {
138
+ phrases[index].endTime = Math.max(phrases[index].startTime, phrases[index + 1].startTime);
139
+ }
140
+ }
141
+ for (let segmentIndex = 0; segmentIndex < sources.length; segmentIndex += 1) {
142
+ const segment = sources[segmentIndex].segment;
143
+ let last;
144
+ for (let index = phrases.length - 1; index >= 0; index -= 1) {
145
+ if (phrases[index].audioSegment === segmentIndex) {
146
+ last = phrases[index];
147
+ break;
148
+ }
149
+ }
150
+ if (last) {
151
+ const segmentEnd = segment.startTime + segment.duration;
152
+ last.endTime = Math.min(segmentEnd, Math.max(last.endTime, segmentEnd - 0.05));
153
+ }
154
+ }
155
+ }
156
+ return {
157
+ phrases,
158
+ ...options.generatedAt ? { generatedAt: options.generatedAt } : {},
159
+ version: options.version ?? DEFAULT_VERSION
160
+ };
161
+ }
162
+
163
+ export {
164
+ buildCaptionTrack
165
+ };
@@ -0,0 +1,332 @@
1
+ import {
2
+ flattenRenderableBlocks,
3
+ getBlockBodyText
4
+ } from "./chunk-WAGJK36Q.js";
5
+ import {
6
+ estimateSpokenWordCount
7
+ } from "./chunk-GHOPHGOK.js";
8
+
9
+ // src/narration/syllables.ts
10
+ var BOUNDARY_PUNCT = /^[.,!?;:'"()[\]{}]+|[.,!?;:'"()[\]{}]+$/g;
11
+ var SYLLABLES_PER_SPOKEN_WORD = 1.4;
12
+ function estimateSyllables(token) {
13
+ const stripped = token.replace(BOUNDARY_PUNCT, "");
14
+ if (!stripped) return 1;
15
+ if (/^[-+]?[\d.,]+$/.test(stripped)) {
16
+ const num = parseFloat(stripped.replace(/,/g, ""));
17
+ if (!Number.isNaN(num) && Number.isFinite(num)) {
18
+ return Math.max(1, Math.round(estimateSpokenWordCount(stripped) * SYLLABLES_PER_SPOKEN_WORD));
19
+ }
20
+ }
21
+ if (/^[A-Z]{2,5}$/.test(stripped) && !/[AEIOUY]/.test(stripped)) {
22
+ return stripped.length;
23
+ }
24
+ const parts = stripped.toLowerCase().split(/[^a-z']+/).filter((p) => p.length > 0);
25
+ if (parts.length === 0) return 1;
26
+ let total = 0;
27
+ for (const part of parts) {
28
+ total += countAlphaSyllables(part.replace(/'/g, ""));
29
+ }
30
+ return Math.max(1, total);
31
+ }
32
+ function countAlphaSyllables(word) {
33
+ if (!word) return 0;
34
+ const groups = word.match(/[aeiouy]+/g);
35
+ let count = groups ? groups.length : 0;
36
+ if (count === 0) return 1;
37
+ if (count > 1 && /[^aeiouy]e$/.test(word)) {
38
+ count -= 1;
39
+ }
40
+ if (count > 1 && /[^aeiouy]ed$/.test(word) && !/[td]ed$/.test(word)) {
41
+ count -= 1;
42
+ }
43
+ if (count > 1 && /[^aeiouy]es$/.test(word) && !/(?:[sxz]|[cs]h)es$/.test(word)) {
44
+ count -= 1;
45
+ }
46
+ if (/[^aeiouy]le$/.test(word)) {
47
+ count += 1;
48
+ }
49
+ return Math.max(1, count);
50
+ }
51
+
52
+ // src/narration/script.ts
53
+ function blockSpokenText(block, includeTitles) {
54
+ const title = includeTitles && block.title ? block.title.trim() : "";
55
+ const body = getBlockBodyText(block);
56
+ if (title && body) return `${title}
57
+ ${body}`;
58
+ return title || body;
59
+ }
60
+ function buildNarrationScript(doc, options) {
61
+ const includeTitles = options?.includeTitles ?? true;
62
+ const separator = options?.blockSeparator ?? "\n\n";
63
+ const pieces = [];
64
+ for (const block of flattenRenderableBlocks(doc.blocks)) {
65
+ const text = blockSpokenText(block, includeTitles);
66
+ if (!text) continue;
67
+ pieces.push({
68
+ blockId: block.id,
69
+ heading: block.title || void 0,
70
+ text
71
+ });
72
+ }
73
+ let sourceText = "";
74
+ const blocks = [];
75
+ for (let i = 0; i < pieces.length; i++) {
76
+ if (i > 0) sourceText += separator;
77
+ const charStart = sourceText.length;
78
+ sourceText += pieces[i].text;
79
+ blocks.push({
80
+ blockId: pieces[i].blockId,
81
+ ...pieces[i].heading !== void 0 ? { heading: pieces[i].heading } : {},
82
+ tokenStart: 0,
83
+ tokenEnd: 0,
84
+ charStart,
85
+ charEnd: sourceText.length
86
+ });
87
+ }
88
+ const tokens = [];
89
+ const tokenRe = /\S+/g;
90
+ let blockIndex = 0;
91
+ let match;
92
+ while ((match = tokenRe.exec(sourceText)) !== null) {
93
+ const charOffset = match.index;
94
+ const charEnd = charOffset + match[0].length;
95
+ while (blockIndex < blocks.length - 1 && charOffset >= blocks[blockIndex].charEnd) {
96
+ blockIndex++;
97
+ }
98
+ const spoken = hasSpokenContent(match[0]);
99
+ tokens.push({
100
+ text: match[0],
101
+ charOffset,
102
+ charEnd,
103
+ blockId: blocks[blockIndex]?.blockId ?? "",
104
+ blockIndex,
105
+ syllables: spoken ? estimateSyllables(match[0]) : 0,
106
+ spokenWordEquiv: spoken ? estimateSpokenWordCount(stripBoundaryPunct(match[0])) : 0,
107
+ pauseAfter: 0,
108
+ spoken
109
+ });
110
+ }
111
+ for (const range of blocks) {
112
+ range.tokenStart = tokens.length;
113
+ range.tokenEnd = 0;
114
+ }
115
+ for (let i = 0; i < tokens.length; i++) {
116
+ const range = blocks[tokens[i].blockIndex];
117
+ if (range) {
118
+ if (i < range.tokenStart) range.tokenStart = i;
119
+ range.tokenEnd = i + 1;
120
+ }
121
+ }
122
+ for (const range of blocks) {
123
+ if (range.tokenStart > range.tokenEnd) {
124
+ range.tokenStart = range.tokenEnd;
125
+ }
126
+ }
127
+ for (let i = 0; i < tokens.length; i++) {
128
+ const token = tokens[i];
129
+ const next = tokens[i + 1];
130
+ if (!next || next.blockIndex !== token.blockIndex) {
131
+ token.pauseAfter = 3;
132
+ } else if (sourceText.slice(token.charEnd, next.charOffset).includes("\n")) {
133
+ token.pauseAfter = 2;
134
+ } else if (endsWithClausePunctuation(token.text)) {
135
+ token.pauseAfter = 1;
136
+ }
137
+ }
138
+ const cumulativeSyllables = new Array(tokens.length + 1);
139
+ cumulativeSyllables[0] = 0;
140
+ for (let i = 0; i < tokens.length; i++) {
141
+ cumulativeSyllables[i + 1] = cumulativeSyllables[i] + tokens[i].syllables;
142
+ }
143
+ return {
144
+ sourceText,
145
+ tokens,
146
+ blocks,
147
+ totalSyllables: cumulativeSyllables[tokens.length],
148
+ cumulativeSyllables
149
+ };
150
+ }
151
+ var TRAILING_CLOSERS = /["')\]}»”’]+$/;
152
+ function hasSpokenContent(token) {
153
+ return /[\p{L}\p{N}]/u.test(token);
154
+ }
155
+ function stripBoundaryPunct(token) {
156
+ return token.replace(/^[.,!?;:'"()[\]{}]+|[.,!?;:'"()[\]{}]+$/g, "");
157
+ }
158
+ function endsWithClausePunctuation(token) {
159
+ const bare = token.replace(TRAILING_CLOSERS, "");
160
+ const last = bare.charAt(bare.length - 1);
161
+ if (!last) return false;
162
+ if (last === ",") {
163
+ return true;
164
+ }
165
+ return last === "." || last === ";" || last === ":" || last === "!" || last === "?";
166
+ }
167
+ function clamp(value, min, max) {
168
+ return value < min ? min : value > max ? max : value;
169
+ }
170
+ function expectedSyllablesAt(script, wordPos) {
171
+ const n = script.tokens.length;
172
+ if (n === 0) return 0;
173
+ const pos = clamp(wordPos, 0, n);
174
+ const idx = Math.floor(pos);
175
+ if (idx >= n) return script.totalSyllables;
176
+ return script.cumulativeSyllables[idx] + (pos - idx) * script.tokens[idx].syllables;
177
+ }
178
+ function wordPosAtExpectedSyllables(script, syllables) {
179
+ const n = script.tokens.length;
180
+ if (n === 0) return 0;
181
+ const target = clamp(syllables, 0, script.totalSyllables);
182
+ let lo = 0;
183
+ let hi = n;
184
+ while (lo < hi) {
185
+ const mid = lo + hi + 1 >> 1;
186
+ if (script.cumulativeSyllables[mid] <= target) lo = mid;
187
+ else hi = mid - 1;
188
+ }
189
+ if (lo >= n) return n;
190
+ const within = target - script.cumulativeSyllables[lo];
191
+ const sylls = script.tokens[lo].syllables;
192
+ return lo + (sylls > 0 ? within / sylls : 0);
193
+ }
194
+ function wordIndexAtChar(script, charOffset) {
195
+ const tokens = script.tokens;
196
+ if (tokens.length === 0) return 0;
197
+ let lo = 0;
198
+ let hi = tokens.length - 1;
199
+ while (lo < hi) {
200
+ const mid = lo + hi >> 1;
201
+ if (tokens[mid].charEnd <= charOffset) lo = mid + 1;
202
+ else hi = mid;
203
+ }
204
+ return lo;
205
+ }
206
+ function wordIndexAtTime(words, tSec) {
207
+ if (words.length === 0) return 0;
208
+ let lo = 0;
209
+ let hi = words.length - 1;
210
+ while (lo < hi) {
211
+ const mid = lo + hi + 1 >> 1;
212
+ if (words[mid].tSec <= tSec) lo = mid;
213
+ else hi = mid - 1;
214
+ }
215
+ return words[lo].tSec <= tSec ? lo : 0;
216
+ }
217
+
218
+ // src/narration/sidecar.ts
219
+ function buildNarrationTimingJson(script, alignment, durationSec, options) {
220
+ const bookmarks = alignment.words.map((word) => {
221
+ const token = script.tokens[word.tokenIndex];
222
+ return {
223
+ id: `word-${word.tokenIndex}`,
224
+ time: word.tSec,
225
+ charOffset: token?.charOffset ?? 0,
226
+ textFragment: token?.text ?? ""
227
+ };
228
+ });
229
+ const blocks = alignment.blocks.map((range) => ({
230
+ ...range.blockId ? { blockId: range.blockId } : {},
231
+ ...range.heading !== void 0 ? { heading: range.heading } : {},
232
+ blockIndex: range.blockIndex,
233
+ charStart: range.charStart,
234
+ charEnd: range.charEnd,
235
+ startSec: range.startSec,
236
+ endSec: range.endSec
237
+ }));
238
+ return {
239
+ version: 3,
240
+ sourceText: script.sourceText,
241
+ duration: Number.isFinite(durationSec) && durationSec >= 0 ? durationSec : 0,
242
+ bookmarks,
243
+ blocks,
244
+ // Write only what `parseNarrationTimingJson` will read back, so the codec
245
+ // is a round-trip pair. A non-finite value would serialize to `null`
246
+ // (JSON has no NaN) and be dropped on parse anyway — omitting it here
247
+ // keeps the emitted sidecar honest rather than carrying a dead field.
248
+ ...finiteNumber(options?.cameraOffsetSec) ? { cameraOffsetSec: options.cameraOffsetSec } : {},
249
+ generator: {
250
+ name: "squisq-teleprompter",
251
+ method: "dsp-align",
252
+ ...options?.baseWpm !== void 0 ? { baseWpm: options.baseWpm } : {}
253
+ }
254
+ };
255
+ }
256
+ function finiteNonNegative(value) {
257
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
258
+ }
259
+ function finiteNumber(value) {
260
+ return typeof value === "number" && Number.isFinite(value);
261
+ }
262
+ function parseNarrationTimingJson(data) {
263
+ try {
264
+ const text = typeof data === "string" ? data : new TextDecoder().decode(data instanceof Uint8Array ? data : new Uint8Array(data));
265
+ const parsed = JSON.parse(text);
266
+ if (typeof parsed !== "object" || parsed === null) return null;
267
+ const raw = parsed;
268
+ if (typeof raw.sourceText !== "string" || !finiteNonNegative(raw.duration)) return null;
269
+ const bookmarks = Array.isArray(raw.bookmarks) ? raw.bookmarks.flatMap((b) => {
270
+ if (typeof b !== "object" || b === null) return [];
271
+ const bookmark = b;
272
+ if (!finiteNonNegative(bookmark.time) || !finiteNonNegative(bookmark.charOffset)) {
273
+ return [];
274
+ }
275
+ return [
276
+ {
277
+ id: typeof bookmark.id === "string" ? bookmark.id : `bookmark-${bookmark.charOffset}`,
278
+ time: bookmark.time,
279
+ charOffset: bookmark.charOffset,
280
+ ...typeof bookmark.textFragment === "string" ? { textFragment: bookmark.textFragment } : {}
281
+ }
282
+ ];
283
+ }) : [];
284
+ let blocks = Array.isArray(raw.blocks) ? raw.blocks.flatMap((b) => {
285
+ if (typeof b !== "object" || b === null) return [];
286
+ const block = b;
287
+ if (!finiteNonNegative(block.startSec) || !finiteNonNegative(block.endSec) || !finiteNonNegative(block.charStart) || !finiteNonNegative(block.charEnd)) {
288
+ return [];
289
+ }
290
+ return [
291
+ {
292
+ ...typeof block.blockId === "string" ? { blockId: block.blockId } : {},
293
+ ...typeof block.heading === "string" ? { heading: block.heading } : {},
294
+ blockIndex: finiteNonNegative(block.blockIndex) ? block.blockIndex : 0,
295
+ charStart: block.charStart,
296
+ charEnd: block.charEnd,
297
+ startSec: block.startSec,
298
+ endSec: Math.max(block.startSec, block.endSec)
299
+ }
300
+ ];
301
+ }) : [];
302
+ blocks = blocks.sort((a, b) => a.startSec - b.startSec);
303
+ let cursor = 0;
304
+ for (const block of blocks) {
305
+ if (block.startSec < cursor) block.startSec = cursor;
306
+ if (block.endSec < block.startSec) block.endSec = block.startSec;
307
+ cursor = block.startSec;
308
+ }
309
+ return {
310
+ version: 3,
311
+ sourceText: raw.sourceText,
312
+ duration: raw.duration,
313
+ bookmarks,
314
+ blocks,
315
+ ...finiteNumber(raw.cameraOffsetSec) ? { cameraOffsetSec: raw.cameraOffsetSec } : {},
316
+ ...typeof raw.generator === "object" && raw.generator !== null ? { generator: raw.generator } : {}
317
+ };
318
+ } catch {
319
+ return null;
320
+ }
321
+ }
322
+
323
+ export {
324
+ estimateSyllables,
325
+ buildNarrationScript,
326
+ expectedSyllablesAt,
327
+ wordPosAtExpectedSyllables,
328
+ wordIndexAtChar,
329
+ wordIndexAtTime,
330
+ buildNarrationTimingJson,
331
+ parseNarrationTimingJson
332
+ };
@@ -15,7 +15,7 @@ import {
15
15
  relativeLuminance,
16
16
  resolveFontFamily,
17
17
  withAlpha
18
- } from "./chunk-RXYKU6EX.js";
18
+ } from "./chunk-Y4D6TDZT.js";
19
19
 
20
20
  // src/schemas/Doc.ts
21
21
  function calculateDuration(audio) {
@@ -2470,7 +2470,8 @@ var EMPTY_PROFILE = Object.freeze({
2470
2470
  hasTable: false,
2471
2471
  hasDate: false,
2472
2472
  hasNumberHighlight: false,
2473
- wordCount: 0
2473
+ wordCount: 0,
2474
+ hasCompoundBody: false
2474
2475
  });
2475
2476
  var DATE_PATTERNS = [
2476
2477
  /\b(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{1,2},?\s+)?\d{4}\b/i,
@@ -2596,13 +2597,15 @@ function profileBlockContents(nodes) {
2596
2597
  hasDate,
2597
2598
  hasNumberHighlight,
2598
2599
  wordCount,
2600
+ hasCompoundBody: nodes.length > 1 && nodes.some((node) => node.type !== "paragraph"),
2599
2601
  hasAsciiDiagram,
2600
2602
  hasTimeline,
2601
2603
  hasTree
2602
2604
  };
2603
2605
  }
2604
- var UNIVERSAL_DEFAULTS = ["title", "sectionHeader", "factCard", "twoColumn"];
2606
+ var UNIVERSAL_DEFAULTS = ["title", "sectionHeader", "content", "factCard", "twoColumn"];
2605
2607
  function recommendedNamesForProfile(profile) {
2608
+ if (profile.hasCompoundBody) return ["content"];
2606
2609
  const names = /* @__PURE__ */ new Set();
2607
2610
  let anyContentSignal = false;
2608
2611
  if (profile.hasAsciiDiagram) {
@@ -2659,12 +2662,14 @@ function recommendedNamesForProfile(profile) {
2659
2662
  if (anyContentSignal) {
2660
2663
  names.add("title");
2661
2664
  names.add("sectionHeader");
2665
+ names.add("content");
2662
2666
  } else {
2663
2667
  for (const n of UNIVERSAL_DEFAULTS) names.add(n);
2664
2668
  }
2665
2669
  return Array.from(names);
2666
2670
  }
2667
2671
  function pickAutoTemplate(profile, blockIndex = 0) {
2672
+ if (profile.hasCompoundBody) return void 0;
2668
2673
  if (profile.hasAsciiDiagram) return "diagram";
2669
2674
  if (profile.hasTimeline) return "timeline";
2670
2675
  if (profile.hasTree) return "tree";
@@ -118,11 +118,38 @@ function estimateNarrationDuration(text, wordsPerSecond = DEFAULT_WORDS_PER_SECO
118
118
  return spokenWords / wordsPerSecond;
119
119
  }
120
120
 
121
+ // src/timing/readingTime.ts
122
+ var DEFAULT_READING_WPM = 200;
123
+ function estimateReadingTime(text, options) {
124
+ const configuredWpm = options?.wordsPerMinute;
125
+ const wpm = typeof configuredWpm === "number" && configuredWpm > 0 ? configuredWpm : DEFAULT_READING_WPM;
126
+ const words = text.split(/\s+/).filter((w) => w.length > 0).length;
127
+ const minutes = words / wpm;
128
+ return {
129
+ words,
130
+ minutes,
131
+ seconds: Math.round(minutes * 60)
132
+ };
133
+ }
134
+ function estimateNarrationTime(text, options) {
135
+ const providedWps = options?.wordsPerSecond;
136
+ const wps = typeof providedWps === "number" && providedWps > 0 ? providedWps : DEFAULT_WORDS_PER_SECOND;
137
+ const spokenWords = countSpokenWords(text);
138
+ const totalSeconds = spokenWords / wps;
139
+ return {
140
+ spokenWords,
141
+ minutes: totalSeconds / 60,
142
+ seconds: Math.round(totalSeconds)
143
+ };
144
+ }
145
+
121
146
  export {
122
147
  DEFAULT_WORDS_PER_SECOND,
123
148
  estimateSpokenWordCount,
124
149
  countSpokenWords,
125
150
  estimateTimeFromText,
126
151
  calculatePrefixDuration,
127
- estimateNarrationDuration
152
+ estimateNarrationDuration,
153
+ estimateReadingTime,
154
+ estimateNarrationTime
128
155
  };
@@ -1,7 +1,10 @@
1
+ import {
2
+ buildNarrationScript,
3
+ parseNarrationTimingJson
4
+ } from "./chunk-764WHWKM.js";
1
5
  import {
2
6
  SHAPE_NAMES,
3
7
  buildChartData,
4
- buildNarrationScript,
5
8
  buildRegistry,
6
9
  coerceTemplateParams,
7
10
  deriveTemplateInputs,
@@ -22,7 +25,6 @@ import {
22
25
  nearestName,
23
26
  normalizeShapeKind,
24
27
  parseDataFence,
25
- parseNarrationTimingJson,
26
28
  parseStandaloneAnnotation,
27
29
  placeLayersInRect,
28
30
  readCustomThemesFromFrontmatter,
@@ -30,7 +32,7 @@ import {
30
32
  templateRegistry,
31
33
  writeCustomTemplatesToFrontmatter,
32
34
  writeCustomThemesToFrontmatter
33
- } from "./chunk-LI547QHN.js";
35
+ } from "./chunk-WAGJK36Q.js";
34
36
  import {
35
37
  iconMarker
36
38
  } from "./chunk-7ZAAICW4.js";
@@ -50,7 +52,7 @@ import {
50
52
  toCells,
51
53
  toGrid,
52
54
  traceBoxes
53
- } from "./chunk-PJRVODWW.js";
55
+ } from "./chunk-C3NAY5YP.js";
54
56
  import {
55
57
  defaultPageStyle,
56
58
  resolveMediaSchedule
@@ -59,21 +61,19 @@ import {
59
61
  DEFAULT_THEME,
60
62
  FRONTMATTER_CUSTOM_TEMPLATES_KEY,
61
63
  FRONTMATTER_CUSTOM_THEMES_KEY,
64
+ VIEWPORT_PRESETS,
65
+ calculateFontScale,
66
+ createTemplateContext,
67
+ getViewportOrientation,
62
68
  hexHueDegrees,
69
+ isTemplateBlock,
63
70
  oklchDarken,
64
71
  oklchLighten,
65
72
  relativeLuminance,
66
73
  resolveFontFamily,
67
74
  resolveTheme,
68
75
  withAlpha
69
- } from "./chunk-RXYKU6EX.js";
70
- import {
71
- VIEWPORT_PRESETS,
72
- calculateFontScale,
73
- createTemplateContext,
74
- getViewportOrientation,
75
- isTemplateBlock
76
- } from "./chunk-BAOV476U.js";
76
+ } from "./chunk-Y4D6TDZT.js";
77
77
  import {
78
78
  parseMarkdown
79
79
  } from "./chunk-CW24CH7I.js";
@@ -94,7 +94,7 @@ import {
94
94
  } from "./chunk-X7BNDUNI.js";
95
95
  import {
96
96
  estimateTimeFromText
97
- } from "./chunk-67L6WRJN.js";
97
+ } from "./chunk-GHOPHGOK.js";
98
98
 
99
99
  // src/doc/utils/animationUtils.ts
100
100
  function getAnimationStyle(animation, _currentTime = 0) {
@@ -769,19 +769,6 @@ function collectAllDocImages(blocks) {
769
769
  walkBlocks(blocks);
770
770
  return images;
771
771
  }
772
- function extractListItems(contents) {
773
- if (!contents) return [];
774
- const items = [];
775
- for (const node of contents) {
776
- if (node.type === "list") {
777
- for (const item of node.children) {
778
- const text = extractPlainText(item).trim();
779
- if (text) items.push(text);
780
- }
781
- }
782
- }
783
- return items;
784
- }
785
772
  function getTemplateDefaults(templateName, headingText, block) {
786
773
  const body = extractBodyText(block.contents);
787
774
  switch (templateName) {
@@ -801,10 +788,10 @@ function getTemplateDefaults(templateName, headingText, block) {
801
788
  return { fact: headingText, explanation: body || headingText };
802
789
  case "comparisonBar":
803
790
  return { leftLabel: "A", leftValue: 60, rightLabel: "B", rightValue: 40 };
804
- case "list": {
805
- const items = extractListItems(block.contents);
806
- return { items: items.length > 0 ? items : ["Item 1", "Item 2", "Item 3"] };
807
- }
791
+ case "list":
792
+ return deriveTemplateInputs(templateName, headingText, block.contents) ?? {
793
+ items: ["Item 1", "Item 2", "Item 3"]
794
+ };
808
795
  case "definitionCard":
809
796
  return { term: headingText, definition: body || headingText };
810
797
  case "dateEvent":
@@ -4229,6 +4216,7 @@ function autoMatchBlocks(blocks, audioFiles) {
4229
4216
  async function resolveAudioMapping(doc, container) {
4230
4217
  const narration = await applyNarrationTiming(doc, container);
4231
4218
  doc = narration.doc;
4219
+ if (doc.audio?.segments?.length > 0) return doc;
4232
4220
  const audioFiles = await discoverNarrationAudio(container);
4233
4221
  const scheduledSrcs = new Set(resolveMediaSchedule(doc).map((clip) => clip.src));
4234
4222
  const available = audioFiles.filter((f) => !scheduledSrcs.has(f.path));