@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.
@@ -703,6 +703,8 @@ interface AccentImage {
703
703
  * Base properties shared by all template blocks.
704
704
  */
705
705
  interface BaseTemplateBlock {
706
+ /** Compiled presentation timing is authoritative; pacing must not merge or split it. */
707
+ timelineLocked?: boolean;
706
708
  /** Unique block ID */
707
709
  id: string;
708
710
  /** Block duration in seconds */
@@ -1310,6 +1312,8 @@ interface DiagramTemplateEdge {
1310
1312
  */
1311
1313
  interface DiagramBlockInput extends BaseTemplateBlock {
1312
1314
  template: 'diagram';
1315
+ /** Paint an opaque themed surface for standalone slides; omitted stays transparent. */
1316
+ background?: 'surface';
1313
1317
  /** Optional diagram title displayed above the canvas. */
1314
1318
  title?: string;
1315
1319
  /** Color scheme for nodes and edges. */
@@ -2330,6 +2334,16 @@ interface DocDiagnostic {
2330
2334
  * A complete visual doc for an article.
2331
2335
  */
2332
2336
  interface Doc {
2337
+ /** Projection-only source/timing metadata from the document narration take. */
2338
+ presentationNarration?: {
2339
+ sourceText: string;
2340
+ src: string;
2341
+ startTime: number;
2342
+ duration: number;
2343
+ bookmarks: AudioBookmark[];
2344
+ };
2345
+ /** Projection-only guard: never compile an accepted plan twice. */
2346
+ presentationApplied?: boolean;
2333
2347
  /** Article ID this doc belongs to */
2334
2348
  articleId: string;
2335
2349
  /** Total duration in seconds (sum of all audio segments) */
@@ -1,4 +1,4 @@
1
- import { a2 as ImageLayer, bu as TextLayer, bh as ShapeLayer, aX as PathLayer } from './Doc-CXx1WL8G.js';
1
+ import { a2 as ImageLayer, bu as TextLayer, bh as ShapeLayer, aX as PathLayer } from './Doc-B6TYHYGc.js';
2
2
 
3
3
  /**
4
4
  * ImageEditDoc — JSON document describing a layered, editable image.
@@ -0,0 +1,114 @@
1
+ // src/narration/sidecar.ts
2
+ function resolveNarrationTimingGenerator(options) {
3
+ const baseWpm = options?.generator?.baseWpm ?? options?.baseWpm;
4
+ return {
5
+ name: options?.generator?.name ?? "squisq-teleprompter",
6
+ method: options?.generator?.method ?? "dsp-align",
7
+ ...baseWpm !== void 0 ? { baseWpm } : {}
8
+ };
9
+ }
10
+ function buildNarrationTimingJson(script, alignment, durationSec, options) {
11
+ const bookmarks = alignment.words.map((word) => {
12
+ const token = script.tokens[word.tokenIndex];
13
+ return {
14
+ id: `word-${word.tokenIndex}`,
15
+ time: word.tSec,
16
+ charOffset: token?.charOffset ?? 0,
17
+ textFragment: token?.text ?? ""
18
+ };
19
+ });
20
+ const blocks = alignment.blocks.map((range) => ({
21
+ ...range.blockId ? { blockId: range.blockId } : {},
22
+ ...range.heading !== void 0 ? { heading: range.heading } : {},
23
+ blockIndex: range.blockIndex,
24
+ charStart: range.charStart,
25
+ charEnd: range.charEnd,
26
+ startSec: range.startSec,
27
+ endSec: range.endSec
28
+ }));
29
+ return {
30
+ version: 3,
31
+ sourceText: script.sourceText,
32
+ duration: Number.isFinite(durationSec) && durationSec >= 0 ? durationSec : 0,
33
+ bookmarks,
34
+ blocks,
35
+ // Write only what `parseNarrationTimingJson` will read back, so the codec
36
+ // is a round-trip pair. A non-finite value would serialize to `null`
37
+ // (JSON has no NaN) and be dropped on parse anyway — omitting it here
38
+ // keeps the emitted sidecar honest rather than carrying a dead field.
39
+ ...finiteNumber(options?.cameraOffsetSec) ? { cameraOffsetSec: options.cameraOffsetSec } : {},
40
+ generator: resolveNarrationTimingGenerator(options)
41
+ };
42
+ }
43
+ function finiteNonNegative(value) {
44
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
45
+ }
46
+ function finiteNumber(value) {
47
+ return typeof value === "number" && Number.isFinite(value);
48
+ }
49
+ function parseNarrationTimingJson(data) {
50
+ try {
51
+ const text = typeof data === "string" ? data : new TextDecoder().decode(data instanceof Uint8Array ? data : new Uint8Array(data));
52
+ const parsed = JSON.parse(text);
53
+ if (typeof parsed !== "object" || parsed === null) return null;
54
+ const raw = parsed;
55
+ if (typeof raw.sourceText !== "string" || !finiteNonNegative(raw.duration)) return null;
56
+ const bookmarks = Array.isArray(raw.bookmarks) ? raw.bookmarks.flatMap((b) => {
57
+ if (typeof b !== "object" || b === null) return [];
58
+ const bookmark = b;
59
+ if (!finiteNonNegative(bookmark.time) || !finiteNonNegative(bookmark.charOffset)) {
60
+ return [];
61
+ }
62
+ return [
63
+ {
64
+ id: typeof bookmark.id === "string" ? bookmark.id : `bookmark-${bookmark.charOffset}`,
65
+ time: bookmark.time,
66
+ charOffset: bookmark.charOffset,
67
+ ...typeof bookmark.textFragment === "string" ? { textFragment: bookmark.textFragment } : {}
68
+ }
69
+ ];
70
+ }) : [];
71
+ let blocks = Array.isArray(raw.blocks) ? raw.blocks.flatMap((b) => {
72
+ if (typeof b !== "object" || b === null) return [];
73
+ const block = b;
74
+ if (!finiteNonNegative(block.startSec) || !finiteNonNegative(block.endSec) || !finiteNonNegative(block.charStart) || !finiteNonNegative(block.charEnd)) {
75
+ return [];
76
+ }
77
+ return [
78
+ {
79
+ ...typeof block.blockId === "string" ? { blockId: block.blockId } : {},
80
+ ...typeof block.heading === "string" ? { heading: block.heading } : {},
81
+ blockIndex: finiteNonNegative(block.blockIndex) ? block.blockIndex : 0,
82
+ charStart: block.charStart,
83
+ charEnd: block.charEnd,
84
+ startSec: block.startSec,
85
+ endSec: Math.max(block.startSec, block.endSec)
86
+ }
87
+ ];
88
+ }) : [];
89
+ blocks = blocks.sort((a, b) => a.startSec - b.startSec);
90
+ let cursor = 0;
91
+ for (const block of blocks) {
92
+ if (block.startSec < cursor) block.startSec = cursor;
93
+ if (block.endSec < block.startSec) block.endSec = block.startSec;
94
+ cursor = block.startSec;
95
+ }
96
+ return {
97
+ version: 3,
98
+ sourceText: raw.sourceText,
99
+ duration: raw.duration,
100
+ bookmarks,
101
+ blocks,
102
+ ...finiteNumber(raw.cameraOffsetSec) ? { cameraOffsetSec: raw.cameraOffsetSec } : {},
103
+ ...typeof raw.generator === "object" && raw.generator !== null ? { generator: raw.generator } : {}
104
+ };
105
+ } catch {
106
+ return null;
107
+ }
108
+ }
109
+
110
+ export {
111
+ resolveNarrationTimingGenerator,
112
+ buildNarrationTimingJson,
113
+ parseNarrationTimingJson
114
+ };
@@ -75,7 +75,8 @@ import {
75
75
  resolveBlockTransition
76
76
  } from "./chunk-4VOD55SX.js";
77
77
  import {
78
- estimateReadingTime
78
+ estimateReadingTime,
79
+ estimateSpokenWordCount
79
80
  } from "./chunk-GHOPHGOK.js";
80
81
  import {
81
82
  SeededRandom,
@@ -6048,9 +6049,11 @@ function nodesFromTemplateData(input) {
6048
6049
  function diagramBlock(input, context) {
6049
6050
  const { theme, viewport, children = [] } = context;
6050
6051
  const colors = input.colorScheme ? resolveColorScheme(context, input.colorScheme) : void 0;
6052
+ const background = input.background === "surface" ? [createBackgroundLayer("diagram-bg", themedSurfaceGradient(context, 170))] : [];
6051
6053
  const resolved = children.length > 0 ? nodesFromChildren(context) : input.nodes && input.nodes.length > 0 ? nodesFromTemplateData(input) : { nodes: [], edges: [] };
6052
6054
  if (resolved.nodes.length === 0) {
6053
6055
  return [
6056
+ ...background,
6054
6057
  {
6055
6058
  type: "text",
6056
6059
  id: "diagram-empty",
@@ -6091,7 +6094,7 @@ function diagramBlock(input, context) {
6091
6094
  w: n.w * scale,
6092
6095
  h: n.h * scale
6093
6096
  });
6094
- const layers = [];
6097
+ const layers = [...background];
6095
6098
  if (input.title) {
6096
6099
  const titleLayer = canvasTitleLayer("diagram-title", input.title, frame, placement, context);
6097
6100
  if (titleLayer) layers.push(titleLayer);
@@ -11005,6 +11008,28 @@ function expandDocBlocks(blocks, options = {}) {
11005
11008
  const splitParts = /* @__PURE__ */ new Map();
11006
11009
  for (const [segmentIndex, segmentBlocks] of blocksBySegment) {
11007
11010
  const audioSegment = audioSegments[segmentIndex];
11011
+ if (segmentBlocks.every(({ block }) => block.timelineLocked === true)) {
11012
+ for (const { block, originalIndex } of segmentBlocks) {
11013
+ const expanded = materializeScheduledBlock(
11014
+ block,
11015
+ originalIndex,
11016
+ totalBlocks,
11017
+ theme,
11018
+ viewport,
11019
+ effectivePersistentLayers,
11020
+ expandedPersistentLayers,
11021
+ registry,
11022
+ failureMode,
11023
+ onDiagnostic,
11024
+ motion,
11025
+ opts.mediaAspectRatios
11026
+ );
11027
+ expanded.startTime = block.sourceStartTime ?? 0;
11028
+ expanded.duration = block.duration;
11029
+ expandedBlocks[originalIndex] = expanded;
11030
+ }
11031
+ continue;
11032
+ }
11008
11033
  if (!audioSegment) {
11009
11034
  let offsetTime = 0;
11010
11035
  for (const { block, originalIndex } of segmentBlocks) {
@@ -12485,6 +12510,215 @@ function buildStartBlock(markdownDoc, rootBlocks, fileName) {
12485
12510
  return config;
12486
12511
  }
12487
12512
 
12513
+ // src/narration/syllables.ts
12514
+ var BOUNDARY_PUNCT = /^[.,!?;:'"()[\]{}]+|[.,!?;:'"()[\]{}]+$/g;
12515
+ var SYLLABLES_PER_SPOKEN_WORD = 1.4;
12516
+ function estimateSyllables(token) {
12517
+ const stripped = token.replace(BOUNDARY_PUNCT, "");
12518
+ if (!stripped) return 1;
12519
+ if (/^[-+]?[\d.,]+$/.test(stripped)) {
12520
+ const num = parseFloat(stripped.replace(/,/g, ""));
12521
+ if (!Number.isNaN(num) && Number.isFinite(num)) {
12522
+ return Math.max(1, Math.round(estimateSpokenWordCount(stripped) * SYLLABLES_PER_SPOKEN_WORD));
12523
+ }
12524
+ }
12525
+ if (/^[A-Z]{2,5}$/.test(stripped) && !/[AEIOUY]/.test(stripped)) {
12526
+ return stripped.length;
12527
+ }
12528
+ const parts = stripped.toLowerCase().split(/[^a-z']+/).filter((p) => p.length > 0);
12529
+ if (parts.length === 0) return 1;
12530
+ let total = 0;
12531
+ for (const part of parts) {
12532
+ total += countAlphaSyllables(part.replace(/'/g, ""));
12533
+ }
12534
+ return Math.max(1, total);
12535
+ }
12536
+ function countAlphaSyllables(word) {
12537
+ if (!word) return 0;
12538
+ const groups = word.match(/[aeiouy]+/g);
12539
+ let count = groups ? groups.length : 0;
12540
+ if (count === 0) return 1;
12541
+ if (count > 1 && /[^aeiouy]e$/.test(word)) {
12542
+ count -= 1;
12543
+ }
12544
+ if (count > 1 && /[^aeiouy]ed$/.test(word) && !/[td]ed$/.test(word)) {
12545
+ count -= 1;
12546
+ }
12547
+ if (count > 1 && /[^aeiouy]es$/.test(word) && !/(?:[sxz]|[cs]h)es$/.test(word)) {
12548
+ count -= 1;
12549
+ }
12550
+ if (/[^aeiouy]le$/.test(word)) {
12551
+ count += 1;
12552
+ }
12553
+ return Math.max(1, count);
12554
+ }
12555
+
12556
+ // src/narration/script.ts
12557
+ function blockSpokenText(block, includeTitles) {
12558
+ const title = includeTitles && block.title ? block.title.trim() : "";
12559
+ const body = getBlockBodyText2(block);
12560
+ if (title && body) return `${title}
12561
+ ${body}`;
12562
+ return title || body;
12563
+ }
12564
+ function buildNarrationScript(doc, options) {
12565
+ const includeTitles = options?.includeTitles ?? true;
12566
+ const separator = options?.blockSeparator ?? "\n\n";
12567
+ const pieces = [];
12568
+ for (const block of flattenRenderableBlocks(doc.blocks)) {
12569
+ const text = blockSpokenText(block, includeTitles);
12570
+ if (!text) continue;
12571
+ pieces.push({
12572
+ blockId: block.id,
12573
+ heading: block.title || void 0,
12574
+ text
12575
+ });
12576
+ }
12577
+ let sourceText = "";
12578
+ const blocks = [];
12579
+ for (let i = 0; i < pieces.length; i++) {
12580
+ if (i > 0) sourceText += separator;
12581
+ const charStart = sourceText.length;
12582
+ sourceText += pieces[i].text;
12583
+ blocks.push({
12584
+ blockId: pieces[i].blockId,
12585
+ ...pieces[i].heading !== void 0 ? { heading: pieces[i].heading } : {},
12586
+ tokenStart: 0,
12587
+ tokenEnd: 0,
12588
+ charStart,
12589
+ charEnd: sourceText.length
12590
+ });
12591
+ }
12592
+ const tokens = [];
12593
+ const tokenRe = /\S+/g;
12594
+ let blockIndex = 0;
12595
+ let match;
12596
+ while ((match = tokenRe.exec(sourceText)) !== null) {
12597
+ const charOffset = match.index;
12598
+ const charEnd = charOffset + match[0].length;
12599
+ while (blockIndex < blocks.length - 1 && charOffset >= blocks[blockIndex].charEnd) {
12600
+ blockIndex++;
12601
+ }
12602
+ const spoken = hasSpokenContent(match[0]);
12603
+ tokens.push({
12604
+ text: match[0],
12605
+ charOffset,
12606
+ charEnd,
12607
+ blockId: blocks[blockIndex]?.blockId ?? "",
12608
+ blockIndex,
12609
+ syllables: spoken ? estimateSyllables(match[0]) : 0,
12610
+ spokenWordEquiv: spoken ? estimateSpokenWordCount(stripBoundaryPunct(match[0])) : 0,
12611
+ pauseAfter: 0,
12612
+ spoken
12613
+ });
12614
+ }
12615
+ for (const range of blocks) {
12616
+ range.tokenStart = tokens.length;
12617
+ range.tokenEnd = 0;
12618
+ }
12619
+ for (let i = 0; i < tokens.length; i++) {
12620
+ const range = blocks[tokens[i].blockIndex];
12621
+ if (range) {
12622
+ if (i < range.tokenStart) range.tokenStart = i;
12623
+ range.tokenEnd = i + 1;
12624
+ }
12625
+ }
12626
+ for (const range of blocks) {
12627
+ if (range.tokenStart > range.tokenEnd) {
12628
+ range.tokenStart = range.tokenEnd;
12629
+ }
12630
+ }
12631
+ for (let i = 0; i < tokens.length; i++) {
12632
+ const token = tokens[i];
12633
+ const next = tokens[i + 1];
12634
+ if (!next || next.blockIndex !== token.blockIndex) {
12635
+ token.pauseAfter = 3;
12636
+ } else if (sourceText.slice(token.charEnd, next.charOffset).includes("\n")) {
12637
+ token.pauseAfter = 2;
12638
+ } else if (endsWithClausePunctuation(token.text)) {
12639
+ token.pauseAfter = 1;
12640
+ }
12641
+ }
12642
+ const cumulativeSyllables = new Array(tokens.length + 1);
12643
+ cumulativeSyllables[0] = 0;
12644
+ for (let i = 0; i < tokens.length; i++) {
12645
+ cumulativeSyllables[i + 1] = cumulativeSyllables[i] + tokens[i].syllables;
12646
+ }
12647
+ return {
12648
+ sourceText,
12649
+ tokens,
12650
+ blocks,
12651
+ totalSyllables: cumulativeSyllables[tokens.length],
12652
+ cumulativeSyllables
12653
+ };
12654
+ }
12655
+ var TRAILING_CLOSERS = /["')\]}»”’]+$/;
12656
+ function hasSpokenContent(token) {
12657
+ return /[\p{L}\p{N}]/u.test(token);
12658
+ }
12659
+ function stripBoundaryPunct(token) {
12660
+ return token.replace(/^[.,!?;:'"()[\]{}]+|[.,!?;:'"()[\]{}]+$/g, "");
12661
+ }
12662
+ function endsWithClausePunctuation(token) {
12663
+ const bare = token.replace(TRAILING_CLOSERS, "");
12664
+ const last = bare.charAt(bare.length - 1);
12665
+ if (!last) return false;
12666
+ if (last === ",") {
12667
+ return true;
12668
+ }
12669
+ return last === "." || last === ";" || last === ":" || last === "!" || last === "?";
12670
+ }
12671
+ function clamp2(value, min, max) {
12672
+ return value < min ? min : value > max ? max : value;
12673
+ }
12674
+ function expectedSyllablesAt(script, wordPos) {
12675
+ const n = script.tokens.length;
12676
+ if (n === 0) return 0;
12677
+ const pos = clamp2(wordPos, 0, n);
12678
+ const idx = Math.floor(pos);
12679
+ if (idx >= n) return script.totalSyllables;
12680
+ return script.cumulativeSyllables[idx] + (pos - idx) * script.tokens[idx].syllables;
12681
+ }
12682
+ function wordPosAtExpectedSyllables(script, syllables) {
12683
+ const n = script.tokens.length;
12684
+ if (n === 0) return 0;
12685
+ const target = clamp2(syllables, 0, script.totalSyllables);
12686
+ let lo = 0;
12687
+ let hi = n;
12688
+ while (lo < hi) {
12689
+ const mid = lo + hi + 1 >> 1;
12690
+ if (script.cumulativeSyllables[mid] <= target) lo = mid;
12691
+ else hi = mid - 1;
12692
+ }
12693
+ if (lo >= n) return n;
12694
+ const within = target - script.cumulativeSyllables[lo];
12695
+ const sylls = script.tokens[lo].syllables;
12696
+ return lo + (sylls > 0 ? within / sylls : 0);
12697
+ }
12698
+ function wordIndexAtChar(script, charOffset) {
12699
+ const tokens = script.tokens;
12700
+ if (tokens.length === 0) return 0;
12701
+ let lo = 0;
12702
+ let hi = tokens.length - 1;
12703
+ while (lo < hi) {
12704
+ const mid = lo + hi >> 1;
12705
+ if (tokens[mid].charEnd <= charOffset) lo = mid + 1;
12706
+ else hi = mid;
12707
+ }
12708
+ return lo;
12709
+ }
12710
+ function wordIndexAtTime(words, tSec) {
12711
+ if (words.length === 0) return 0;
12712
+ let lo = 0;
12713
+ let hi = words.length - 1;
12714
+ while (lo < hi) {
12715
+ const mid = lo + hi + 1 >> 1;
12716
+ if (words[mid].tSec <= tSec) lo = mid;
12717
+ else hi = mid - 1;
12718
+ }
12719
+ return words[lo].tSec <= tSec ? lo : 0;
12720
+ }
12721
+
12488
12722
  export {
12489
12723
  expandPersistentLayers,
12490
12724
  getPersistentLayersFromTheme,
@@ -12622,5 +12856,11 @@ export {
12622
12856
  flattenRenderableBlocks,
12623
12857
  countBlocks,
12624
12858
  getBlockDepth,
12625
- getBlockBodyText2 as getBlockBodyText
12859
+ getBlockBodyText2 as getBlockBodyText,
12860
+ estimateSyllables,
12861
+ buildNarrationScript,
12862
+ expectedSyllablesAt,
12863
+ wordPosAtExpectedSyllables,
12864
+ wordIndexAtChar,
12865
+ wordIndexAtTime
12626
12866
  };
@@ -1,10 +1,13 @@
1
1
  import {
2
- buildNarrationScript,
3
2
  parseNarrationTimingJson
4
- } from "./chunk-PNXBDMNS.js";
3
+ } from "./chunk-3XEABCT3.js";
4
+ import {
5
+ applySavedPresentation
6
+ } from "./chunk-NM747LUO.js";
5
7
  import {
6
8
  SHAPE_NAMES,
7
9
  buildChartData,
10
+ buildNarrationScript,
8
11
  buildRegistry,
9
12
  coerceTemplateParams,
10
13
  deriveTemplateInputs,
@@ -32,7 +35,7 @@ import {
32
35
  templateRegistry,
33
36
  writeCustomTemplatesToFrontmatter,
34
37
  writeCustomThemesToFrontmatter
35
- } from "./chunk-L7IIFWWW.js";
38
+ } from "./chunk-7IGKCHDP.js";
36
39
  import {
37
40
  ASCII_TREE_VOCAB,
38
41
  ASCII_VOCAB,
@@ -1033,6 +1036,8 @@ function resolveDocumentTitle(doc, provided) {
1033
1036
  return provided?.trim() ?? "";
1034
1037
  }
1035
1038
  function buildPreviewDoc(doc, options) {
1039
+ doc = applySavedPresentation(doc);
1040
+ if (doc.presentationApplied) options = { ...options, interleaveImages: false };
1036
1041
  const flat = flattenRenderableBlocks(doc.blocks);
1037
1042
  const allImages = collectAllDocImages(doc.blocks);
1038
1043
  const usedImageSrcs = /* @__PURE__ */ new Set();
@@ -4418,6 +4423,16 @@ async function applyNarrationTiming(doc, container) {
4418
4423
  ...doc,
4419
4424
  blocks,
4420
4425
  duration,
4426
+ presentationNarration: {
4427
+ sourceText: timing.sourceText,
4428
+ src: clip.src,
4429
+ startTime: clip.startAt,
4430
+ duration: playedDuration,
4431
+ bookmarks: timing.bookmarks.flatMap((bookmark) => {
4432
+ const time = timeMap ? timeMap.sourceToPlayed(bookmark.time) : bookmark.time;
4433
+ return time !== null && time >= 0 && time <= playedDuration ? [{ ...bookmark, time: clip.startAt + time }] : [];
4434
+ })
4435
+ },
4421
4436
  ...captions ? { captions } : {},
4422
4437
  ...ctx.diagnostics.length > 0 ? { diagnostics: [...doc.diagnostics ?? [], ...ctx.diagnostics] } : {}
4423
4438
  };