@bendyline/squisq 2.11.17 → 2.11.18

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.
@@ -1,19 +1,21 @@
1
1
  import {
2
- extractContent,
3
- mapElementToBlock,
4
- stripMarkdown
5
- } from "./chunk-SVN7T6BK.js";
6
- import {
2
+ buildNarrationScript,
7
3
  coerceTemplateParams,
8
4
  deriveTemplateInputs,
9
5
  flattenRenderableBlocks,
6
+ getPinnedBlockMeta,
10
7
  markdownToDoc
11
- } from "./chunk-L7IIFWWW.js";
8
+ } from "./chunk-7IGKCHDP.js";
12
9
  import {
13
10
  isAsciiDiagramFence,
14
11
  isAsciiTimelineFence,
15
12
  isTreeFence
16
13
  } from "./chunk-A5QJJZ5S.js";
14
+ import {
15
+ extractContent,
16
+ mapElementToBlock,
17
+ stripMarkdown
18
+ } from "./chunk-SVN7T6BK.js";
17
19
  import {
18
20
  cloneAndFreezeData,
19
21
  isTemplateBlock
@@ -37,6 +39,288 @@ import {
37
39
  hashString
38
40
  } from "./chunk-XRW6P7IF.js";
39
41
 
42
+ // src/transform/presentationPlan.ts
43
+ var PRESENTATION_KEY = "squisq-presentation";
44
+ var PRESENTATION_LAYOUTS = [
45
+ "title",
46
+ "statement",
47
+ "list",
48
+ "comparison",
49
+ "steps",
50
+ "image"
51
+ ];
52
+ var PRESENTATION_LIMITS = {
53
+ sourceCharacters: 4e4,
54
+ beats: 80,
55
+ serializedCharacters: 16e4
56
+ };
57
+ function record(value) {
58
+ return !!value && typeof value === "object" && !Array.isArray(value);
59
+ }
60
+ function keys(value, required, optional = []) {
61
+ return required.every((key) => Object.prototype.hasOwnProperty.call(value, key)) && Object.keys(value).every((key) => required.includes(key) || optional.includes(key));
62
+ }
63
+ function text(value, max) {
64
+ return typeof value === "string" && value.trim().length > 0 && value.length <= max && !Array.from(value).some((character) => {
65
+ const code = character.charCodeAt(0);
66
+ return code < 32 && code !== 9 && code !== 10 && code !== 13;
67
+ });
68
+ }
69
+ function parsePresentationPlan(input) {
70
+ let value = input;
71
+ if (typeof value === "string") {
72
+ if (value.length > PRESENTATION_LIMITS.serializedCharacters) return null;
73
+ try {
74
+ value = JSON.parse(value);
75
+ } catch {
76
+ return null;
77
+ }
78
+ }
79
+ if (!record(value) || !keys(value, ["version", "sourceText", "origin", "beats"]) || value.version !== 1 || !text(value.sourceText, PRESENTATION_LIMITS.sourceCharacters) || typeof value.origin !== "string" || !["automatic", "ai"].includes(value.origin) || !Array.isArray(value.beats) || !value.beats.length || value.beats.length > PRESENTATION_LIMITS.beats)
80
+ return null;
81
+ const beats = [];
82
+ const ids = /* @__PURE__ */ new Set();
83
+ let end = 0;
84
+ for (const beat of value.beats) {
85
+ if (!record(beat) || !keys(
86
+ beat,
87
+ ["id", "sourceStart", "sourceEnd", "layout", "headline", "points"],
88
+ ["imageSrc"]
89
+ ) || !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)))
90
+ return null;
91
+ if (["comparison", "steps", "list"].includes(String(beat.layout)) && beat.points.length < 2)
92
+ return null;
93
+ if (beat.layout === "comparison" && beat.points.length !== 2) return null;
94
+ if (beat.layout === "image" ? !text(beat.imageSrc, 2048) : "imageSrc" in beat) return null;
95
+ ids.add(beat.id);
96
+ end = beat.sourceEnd;
97
+ beats.push({
98
+ id: beat.id,
99
+ sourceStart: beat.sourceStart,
100
+ sourceEnd: beat.sourceEnd,
101
+ layout: beat.layout,
102
+ headline: beat.headline,
103
+ points: [...beat.points],
104
+ ...beat.layout === "image" ? { imageSrc: beat.imageSrc } : {}
105
+ });
106
+ }
107
+ if (value.sourceText.slice(end).trim()) return null;
108
+ return {
109
+ version: 1,
110
+ sourceText: value.sourceText,
111
+ origin: value.origin,
112
+ beats
113
+ };
114
+ }
115
+ function serializePresentationPlan(plan) {
116
+ const parsed = parsePresentationPlan(plan);
117
+ if (!parsed) throw new Error("The presentation plan is invalid.");
118
+ const result = JSON.stringify(parsed);
119
+ if (result.length > PRESENTATION_LIMITS.serializedCharacters)
120
+ throw new Error("The presentation plan is too large.");
121
+ return result;
122
+ }
123
+
124
+ // src/transform/presentationHints.ts
125
+ var PRESENTATION_HINTS_KEY = "squisq-presentation-hints";
126
+ var DYNAMIC_PRESENTATION_STYLE = "dynamic-slides";
127
+ function defaultPresentationHints() {
128
+ return { version: 1, density: "balanced", inbetweens: true, blocks: [] };
129
+ }
130
+ function record2(value) {
131
+ return !!value && typeof value === "object" && !Array.isArray(value);
132
+ }
133
+ function exact(value, required, optional = []) {
134
+ return required.every((key) => Object.prototype.hasOwnProperty.call(value, key)) && Object.keys(value).every((key) => required.includes(key) || optional.includes(key));
135
+ }
136
+ function text2(value, max) {
137
+ return typeof value === "string" && !!value.trim() && value.length <= max && [...value].every((char) => char.charCodeAt(0) >= 32 || ["\n", "\r", " "].includes(char));
138
+ }
139
+ function parsePresentationHints(input) {
140
+ let value = input;
141
+ if (typeof value === "string") {
142
+ if (value.length > 16e4) return null;
143
+ try {
144
+ value = JSON.parse(value);
145
+ } catch {
146
+ return null;
147
+ }
148
+ }
149
+ if (!record2(value) || !exact(value, ["version", "density", "inbetweens", "blocks"]) || value.version !== 1 || !["concise", "balanced", "detailed"].includes(String(value.density)) || typeof value.inbetweens !== "boolean" || !Array.isArray(value.blocks) || value.blocks.length > 80)
150
+ return null;
151
+ const ids = /* @__PURE__ */ new Set();
152
+ const blocks = [];
153
+ for (const entry of value.blocks) {
154
+ if (!record2(entry) || !exact(entry, ["blockId"], ["layout", "inbetweens", "wording", "ai"]) || !text2(entry.blockId, 1024) || ids.has(entry.blockId) || entry.layout !== void 0 && !PRESENTATION_LAYOUTS.includes(entry.layout) || entry.inbetweens !== void 0 && typeof entry.inbetweens !== "boolean")
155
+ return null;
156
+ const hint = { blockId: entry.blockId };
157
+ if (entry.layout !== void 0) hint.layout = entry.layout;
158
+ if (entry.inbetweens !== void 0) hint.inbetweens = entry.inbetweens;
159
+ if (entry.ai !== void 0) {
160
+ if (!Array.isArray(entry.ai) || entry.ai.length > 80) return null;
161
+ hint.ai = [];
162
+ for (const item of entry.ai) {
163
+ if (!record2(item) || !exact(item, ["source", "layout", "headline", "points"]) || !text2(item.source, 4e3) || !PRESENTATION_LAYOUTS.includes(item.layout) || !text2(item.headline, 110) || !Array.isArray(item.points) || item.points.length > 4 || !item.points.every((point) => text2(point, 180)) || ["list", "steps"].includes(String(item.layout)) && item.points.length < 2 || item.layout === "comparison" && item.points.length !== 2)
164
+ return null;
165
+ hint.ai.push({
166
+ source: item.source,
167
+ layout: item.layout,
168
+ headline: item.headline,
169
+ points: [...item.points]
170
+ });
171
+ }
172
+ }
173
+ if (entry.wording !== void 0) {
174
+ const wording = entry.wording;
175
+ if (!record2(wording) || !exact(wording, ["source"], ["headline", "points"]) || !text2(wording.source, 4e4) || wording.headline === void 0 && wording.points === void 0 || wording.headline !== void 0 && !text2(wording.headline, 110) || wording.points !== void 0 && (!Array.isArray(wording.points) || wording.points.length > 4 || !wording.points.every((point) => text2(point, 180))))
176
+ return null;
177
+ hint.wording = {
178
+ source: wording.source,
179
+ ...wording.headline !== void 0 ? { headline: wording.headline } : {},
180
+ ...wording.points !== void 0 ? { points: [...wording.points] } : {}
181
+ };
182
+ }
183
+ ids.add(entry.blockId);
184
+ blocks.push(hint);
185
+ }
186
+ return {
187
+ version: 1,
188
+ density: value.density,
189
+ inbetweens: value.inbetweens,
190
+ blocks
191
+ };
192
+ }
193
+ function serializePresentationHints(hints) {
194
+ const parsed = parsePresentationHints(hints);
195
+ if (!parsed) throw new Error("The slide hints are invalid.");
196
+ const json = JSON.stringify(parsed);
197
+ if (json.length > 16e4) throw new Error("The slide hints are too large.");
198
+ return json;
199
+ }
200
+ function presentationSections(doc) {
201
+ const script = buildNarrationScript(doc);
202
+ return script.blocks.map((block) => ({
203
+ blockId: block.blockId,
204
+ heading: block.heading,
205
+ sourceStart: block.charStart,
206
+ sourceEnd: block.charEnd,
207
+ text: script.sourceText.slice(block.charStart, block.charEnd)
208
+ }));
209
+ }
210
+ function usesDynamicPresentation(doc) {
211
+ const fm = doc.frontmatter ?? {};
212
+ const style = Object.prototype.hasOwnProperty.call(fm, "squisq-transform") ? fm["squisq-transform"] : fm["transform-style"];
213
+ return typeof style === "string" && style.trim().toLowerCase().replace(/\s+/g, "-") === DYNAMIC_PRESENTATION_STYLE;
214
+ }
215
+
216
+ // src/transform/blockAnalyzer.ts
217
+ function contentsToPlainText(contents) {
218
+ return contents.map((node) => extractPlainText(node)).join("\n");
219
+ }
220
+ function collectImages(node) {
221
+ const results = [];
222
+ if ("type" in node && node.type === "image" && "url" in node) {
223
+ const img = node;
224
+ if (img.url) {
225
+ results.push({ src: img.url, alt: img.alt ?? "" });
226
+ }
227
+ }
228
+ for (const child of getChildren(node)) {
229
+ results.push(...collectImages(child));
230
+ }
231
+ return results;
232
+ }
233
+ function extractBlockImages(block, blockIndex) {
234
+ if (!block.contents || block.contents.length === 0) return [];
235
+ const images = [];
236
+ for (const node of block.contents) {
237
+ for (const img of collectImages(node)) {
238
+ images.push({ ...img, sourceBlockIndex: blockIndex });
239
+ }
240
+ }
241
+ return images;
242
+ }
243
+ function extractDocImages(blocks) {
244
+ const seen = /* @__PURE__ */ new Set();
245
+ const images = [];
246
+ function walk(blockList) {
247
+ for (const block of blockList) {
248
+ if (block.contents) {
249
+ for (const node of block.contents) {
250
+ for (const img of collectImages(node)) {
251
+ if (!seen.has(img.src)) {
252
+ seen.add(img.src);
253
+ images.push({ src: img.src, alt: img.alt });
254
+ }
255
+ }
256
+ }
257
+ }
258
+ if (block.children) {
259
+ walk(block.children);
260
+ }
261
+ }
262
+ }
263
+ walk(blocks);
264
+ return images;
265
+ }
266
+ function collectLeafBlocks(blocks) {
267
+ const result = [];
268
+ for (const block of blocks) {
269
+ if (block.children && block.children.length > 0) {
270
+ if (!block.contents || block.contents.length === 0) {
271
+ result.push(block);
272
+ }
273
+ result.push(...collectLeafBlocks(block.children));
274
+ } else {
275
+ result.push(block);
276
+ }
277
+ }
278
+ return result;
279
+ }
280
+ function analyzeBlocks(blocks, options) {
281
+ return analyzeFlatBlocks(collectLeafBlocks(blocks), options);
282
+ }
283
+ function analyzeFlatBlocks(flat, options) {
284
+ const results = [];
285
+ for (const block of flat) {
286
+ let plainText = "";
287
+ if (block.contents && block.contents.length > 0) {
288
+ plainText = contentsToPlainText(block.contents);
289
+ } else if (block.title) {
290
+ plainText = block.title;
291
+ }
292
+ const stripped = stripMarkdown(plainText);
293
+ const bodyWordCount = stripped.split(/\s+/).filter((w) => w.length > 0).length;
294
+ const extraction = extractContent(stripped, options);
295
+ const images = extractBlockImages(block, results.length);
296
+ results.push({
297
+ block,
298
+ plainText: stripped,
299
+ extractions: extraction.elements,
300
+ hasChildren: !!(block.children && block.children.length > 0),
301
+ bodyWordCount,
302
+ images
303
+ });
304
+ }
305
+ return results;
306
+ }
307
+
308
+ // src/transform/styles/dynamicSlides.ts
309
+ var dynamicSlidesStyle = {
310
+ id: "dynamic-slides",
311
+ name: "Dynamic slides",
312
+ description: "Advanced summarization: generate concise slides and diagrams from the current text and your hints",
313
+ contentMode: "presentation",
314
+ minConfidence: 0.25,
315
+ transformRatio: 1,
316
+ preferredTypes: [],
317
+ colorSchemes: ["blue", "green", "orange"],
318
+ insertSectionHeaders: false,
319
+ interleaveImages: false,
320
+ blocksPerSection: { max: 4 },
321
+ transitionStyle: "fade"
322
+ };
323
+
40
324
  // src/transform/styles/documentary.ts
