@bendyline/squisq 2.11.5 → 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.
@@ -2,18 +2,24 @@ import {
2
2
  extractContent,
3
3
  mapElementToBlock,
4
4
  stripMarkdown
5
- } from "./chunk-NKNMGIIN.js";
5
+ } from "./chunk-SVN7T6BK.js";
6
+ import {
7
+ markdownToDoc
8
+ } from "./chunk-3YNFMFNK.js";
6
9
  import {
7
10
  cloneAndFreezeData,
8
11
  isTemplateBlock
9
- } from "./chunk-BAOV476U.js";
12
+ } from "./chunk-Y4D6TDZT.js";
13
+ import {
14
+ parseMarkdown
15
+ } from "./chunk-CW24CH7I.js";
10
16
  import {
11
17
  extractPlainText,
12
18
  getChildren
13
19
  } from "./chunk-X7BNDUNI.js";
14
20
  import {
15
21
  estimateTimeFromText
16
- } from "./chunk-67L6WRJN.js";
22
+ } from "./chunk-GHOPHGOK.js";
17
23
  import {
18
24
  SeededRandom,
19
25
  hashString
@@ -506,7 +512,7 @@ function buildBlockSequence(analyzed, selected, config, images, rng) {
506
512
  elementsSinceImage++;
507
513
  if (config.interleaveImages && imageIndex < images.length && elementsSinceImage >= imageInterval) {
508
514
  const img = images[imageIndex];
509
- const imageBlock = {
515
+ const imageBlock2 = {
510
516
  template: "imageWithCaption",
511
517
  id: `transform-img-${blockIdCounter++}`,
512
518
  duration: 4,
@@ -516,7 +522,7 @@ function buildBlockSequence(analyzed, selected, config, images, rng) {
516
522
  ambientMotion: rng.pick(AMBIENT_MOTIONS) ?? "zoomIn",
517
523
  sourceBlockId: ab.block.id
518
524
  };
519
- blocks.push(imageBlock);
525
+ blocks.push(imageBlock2);
520
526
  imageIndex++;
521
527
  insertedCount++;
522
528
  elementsSinceImage = 0;
@@ -839,6 +845,310 @@ function firstTitle(doc) {
839
845
  return void 0;
840
846
  }
841
847
 
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
+ ];
859
+ function transformNarratedSegment(input, options = {}) {
860
+ const seed = options.seed ?? hashString(`${input.articleId}-${input.segmentId}`);
861
+ const heading = input.title.replace(/[\r\n]+/g, " ").trim() || input.segmentId;
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}
877
+
878
+ ${chunk}`).join("\n\n");
879
+ const source = markdownToDoc(parseMarkdown(markdown), {
880
+ articleId: `${input.articleId}:${input.segmentId}`,
881
+ defaultDuration: Math.max(3, input.duration)
882
+ });
883
+ const sourceDoc = {
884
+ ...source,
885
+ articleId: input.articleId,
886
+ duration: input.duration,
887
+ audio: {
888
+ segments: [
889
+ {
890
+ src: input.audioSrc,
891
+ name: input.audioName ?? input.segmentId,
892
+ duration: input.duration,
893
+ startTime: 0
894
+ }
895
+ ]
896
+ }
897
+ };
898
+ const transformed = applyTransform(sourceDoc, options.style ?? "documentary", {
899
+ seed,
900
+ images: input.images,
901
+ themeId: options.themeId,
902
+ registry: options.registry,
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),
909
+ ...options.minConfidence === void 0 ? {} : { minConfidence: options.minConfidence },
910
+ ...options.slidesPerMinute === void 0 ? {} : { budget: { slidesPerMinute: options.slidesPerMinute } }
911
+ }
912
+ });
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) => ({
939
+ ...block,
940
+ audioSegment: 0
941
+ }));
942
+ return {
943
+ ...transformed,
944
+ seed,
945
+ doc: {
946
+ ...transformed.doc,
947
+ articleId: input.articleId,
948
+ duration: input.duration,
949
+ audio: sourceDoc.audio,
950
+ blocks: timedBlocks
951
+ }
952
+ };
953
+ }
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
+ });
1002
+ videos.forEach((video, index) => {
1003
+ items.push({
1004
+ block: videoBlock(video, index, segmentId, videoNominal(video, slot, pacing)),
1005
+ at: videoPosition(index, videos.length) * duration,
1006
+ rank: 2
1007
+ });
1008
+ });
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
+ };
1150
+ }
1151
+
842
1152
  export {
843
1153
  DEFAULT_TRANSFORM_STYLE_ID,
844
1154
  createTransformStyleRegistry,
@@ -847,5 +1157,7 @@ export {
847
1157
  getTransformStyleSummaries,
848
1158
  extractDocImages,
849
1159
  analyzeBlocks,
850
- applyTransform
1160
+ allocateTiming,
1161
+ applyTransform,
1162
+ transformNarratedSegment
851
1163
  };
@@ -1,9 +1,11 @@
1
1
  import {
2
2
  buildNarrationScript,
3
3
  expectedSyllablesAt,
4
- flattenRenderableBlocks,
5
4
  wordPosAtExpectedSyllables
6
- } from "./chunk-AIB5WEK2.js";
5
+ } from "./chunk-EF353KGH.js";
6
+ import {
7
+ flattenRenderableBlocks
8
+ } from "./chunk-3YNFMFNK.js";
7
9
 
8
10
  // src/narration/types.ts
9
11
  var DEFAULT_FEATURE_CONFIG = Object.freeze({
@@ -1,7 +1,10 @@
1
+ import {
2
+ buildNarrationScript,
3
+ parseNarrationTimingJson
4
+ } from "./chunk-EF353KGH.js";
1
5
  import {
2
6
  SHAPE_NAMES,
3
7
  buildChartData,
4
- buildNarrationScript,
5
8
  buildRegistry,
6
9
  coerceTemplateParams,
7
10
  deriveTemplateInputs,
@@ -22,7 +25,6 @@ import {
22
25
  nearestName,
23
26
  normalizeShapeKind,
24
27
  parseDataFence,
25
- parseNarrationTimingJson,
26
28
  parseStandaloneAnnotation,
27
29
  placeLayersInRect,
28
30
  readCustomThemesFromFrontmatter,
@@ -30,7 +32,7 @@ import {
30
32
  templateRegistry,
31
33
  writeCustomTemplatesToFrontmatter,
32
34
  writeCustomThemesToFrontmatter
33
- } from "./chunk-AIB5WEK2.js";
35
+ } from "./chunk-3YNFMFNK.js";
34
36
  import {
35
37
  iconMarker
36
38
  } from "./chunk-7ZAAICW4.js";
@@ -59,21 +61,19 @@ import {
59
61
  DEFAULT_THEME,
60
62
  FRONTMATTER_CUSTOM_TEMPLATES_KEY,
61
63
  FRONTMATTER_CUSTOM_THEMES_KEY,
64
+ VIEWPORT_PRESETS,
65
+ calculateFontScale,
66
+ createTemplateContext,
67
+ getViewportOrientation,
62
68
  hexHueDegrees,
69
+ isTemplateBlock,
63
70
  oklchDarken,
64
71
  oklchLighten,
65
72
  relativeLuminance,
66
73
  resolveFontFamily,
67
74
  resolveTheme,
68
75
  withAlpha
69
- } from "./chunk-RXYKU6EX.js";
70
- import {
71
- VIEWPORT_PRESETS,
72
- calculateFontScale,
73
- createTemplateContext,
74
- getViewportOrientation,
75
- isTemplateBlock
76
- } from "./chunk-BAOV476U.js";
76
+ } from "./chunk-Y4D6TDZT.js";
77
77
  import {
78
78
  parseMarkdown
79
79
  } from "./chunk-CW24CH7I.js";
@@ -94,7 +94,7 @@ import {
94
94
  } from "./chunk-X7BNDUNI.js";
95
95
  import {
96
96
  estimateTimeFromText
97
- } from "./chunk-67L6WRJN.js";
97
+ } from "./chunk-GHOPHGOK.js";
98
98
 
99
99
  // src/doc/utils/animationUtils.ts
100
100
  function getAnimationStyle(animation, _currentTime = 0) {
@@ -4216,6 +4216,7 @@ function autoMatchBlocks(blocks, audioFiles) {
4216
4216
  async function resolveAudioMapping(doc, container) {
4217
4217
  const narration = await applyNarrationTiming(doc, container);
4218
4218
  doc = narration.doc;
4219
+ if (doc.audio?.segments?.length > 0) return doc;
4219
4220
  const audioFiles = await discoverNarrationAudio(container);
4220
4221
  const scheduledSrcs = new Set(resolveMediaSchedule(doc).map((clip) => clip.src));
4221
4222
  const available = audioFiles.filter((f) => !scheduledSrcs.has(f.path));
@@ -538,9 +538,6 @@ function cleanDescription(sentence, value) {
538
538
  for (const pattern of prepositionPatterns) {
539
539
  desc = desc.replace(pattern, "");
540
540
  }
541
- if (desc === sentence) {
542
- desc = sentence.replace(value, "").trim();
543
- }
544
541
  desc = desc.replace(/^[,:\-–—]+\s*/, "").replace(/\s*[,:\-–—]+$/, "");
545
542
  desc = desc.replace(
546
543
  /\s+(?:of|on|in|at|by|to|for|from|with|about|over|under|between|than|into|onto|upon)\s*([,.])/gi,
@@ -2,7 +2,7 @@ import {
2
2
  DEFAULT_THEME,
3
3
  applySurface,
4
4
  resolveFontFamily
5
- } from "./chunk-RXYKU6EX.js";
5
+ } from "./chunk-Y4D6TDZT.js";
6
6
 
7
7
  // src/jsonForm/chooseControl.ts
8
8
  var ENUM_SEGMENTED_LIMIT = 4;