@bendyline/squisq 2.11.6 → 2.11.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10494,6 +10494,7 @@ function expandDocBlocks(blocks, options = {}) {
10494
10494
  blocksBySegment.get(segmentIndex).push({ block, originalIndex: index });
10495
10495
  });
10496
10496
  const expandedBlocks = new Array(blocks.length);
10497
+ const splitParts = /* @__PURE__ */ new Map();
10497
10498
  for (const [segmentIndex, segmentBlocks] of blocksBySegment) {
10498
10499
  const audioSegment = audioSegments[segmentIndex];
10499
10500
  if (!audioSegment) {
@@ -10655,6 +10656,7 @@ function expandDocBlocks(blocks, options = {}) {
10655
10656
  const partDuration = block.duration / numParts;
10656
10657
  if (partDuration >= MIN_TRANSITION_GAP) {
10657
10658
  block.duration = partDuration;
10659
+ const parts = [];
10658
10660
  for (let p = 1; p < numParts; p++) {
10659
10661
  const splitBlock = {
10660
10662
  id: `${block.id}-split-${p}`,
@@ -10668,13 +10670,23 @@ function expandDocBlocks(blocks, options = {}) {
10668
10670
  transition: { type: "dissolve", duration: 1 },
10669
10671
  template: block.template
10670
10672
  };
10671
- expandedBlocks.push(splitBlock);
10673
+ parts.push(splitBlock);
10672
10674
  }
10675
+ splitParts.set(block, parts);
10673
10676
  }
10674
10677
  }
10675
10678
  }
10676
10679
  }
10677
10680
  }
10681
+ if (splitParts.size > 0) {
10682
+ const sequenced = [];
10683
+ for (const block of expandedBlocks) {
10684
+ sequenced.push(block);
10685
+ const parts = block ? splitParts.get(block) : void 0;
10686
+ if (parts) sequenced.push(...parts);
10687
+ }
10688
+ expandedBlocks.splice(0, expandedBlocks.length, ...sequenced);
10689
+ }
10678
10690
  return mergeShortBlocks ? expandedBlocks.filter((block) => block && block.duration > 0) : expandedBlocks.filter(Boolean);
10679
10691
  }
10680
10692
  function getAvailableTemplates() {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  flattenRenderableBlocks,
3
3
  getBlockBodyText
4
- } from "./chunk-WAGJK36Q.js";
4
+ } from "./chunk-3YNFMFNK.js";
5
5
  import {
6
6
  estimateSpokenWordCount
7
7
  } from "./chunk-GHOPHGOK.js";
@@ -5,7 +5,7 @@ import {
5
5
  } from "./chunk-SVN7T6BK.js";