41
325
  var documentaryStyle = {
42
326
  id: "documentary",
@@ -144,6 +428,7 @@ var headingsAndFeaturesStyle = {
144
428
 
145
429
  // src/transform/registry.ts
146
430
  var TRANSFORM_STYLES = Object.freeze({
431
+ [dynamicSlidesStyle.id]: cloneAndFreezeData(dynamicSlidesStyle),
147
432
  [documentaryStyle.id]: cloneAndFreezeData(documentaryStyle),
148
433
  [magazineStyle.id]: cloneAndFreezeData(magazineStyle),
149
434
  [dataDrivenStyle.id]: cloneAndFreezeData(dataDrivenStyle),
@@ -179,8 +464,8 @@ function validateTransformStyle(style) {
179
464
  fail(`${key} must be a finite number between 0 and 1`);
180
465
  }
181
466
  }
182
- if (candidate.contentMode !== void 0 && candidate.contentMode !== "brief" && candidate.contentMode !== "headings-and-features") {
183
- fail("contentMode must be brief or headings-and-features");
467
+ if (candidate.contentMode !== void 0 && candidate.contentMode !== "brief" && candidate.contentMode !== "headings-and-features" && candidate.contentMode !== "presentation") {
468
+ fail("contentMode must be brief, headings-and-features, or presentation");
184
469
  }
185
470
  if (!Array.isArray(candidate.preferredTypes) || candidate.preferredTypes.some((type) => !EXTRACTION_TYPES.has(type))) {
186
471
  fail("preferredTypes contains an unknown extraction type");
@@ -297,96 +582,202 @@ function getTransformStyleSummaries(registry) {
297
582
  });
298
583
  }
299
584
 
300
- // src/transform/blockAnalyzer.ts
301
- function contentsToPlainText(contents) {
302
- return contents.map((node) => extractPlainText(node)).join("\n");
585
+ // src/transform/timingAllocator.ts
586
+ var MIN_BLOCK_DURATION = 3;
587
+ var MAX_BLOCK_DURATION = 20;
588
+ var DEFAULT_BLOCK_DURATION = 6;
589
+ function allocateTiming(blocks, totalDuration) {
590
+ if (blocks.length === 0) return [];
591
+ const hasAnchors = blocks.some(
592
+ (b) => isTemplateBlock(b) && typeof b.sourceStartTime === "number"
593
+ );
594
+ return hasAnchors ? allocateAnchored(blocks, totalDuration) : allocateRescaled(blocks, totalDuration);
303
595
  }
304
- function collectImages(node) {
305
- const results = [];
306
- if ("type" in node && node.type === "image" && "url" in node) {
307
- const img = node;
308
- if (img.url) {
309
- results.push({ src: img.url, alt: img.alt ?? "" });
596
+ function allocateRescaled(blocks, totalDuration) {
597
+ const effectiveDuration = totalDuration > 0 ? totalDuration : estimateDefaultDuration(blocks);
598
+ const durations = blocks.map((block) => {
599
+ if (!isTemplateBlock(block) && block.duration > 0) {
600
+ return block.duration;
601
+ }
602
+ if (isTemplateBlock(block) && block.duration > 0) {
603
+ return block.duration;
310
604
  }
605
+ return DEFAULT_BLOCK_DURATION;
606
+ });
607
+ const rawTotal = durations.reduce((sum, d) => sum + d, 0);
608
+ if (rawTotal > 0 && effectiveDuration > 0) {
609
+ fitDurations(durations, effectiveDuration);
311
610
  }
312
- for (const child of getChildren(node)) {
313
- results.push(...collectImages(child));
611
+ let currentTime = 0;
612
+ const result = [];
613
+ for (let i = 0; i < blocks.length; i++) {
614
+ result.push(toOutputBlock(blocks[i], currentTime, durations[i]));
615
+ currentTime += durations[i];
314
616
  }
315
- return results;
617
+ return result;
316
618
  }
317
- function extractBlockImages(block, blockIndex) {
318
- if (!block.contents || block.contents.length === 0) return [];
319
- const images = [];
320
- for (const node of block.contents) {
321
- for (const img of collectImages(node)) {
322
- images.push({ ...img, sourceBlockIndex: blockIndex });
619
+ var MIN_FLOATING_DURATION = 1;
620
+ function allocateAnchored(blocks, totalDuration) {
621
+ const entries = blocks.map((block) => {
622
+ if (isTemplateBlock(block)) {
623
+ return {
624
+ block,
625
+ anchorStart: typeof block.sourceStartTime === "number" ? block.sourceStartTime : null,
626
+ rawDuration: block.duration > 0 ? block.duration : DEFAULT_BLOCK_DURATION
627
+ };
323
628
  }
324
- }
325
- return images;
326
- }
327
- function extractDocImages(blocks) {
328
- const seen = /* @__PURE__ */ new Set();
329
- const images = [];
330
- function walk(blockList) {
331
- for (const block of blockList) {
332
- if (block.contents) {
333
- for (const node of block.contents) {
334
- for (const img of collectImages(node)) {
335
- if (!seen.has(img.src)) {
336
- seen.add(img.src);
337
- images.push({ src: img.src, alt: img.alt });
338
- }
339
- }
340
- }
629
+ return {
630
+ block,
631
+ anchorStart: block.startTime,
632
+ rawDuration: block.duration > 0 ? block.duration : DEFAULT_BLOCK_DURATION
633
+ };
634
+ });
635
+ const starts = new Array(entries.length);
636
+ let cursor = 0;
637
+ let seenAnchor = false;
638
+ let i = 0;
639
+ while (i < entries.length) {
640
+ const entry = entries[i];
641
+ if (entry.anchorStart !== null) {
642
+ const start = Math.max(cursor, entry.anchorStart);
643
+ starts[i] = start;
644
+ const hint = isTemplateBlock(entry.block) ? entry.block.sourceDuration ?? entry.rawDuration : entry.rawDuration;
645
+ cursor = start + Math.max(MIN_FLOATING_DURATION, hint);
646
+ seenAnchor = true;
647
+ i++;
648
+ continue;
649
+ }
650
+ const runStart = i;
651
+ while (i < entries.length && entries[i].anchorStart === null) i++;
652
+ const run = entries.slice(runStart, i);
653
+ const rawSum = run.reduce((sum, e) => sum + e.rawDuration, 0);
654
+ const nextAnchorStart = i < entries.length ? Math.max(entries[i].anchorStart ?? cursor, cursor) : null;
655
+ if (nextAnchorStart === null) {
656
+ let acc = cursor;
657
+ for (let k = 0; k < run.length; k++) {
658
+ starts[runStart + k] = acc;
659
+ acc += run[k].rawDuration;
341
660
  }
342
- if (block.children) {
343
- walk(block.children);
661
+ cursor = acc;
662
+ } else if (!seenAnchor) {
663
+ const gap = nextAnchorStart - cursor;
664
+ let acc = cursor;
665
+ for (let k = 0; k < run.length; k++) {
666
+ starts[runStart + k] = acc;
667
+ acc += rawSum > 0 ? run[k].rawDuration / rawSum * gap : gap / run.length;
344
668
  }
669
+ cursor = nextAnchorStart;
670
+ } else {
671
+ const gap = Math.max(0, nextAnchorStart - cursor);
672
+ const scale = rawSum > 0 ? Math.min(1, gap / rawSum) : 0;
673
+ let acc = Math.max(cursor, nextAnchorStart - rawSum * scale);
674
+ for (let k = 0; k < run.length; k++) {
675
+ starts[runStart + k] = acc;
676
+ acc += run[k].rawDuration * scale;
677
+ }
678
+ cursor = nextAnchorStart;
345
679
  }
346
680
  }
347
- walk(blocks);
348
- return images;
349
- }
350
- function collectLeafBlocks(blocks) {
681
+ for (let k = 0; k < starts.length; k++) {
682
+ const floor = k > 0 ? starts[k - 1] + MIN_FLOATING_DURATION : 0;
683
+ if (starts[k] < floor) starts[k] = floor;
684
+ }
351
685
  const result = [];
352
- for (const block of blocks) {
353
- if (block.children && block.children.length > 0) {
354
- if (!block.contents || block.contents.length === 0) {
355
- result.push(block);
356
- }
357
- result.push(...collectLeafBlocks(block.children));
686
+ for (let k = 0; k < entries.length; k++) {
687
+ const entry = entries[k];
688
+ let duration;
689
+ if (k + 1 < entries.length) {
690
+ duration = Math.max(MIN_FLOATING_DURATION, starts[k + 1] - starts[k]);
358
691
  } else {
359
- result.push(block);
692
+ const authored = isTemplateBlock(entry.block) ? entry.block.sourceDuration ?? entry.rawDuration : entry.rawDuration;
693
+ duration = Math.max(MIN_FLOATING_DURATION, authored, totalDuration - starts[k]);
360
694
  }
695
+ result.push(toOutputBlock(entry.block, starts[k], duration));
361
696
  }
362
697
  return result;
363
698
  }
364
- function analyzeBlocks(blocks, options) {
365
- return analyzeFlatBlocks(collectLeafBlocks(blocks), options);
699
+ function toOutputBlock(block, startTime, duration) {
700
+ if (isTemplateBlock(block)) {
701
+ return {
702
+ ...block,
703
+ startTime,
704
+ duration,
705
+ title: getTemplateTitle(block)
706
+ };
707
+ }
708
+ return { ...block, startTime, duration };
366
709
  }
367
- function analyzeFlatBlocks(flat, options) {
368
- const results = [];
369
- for (const block of flat) {
370
- let plainText = "";
371
- if (block.contents && block.contents.length > 0) {
372
- plainText = contentsToPlainText(block.contents);
373
- } else if (block.title) {
374
- plainText = block.title;
710
+ function clampDuration(d) {
711
+ return Math.max(MIN_BLOCK_DURATION, Math.min(MAX_BLOCK_DURATION, d));
712
+ }
713
+ var SCALE_SEARCH_ITERATIONS = 60;
714
+ function totalAtScale(durations, scale) {
715
+ let sum = 0;
716
+ for (const d of durations) sum += clampDuration(d * scale);
717
+ return sum;
718
+ }
719
+ function fitDurations(durations, target) {
720
+ const rawTotal = durations.reduce((sum, d) => sum + d, 0);
721
+ if (rawTotal <= 0) return;
722
+ const plain = target / rawTotal;
723
+ const clampsNothing = durations.every((d) => {
724
+ const scaled = d * plain;
725
+ return scaled >= MIN_BLOCK_DURATION && scaled <= MAX_BLOCK_DURATION;
726
+ });
727
+ if (clampsNothing) {
728
+ for (let i = 0; i < durations.length; i++) durations[i] *= plain;
729
+ return;
730
+ }
731
+ let lo = 0;
732
+ let hi = 1;
733
+ for (let i = 0; i < SCALE_SEARCH_ITERATIONS && totalAtScale(durations, hi) < target; i++) {
734
+ hi *= 2;
735
+ }
736
+ for (let i = 0; i < SCALE_SEARCH_ITERATIONS; i++) {
737
+ const mid = (lo + hi) / 2;
738
+ if (totalAtScale(durations, mid) < target) lo = mid;
739
+ else hi = mid;
740
+ }
741
+ const scale = (lo + hi) / 2;
742
+ let clampedTotal = 0;
743
+ let freeRaw = 0;
744
+ for (const d of durations) {
745
+ const scaled = d * scale;
746
+ if (scaled < MIN_BLOCK_DURATION) clampedTotal += MIN_BLOCK_DURATION;
747
+ else if (scaled > MAX_BLOCK_DURATION) clampedTotal += MAX_BLOCK_DURATION;
748
+ else freeRaw += d;
749
+ }
750
+ const exact2 = freeRaw > 0 ? (target - clampedTotal) / freeRaw : scale;
751
+ const finalScale = Number.isFinite(exact2) && exact2 > 0 ? exact2 : scale;
752
+ for (let i = 0; i < durations.length; i++) {
753
+ durations[i] = clampDuration(durations[i] * finalScale);
754
+ }
755
+ }
756
+ function estimateDefaultDuration(blocks) {
757
+ let totalSeconds = 0;
758
+ for (const block of blocks) {
759
+ if (!isTemplateBlock(block) && block.duration > 0) {
760
+ totalSeconds += block.duration;
761
+ } else {
762
+ totalSeconds += DEFAULT_BLOCK_DURATION;
375
763
  }
376
- const stripped = stripMarkdown(plainText);
377
- const bodyWordCount = stripped.split(/\s+/).filter((w) => w.length > 0).length;
378
- const extraction = extractContent(stripped, options);
379
- const images = extractBlockImages(block, results.length);
380
- results.push({
381
- block,
382
- plainText: stripped,
383
- extractions: extraction.elements,
384
- hasChildren: !!(block.children && block.children.length > 0),
385
- bodyWordCount,
386
- images
387
- });
388
764
  }
389
- return results;
765
+ return Math.max(totalSeconds, blocks.length * MIN_BLOCK_DURATION);
766
+ }
767
+ function getTemplateTitle(block) {
768
+ if ("title" in block && typeof block.title === "string") {
769
+ return block.title;
770
+ }
771
+ if ("stat" in block && typeof block.stat === "string") {
772
+ return block.stat;
773
+ }
774
+ if ("fact" in block && typeof block.fact === "string") {
775
+ return block.fact;
776
+ }
777
+ if ("quote" in block && typeof block.quote === "string") {
778
+ return block.quote.slice(0, 60);
779
+ }
780
+ return void 0;
390
781
  }
391
782
 
392
783
  // src/transform/contentSummary.ts
@@ -418,8 +809,8 @@ var PROSE_KEYS = /* @__PURE__ */ new Set([
418
809
  "quote",
419
810
  "text"
420
811
  ]);
421
- function briefText(text) {
422
- const normalized = text.replace(/\s+/g, " ").trim();
812
+ function briefText(text3) {
813
+ const normalized = text3.replace(/\s+/g, " ").trim();
423
814
  const sentence = normalized.match(/^.*?[.!?](?:["'”’])?(?=\s|$)/)?.[0] ?? normalized;
424
815
  const words = sentence.split(/\s+/).filter(Boolean);
425
816
  let brief = words.slice(0, 24).join(" ");
@@ -480,11 +871,11 @@ function extractFeatureContents(contents, keepData = false) {
480
871
  for (const node of contents ?? []) visit(node);
481
872
  return features;
482
873
  }
483
- function withoutProse(record) {
484
- return record && Object.fromEntries(Object.entries(record).filter(([key]) => !PROSE_KEYS.has(key)));
874
+ function withoutProse(record3) {
875
+ return record3 && Object.fromEntries(Object.entries(record3).filter(([key]) => !PROSE_KEYS.has(key)));
485
876
  }
486
877
  function typedMediaContents(block) {
487
- const record = {
878
+ const record3 = {
488
879
  ...block,
489
880
  ...block.templateData,
490
881
  ...coerceTemplateParams(block.template ?? "", block.templateOverrides ?? {}).input
@@ -498,23 +889,23 @@ function typedMediaContents(block) {
498
889
  });
499
890
  }
500
891
  };
501
- addImage(record.imageSrc, record.imageAlt);
892
+ addImage(record3.imageSrc, record3.imageAlt);
502
893
  for (const key of ["accentImage", "backgroundImage"]) {
503
- const image = record[key];
894
+ const image = record3[key];
504
895
  if (image && typeof image === "object") {
505
896
  const data = image;
506
897
  addImage(data.src, data.alt);
507
898
  }
508
899
  }
509
- if (Array.isArray(record.images)) {
510
- for (const image of record.images) {
900
+ if (Array.isArray(record3.images)) {
901
+ for (const image of record3.images) {
511
902
  if (image && typeof image === "object") {
512
903
  const data = image;
513
904
  addImage(data.src, data.alt);
514
905
  }
515
906
  }
516
907
  }
517
- if (typeof record.videoSrc === "string" && record.videoSrc) {
908
+ if (typeof record3.videoSrc === "string" && record3.videoSrc) {
518
909
  const htmlChildren = [
519
910
  {
520
911
  type: "htmlElement",
@@ -522,8 +913,8 @@ function typedMediaContents(block) {
522
913
  selfClosing: false,
523
914
  children: [],
524
915
  attributes: {
525
- src: record.videoSrc,
526
- ...typeof record.posterSrc === "string" ? { poster: record.posterSrc } : {}
916
+ src: record3.videoSrc,
917
+ ...typeof record3.posterSrc === "string" ? { poster: record3.posterSrc } : {}
527
918
  }
528
919
  }
529
920
  ];
@@ -833,207 +1224,27 @@ function translateTemplateBlock(block, target) {
833
1224
  return null;
834
1225
  }
835
1226
 
836
- // src/transform/timingAllocator.ts
837
- var MIN_BLOCK_DURATION = 3;
838
- var MAX_BLOCK_DURATION = 20;
839
- var DEFAULT_BLOCK_DURATION = 6;
840
- function allocateTiming(blocks, totalDuration) {
841
- if (blocks.length === 0) return [];
842
- const hasAnchors = blocks.some(
843
- (b) => isTemplateBlock(b) && typeof b.sourceStartTime === "number"
844
- );
845
- return hasAnchors ? allocateAnchored(blocks, totalDuration) : allocateRescaled(blocks, totalDuration);
846
- }
847
- function allocateRescaled(blocks, totalDuration) {
848
- const effectiveDuration = totalDuration > 0 ? totalDuration : estimateDefaultDuration(blocks);
849
- const durations = blocks.map((block) => {
850
- if (!isTemplateBlock(block) && block.duration > 0) {
851
- return block.duration;
852
- }
853
- if (isTemplateBlock(block) && block.duration > 0) {
854
- return block.duration;
855
- }
856
- return DEFAULT_BLOCK_DURATION;
857
- });
858
- const rawTotal = durations.reduce((sum, d) => sum + d, 0);
859
- if (rawTotal > 0 && effectiveDuration > 0) {
860
- fitDurations(durations, effectiveDuration);
861
- }
862
- let currentTime = 0;
863
- const result = [];
864
- for (let i = 0; i < blocks.length; i++) {
865
- result.push(toOutputBlock(blocks[i], currentTime, durations[i]));
866
- currentTime += durations[i];
867
- }
868
- return result;
869
- }
870
- var MIN_FLOATING_DURATION = 1;
871
- function allocateAnchored(blocks, totalDuration) {
872
- const entries = blocks.map((block) => {
873
- if (isTemplateBlock(block)) {
874
- return {
875
- block,
876
- anchorStart: typeof block.sourceStartTime === "number" ? block.sourceStartTime : null,
877
- rawDuration: block.duration > 0 ? block.duration : DEFAULT_BLOCK_DURATION
878
- };
879
- }
1227
+ // src/transform/applyTransform.ts
1228
+ function applyTransform(doc, style, options) {
1229
+ const baseConfig = resolveTransformStyle(style, options?.registry);
1230
+ if (baseConfig.contentMode === "presentation") {
880
1231
  return {
881
- block,
882
- anchorStart: block.startTime,
883
- rawDuration: block.duration > 0 ? block.duration : DEFAULT_BLOCK_DURATION
1232
+ doc: {
1233
+ ...doc,
1234
+ frontmatter: { ...doc.frontmatter, "squisq-transform": DYNAMIC_PRESENTATION_STYLE }
1235
+ },
1236
+ stats: { totalInputBlocks: doc.blocks.length, transformedBlocks: 0, insertedBlocks: 0 }
884
1237
  };
885
- });
886
- const starts = new Array(entries.length);
887
- let cursor = 0;
888
- let seenAnchor = false;
889
- let i = 0;
890
- while (i < entries.length) {
891
- const entry = entries[i];
892
- if (entry.anchorStart !== null) {
893
- const start = Math.max(cursor, entry.anchorStart);
894
- starts[i] = start;
895
- const hint = isTemplateBlock(entry.block) ? entry.block.sourceDuration ?? entry.rawDuration : entry.rawDuration;
896
- cursor = start + Math.max(MIN_FLOATING_DURATION, hint);
897
- seenAnchor = true;
898
- i++;
899
- continue;
900
- }
901
- const runStart = i;
902
- while (i < entries.length && entries[i].anchorStart === null) i++;
903
- const run = entries.slice(runStart, i);
904
- const rawSum = run.reduce((sum, e) => sum + e.rawDuration, 0);
905
- const nextAnchorStart = i < entries.length ? Math.max(entries[i].anchorStart ?? cursor, cursor) : null;
906
- if (nextAnchorStart === null) {
907
- let acc = cursor;
908
- for (let k = 0; k < run.length; k++) {
909
- starts[runStart + k] = acc;
910
- acc += run[k].rawDuration;
911
- }
912
- cursor = acc;
913
- } else if (!seenAnchor) {
914
- const gap = nextAnchorStart - cursor;
915
- let acc = cursor;
916
- for (let k = 0; k < run.length; k++) {
917
- starts[runStart + k] = acc;
918
- acc += rawSum > 0 ? run[k].rawDuration / rawSum * gap : gap / run.length;
919
- }
920
- cursor = nextAnchorStart;
921
- } else {
922
- const gap = Math.max(0, nextAnchorStart - cursor);
923
- const scale = rawSum > 0 ? Math.min(1, gap / rawSum) : 0;
924
- let acc = Math.max(cursor, nextAnchorStart - rawSum * scale);
925
- for (let k = 0; k < run.length; k++) {
926
- starts[runStart + k] = acc;
927
- acc += run[k].rawDuration * scale;
928
- }
929
- cursor = nextAnchorStart;
930
- }
931
- }
932
- for (let k = 0; k < starts.length; k++) {
933
- const floor = k > 0 ? starts[k - 1] + MIN_FLOATING_DURATION : 0;
934
- if (starts[k] < floor) starts[k] = floor;
935
- }
936
- const result = [];
937
- for (let k = 0; k < entries.length; k++) {
938
- const entry = entries[k];
939
- let duration;
940
- if (k + 1 < entries.length) {
941
- duration = Math.max(MIN_FLOATING_DURATION, starts[k + 1] - starts[k]);
942
- } else {
943
- const authored = isTemplateBlock(entry.block) ? entry.block.sourceDuration ?? entry.rawDuration : entry.rawDuration;
944
- duration = Math.max(MIN_FLOATING_DURATION, authored, totalDuration - starts[k]);
945
- }
946
- result.push(toOutputBlock(entry.block, starts[k], duration));
947
1238
  }
948
- return result;
949
- }
950
- function toOutputBlock(block, startTime, duration) {
951
- if (isTemplateBlock(block)) {
1239
+ if (Object.prototype.hasOwnProperty.call(doc.frontmatter ?? {}, PRESENTATION_KEY) && !usesDynamicPresentation(doc)) {
952
1240
  return {
953
- ...block,
954
- startTime,
955
- duration,
956
- title: getTemplateTitle(block)
1241
+ doc,
1242
+ stats: { totalInputBlocks: doc.blocks.length, transformedBlocks: 0, insertedBlocks: 0 }
957
1243
  };
958
1244
  }
959
- return { ...block, startTime, duration };
960
- }
961
- function clampDuration(d) {
962
- return Math.max(MIN_BLOCK_DURATION, Math.min(MAX_BLOCK_DURATION, d));
963
- }
964
- var SCALE_SEARCH_ITERATIONS = 60;
965
- function totalAtScale(durations, scale) {
966
- let sum = 0;
967
- for (const d of durations) sum += clampDuration(d * scale);
968
- return sum;
969
- }
970
- function fitDurations(durations, target) {
971
- const rawTotal = durations.reduce((sum, d) => sum + d, 0);
972
- if (rawTotal <= 0) return;
973
- const plain = target / rawTotal;
974
- const clampsNothing = durations.every((d) => {
975
- const scaled = d * plain;
976
- return scaled >= MIN_BLOCK_DURATION && scaled <= MAX_BLOCK_DURATION;
977
- });
978
- if (clampsNothing) {
979
- for (let i = 0; i < durations.length; i++) durations[i] *= plain;
980
- return;
981
- }
982
- let lo = 0;
983
- let hi = 1;
984
- for (let i = 0; i < SCALE_SEARCH_ITERATIONS && totalAtScale(durations, hi) < target; i++) {
985
- hi *= 2;
986
- }
987
- for (let i = 0; i < SCALE_SEARCH_ITERATIONS; i++) {
988
- const mid = (lo + hi) / 2;
989
- if (totalAtScale(durations, mid) < target) lo = mid;
990
- else hi = mid;
991
- }
992
- const scale = (lo + hi) / 2;
993
- let clampedTotal = 0;
994
- let freeRaw = 0;
995
- for (const d of durations) {
996
- const scaled = d * scale;
997
- if (scaled < MIN_BLOCK_DURATION) clampedTotal += MIN_BLOCK_DURATION;
998
- else if (scaled > MAX_BLOCK_DURATION) clampedTotal += MAX_BLOCK_DURATION;
999
- else freeRaw += d;
1000
- }
1001
- const exact = freeRaw > 0 ? (target - clampedTotal) / freeRaw : scale;
1002
- const finalScale = Number.isFinite(exact) && exact > 0 ? exact : scale;
1003
- for (let i = 0; i < durations.length; i++) {
1004
- durations[i] = clampDuration(durations[i] * finalScale);
1005
- }
1006
- }
1007
- function estimateDefaultDuration(blocks) {
1008
- let totalSeconds = 0;
1009
- for (const block of blocks) {
1010
- if (!isTemplateBlock(block) && block.duration > 0) {
1011
- totalSeconds += block.duration;
1012
- } else {
1013
- totalSeconds += DEFAULT_BLOCK_DURATION;
1014
- }
1015
- }
1016
- return Math.max(totalSeconds, blocks.length * MIN_BLOCK_DURATION);
1017
- }
1018
- function getTemplateTitle(block) {
1019
- if ("title" in block && typeof block.title === "string") {
1020
- return block.title;
1021
- }
1022
- if ("stat" in block && typeof block.stat === "string") {
1023
- return block.stat;
1024
- }
1025
- if ("fact" in block && typeof block.fact === "string") {
1026
- return block.fact;
1027
- }
1028
- if ("quote" in block && typeof block.quote === "string") {
1029
- return block.quote.slice(0, 60);
1245
+ if (usesDynamicPresentation(doc)) {
1246
+ doc = { ...doc, frontmatter: { ...doc.frontmatter, "squisq-transform": baseConfig.id } };
1030
1247
  }
1031
- return void 0;
1032
- }
1033
-
1034
- // src/transform/applyTransform.ts
1035
- function applyTransform(doc, style, options) {
1036
- const baseConfig = resolveTransformStyle(style, options?.registry);
1037
1248
  const config = options?.overrides ? { ...baseConfig, ...options.overrides, id: baseConfig.id } : baseConfig;
1038
1249
  const seed = options?.seed ?? hashString(doc.articleId || "transform");
1039
1250
  const images = options?.images ?? extractDocImages(doc.blocks);
@@ -1334,21 +1545,21 @@ function standaloneImagePool(images, content) {
1334
1545
  ...available.filter((image) => accents.has(image.src))
1335
1546
  ];
1336
1547
  }
1337
- function chunkNarration(text, count) {
1338
- if (count <= 1) return [text];
1548
+ function chunkNarration(text3, count) {
1549
+ if (count <= 1) return [text3];
1339
1550
  const sentences = [];
1340
1551
  const boundary = /[.!?]["'’”)\]]*\s+/g;
1341
1552
  let start = 0;
1342
1553
  let match;
1343
- while ((match = boundary.exec(text)) !== null) {
1554
+ while ((match = boundary.exec(text3)) !== null) {
1344
1555
  const end = match.index + match[0].length;
1345
- const sentence = text.slice(start, end).trim();
1556
+ const sentence = text3.slice(start, end).trim();
1346
1557
  if (sentence) sentences.push(sentence);
1347
1558
  start = end;
1348
1559
  }
1349
- const tail = text.slice(start).trim();
1560
+ const tail = text3.slice(start).trim();
1350
1561
  if (tail) sentences.push(tail);
1351
- if (sentences.length < 2) return [text];
1562
+ if (sentences.length < 2) return [text3];
1352
1563
  const chunkCount = Math.min(count, sentences.length);
1353
1564
  const words = sentences.map((sentence) => sentence.split(/\s+/).length);
1354
1565
  const totalWords = words.reduce((sum, n) => sum + n, 0);
@@ -1419,15 +1630,335 @@ function videoBlock(video, index, segmentId, duration) {
1419
1630
  };
1420
1631
  }
1421
1632
 
1633
+ // src/transform/presentationPlanner.ts
1634
+ function clip(text3, words = 12) {
1635
+ const clean = text3.replace(/\s+/g, " ").trim();
1636
+ const parts = clean.split(" ");
1637
+ return (parts.length > words ? parts.slice(0, words).join(" ") + "\u2026" : clean).slice(
1638
+ 0,
1639
+ words === 12 ? 110 : 180
1640
+ );
1641
+ }
1642
+ function field(block, key) {
1643
+ if (!block || typeof block !== "object") return void 0;
1644
+ const value = block[key];
1645
+ return typeof value === "string" && value.trim() ? value : void 0;
1646
+ }
1647
+ function createPresentationPlan(doc, hints = defaultPresentationHints()) {
1648
+ if (!parsePresentationHints(hints)) throw new Error("The slide hints are invalid.");
1649
+ const script = buildNarrationScript(doc);
1650
+ if (!script.sourceText.trim()) throw new Error("Add some text before making a presentation.");
1651
+ if (script.sourceText.length > PRESENTATION_LIMITS.sourceCharacters)
1652
+ throw new Error(
1653
+ "This document is too long for one presentation. Split it into shorter documents."
1654
+ );
1655
+ const images = extractDocImages(doc.blocks);
1656
+ const beats = [];
1657
+ const supportingOpeners = /* @__PURE__ */ new Set();
1658
+ const wordLimit = { concise: 28, balanced: 42, detailed: 64 }[hints.density];
1659
+ for (const block of script.blocks) {
1660
+ const source = script.sourceText.slice(block.charStart, block.charEnd);
1661
+ const words = [...source.matchAll(/\S+/gu)];
1662
+ const hint = hints.blocks.find((item) => item.blockId === block.blockId);
1663
+ const wording = hint?.wording?.source === source ? hint.wording : void 0;
1664
+ const inbetweens = hint?.inbetweens ?? hints.inbetweens;
1665
+ let first = 0;
1666
+ while (first < words.length) {
1667
+ let last = Math.min(first + wordLimit, words.length);
1668
+ if (last < words.length) {
1669
+ for (let cursor = last; cursor > first + 18; cursor--) {
1670
+ if (/[.!?][”"')\]]?$/u.test(words[cursor - 1][0])) {
1671
+ last = cursor;
1672
+ break;
1673
+ }
1674
+ }
1675
+ }
1676
+ const start = block.charStart + words[first].index;
1677
+ const end = block.charStart + words[last - 1].index + words[last - 1][0].length;
1678
+ const passage = script.sourceText.slice(start, end);
1679
+ const index = beats.length;
1680
+ const suggested = transformNarratedSegment(
1681
+ {
1682
+ articleId: doc.articleId,
1683
+ segmentId: `presentation-${index}`,
1684
+ title: block.heading ?? "",
1685
+ text: passage,
1686
+ duration: Math.max(4, (last - first) / 2.5),
1687
+ audioSrc: "",
1688
+ includeTitleBlock: false
1689
+ },
1690
+ { slidesPerMinute: 4, minConfidence: 0.25, style: "documentary" }
1691
+ ).doc.blocks[0];
1692
+ const sentences = passage.split(/(?<=[.!?])\s+|\n+/u).map((part) => part.trim()).filter(Boolean);
1693
+ const bodySentences = sentences.filter((part) => part !== block.heading);
1694
+ let points = (bodySentences.length ? bodySentences : sentences).slice(0, 3).map((part) => clip(part, 22));
1695
+ let headline = clip(
1696
+ first === 0 && block.heading ? block.heading : field(suggested, "fact") ?? field(suggested, "description") ?? field(suggested, "quote") ?? sentences[0] ?? passage
1697
+ );
1698
+ if (!headline) headline = clip(passage);
1699
+ let layout = index === 0 ? "title" : points.length >= 2 && index % 3 === 1 ? "list" : "statement";
1700
+ if (points.length >= 2 && /\b(first|then|finally|steps?|next)\b/iu.test(passage))
1701
+ layout = "steps";
1702
+ if (points.length === 2 && /\b(versus|whereas|compared|instead)\b/iu.test(passage))
1703
+ layout = "comparison";
1704
+ const image = images.length && index > 0 && index % 3 === 2 ? images[Math.floor(index / 3) % images.length] : void 0;
1705
+ if (image) layout = "image";
1706
+ if (first === 0 && wording) {
1707
+ headline = wording.headline ?? headline;
1708
+ points = wording.points ?? points;
1709
+ }
1710
+ if (hint?.layout) {
1711
+ const eligible = hint.layout === "image" ? !!image : hint.layout === "comparison" ? points.length === 2 : ["steps", "list"].includes(hint.layout) ? points.length >= 2 : true;
1712
+ if (eligible) layout = hint.layout;
1713
+ }
1714
+ const beat = {
1715
+ id: `beat-${index + 1}`,
1716
+ sourceStart: start,
1717
+ sourceEnd: end,
1718
+ layout,
1719
+ headline,
1720
+ points,
1721
+ ...layout === "image" && image ? { imageSrc: image.src } : {}
1722
+ };
1723
+ const explainer = inbetweens && last - first >= 24 && (layout === "steps" || layout === "comparison" || !!image);
1724
+ if (explainer) {
1725
+ const split = block.charStart + words[first + Math.floor((last - first) / 2)].index;
1726
+ const { imageSrc: _image, ...textBeat } = beat;
1727
+ supportingOpeners.add(`beat-${beats.length + 1}`);
1728
+ beats.push({
1729
+ ...textBeat,
1730
+ id: `beat-${beats.length + 1}`,
1731
+ layout: "statement",
1732
+ sourceEnd: split
1733
+ });
1734
+ beats.push({ ...beat, id: `beat-${beats.length + 1}`, sourceStart: split });
1735
+ } else beats.push(beat);
1736
+ first = last;
1737
+ }
1738
+ }
1739
+ if (beats.length > PRESENTATION_LIMITS.beats)
1740
+ throw new Error(
1741
+ "This document has too many sections for one presentation. Split it into shorter documents."
1742
+ );
1743
+ const closing = beats[beats.length - 1];
1744
+ if (beats.length > 2 && closing?.layout === "statement" && !hints.blocks.some(
1745
+ (hint) => hint.layout && script.blocks.some(
1746
+ (block) => block.blockId === hint.blockId && closing.sourceStart >= block.charStart && closing.sourceStart < block.charEnd
1747
+ )
1748
+ ))
1749
+ closing.layout = "title";
1750
+ let origin = "automatic";
1751
+ for (const beat of beats) {
1752
+ const section = script.blocks.find(
1753
+ (block) => beat.sourceStart >= block.charStart && beat.sourceStart < block.charEnd
1754
+ );
1755
+ const hint = hints.blocks.find((item) => item.blockId === section?.blockId);
1756
+ const cached = hint?.ai?.find(
1757
+ (item) => item.source === script.sourceText.slice(beat.sourceStart, beat.sourceEnd)
1758
+ );
1759
+ if (cached && (cached.layout !== "image" || beat.imageSrc)) {
1760
+ beat.headline = cached.headline;
1761
+ beat.points = [...cached.points];
1762
+ beat.layout = cached.layout;
1763
+ origin = "ai";
1764
+ }
1765
+ if (hint?.layout && !supportingOpeners.has(beat.id) && (hint.layout !== "image" || beat.imageSrc) && (hint.layout !== "comparison" || beat.points.length === 2) && (!["steps", "list"].includes(hint.layout) || beat.points.length >= 2))
1766
+ beat.layout = hint.layout;
1767
+ if (section && beat.sourceStart === section.charStart && hint?.wording?.source === script.sourceText.slice(section.charStart, section.charEnd)) {
1768
+ beat.headline = hint.wording.headline ?? beat.headline;
1769
+ beat.points = hint.wording.points ?? beat.points;
1770
+ }
1771
+ if (["steps", "list", "comparison"].includes(beat.layout) && (beat.points.length < 2 || beat.layout === "comparison" && beat.points.length !== 2))
1772
+ beat.layout = "statement";
1773
+ if (beat.layout !== "image") delete beat.imageSrc;
1774
+ }
1775
+ return { version: 1, sourceText: script.sourceText, origin, beats };
1776
+ }
1777
+
1778
+ // src/transform/presentationCompiler.ts
1779
+ function compilePresentationPlan(doc, plan) {
1780
+ const checked = parsePresentationPlan(plan);
1781
+ if (!checked) throw new Error("The saved presentation is invalid. Make the presentation again.");
1782
+ const script = buildNarrationScript(doc);
1783
+ if (script.sourceText !== checked.sourceText)
1784
+ throw new Error("The document text has changed. Make the presentation again to match it.");
1785
+ const narration = doc.presentationNarration;
1786
+ const hasNarration = (doc.documentMedia ?? []).some(
1787
+ (clip2) => clip2.anchor === "document" && clip2.src
1788
+ );
1789
+ if (hasNarration && !narration)
1790
+ throw new Error(
1791
+ "Narration timings are unavailable. Restore the timing file or generate narration again."
1792
+ );
1793
+ if (narration && narration.sourceText !== checked.sourceText)
1794
+ throw new Error(
1795
+ "The narration no longer matches the document. Generate narration again before presenting."
1796
+ );
1797
+ if (narration && flattenRenderableBlocks(doc.blocks).some((block) => {
1798
+ const pin = getPinnedBlockMeta(block);
1799
+ return pin.duration !== void 0 || pin.startTime !== void 0;
1800
+ }))
1801
+ throw new Error(
1802
+ "Remove manually pinned slide times before fitting a presentation to narration."
1803
+ );
1804
+ const images = new Set(extractDocImages(doc.blocks).map((image) => image.src));
1805
+ if (checked.beats.some((beat) => beat.imageSrc && !images.has(beat.imageSrc)))
1806
+ throw new Error(
1807
+ "A presentation image is no longer in the document. Make the presentation again."
1808
+ );
1809
+ const duration = narration ? narration.startTime + narration.duration : Math.max(4, script.tokens.length / 2.5);
1810
+ const bookmarks = (narration?.bookmarks ?? []).filter(
1811
+ (word) => Number.isFinite(word.time) && word.time >= 0 && word.time <= duration && Number.isInteger(word.charOffset) && word.charOffset >= 0 && word.charOffset < script.sourceText.length
1812
+ ).sort((a, b) => a.charOffset - b.charOffset);
1813
+ let previous = 0;
1814
+ const starts = checked.beats.map((beat, index) => {
1815
+ if (!index) return 0;
1816
+ let time;
1817
+ if (narration && bookmarks.length) {
1818
+ time = bookmarks.find((word) => word.charOffset >= beat.sourceStart)?.time ?? duration;
1819
+ } else {
1820
+ const before = script.tokens.filter((token) => token.charOffset < beat.sourceStart).length;
1821
+ time = (narration?.startTime ?? 0) + before / Math.max(1, script.tokens.length) * (narration?.duration ?? duration);
1822
+ }
1823
+ previous = Math.min(duration, Math.max(previous, time));
1824
+ return previous;
1825
+ });
1826
+ const blocks = [];
1827
+ checked.beats.forEach((beat, index) => {
1828
+ const start = starts[index];
1829
+ const end = starts[index + 1] ?? duration;
1830
+ if (end <= start) return;
1831
+ const source = script.blocks.find(
1832
+ (block2) => beat.sourceStart >= block2.charStart && beat.sourceStart < block2.charEnd
1833
+ );
1834
+ const base = {
1835
+ id: beat.id,
1836
+ duration: end - start,
1837
+ startTime: start,
1838
+ audioSegment: 0,
1839
+ sourceStartTime: start,
1840
+ sourceDuration: end - start,
1841
+ timelineLocked: true,
1842
+ sourceBlockId: source?.blockId,
1843
+ sourceCharOffset: beat.sourceStart,
1844
+ transition: { type: "fade", duration: Math.min(0.3, (end - start) / 5) }
1845
+ };
1846
+ const block = visualBlock(beat, base, index);
1847
+ blocks.push(block);
1848
+ });
1849
+ return {
1850
+ ...doc,
1851
+ blocks,
1852
+ duration,
1853
+ presentationApplied: true,
1854
+ // The plan supplies its own opening. Preserve an explicit author override.
1855
+ frontmatter: { "squisq-cover-slide": false, ...doc.frontmatter },
1856
+ audio: doc.audio.segments.some((segment) => segment.src) ? doc.audio : { segments: [{ src: "", name: "presentation-clock", startTime: 0, duration }] }
1857
+ };
1858
+ }
1859
+ function visualBlock(beat, base, index) {
1860
+ const colorScheme = ["blue", "green", "orange"][index % 3];
1861
+ switch (beat.layout) {
1862
+ case "title":
1863
+ return { ...base, template: "title", title: beat.headline, subtitle: beat.points[0] ?? "" };
1864
+ case "statement":
1865
+ return {
1866
+ ...base,
1867
+ template: "factCard",
1868
+ fact: beat.headline,
1869
+ explanation: beat.points.join("\n\n")
1870
+ };
1871
+ case "list":
1872
+ return { ...base, template: "list", title: beat.headline, items: beat.points, colorScheme };
1873
+ case "comparison":
1874
+ return {
1875
+ ...base,
1876
+ template: "twoColumn",
1877
+ header: beat.headline,
1878
+ left: { label: beat.points[0] },
1879
+ right: { label: beat.points[1] }
1880
+ };
1881
+ case "image":
1882
+ return {
1883
+ ...base,
1884
+ template: "imageWithCaption",
1885
+ imageSrc: beat.imageSrc,
1886
+ imageAlt: beat.headline,
1887
+ caption: beat.headline,
1888
+ captionPosition: "bottom",
1889
+ ambientMotion: "zoomIn"
1890
+ };
1891
+ case "steps":
1892
+ return {
1893
+ ...base,
1894
+ template: "diagram",
1895
+ background: "surface",
1896
+ title: beat.headline,
1897
+ colorScheme,
1898
+ nodes: beat.points.map((point, i) => ({
1899
+ id: `step-${i}`,
1900
+ label: point,
1901
+ x: i * 320,
1902
+ y: 0,
1903
+ w: 260,
1904
+ h: 180
1905
+ })),
1906
+ edges: beat.points.slice(1).map((_, i) => ({ source: `step-${i}`, target: `step-${i + 1}`, directed: true }))
1907
+ };
1908
+ }
1909
+ }
1910
+ function applySavedPresentation(doc) {
1911
+ if (doc.presentationApplied) return doc;
1912
+ const dynamic = usesDynamicPresentation(doc);
1913
+ if (!dynamic && !Object.prototype.hasOwnProperty.call(doc.frontmatter ?? {}, PRESENTATION_KEY))
1914
+ return doc;
1915
+ try {
1916
+ const rawHints = doc.frontmatter?.[PRESENTATION_HINTS_KEY];
1917
+ const hints = rawHints === void 0 ? defaultPresentationHints() : parsePresentationHints(rawHints);
1918
+ if (dynamic && !hints)
1919
+ throw new Error("The slide hints are invalid. Review the summarization settings.");
1920
+ const plan = dynamic ? createPresentationPlan(doc, hints) : parsePresentationPlan(doc.frontmatter?.[PRESENTATION_KEY]);
1921
+ if (!plan) throw new Error("The saved presentation is invalid. Make the presentation again.");
1922
+ return compilePresentationPlan(doc, plan);
1923
+ } catch (error) {
1924
+ return {
1925
+ ...doc,
1926
+ diagnostics: [
1927
+ ...doc.diagnostics ?? [],
1928
+ {
1929
+ severity: "error",
1930
+ code: "presentation-invalid",
1931
+ message: error instanceof Error ? error.message : "The presentation could not be loaded."
1932
+ }
1933
+ ]
1934
+ };
1935
+ }
1936
+ }
1937
+
1422
1938
  export {
1939
+ PRESENTATION_KEY,
1940
+ PRESENTATION_LAYOUTS,
1941
+ PRESENTATION_LIMITS,
1942
+ parsePresentationPlan,
1943
+ serializePresentationPlan,
1944
+ PRESENTATION_HINTS_KEY,
1945
+ DYNAMIC_PRESENTATION_STYLE,
1946
+ defaultPresentationHints,
1947
+ parsePresentationHints,
1948
+ serializePresentationHints,
1949
+ presentationSections,
1950
+ usesDynamicPresentation,
1951
+ extractDocImages,
1952
+ analyzeBlocks,
1423
1953
  DEFAULT_TRANSFORM_STYLE_ID,
1424
1954
  createTransformStyleRegistry,
1425
1955
  resolveTransformStyle,
1426
1956
  getTransformStyleIds,
1427
1957
  getTransformStyleSummaries,
1428
- extractDocImages,
1429
- analyzeBlocks,
1430
1958
  allocateTiming,
1431
1959
  applyTransform,
1432
- transformNarratedSegment
1960
+ transformNarratedSegment,
1961
+ createPresentationPlan,
1962
+ compilePresentationPlan,
1963
+ applySavedPresentation
1433
1964
  };