@bendyline/squisq 2.11.6 → 2.11.8
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.
- package/dist/{Doc-BkxT7G80.d.ts → Doc-CynsGqL4.d.ts} +208 -1
- package/dist/{ImageEditDoc-C8ZB54Vv.d.ts → ImageEditDoc-BVZe_5y4.d.ts} +1 -1
- package/dist/{chunk-UUI35INI.js → chunk-5FBI3ZW4.js} +7 -42
- package/dist/{chunk-BUJ5RV2P.js → chunk-JJO6JFNZ.js} +3 -2
- package/dist/{chunk-GRGLZF4A.js → chunk-JM2QARY2.js} +42 -10
- package/dist/chunk-JSGIWW35.js +70 -0
- package/dist/{chunk-J2IPSQKP.js → chunk-KFZOCWPK.js} +256 -44
- package/dist/chunk-L7QZORLB.js +230 -0
- package/dist/chunk-NEWNNLWN.js +64 -0
- package/dist/{chunk-6ECMNUIP.js → chunk-OU5FYBOB.js} +170 -4
- package/dist/{chunk-WAGJK36Q.js → chunk-RMCUZDW5.js} +30 -9
- package/dist/{chunk-XJXBI3N5.js → chunk-XZVBR5BP.js} +1 -1
- package/dist/{chunk-GAZKTT4R.js → chunk-YNO6JV36.js} +96 -33
- package/dist/{chunk-764WHWKM.js → chunk-ZEONJCP3.js} +1 -1
- package/dist/doc/index.d.ts +5 -5
- package/dist/doc/index.js +22 -20
- package/dist/fence/index.d.ts +1 -1
- package/dist/generate/index.d.ts +1 -1
- package/dist/imageEdit/index.d.ts +47 -4
- package/dist/imageEdit/index.js +5 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +48 -39
- package/dist/jsonForm/index.d.ts +1 -1
- package/dist/jsonForm/index.js +4 -2
- package/dist/markdown/index.js +3 -3
- package/dist/{materializePageSection-Dp2aHFbc.d.ts → materializePageSection-CAJoa8fh.d.ts} +1 -1
- package/dist/mediaEdit/index.d.ts +467 -0
- package/dist/mediaEdit/index.js +990 -0
- package/dist/narration/index.d.ts +20 -12
- package/dist/narration/index.js +6 -4
- package/dist/schemas/index.d.ts +4 -4
- package/dist/schemas/index.js +7 -3
- package/dist/{themeLibrary-BbSjnGmd.d.ts → themeLibrary-z7t3x5CU.d.ts} +1 -1
- package/dist/timing/index.d.ts +1 -1
- package/dist/transform/index.d.ts +36 -2
- package/dist/transform/index.js +6 -5
- package/package.json +6 -1
- package/dist/{chunk-CW24CH7I.js → chunk-HUXK7EOT.js} +3 -3
|
@@ -5,14 +5,14 @@ import {
|
|
|
5
5
|
} from "./chunk-SVN7T6BK.js";
|
|
6
6
|
import {
|
|
7
7
|
markdownToDoc
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-RMCUZDW5.js";
|
|
9
9
|
import {
|
|
10
10
|
cloneAndFreezeData,
|
|
11
11
|
isTemplateBlock
|
|
12
12
|
} from "./chunk-Y4D6TDZT.js";
|
|
13
13
|
import {
|
|
14
14
|
parseMarkdown
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-HUXK7EOT.js";
|
|
16
16
|
import {
|
|
17
17
|
extractPlainText,
|
|
18
18
|
getChildren
|
|
@@ -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
|
|
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(
|
|
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
|
|
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
|
-
${
|
|
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
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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
|
|
912
|
-
|
|
913
|
-
|
|
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
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
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
|
-
|
|
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 {
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// src/mediaEdit/recipe.ts
|
|
2
|
+
var MEDIA_FX_ORDER = ["highpass", "denoise", "debreath", "loudness"];
|
|
3
|
+
var MEDIA_FX_SPECS = Object.freeze({
|
|
4
|
+
highpass: { defaultValue: 80, min: 20, max: 300 },
|
|
5
|
+
denoise: { defaultValue: 0.8, min: 0, max: 1 },
|
|
6
|
+
debreath: { defaultValue: -15, min: -40, max: 0 },
|
|
7
|
+
loudness: { defaultValue: -16, min: -31, max: -5 }
|
|
8
|
+
});
|
|
9
|
+
var MEDIA_EDIT_PARAMS = [
|
|
10
|
+
{ key: "fx", htmlSuffix: "fx" },
|
|
11
|
+
{ key: "cuts", htmlSuffix: "cuts" },
|
|
12
|
+
{ key: "gain", htmlSuffix: "gain" },
|
|
13
|
+
{ key: "fadeIn", htmlSuffix: "fade-in" },
|
|
14
|
+
{ key: "fadeOut", htmlSuffix: "fade-out" },
|
|
15
|
+
{ key: "crop", htmlSuffix: "crop" },
|
|
16
|
+
{ key: "group", htmlSuffix: "group" }
|
|
17
|
+
];
|
|
18
|
+
function mediaEditHtmlAttribute(kind, key) {
|
|
19
|
+
const param = MEDIA_EDIT_PARAMS.find((p) => p.key === key);
|
|
20
|
+
return `data-squisq-${kind}-${param?.htmlSuffix ?? key}`;
|
|
21
|
+
}
|
|
22
|
+
function formatMediaNumber(value, digits = 3) {
|
|
23
|
+
const rounded = Number(value.toFixed(digits));
|
|
24
|
+
return Object.is(rounded, -0) ? "0" : String(rounded);
|
|
25
|
+
}
|
|
26
|
+
function finiteNumber(raw) {
|
|
27
|
+
if (raw == null) return null;
|
|
28
|
+
const trimmed = raw.trim();
|
|
29
|
+
if (!/^[-+]?(\d+(\.\d*)?|\.\d+)$/.test(trimmed)) return null;
|
|
30
|
+
const n = Number(trimmed);
|
|
31
|
+
return Number.isFinite(n) ? n : null;
|
|
32
|
+
}
|
|
33
|
+
function clamp(value, min, max) {
|
|
34
|
+
return Math.min(max, Math.max(min, value));
|
|
35
|
+
}
|
|
36
|
+
function parseSeconds(raw) {
|
|
37
|
+
const trimmed = raw.trim();
|
|
38
|
+
const bare = finiteNumber(trimmed);
|
|
39
|
+
if (bare != null) return bare >= 0 ? bare : null;
|
|
40
|
+
const ms = trimmed.match(/^(\d+(?:\.\d+)?)ms$/i);
|
|
41
|
+
if (ms) return Number(ms[1]) / 1e3;
|
|
42
|
+
const mmss = trimmed.match(/^(\d+):([0-5]?\d)(?:\.(\d+))?$/);
|
|
43
|
+
if (mmss) return Number(mmss[1]) * 60 + Number(mmss[2]) + Number(`0.${mmss[3] ?? "0"}`);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
function isFxOpId(name) {
|
|
47
|
+
return MEDIA_FX_ORDER.includes(name);
|
|
48
|
+
}
|
|
49
|
+
function parseMediaFx(raw) {
|
|
50
|
+
if (raw == null) return null;
|
|
51
|
+
const tokens = raw.split(/[\s,]+/).filter(Boolean);
|
|
52
|
+
if (tokens.length === 0) return null;
|
|
53
|
+
const byId = /* @__PURE__ */ new Map();
|
|
54
|
+
const unknown = [];
|
|
55
|
+
for (const token of tokens) {
|
|
56
|
+
const colon = token.indexOf(":");
|
|
57
|
+
const name = (colon === -1 ? token : token.slice(0, colon)).toLowerCase();
|
|
58
|
+
if (!isFxOpId(name)) {
|
|
59
|
+
unknown.push(token);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const spec = MEDIA_FX_SPECS[name];
|
|
63
|
+
let value = spec.defaultValue;
|
|
64
|
+
if (colon !== -1) {
|
|
65
|
+
value = finiteNumber(token.slice(colon + 1));
|
|
66
|
+
if (value != null && name === "debreath") value = -Math.abs(value);
|
|
67
|
+
}
|
|
68
|
+
if (value == null) {
|
|
69
|
+
unknown.push(token);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
byId.set(name, clamp(value, spec.min, spec.max));
|
|
73
|
+
}
|
|
74
|
+
const ops = MEDIA_FX_ORDER.filter((id) => byId.has(id)).map((id) => ({
|
|
75
|
+
id,
|
|
76
|
+
value: byId.get(id)
|
|
77
|
+
}));
|
|
78
|
+
return { ops, unknown };
|
|
79
|
+
}
|
|
80
|
+
function serializeMediaFx(chain) {
|
|
81
|
+
return [
|
|
82
|
+
...chain.ops.map((op) => `${op.id}:${formatMediaNumber(op.value)}`),
|
|
83
|
+
...chain.unknown
|
|
84
|
+
].join(" ");
|
|
85
|
+
}
|
|
86
|
+
function hasRenderableFx(chain) {
|
|
87
|
+
return chain != null && chain.ops.length > 0;
|
|
88
|
+
}
|
|
89
|
+
var CUT_EPSILON = 1e-3;
|
|
90
|
+
function normalizeMediaCuts(cuts, windowStart = 0, windowEnd = Number.POSITIVE_INFINITY) {
|
|
91
|
+
const clamped = cuts.filter((c) => Number.isFinite(c.start) && Number.isFinite(c.end)).map((c) => ({
|
|
92
|
+
start: Math.max(windowStart, Math.min(c.start, c.end)),
|
|
93
|
+
end: Math.min(windowEnd, Math.max(c.start, c.end))
|
|
94
|
+
})).filter((c) => c.end - c.start >= CUT_EPSILON).sort((a, b) => a.start - b.start);
|
|
95
|
+
const merged = [];
|
|
96
|
+
for (const cut of clamped) {
|
|
97
|
+
const last = merged[merged.length - 1];
|
|
98
|
+
if (last && cut.start <= last.end + CUT_EPSILON) {
|
|
99
|
+
last.end = Math.max(last.end, cut.end);
|
|
100
|
+
} else {
|
|
101
|
+
merged.push({ ...cut });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return merged;
|
|
105
|
+
}
|
|
106
|
+
function parseMediaCuts(raw) {
|
|
107
|
+
if (raw == null) return null;
|
|
108
|
+
const cuts = [];
|
|
109
|
+
for (const token of raw.split(/[\s,]+/).filter(Boolean)) {
|
|
110
|
+
const dash = token.indexOf("-", 1);
|
|
111
|
+
if (dash === -1) continue;
|
|
112
|
+
const start = parseSeconds(token.slice(0, dash));
|
|
113
|
+
const end = parseSeconds(token.slice(dash + 1));
|
|
114
|
+
if (start == null || end == null || end <= start) continue;
|
|
115
|
+
cuts.push({ start, end });
|
|
116
|
+
}
|
|
117
|
+
const normalized = normalizeMediaCuts(cuts);
|
|
118
|
+
return normalized.length > 0 ? normalized : null;
|
|
119
|
+
}
|
|
120
|
+
function serializeMediaCuts(cuts) {
|
|
121
|
+
return normalizeMediaCuts(cuts).map((c) => `${formatMediaNumber(c.start)}-${formatMediaNumber(c.end)}`).join(" ");
|
|
122
|
+
}
|
|
123
|
+
var MIN_CROP_EDGE = 0.02;
|
|
124
|
+
function parseMediaCrop(raw) {
|
|
125
|
+
if (raw == null) return null;
|
|
126
|
+
const parts = raw.split(/[\s,]+/).filter(Boolean).map((p) => finiteNumber(p));
|
|
127
|
+
if (parts.length !== 4 || parts.some((p) => p == null)) return null;
|
|
128
|
+
return normalizeMediaCrop({
|
|
129
|
+
x: parts[0],
|
|
130
|
+
y: parts[1],
|
|
131
|
+
w: parts[2],
|
|
132
|
+
h: parts[3]
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
function normalizeMediaCrop(crop) {
|
|
136
|
+
const x = clamp(crop.x, 0, 1 - MIN_CROP_EDGE);
|
|
137
|
+
const y = clamp(crop.y, 0, 1 - MIN_CROP_EDGE);
|
|
138
|
+
const w = clamp(crop.w, MIN_CROP_EDGE, 1 - x);
|
|
139
|
+
const h = clamp(crop.h, MIN_CROP_EDGE, 1 - y);
|
|
140
|
+
const full = x < 1e-4 && y < 1e-4 && w > 1 - 1e-4 && h > 1 - 1e-4;
|
|
141
|
+
return full ? null : { x, y, w, h };
|
|
142
|
+
}
|
|
143
|
+
function serializeMediaCrop(crop) {
|
|
144
|
+
return [crop.x, crop.y, crop.w, crop.h].map((n) => formatMediaNumber(n, 4)).join(" ");
|
|
145
|
+
}
|
|
146
|
+
var GAIN_RANGE = { min: -60, max: 24 };
|
|
147
|
+
var GROUP_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
148
|
+
function parseMediaEdits(values) {
|
|
149
|
+
const edits = {};
|
|
150
|
+
const fx = parseMediaFx(values.fx);
|
|
151
|
+
if (fx && (fx.ops.length > 0 || fx.unknown.length > 0)) edits.fx = fx;
|
|
152
|
+
const cuts = parseMediaCuts(values.cuts);
|
|
153
|
+
if (cuts) edits.cuts = cuts;
|
|
154
|
+
const gain = finiteNumber(values.gain);
|
|
155
|
+
if (gain != null && gain !== 0) edits.gain = clamp(gain, GAIN_RANGE.min, GAIN_RANGE.max);
|
|
156
|
+
const fadeIn = values.fadeIn == null ? null : parseSeconds(values.fadeIn);
|
|
157
|
+
if (fadeIn != null && fadeIn > 0) edits.fadeIn = fadeIn;
|
|
158
|
+
const fadeOut = values.fadeOut == null ? null : parseSeconds(values.fadeOut);
|
|
159
|
+
if (fadeOut != null && fadeOut > 0) edits.fadeOut = fadeOut;
|
|
160
|
+
const crop = parseMediaCrop(values.crop);
|
|
161
|
+
if (crop) edits.crop = crop;
|
|
162
|
+
const group = values.group?.trim();
|
|
163
|
+
if (group && GROUP_RE.test(group)) edits.group = group;
|
|
164
|
+
return Object.keys(edits).length > 0 ? edits : void 0;
|
|
165
|
+
}
|
|
166
|
+
function serializeMediaEdits(edits) {
|
|
167
|
+
const fx = edits?.fx && (edits.fx.ops.length > 0 || edits.fx.unknown.length > 0);
|
|
168
|
+
return {
|
|
169
|
+
fx: fx && edits?.fx ? serializeMediaFx(edits.fx) : null,
|
|
170
|
+
cuts: edits?.cuts && edits.cuts.length > 0 ? serializeMediaCuts(edits.cuts) : null,
|
|
171
|
+
gain: edits?.gain ? formatMediaNumber(edits.gain, 2) : null,
|
|
172
|
+
fadeIn: edits?.fadeIn ? formatMediaNumber(edits.fadeIn) : null,
|
|
173
|
+
fadeOut: edits?.fadeOut ? formatMediaNumber(edits.fadeOut) : null,
|
|
174
|
+
crop: edits?.crop ? serializeMediaCrop(edits.crop) : null,
|
|
175
|
+
group: edits?.group ?? null
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function mediaEditValuesFromParams(params) {
|
|
179
|
+
const out = {};
|
|
180
|
+
for (const { key } of MEDIA_EDIT_PARAMS) {
|
|
181
|
+
if (params[key] != null) out[key] = params[key];
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
function mediaEditValuesFromAttributes(kind, attributes) {
|
|
186
|
+
const out = {};
|
|
187
|
+
for (const { key } of MEDIA_EDIT_PARAMS) {
|
|
188
|
+
const value = attributes[mediaEditHtmlAttribute(kind, key)];
|
|
189
|
+
if (value != null) out[key] = value;
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
function fnv1a64Hex(text) {
|
|
194
|
+
let h1 = 2166136261;
|
|
195
|
+
let h2 = 16777619 ^ 1540483477;
|
|
196
|
+
for (let i = 0; i < text.length; i++) {
|
|
197
|
+
const c = text.charCodeAt(i);
|
|
198
|
+
h1 = Math.imul(h1 ^ c, 16777619);
|
|
199
|
+
h2 = Math.imul(h2 ^ c, 1540483477);
|
|
200
|
+
h2 ^= h2 >>> 15;
|
|
201
|
+
}
|
|
202
|
+
return (h1 >>> 0).toString(16).padStart(8, "0") + (h2 >>> 0).toString(16).padStart(8, "0");
|
|
203
|
+
}
|
|
204
|
+
function mediaRenderKey(src, fx) {
|
|
205
|
+
const canonical = fx.ops.map((op) => `${op.id}:${formatMediaNumber(op.value)}`).join(" ");
|
|
206
|
+
return fnv1a64Hex(`${src}
|
|
207
|
+
${canonical}`).slice(0, 12);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export {
|
|
211
|
+
MEDIA_FX_ORDER,
|
|
212
|
+
MEDIA_FX_SPECS,
|
|
213
|
+
MEDIA_EDIT_PARAMS,
|
|
214
|
+
mediaEditHtmlAttribute,
|
|
215
|
+
formatMediaNumber,
|
|
216
|
+
parseMediaFx,
|
|
217
|
+
serializeMediaFx,
|
|
218
|
+
hasRenderableFx,
|
|
219
|
+
normalizeMediaCuts,
|
|
220
|
+
parseMediaCuts,
|
|
221
|
+
serializeMediaCuts,
|
|
222
|
+
parseMediaCrop,
|
|
223
|
+
normalizeMediaCrop,
|
|
224
|
+
serializeMediaCrop,
|
|
225
|
+
parseMediaEdits,
|
|
226
|
+
serializeMediaEdits,
|
|
227
|
+
mediaEditValuesFromParams,
|
|
228
|
+
mediaEditValuesFromAttributes,
|
|
229
|
+
mediaRenderKey
|
|
230
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// src/mediaEdit/dsp/biquad.ts
|
|
2
|
+
var BUTTERWORTH_Q = Math.SQRT1_2;
|
|
3
|
+
function createBiquadState() {
|
|
4
|
+
return { x1: 0, x2: 0, y1: 0, y2: 0 };
|
|
5
|
+
}
|
|
6
|
+
function omega(sampleRate, freqHz) {
|
|
7
|
+
const w0 = 2 * Math.PI * freqHz / sampleRate;
|
|
8
|
+
return { cosW0: Math.cos(w0), sinW0: Math.sin(w0) };
|
|
9
|
+
}
|
|
10
|
+
function highpassCoeffs(sampleRate, freqHz, q = BUTTERWORTH_Q) {
|
|
11
|
+
const { cosW0, sinW0 } = omega(sampleRate, freqHz);
|
|
12
|
+
const alpha = sinW0 / (2 * q);
|
|
13
|
+
const a0 = 1 + alpha;
|
|
14
|
+
return {
|
|
15
|
+
b0: (1 + cosW0) / 2 / a0,
|
|
16
|
+
b1: -(1 + cosW0) / a0,
|
|
17
|
+
b2: (1 + cosW0) / 2 / a0,
|
|
18
|
+
a1: -2 * cosW0 / a0,
|
|
19
|
+
a2: (1 - alpha) / a0
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function lowpassCoeffs(sampleRate, freqHz, q = BUTTERWORTH_Q) {
|
|
23
|
+
const { cosW0, sinW0 } = omega(sampleRate, freqHz);
|
|
24
|
+
const alpha = sinW0 / (2 * q);
|
|
25
|
+
const a0 = 1 + alpha;
|
|
26
|
+
return {
|
|
27
|
+
b0: (1 - cosW0) / 2 / a0,
|
|
28
|
+
b1: (1 - cosW0) / a0,
|
|
29
|
+
b2: (1 - cosW0) / 2 / a0,
|
|
30
|
+
a1: -2 * cosW0 / a0,
|
|
31
|
+
a2: (1 - alpha) / a0
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function biquadRun(coeffs, state, input, output) {
|
|
35
|
+
let { x1, x2, y1, y2 } = state;
|
|
36
|
+
const { b0, b1, b2, a1, a2 } = coeffs;
|
|
37
|
+
for (let i = 0; i < input.length; i++) {
|
|
38
|
+
const x0 = input[i];
|
|
39
|
+
const y0 = b0 * x0 + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2;
|
|
40
|
+
output[i] = y0;
|
|
41
|
+
x2 = x1;
|
|
42
|
+
x1 = x0;
|
|
43
|
+
y2 = y1;
|
|
44
|
+
y1 = y0;
|
|
45
|
+
}
|
|
46
|
+
return { x1, x2, y1, y2 };
|
|
47
|
+
}
|
|
48
|
+
function createBiquadCascade(stages) {
|
|
49
|
+
const states = stages.map(() => createBiquadState());
|
|
50
|
+
return {
|
|
51
|
+
process(block) {
|
|
52
|
+
for (let s = 0; s < stages.length; s++) {
|
|
53
|
+
states[s] = biquadRun(stages[s], states[s], block, block);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export {
|
|
60
|
+
highpassCoeffs,
|
|
61
|
+
lowpassCoeffs,
|
|
62
|
+
biquadRun,
|
|
63
|
+
createBiquadCascade
|
|
64
|
+
};
|