6
6
  import {
7
7
  markdownToDoc
8
- } from "./chunk-WAGJK36Q.js";
8
+ } from "./chunk-3YNFMFNK.js";
9
9
  import {
10
10
  cloneAndFreezeData,
11
11
  isTemplateBlock
@@ -512,7 +512,7 @@ function buildBlockSequence(analyzed, selected, config, images, rng) {
512
512
  elementsSinceImage++;
513
513
  if (config.interleaveImages && imageIndex < images.length && elementsSinceImage >= imageInterval) {
514
514
  const img = images[imageIndex];
515
- const imageBlock = {
515
+ const imageBlock2 = {
516
516
  template: "imageWithCaption",
517
517
  id: `transform-img-${blockIdCounter++}`,
518
518
  duration: 4,
@@ -522,7 +522,7 @@ function buildBlockSequence(analyzed, selected, config, images, rng) {
522
522
  ambientMotion: rng.pick(AMBIENT_MOTIONS) ?? "zoomIn",
523
523
  sourceBlockId: ab.block.id
524
524
  };
525
- blocks.push(imageBlock);
525
+ blocks.push(imageBlock2);
526
526
  imageIndex++;
527
527
  insertedCount++;
528
528
  elementsSinceImage = 0;
@@ -846,12 +846,37 @@ function firstTitle(doc) {
846
846
  }
847
847
 
848
848
  // src/transform/narratedSegment.ts
849
+ var DEFAULT_SLIDES_PER_MINUTE = 5;
850
+ var DEFAULT_MAX_SLIDE_DURATION = 15;
851
+ var DEFAULT_MIN_SLIDE_DURATION = 6;
852
+ var DEFAULT_MAX_VIDEOS = 2;
853
+ var AMBIENT_MOTIONS2 = [
854
+ "zoomIn",
855
+ "zoomOut",
856
+ "panLeft",
857
+ "panRight"
858
+ ];
849
859
  function transformNarratedSegment(input, options = {}) {
850
860
  const seed = options.seed ?? hashString(`${input.articleId}-${input.segmentId}`);
851
861
  const heading = input.title.replace(/[\r\n]+/g, " ").trim() || input.segmentId;
852
- const source = markdownToDoc(parseMarkdown(`# ${heading}
862
+ const pacing = {
863
+ slidesPerMinute: options.slidesPerMinute ?? DEFAULT_SLIDES_PER_MINUTE,
864
+ maxSlideDuration: options.maxSlideDuration ?? DEFAULT_MAX_SLIDE_DURATION,
865
+ minSlideDuration: options.minSlideDuration ?? DEFAULT_MIN_SLIDE_DURATION,
866
+ maxVideos: options.maxVideos ?? DEFAULT_MAX_VIDEOS
867
+ };
868
+ const images = uniqueImages(input.images ?? []);
869
+ const videos = input.videos ?? [];
870
+ const contentTarget = contentSlotCount(
871
+ input.duration,
872
+ pacing,
873
+ images.length + Math.min(videos.length, pacing.maxVideos),
874
+ !!input.includeTitleBlock
875
+ );
876
+ const markdown = chunkNarration(input.text, contentTarget).map((chunk) => `# ${heading}
853
877
 
854
- ${input.text}`), {
878
+ ${chunk}`).join("\n\n");
879
+ const source = markdownToDoc(parseMarkdown(markdown), {
855
880
  articleId: `${input.articleId}:${input.segmentId}`,
856
881
  defaultDuration: Math.max(3, input.duration)
857
882
  });
@@ -876,23 +901,41 @@ ${input.text}`), {
876
901
  themeId: options.themeId,
877
902
  registry: options.registry,
878
903
  overrides: {
904
+ // The style's transformRatio is a share of source blocks, and a
905
+ // narrated segment is one block — so it would always cap promotion at
906
+ // a single extraction. Size the cap by content slots instead; the
907
+ // slides-per-minute budget and blocksPerSection still apply.
908
+ transformRatio: (contentTarget + 0.5) / Math.max(1, source.blocks.length),
879
909
  ...options.minConfidence === void 0 ? {} : { minConfidence: options.minConfidence },
880
910
  ...options.slidesPerMinute === void 0 ? {} : { budget: { slidesPerMinute: options.slidesPerMinute } }
881
911
  }
882
912
  });
883
- let blocks = transformed.doc.blocks;
884
- if (input.includeTitleBlock) {
885
- const title = {
886
- template: "title",
887
- id: `${input.segmentId}-title`,
888
- duration: 4,
889
- audioSegment: 0,
890
- title: input.title
891
- };
892
- blocks = [title, ...blocks];
893
- }
894
- blocks = interleaveVideoBlocks(blocks, input.videos ?? [], input.segmentId);
895
- const timedBlocks = allocateTiming(blocks, input.duration).map((block) => ({
913
+ const title = input.includeTitleBlock ? {
914
+ template: "title",
915
+ id: `${input.segmentId}-title`,
916
+ duration: 4,
917
+ audioSegment: 0,
918
+ title: input.title
919
+ } : void 0;
920
+ const transformedBlocks = transformed.doc.blocks;
921
+ const sourceIds = new Set(source.blocks.map((block) => block.id));
922
+ const promoted = transformedBlocks.filter((block) => !sourceIds.has(block.id));
923
+ const kept = promoted.length > 0 ? promoted : transformedBlocks.slice(0, 1);
924
+ const content = kept.map((block) => ({
925
+ ...block,
926
+ id: namespacedId(block.id, input.segmentId)
927
+ }));
928
+ const paced = paceSegment({
929
+ segmentId: input.segmentId,
930
+ duration: input.duration,
931
+ title,
932
+ content,
933
+ videos,
934
+ images,
935
+ pacing,
936
+ rng: new SeededRandom(seed ^ 1540483477)
937
+ });
938
+ const timedBlocks = allocateTiming(paced, input.duration).map((block) => ({
896
939
  ...block,
897
940
  audioSegment: 0
898
941
  }));
@@ -908,33 +951,202 @@ ${input.text}`), {
908
951
  }
909
952
  };
910
953
  }
911
- function interleaveVideoBlocks(blocks, videos, segmentId) {
912
- if (videos.length === 0) return blocks;
913
- const result = [...blocks];
954
+ function paceTarget(duration, pacing) {
955
+ return Math.max(
956
+ 1,
957
+ Math.round(duration / 60 * pacing.slidesPerMinute),
958
+ Math.ceil(duration / pacing.maxSlideDuration)
959
+ );
960
+ }
961
+ function contentSlotCount(duration, pacing, visualCount, hasTitle) {
962
+ const target = paceTarget(duration, pacing);
963
+ const share = visualCount >= 3 ? 0.2 : 0.4;
964
+ return Math.max(1, Math.round(target * share), target - (hasTitle ? 1 : 0) - visualCount);
965
+ }
966
+ function paceSegment(args) {
967
+ const { segmentId, duration, title, content, pacing, rng } = args;
968
+ const fixedCount = (title ? 1 : 0) + content.length;
969
+ const maxCount = Math.max(1, Math.floor(duration / pacing.minSlideDuration));
970
+ const target = paceTarget(duration, pacing);
971
+ const videoCount = args.videos.length === 0 ? 0 : Math.min(
972
+ args.videos.length,
973
+ pacing.maxVideos,
974
+ Math.max(1, Math.floor(target * 0.3)),
975
+ Math.max(0, maxCount - fixedCount)
976
+ );
977
+ const videos = pickVideos(args.videos, videoCount);
978
+ const pool = standaloneImagePool(args.images, content);
979
+ let count = Math.max(target, fixedCount + videos.length);
980
+ let imageCount = 0;
981
+ for (; ; ) {
982
+ imageCount = Math.min(pool.length, Math.max(0, count - fixedCount - videos.length));
983
+ const longest = longestFittedSlide(duration, count, title, content, videos, imageCount, pacing);
984
+ if (longest <= pacing.maxSlideDuration || imageCount >= pool.length || count >= maxCount) {
985
+ break;
986
+ }
987
+ count++;
988
+ }
989
+ const slot = duration / Math.max(1, count);
990
+ const items = [];
991
+ if (title) {
992
+ items.push({
993
+ block: { ...title, duration: Math.min(slot, pacing.minSlideDuration) },
994
+ at: -1,
995
+ rank: 0
996
+ });
997
+ }
998
+ content.forEach((block, index) => {
999
+ const anchor = typeof block.sourceStartTime === "number" ? block.sourceStartTime : (index + 0.5) / content.length * duration;
1000
+ items.push({ block: { ...unanchored(block), duration: slot }, at: anchor, rank: 1 });
1001
+ });
914
1002
  videos.forEach((video, index) => {
915
- const block = {
916
- template: "videoWithCaption",
917
- id: `${segmentId}-video-${index}`,
918
- duration: Math.max(3, video.clipEnd - video.clipStart),
919
- audioSegment: 0,
920
- videoSrc: video.src,
921
- posterSrc: video.posterSrc,
922
- videoAlt: video.alt ?? "",
923
- clipStart: video.clipStart,
924
- clipEnd: video.clipEnd,
925
- sourceDuration: video.sourceDuration,
926
- caption: video.alt,
927
- captionPosition: "bottom",
928
- videoCredit: video.credit,
929
- videoLicense: video.license
930
- };
931
- const insertion = Math.min(
932
- result.length,
933
- Math.round((index + 1) * result.length / (videos.length + 1))
934
- );
935
- result.splice(insertion + index, 0, block);
1003
+ items.push({
1004
+ block: videoBlock(video, index, segmentId, videoNominal(video, slot, pacing)),
1005
+ at: videoPosition(index, videos.length) * duration,
1006
+ rank: 2
1007
+ });
936
1008
  });
937
- return result;
1009
+ for (let index = 0; index < imageCount; index++) {
1010
+ items.push({
1011
+ block: imageBlock(pool[index], index, segmentId, slot, rng),
1012
+ at: (index + 0.5) / imageCount * duration,
1013
+ rank: 3
1014
+ });
1015
+ }
1016
+ return items.map((item, order) => ({ ...item, order })).sort((a, b) => a.at - b.at || a.rank - b.rank || a.order - b.order).map((item) => item.block);
1017
+ }
1018
+ function longestFittedSlide(duration, count, title, content, videos, imageCount, pacing) {
1019
+ const slot = duration / Math.max(1, count);
1020
+ const nominal = [
1021
+ ...title ? [Math.min(slot, pacing.minSlideDuration)] : [],
1022
+ ...content.map(() => slot),
1023
+ ...videos.map((video) => videoNominal(video, slot, pacing)),
1024
+ ...Array.from({ length: imageCount }, () => slot)
1025
+ ];
1026
+ const total = nominal.reduce((sum, d) => sum + d, 0);
1027
+ return total > 0 ? Math.max(...nominal) * (duration / total) : duration;
1028
+ }
1029
+ function videoNominal(video, slot, pacing) {
1030
+ return Math.min(slot, Math.max(pacing.minSlideDuration, video.clipEnd - video.clipStart));
1031
+ }
1032
+ function videoPosition(index, count) {
1033
+ if (count === 1) return 0.4;
1034
+ if (count === 2) return index === 0 ? 0.3 : 0.7;
1035
+ return (index + 1) / (count + 1);
1036
+ }
1037
+ function pickVideos(videos, count) {
1038
+ const picked = [];
1039
+ const seenSources = /* @__PURE__ */ new Set();
1040
+ for (const video of videos) {
1041
+ if (picked.length >= count) break;
1042
+ if (seenSources.has(video.src)) continue;
1043
+ seenSources.add(video.src);
1044
+ picked.push(video);
1045
+ }
1046
+ for (const video of videos) {
1047
+ if (picked.length >= count) break;
1048
+ if (!picked.includes(video)) picked.push(video);
1049
+ }
1050
+ return picked;
1051
+ }
1052
+ function standaloneImagePool(images, content) {
1053
+ const shown = /* @__PURE__ */ new Set();
1054
+ const accents = /* @__PURE__ */ new Set();
1055
+ for (const block of content) {
1056
+ const media = block;
1057
+ if (block.template === "imageWithCaption" && media.imageSrc) shown.add(media.imageSrc);
1058
+ if (media.accentImage?.src) accents.add(media.accentImage.src);
1059
+ if (media.backgroundImage?.src) accents.add(media.backgroundImage.src);
1060
+ }
1061
+ const available = images.filter((image) => !shown.has(image.src));
1062
+ return [
1063
+ ...available.filter((image) => !accents.has(image.src)),
1064
+ ...available.filter((image) => accents.has(image.src))
1065
+ ];
1066
+ }
1067
+ function chunkNarration(text, count) {
1068
+ if (count <= 1) return [text];
1069
+ const sentences = [];
1070
+ const boundary = /[.!?]["'’”)\]]*\s+/g;
1071
+ let start = 0;
1072
+ let match;
1073
+ while ((match = boundary.exec(text)) !== null) {
1074
+ const end = match.index + match[0].length;
1075
+ const sentence = text.slice(start, end).trim();
1076
+ if (sentence) sentences.push(sentence);
1077
+ start = end;
1078
+ }
1079
+ const tail = text.slice(start).trim();
1080
+ if (tail) sentences.push(tail);
1081
+ if (sentences.length < 2) return [text];
1082
+ const chunkCount = Math.min(count, sentences.length);
1083
+ const words = sentences.map((sentence) => sentence.split(/\s+/).length);
1084
+ const totalWords = words.reduce((sum, n) => sum + n, 0);
1085
+ const chunks = [];
1086
+ let current = [];
1087
+ let seenWords = 0;
1088
+ sentences.forEach((sentence, index) => {
1089
+ current.push(sentence);
1090
+ seenWords += words[index];
1091
+ const due = totalWords * (chunks.length + 1) / chunkCount;
1092
+ if (seenWords >= due && chunks.length < chunkCount - 1) {
1093
+ chunks.push(current);
1094
+ current = [];
1095
+ }
1096
+ });
1097
+ if (current.length > 0) chunks.push(current);
1098
+ return chunks.map((chunk) => chunk.join(" "));
1099
+ }
1100
+ function uniqueImages(images) {
1101
+ const seen = /* @__PURE__ */ new Set();
1102
+ return images.filter((image) => {
1103
+ if (!image.src || seen.has(image.src)) return false;
1104
+ seen.add(image.src);
1105
+ return true;
1106
+ });
1107
+ }
1108
+ function namespacedId(id, segmentId) {
1109
+ return id.startsWith(`${segmentId}-`) ? id : `${segmentId}-${id}`;
1110
+ }
1111
+ function unanchored(block) {
1112
+ const { sourceStartTime: _start, sourceDuration: _duration, ...rest } = block;
1113
+ return rest;
1114
+ }
1115
+ function imageBlock(image, index, segmentId, duration, rng) {
1116
+ const caption = image.alt && !/\.(jpe?g|png|gif|svg|webp)/i.test(image.alt) ? image.alt : "";
1117
+ return {
1118
+ template: "imageWithCaption",
1119
+ id: `${segmentId}-image-${index}`,
1120
+ duration,
1121
+ audioSegment: 0,
1122
+ imageSrc: image.src,
1123
+ imageAlt: image.alt ?? "",
1124
+ caption,
1125
+ captionPosition: "bottom",
1126
+ ambientMotion: rng.pick(AMBIENT_MOTIONS2) ?? "zoomIn",
1127
+ imageCredit: image.credit,
1128
+ imageLicense: image.license
1129
+ };
1130
+ }
1131
+ function videoBlock(video, index, segmentId, duration) {
1132
+ return {
1133
+ template: "videoWithCaption",
1134
+ id: `${segmentId}-video-${index}`,
1135
+ duration,
1136
+ audioSegment: 0,
1137
+ videoSrc: video.src,
1138
+ posterSrc: video.posterSrc,
1139
+ videoAlt: video.alt ?? "",
1140
+ clipStart: video.clipStart,
1141
+ clipEnd: video.clipEnd,
1142
+ // The clip's source file length (VideoWithCaptionInput's meaning), not a
1143
+ // narration timing hint.
1144
+ sourceDuration: video.sourceDuration,
1145
+ caption: video.alt,
1146
+ captionPosition: "bottom",
1147
+ videoCredit: video.credit,
1148
+ videoLicense: video.license
1149
+ };
938
1150
  }
939
1151
 
940
1152
  export {
@@ -2,10 +2,10 @@ import {
2
2
  buildNarrationScript,
3
3
  expectedSyllablesAt,
4
4
  wordPosAtExpectedSyllables
5
- } from "./chunk-764WHWKM.js";
5
+ } from "./chunk-EF353KGH.js";
6
6
  import {
7
7
  flattenRenderableBlocks
8
- } from "./chunk-WAGJK36Q.js";
8
+ } from "./chunk-3YNFMFNK.js";
9
9
 
10
10
  // src/narration/types.ts
11
11
  var DEFAULT_FEATURE_CONFIG = Object.freeze({
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  buildNarrationScript,
3
3
  parseNarrationTimingJson
4
- } from "./chunk-764WHWKM.js";
4
+ } from "./chunk-EF353KGH.js";
5
5
  import {
6
6
  SHAPE_NAMES,
7
7
  buildChartData,
@@ -32,7 +32,7 @@ import {
32
32
  templateRegistry,
33
33
  writeCustomTemplatesToFrontmatter,
34
34
  writeCustomThemesToFrontmatter
35
- } from "./chunk-WAGJK36Q.js";
35
+ } from "./chunk-3YNFMFNK.js";
36
36
  import {
37
37
  iconMarker
38
38
  } from "./chunk-7ZAAICW4.js";
@@ -29,10 +29,11 @@ function getSegmentAtTime(audio, time) {
29
29
  }
30
30
  return audio.segments.length - 1;
31
31
  }
32
+ var BLOCK_START_TOLERANCE = 1e-3;
32
33
  function getBlockAtTime(blocks, time) {
33
34
  for (let i = blocks.length - 1; i >= 0; i--) {
34
35
  const block = blocks[i];
35
- if (time >= block.startTime && time < block.startTime + block.duration) {
36
+ if (time + BLOCK_START_TOLERANCE >= block.startTime && time < block.startTime + block.duration) {
36
37
  return block;
37
38
  }
38
39
  }
package/dist/doc/index.js CHANGED
@@ -70,8 +70,8 @@ import {
70
70
  validateMarkdownDoc,
71
71
  validateMarkdownSource,
72
72
  writeDashboardLayoutsToFrontmatter
73
- } from "../chunk-GRGLZF4A.js";
74
- import "../chunk-764WHWKM.js";
73
+ } from "../chunk-SOIV7KDO.js";
74
+ import "../chunk-EF353KGH.js";
75
75
  import {
76
76
  ASCII_CHAR_H,
77
77
  ASCII_CHAR_W,
@@ -193,7 +193,7 @@ import {
193
193
  wrapWithPersistentLayers,
194
194
  writeCustomTemplatesToFrontmatter,
195
195
  writeCustomThemesToFrontmatter
196
- } from "../chunk-WAGJK36Q.js";
196
+ } from "../chunk-3YNFMFNK.js";
197
197
  import {
198
198
  PATH_SHAPE_KINDS,
199
199
  anchorPoint,
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  recordSlideShown,
28
28
  traceWordPosAt,
29
29
  vadStep
30
- } from "./chunk-UUI35INI.js";
30
+ } from "./chunk-SKKBRLLM.js";
31
31
  import {
32
32
  fenceRendererLangs
33
33
  } from "./chunk-PGI7HSWE.js";
@@ -65,7 +65,7 @@ import {
65
65
  getTransformStyleSummaries,
66
66
  resolveTransformStyle,
67
67
  transformNarratedSegment
68
- } from "./chunk-J2IPSQKP.js";
68
+ } from "./chunk-IYZTF4VO.js";
69
69
  import {
70
70
  extractContent,
71
71
  mapElementToBlock,
@@ -151,7 +151,7 @@ import {
151
151
  pipStyleVars,
152
152
  serializeTheme,
153
153
  validateDocSchema
154
- } from "./chunk-BUJ5RV2P.js";
154
+ } from "./chunk-YGGYYBJS.js";
155
155
  import {
156
156
  calculateBearing,
157
157
  decodeGeohash,
@@ -236,7 +236,7 @@ import {
236
236
  validateMarkdownDoc,
237
237
  validateMarkdownSource,
238
238
  writeDashboardLayoutsToFrontmatter
239
- } from "./chunk-GRGLZF4A.js";
239
+ } from "./chunk-SOIV7KDO.js";
240
240
  import {
241
241
  buildNarrationScript,
242
242
  buildNarrationTimingJson,
@@ -246,7 +246,7 @@ import {
246
246
  wordIndexAtChar,
247
247
  wordIndexAtTime,
248
248
  wordPosAtExpectedSyllables
249
- } from "./chunk-764WHWKM.js";
249
+ } from "./chunk-EF353KGH.js";
250
250
  import {
251
251
  ASCII_CHAR_H,
252
252
  ASCII_CHAR_W,
@@ -368,7 +368,7 @@ import {
368
368
  wrapWithPersistentLayers,
369
369
  writeCustomTemplatesToFrontmatter,
370
370
  writeCustomThemesToFrontmatter
371
- } from "./chunk-WAGJK36Q.js";
371
+ } from "./chunk-3YNFMFNK.js";
372
372
  import {
373
373
  PATH_SHAPE_KINDS,
374
374
  anchorPoint,
@@ -16,7 +16,7 @@ import {
16
16
  setByPointer,
17
17
  toPointer
18
18
  } from "../chunk-VU4T74LI.js";
19
- import "../chunk-BUJ5RV2P.js";
19
+ import "../chunk-YGGYYBJS.js";
20
20
  import "../chunk-GAZKTT4R.js";
21
21
  import "../chunk-Y4D6TDZT.js";
22
22
  import "../chunk-4VOD55SX.js";
@@ -27,7 +27,7 @@ import {
27
27
  recordSlideShown,
28
28
  traceWordPosAt,
29
29
  vadStep
30
- } from "../chunk-UUI35INI.js";
30
+ } from "../chunk-SKKBRLLM.js";
31
31
  import {
32
32
  buildNarrationScript,
33
33
  buildNarrationTimingJson,
@@ -37,8 +37,8 @@ import {
37
37
  wordIndexAtChar,
38
38
  wordIndexAtTime,
39
39
  wordPosAtExpectedSyllables
40
- } from "../chunk-764WHWKM.js";
41
- import "../chunk-WAGJK36Q.js";
40
+ } from "../chunk-EF353KGH.js";
41
+ import "../chunk-3YNFMFNK.js";
42
42
  import "../chunk-PUS54YU6.js";
43
43
  import "../chunk-C3NAY5YP.js";
44
44
  import "../chunk-Y4D6TDZT.js";
@@ -13,7 +13,7 @@ import {
13
13
  pipStyleVars,
14
14
  serializeTheme,
15
15
  validateDocSchema
16
- } from "../chunk-BUJ5RV2P.js";
16
+ } from "../chunk-YGGYYBJS.js";
17
17
  import {
18
18
  defaultPageStyle,
19
19
  getDocPlaybackDuration,
@@ -151,6 +151,26 @@ declare function applyTransform(doc: Doc, style: TransformStyleInput, options?:
151
151
  * Converts a narrated text segment and its media into a timed Doc using the
152
152
  * canonical squisq transform pipeline. This is the reusable bridge for hosts
153
153
  * that generate one visual sequence per narration segment.
154
+ *
155
+ * Pacing: a narrated segment is a single source block, so the text transform
156
+ * alone promotes only one or two extractions — a 60s segment would become one
157
+ * 60s slide, which timed players (`expandDocBlocks`) then split into repeated
158
+ * copies of that same slide. `paceSegment()` restores a visual rhythm: it aims
159
+ * for `slidesPerMinute`, places up to `maxVideos` clips, and fills the rest
160
+ * with standalone image slides until no slide runs past `maxSlideDuration`
161
+ * (or the images run out). Image-poor segments get more text slides instead:
162
+ * their narration is split into sentence runs, one source block each, since
163
+ * the transform promotes at most one extraction per type from a block.
164
+ *
165
+ * The output timeline is sequential and unanchored: content blocks are ORDERED
166
+ * by where their extraction is spoken, but carry no `sourceStartTime`. When a
167
+ * segment mixes anchored and floating blocks, `expandDocBlocks()` hoists every
168
+ * anchored block ahead of the floating ones and stretches whatever precedes
169
+ * the first anchor (typically a host's section header), so a fully sequential
170
+ * segment is the only shape that survives re-timing by hosts and players.
171
+ *
172
+ * Block ids are prefixed with the segment id so segments composed into one
173
+ * Doc never collide (the transform itself numbers from `transform-0`).
154
174
  */
155
175
 
156
176
  interface NarratedSegmentVideo {
@@ -179,7 +199,21 @@ interface NarratedSegmentTransformOptions extends Pick<TransformOptions, 'themeI
179
199
  style?: TransformStyleInput;
180
200
  seed?: number;
181
201
  minConfidence?: number;
202
+ /**
203
+ * Visual pace target. Also caps the text transform's promotions when set.
204
+ * Pacing defaults to 5 when omitted.
205
+ */
182
206
  slidesPerMinute?: number;
207
+ /** Longest a slide should hold before another visual is added. Default 15s. */
208
+ maxSlideDuration?: number;
209
+ /**
210
+ * Shortest slide the pacer schedules; bounds the slide count so timed
211
+ * players don't merge slides back together (they fold anything under ~5s).
212
+ * Default 6s.
213
+ */
214
+ minSlideDuration?: number;
215
+ /** Most video clips placed in one segment. Default 2. */
216
+ maxVideos?: number;
183
217
  }
184
218
  interface NarratedSegmentResult extends TransformResult {
185
219
  seed: number;
@@ -9,9 +9,9 @@ import {
9
9
  getTransformStyleSummaries,
10
10
  resolveTransformStyle,
11
11
  transformNarratedSegment
12
- } from "../chunk-J2IPSQKP.js";
12
+ } from "../chunk-IYZTF4VO.js";
13
13
  import "../chunk-SVN7T6BK.js";
14
- import "../chunk-WAGJK36Q.js";
14
+ import "../chunk-3YNFMFNK.js";
15
15
  import "../chunk-PUS54YU6.js";
16
16
  import "../chunk-C3NAY5YP.js";
17
17
  import "../chunk-Y4D6TDZT.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bendyline/squisq",
3
- "version": "2.11.6",
3
+ "version": "2.11.7",
4
4
  "description": "Headless utilities for doc/block rendering, spatial math, Markdown, and storage",
5
5
  "license": "MIT",
6
6
  "author": "Bendyline",