@vanillaskyai/video 0.1.0
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/CHANGELOG.md +45 -0
- package/LICENSE +201 -0
- package/PUBLIC-API.md +276 -0
- package/README.md +152 -0
- package/SECURITY.md +22 -0
- package/SUPPORT.md +28 -0
- package/bin/vanillasky.js +3 -0
- package/dist/bg-confetti-WNQXS7ZS.js +89 -0
- package/dist/bg-emoji-ETI47GLS.js +150 -0
- package/dist/bg-media-SVUZAMGX.js +99 -0
- package/dist/brand-message-F3RD4P3P.js +253 -0
- package/dist/builtin-server-F7YXRVCH.js +8 -0
- package/dist/catalog-types-BIhSpOWK.d.ts +74 -0
- package/dist/chart-bar-CNSC7BNK.js +304 -0
- package/dist/chart-counter-MCRHKN77.js +299 -0
- package/dist/chart-progress-ring-RMI4A7ZK.js +296 -0
- package/dist/check-runtime.d.ts +15 -0
- package/dist/check-runtime.js +102 -0
- package/dist/chunk-2E6T633S.js +27 -0
- package/dist/chunk-352ZZCET.js +83 -0
- package/dist/chunk-73NTSFFI.js +81 -0
- package/dist/chunk-A2X45FET.js +46 -0
- package/dist/chunk-ABPKM4GK.js +187 -0
- package/dist/chunk-ABZACD4G.js +216 -0
- package/dist/chunk-BCRUUJ6A.js +756 -0
- package/dist/chunk-CHLGW22F.js +3551 -0
- package/dist/chunk-CJ7F2CE6.js +104 -0
- package/dist/chunk-CR7FE7BW.js +109 -0
- package/dist/chunk-E7CL7UPB.js +109 -0
- package/dist/chunk-EE6PK6XC.js +18 -0
- package/dist/chunk-ERY4S6QV.js +385 -0
- package/dist/chunk-EVG2ZYIY.js +125 -0
- package/dist/chunk-FNAEQ7QO.js +155 -0
- package/dist/chunk-FQ47WXAJ.js +38 -0
- package/dist/chunk-FSG2PYWG.js +336 -0
- package/dist/chunk-GHJEAP4O.js +504 -0
- package/dist/chunk-GRMUV7QY.js +262 -0
- package/dist/chunk-IFOW65Z6.js +205 -0
- package/dist/chunk-IXCUY3B3.js +786 -0
- package/dist/chunk-L5BTTCZV.js +97 -0
- package/dist/chunk-LMG3GHLL.js +107 -0
- package/dist/chunk-OOPIETLW.js +101 -0
- package/dist/chunk-P22HHFDK.js +43 -0
- package/dist/chunk-QAAO7OAY.js +19 -0
- package/dist/chunk-S7HFLYNN.js +35 -0
- package/dist/chunk-SPVTJH3F.js +24 -0
- package/dist/chunk-W2XJKG6C.js +3177 -0
- package/dist/chunk-XB7LH4J2.js +301 -0
- package/dist/chunk-XGRA2MUV.js +242 -0
- package/dist/chunk-XTOXFMLM.js +105 -0
- package/dist/chunk-YC5MFXA5.js +132 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1681 -0
- package/dist/compose-video-CN3VSWST.js +13 -0
- package/dist/cta-logo-SVA5GP2A.js +209 -0
- package/dist/cta-media-E7VLLK42.js +215 -0
- package/dist/events-_wfjbmEp.d.ts +81 -0
- package/dist/incoming-call-62MFZL3Z.js +320 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +14 -0
- package/dist/infographic-before-after-AXCDAWND.js +460 -0
- package/dist/infographic-feature-list-GW5GUE6O.js +266 -0
- package/dist/infographic-problem-solution-7NURJANA.js +340 -0
- package/dist/infographic-stat-row-J7IZR6ZN.js +241 -0
- package/dist/infographic-steps-VSZM6F5M.js +400 -0
- package/dist/kit-BMNcHqa9.d.ts +79 -0
- package/dist/prompt-input-LOUCRXCY.js +236 -0
- package/dist/react.d.ts +81 -0
- package/dist/react.js +835 -0
- package/dist/reaction-QG4CYZLQ.js +119 -0
- package/dist/server.d.ts +141 -0
- package/dist/server.js +498 -0
- package/dist/showcase-code-RUTFZR3H.js +351 -0
- package/dist/showcase-phone-6CP6XEU5.js +272 -0
- package/dist/showcase-terminal-MGAEQWPR.js +306 -0
- package/dist/showcase-web-Y4W2HVRJ.js +390 -0
- package/dist/social-conversation-75SD2637.js +695 -0
- package/dist/social-milestone-XI2QE5BO.js +292 -0
- package/dist/social-notification-2UOHQCUY.js +244 -0
- package/dist/social-review-stack-FDYZGZTJ.js +239 -0
- package/dist/social-testimonial-OQ7N52GJ.js +284 -0
- package/dist/social-tweet-5OLCNPPC.js +295 -0
- package/dist/state-DZcKuS32.d.ts +3 -0
- package/dist/system-prompt-A6UXNTPK.js +8 -0
- package/dist/template-catalog.d.ts +24 -0
- package/dist/template-catalog.js +6 -0
- package/dist/templates.d.ts +25 -0
- package/dist/templates.js +19 -0
- package/dist/test.d.ts +149 -0
- package/dist/test.js +272 -0
- package/dist/text-stream-6XHC7J47.js +10 -0
- package/dist/types-B-PZR6G_.d.ts +208 -0
- package/dist/validate-T7GBU2YF.js +8 -0
- package/docs/agent-integration.md +39 -0
- package/docs/architecture.md +84 -0
- package/docs/branding-and-personalization.md +87 -0
- package/docs/concepts.md +106 -0
- package/docs/custom-templates.md +329 -0
- package/docs/customization.md +141 -0
- package/docs/errors.md +70 -0
- package/docs/getting-started.md +111 -0
- package/docs/input-and-first-scene.md +41 -0
- package/docs/integrate-nextjs.md +79 -0
- package/docs/media-and-audio.md +69 -0
- package/docs/motion-and-effects.md +107 -0
- package/docs/persistence.md +95 -0
- package/docs/production.md +122 -0
- package/docs/prompt-and-input.md +185 -0
- package/docs/provider-integration.md +115 -0
- package/docs/reference/protocol.md +96 -0
- package/docs/reference/provider-adapters.md +158 -0
- package/docs/responsive-orientation.md +37 -0
- package/docs/security.md +38 -0
- package/docs/streaming-protocol.md +15 -0
- package/docs/testing.md +137 -0
- package/docs/use-cases.md +59 -0
- package/examples/custom-template/README.md +19 -0
- package/examples/custom-template/minimal-text.tsx +82 -0
- package/examples/custom-template/structured-data.tsx +104 -0
- package/examples/custom-template/supplied-media.tsx +92 -0
- package/examples/nextjs-quickstart/.env.example +2 -0
- package/examples/nextjs-quickstart/README.md +22 -0
- package/examples/nextjs-quickstart/next-env.d.ts +4 -0
- package/examples/nextjs-quickstart/package.json +24 -0
- package/examples/nextjs-quickstart/src/app/api/video/route.ts +22 -0
- package/examples/nextjs-quickstart/src/app/layout.tsx +5 -0
- package/examples/nextjs-quickstart/src/app/page.tsx +31 -0
- package/examples/nextjs-quickstart/tsconfig.json +26 -0
- package/package.json +167 -0
- package/registry/items/backgrounds.json +52 -0
- package/registry/items/barChart.json +237 -0
- package/registry/items/beforeAfter.json +180 -0
- package/registry/items/bigNumber.json +225 -0
- package/registry/items/brandMessage.json +158 -0
- package/registry/items/cardList.json +206 -0
- package/registry/items/chatMessenger.json +151 -0
- package/registry/items/chatWhatsapp.json +151 -0
- package/registry/items/codeEditor.json +184 -0
- package/registry/items/confetti.json +107 -0
- package/registry/items/ctaLogo.json +160 -0
- package/registry/items/ctaMedia.json +193 -0
- package/registry/items/emojiBurst.json +101 -0
- package/registry/items/incomingCall.json +170 -0
- package/registry/items/media.json +179 -0
- package/registry/items/milestone.json +205 -0
- package/registry/items/motion.json +45 -0
- package/registry/items/notification.json +182 -0
- package/registry/items/phoneMockup.json +254 -0
- package/registry/items/problemSolution.json +186 -0
- package/registry/items/progressRing.json +199 -0
- package/registry/items/promptInput.json +148 -0
- package/registry/items/reaction.json +170 -0
- package/registry/items/reviewStack.json +224 -0
- package/registry/items/steps.json +207 -0
- package/registry/items/template-context.json +31 -0
- package/registry/items/terminal.json +212 -0
- package/registry/items/testimonial.json +164 -0
- package/registry/items/theme.json +47 -0
- package/registry/items/tripleStats.json +228 -0
- package/registry/items/tweet.json +203 -0
- package/registry/items/typography.json +47 -0
- package/registry/items/webMockup.json +273 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// src/visual-system/emoji/index.tsx
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
var NATIVE_EMOJI_FONT = "Apple Color Emoji,Segoe UI Emoji,Noto Color Emoji,sans-serif";
|
|
4
|
+
var NATIVE_EMOJI = /\p{Emoji_Presentation}|\p{Regional_Indicator}|\u{FE0F}|\u{20E3}/u;
|
|
5
|
+
function isNativeEmoji(char) {
|
|
6
|
+
return NATIVE_EMOJI.test(char);
|
|
7
|
+
}
|
|
8
|
+
var Emoji = ({ char, size, verticalAlign = "-0.15em", style }) => {
|
|
9
|
+
const nativeEmoji = isNativeEmoji(char);
|
|
10
|
+
return /* @__PURE__ */ jsx(
|
|
11
|
+
"span",
|
|
12
|
+
{
|
|
13
|
+
role: nativeEmoji ? "img" : void 0,
|
|
14
|
+
"aria-label": nativeEmoji ? char : void 0,
|
|
15
|
+
style: {
|
|
16
|
+
...nativeEmoji ? { fontFamily: NATIVE_EMOJI_FONT } : {},
|
|
17
|
+
fontSize: size,
|
|
18
|
+
lineHeight: 1,
|
|
19
|
+
display: "inline-block",
|
|
20
|
+
verticalAlign,
|
|
21
|
+
...style
|
|
22
|
+
},
|
|
23
|
+
children: char
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// src/visual-system/emoji/emoji-text.tsx
|
|
29
|
+
import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
|
|
30
|
+
function toGraphemes(text) {
|
|
31
|
+
const Seg = Intl.Segmenter;
|
|
32
|
+
if (Seg) {
|
|
33
|
+
const seg = new Seg(void 0, { granularity: "grapheme" });
|
|
34
|
+
const out = [];
|
|
35
|
+
for (const { segment } of seg.segment(text)) out.push(segment);
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
return Array.from(text);
|
|
39
|
+
}
|
|
40
|
+
function renderWithEmoji(text, fontSizePx, opts) {
|
|
41
|
+
if (!text) return [text];
|
|
42
|
+
if (!isNativeEmoji(text)) return [text];
|
|
43
|
+
const clusters = toGraphemes(text);
|
|
44
|
+
const nodes = [];
|
|
45
|
+
let textBuf = "";
|
|
46
|
+
let key = 0;
|
|
47
|
+
const flushText = () => {
|
|
48
|
+
if (textBuf) {
|
|
49
|
+
nodes.push(textBuf);
|
|
50
|
+
textBuf = "";
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
for (const cluster of clusters) {
|
|
54
|
+
if (isNativeEmoji(cluster)) {
|
|
55
|
+
flushText();
|
|
56
|
+
nodes.push(
|
|
57
|
+
/* @__PURE__ */ jsx2(
|
|
58
|
+
Emoji,
|
|
59
|
+
{
|
|
60
|
+
char: cluster,
|
|
61
|
+
size: fontSizePx,
|
|
62
|
+
verticalAlign: opts?.verticalAlign,
|
|
63
|
+
style: opts?.emojiStyle
|
|
64
|
+
},
|
|
65
|
+
`e${key++}`
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
} else {
|
|
69
|
+
textBuf += cluster;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
flushText();
|
|
73
|
+
return nodes;
|
|
74
|
+
}
|
|
75
|
+
function planTypewriterEmoji(text) {
|
|
76
|
+
if (!text || !isNativeEmoji(text)) return null;
|
|
77
|
+
const starts = /* @__PURE__ */ new Map();
|
|
78
|
+
const covered = /* @__PURE__ */ new Set();
|
|
79
|
+
let idx = 0;
|
|
80
|
+
let found = false;
|
|
81
|
+
for (const cluster of toGraphemes(text)) {
|
|
82
|
+
const len = cluster.length;
|
|
83
|
+
if (isNativeEmoji(cluster)) {
|
|
84
|
+
found = true;
|
|
85
|
+
starts.set(idx, cluster);
|
|
86
|
+
for (let k = 1; k < len; k++) covered.add(idx + k);
|
|
87
|
+
}
|
|
88
|
+
idx += len;
|
|
89
|
+
}
|
|
90
|
+
return found ? { starts, covered } : null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export {
|
|
94
|
+
Emoji,
|
|
95
|
+
renderWithEmoji,
|
|
96
|
+
planTypewriterEmoji
|
|
97
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import {
|
|
2
|
+
validateTemplateSchemaValue
|
|
3
|
+
} from "./chunk-352ZZCET.js";
|
|
4
|
+
import {
|
|
5
|
+
getTemplateSchemaGates,
|
|
6
|
+
isMeaningfullyPresentTemplateValue
|
|
7
|
+
} from "./chunk-73NTSFFI.js";
|
|
8
|
+
|
|
9
|
+
// src/visual-system/catalog/validate.ts
|
|
10
|
+
function* walkSchemaValue(property, value, path) {
|
|
11
|
+
yield { property, value, path };
|
|
12
|
+
if (property.type === "array" && property.items && Array.isArray(value)) {
|
|
13
|
+
for (const [index, item] of value.entries()) {
|
|
14
|
+
yield* walkSchemaValue(property.items, item, `${path}.${index}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (property.type === "object" && value && typeof value === "object" && !Array.isArray(value)) {
|
|
18
|
+
for (const [name, child] of Object.entries(property.properties ?? {})) {
|
|
19
|
+
if (Object.hasOwn(value, name)) {
|
|
20
|
+
yield* walkSchemaValue(child, value[name], `${path}.${name}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function schemaValues(schema, variables) {
|
|
26
|
+
const values = [];
|
|
27
|
+
for (const [name, property] of Object.entries(schema.properties)) {
|
|
28
|
+
if (Object.hasOwn(variables, name)) values.push(...walkSchemaValue(property, variables[name], name));
|
|
29
|
+
}
|
|
30
|
+
return values;
|
|
31
|
+
}
|
|
32
|
+
function validateSchema(schema, value) {
|
|
33
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
34
|
+
throw new Error("Template variable variables must be object");
|
|
35
|
+
}
|
|
36
|
+
const object = value;
|
|
37
|
+
const requiredFields = new Set(schema.required ?? []);
|
|
38
|
+
for (const required of schema.required ?? []) {
|
|
39
|
+
if (!Object.hasOwn(object, required)) throw new Error(`Template variable ${required} is required`);
|
|
40
|
+
}
|
|
41
|
+
if (schema.additionalProperties === false) {
|
|
42
|
+
for (const name of Object.keys(object)) {
|
|
43
|
+
if (!Object.hasOwn(schema.properties, name)) throw new Error(`Template variable ${name} is not declared`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
for (const [name, property] of Object.entries(schema.properties)) {
|
|
47
|
+
if (Object.hasOwn(object, name)) validateTemplateSchemaValue(property, object[name], name, {
|
|
48
|
+
errorPrefix: "Template variable ",
|
|
49
|
+
includeTypeArticle: true,
|
|
50
|
+
allowEmptyOptionalMedia: property.default === "",
|
|
51
|
+
optional: !requiredFields.has(name)
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function validateConditionalPresence(templateId, groups, variables) {
|
|
56
|
+
for (const group of groups) {
|
|
57
|
+
if (group.some((field) => Object.hasOwn(variables, field) && isMeaningfullyPresentTemplateValue(variables[field]))) continue;
|
|
58
|
+
throw new Error(`Template ${templateId} requires a non-empty value for ${group.join(" or ")}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function suppliedUrls(input) {
|
|
62
|
+
const urls = /* @__PURE__ */ new Set();
|
|
63
|
+
for (const media of input.suppliedMedia ?? []) {
|
|
64
|
+
urls.add(media.url);
|
|
65
|
+
if (media.posterUrl) urls.add(media.posterUrl);
|
|
66
|
+
}
|
|
67
|
+
return urls;
|
|
68
|
+
}
|
|
69
|
+
function createTemplateSceneValidator(options) {
|
|
70
|
+
return (scene, context) => {
|
|
71
|
+
const template = options.kit.getTemplateMetadata(scene.templateId);
|
|
72
|
+
if (!template) throw new Error(`Template ${scene.templateId} is not installed`);
|
|
73
|
+
const schema = template.schema;
|
|
74
|
+
const gates = getTemplateSchemaGates(schema);
|
|
75
|
+
validateConditionalPresence(scene.templateId, gates.requiredAnyOf, scene.variables);
|
|
76
|
+
validateSchema(schema, scene.variables);
|
|
77
|
+
const values = schemaValues(schema, scene.variables);
|
|
78
|
+
const allowedUrls = suppliedUrls(context.input);
|
|
79
|
+
const suppliedImageUrls = new Set((context.input.suppliedMedia ?? []).filter(({ type }) => type === "image").map(({ url }) => url));
|
|
80
|
+
if (gates.requiresScreenshot) {
|
|
81
|
+
const screenshots = values.filter(({ property }) => property.format === "supplied-image").map(({ value }) => value).filter((value) => typeof value === "string" && Boolean(value.trim()));
|
|
82
|
+
if (!screenshots.length || screenshots.some((url) => !suppliedImageUrls.has(url))) {
|
|
83
|
+
throw new Error(`Template ${scene.templateId} requires a supplied screenshot image`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
for (const { path, property, value } of values) {
|
|
87
|
+
const isMedia = property.format === "uri" || property.format === "supplied-image";
|
|
88
|
+
if (!isMedia || typeof value !== "string" || !value.trim()) continue;
|
|
89
|
+
const permitted = allowedUrls.has(value) || options.allowMediaUrl?.(value, {
|
|
90
|
+
...context,
|
|
91
|
+
scene,
|
|
92
|
+
variable: path
|
|
93
|
+
});
|
|
94
|
+
if (!permitted) throw new Error(`Template variable ${path} must use supplied media`);
|
|
95
|
+
}
|
|
96
|
+
if (gates.requiresQuote) {
|
|
97
|
+
const quoteValues = values.filter(({ property, value }) => property.format === "grounded-quote" && typeof value === "string" && value.trim()).map(({ value }) => String(value).trim());
|
|
98
|
+
if (!quoteValues.length || quoteValues.some((quote) => !context.input.input.includes(quote))) {
|
|
99
|
+
throw new Error(`Template ${scene.templateId} requires an exact quote from the raw input`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export {
|
|
106
|
+
createTemplateSceneValidator
|
|
107
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseVideoEvent
|
|
3
|
+
} from "./chunk-ERY4S6QV.js";
|
|
4
|
+
import {
|
|
5
|
+
VIDEO_PROTOCOL_VERSION
|
|
6
|
+
} from "./chunk-EE6PK6XC.js";
|
|
7
|
+
|
|
8
|
+
// src/protocol/sse.ts
|
|
9
|
+
var VIDEO_STREAM_HEADER = "x-vanillasky-video-stream";
|
|
10
|
+
var VIDEO_STREAM_CONTENT_TYPE = "text/event-stream; charset=utf-8";
|
|
11
|
+
function encodeVideoSseEvent(event) {
|
|
12
|
+
const valid = parseVideoEvent(event);
|
|
13
|
+
return `id: ${valid.eventId}
|
|
14
|
+
event: video
|
|
15
|
+
data: ${JSON.stringify(valid)}
|
|
16
|
+
|
|
17
|
+
`;
|
|
18
|
+
}
|
|
19
|
+
function normalizeNewlines(value) {
|
|
20
|
+
return value.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
21
|
+
}
|
|
22
|
+
async function* decodeVideoSse(stream) {
|
|
23
|
+
const reader = stream.getReader();
|
|
24
|
+
const decoder = new TextDecoder();
|
|
25
|
+
let buffer = "";
|
|
26
|
+
let doneMarker = false;
|
|
27
|
+
const parseBlock = (block) => {
|
|
28
|
+
const lines = block.split("\n");
|
|
29
|
+
if (lines.every((line) => !line || line.startsWith(":"))) return void 0;
|
|
30
|
+
let id;
|
|
31
|
+
let eventName;
|
|
32
|
+
const data = [];
|
|
33
|
+
for (const line of lines) {
|
|
34
|
+
if (!line || line.startsWith(":")) continue;
|
|
35
|
+
const separator = line.indexOf(":");
|
|
36
|
+
const field = separator < 0 ? line : line.slice(0, separator);
|
|
37
|
+
const value = separator < 0 ? "" : line.slice(separator + 1).replace(/^ /, "");
|
|
38
|
+
if (field === "id") id = value;
|
|
39
|
+
else if (field === "event") eventName = value;
|
|
40
|
+
else if (field === "data") data.push(value);
|
|
41
|
+
}
|
|
42
|
+
const payload = data.join("\n");
|
|
43
|
+
if (payload === "[DONE]") {
|
|
44
|
+
doneMarker = true;
|
|
45
|
+
return void 0;
|
|
46
|
+
}
|
|
47
|
+
if (!payload) return void 0;
|
|
48
|
+
if (doneMarker) throw new Error("Video response received an event after [DONE]");
|
|
49
|
+
if (eventName && eventName !== "video") {
|
|
50
|
+
throw new Error(`Unsupported SSE event ${eventName}`);
|
|
51
|
+
}
|
|
52
|
+
const event = parseVideoEvent(JSON.parse(payload));
|
|
53
|
+
if (id && id !== event.eventId) throw new Error("SSE id does not match event.eventId");
|
|
54
|
+
return event;
|
|
55
|
+
};
|
|
56
|
+
try {
|
|
57
|
+
while (true) {
|
|
58
|
+
const result = await reader.read();
|
|
59
|
+
buffer += normalizeNewlines(decoder.decode(result.value, { stream: !result.done }));
|
|
60
|
+
let boundary = buffer.indexOf("\n\n");
|
|
61
|
+
while (boundary >= 0) {
|
|
62
|
+
const block = buffer.slice(0, boundary);
|
|
63
|
+
buffer = buffer.slice(boundary + 2);
|
|
64
|
+
const event = parseBlock(block);
|
|
65
|
+
if (event) yield event;
|
|
66
|
+
boundary = buffer.indexOf("\n\n");
|
|
67
|
+
}
|
|
68
|
+
if (result.done) break;
|
|
69
|
+
}
|
|
70
|
+
if (buffer.trim()) {
|
|
71
|
+
const event = parseBlock(buffer.trim());
|
|
72
|
+
if (event) yield event;
|
|
73
|
+
}
|
|
74
|
+
if (!doneMarker) throw new Error("Video response SSE stream ended without [DONE]");
|
|
75
|
+
} finally {
|
|
76
|
+
await reader.cancel().catch(() => void 0);
|
|
77
|
+
reader.releaseLock();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function videoSseHeaders(headers) {
|
|
81
|
+
const result = new Headers(headers);
|
|
82
|
+
result.set("Content-Type", VIDEO_STREAM_CONTENT_TYPE);
|
|
83
|
+
result.set(VIDEO_STREAM_HEADER, VIDEO_PROTOCOL_VERSION);
|
|
84
|
+
result.set("Cache-Control", "no-cache, no-transform");
|
|
85
|
+
result.set("X-Accel-Buffering", "no");
|
|
86
|
+
result.set("X-Content-Type-Options", "nosniff");
|
|
87
|
+
const exposed = result.get("Access-Control-Expose-Headers");
|
|
88
|
+
if (!exposed) result.set("Access-Control-Expose-Headers", VIDEO_STREAM_HEADER);
|
|
89
|
+
else if (!exposed.toLowerCase().split(",").map((value) => value.trim()).includes(VIDEO_STREAM_HEADER)) {
|
|
90
|
+
result.set("Access-Control-Expose-Headers", `${exposed}, ${VIDEO_STREAM_HEADER}`);
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export {
|
|
96
|
+
VIDEO_STREAM_HEADER,
|
|
97
|
+
VIDEO_STREAM_CONTENT_TYPE,
|
|
98
|
+
encodeVideoSseEvent,
|
|
99
|
+
decodeVideoSse,
|
|
100
|
+
videoSseHeaders
|
|
101
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// src/visual-system/parse-list.ts
|
|
2
|
+
function parseList(value, max) {
|
|
3
|
+
if (value === null || value === void 0) return [];
|
|
4
|
+
let arr;
|
|
5
|
+
if (Array.isArray(value)) {
|
|
6
|
+
arr = value;
|
|
7
|
+
} else if (typeof value === "string") {
|
|
8
|
+
const trimmed = value.trim();
|
|
9
|
+
if (!trimmed) return [];
|
|
10
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(trimmed);
|
|
13
|
+
arr = Array.isArray(parsed) ? parsed : [trimmed];
|
|
14
|
+
} catch {
|
|
15
|
+
arr = trimmed.split(",");
|
|
16
|
+
}
|
|
17
|
+
} else {
|
|
18
|
+
arr = trimmed.split(",");
|
|
19
|
+
}
|
|
20
|
+
} else if (typeof value === "number" || typeof value === "boolean") {
|
|
21
|
+
arr = [value];
|
|
22
|
+
} else if (typeof value === "object") {
|
|
23
|
+
arr = [value];
|
|
24
|
+
} else {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
const out = arr.map((item) => {
|
|
28
|
+
if (item === null || item === void 0) return "";
|
|
29
|
+
if (typeof item === "string") return item.trim();
|
|
30
|
+
if (typeof item === "number" || typeof item === "boolean") return String(item);
|
|
31
|
+
if (typeof item === "object") {
|
|
32
|
+
const o = item;
|
|
33
|
+
const picked = o.label ?? o.name ?? o.text ?? o.title ?? o.description ?? o.value;
|
|
34
|
+
return picked !== void 0 ? String(picked).trim() : "";
|
|
35
|
+
}
|
|
36
|
+
return String(item).trim();
|
|
37
|
+
}).filter((s) => s.length > 0);
|
|
38
|
+
return typeof max === "number" ? out.slice(0, max) : out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export {
|
|
42
|
+
parseList
|
|
43
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// src/visual-system/scene-templates/celebration-particle-timing.ts
|
|
2
|
+
var SECOND_WAVE_INTERVAL = 4;
|
|
3
|
+
var SECOND_WAVE_START = 0.33;
|
|
4
|
+
var SECOND_WAVE_DURATION = 1;
|
|
5
|
+
var EXIT_FADE_START = 0.76;
|
|
6
|
+
var EXIT_FADE_END = 0.85;
|
|
7
|
+
function getCelebrationParticleTiming(sceneProgress, particleIndex, durationFactor) {
|
|
8
|
+
if (sceneProgress >= EXIT_FADE_END) return null;
|
|
9
|
+
const isSecondWave = particleIndex % SECOND_WAVE_INTERVAL === 0;
|
|
10
|
+
if (isSecondWave && sceneProgress <= SECOND_WAVE_START) return null;
|
|
11
|
+
const waveProgress = isSecondWave ? (sceneProgress - SECOND_WAVE_START) / SECOND_WAVE_DURATION : sceneProgress;
|
|
12
|
+
const progress = Math.min(1, Math.max(0, waveProgress / durationFactor));
|
|
13
|
+
const opacity = sceneProgress <= EXIT_FADE_START ? 1 : (EXIT_FADE_END - sceneProgress) / (EXIT_FADE_END - EXIT_FADE_START);
|
|
14
|
+
return { progress, opacity };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
getCelebrationParticleTiming
|
|
19
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// src/visual-system/scene-templates/text-utils.ts
|
|
2
|
+
function stripPipe(text) {
|
|
3
|
+
const idx = text.indexOf("|");
|
|
4
|
+
return idx === -1 ? text : text.slice(0, idx);
|
|
5
|
+
}
|
|
6
|
+
function getResponsiveFontSize(text, width, role = "headline", height) {
|
|
7
|
+
const s = height ? Math.min(width, height) / 1080 : width / 1080;
|
|
8
|
+
const words = text.split(/\s+/).filter(Boolean).length;
|
|
9
|
+
const chars = text.length;
|
|
10
|
+
if (role === "subtitle") {
|
|
11
|
+
if (chars > 60) return 24 * s;
|
|
12
|
+
if (chars > 30) return 28 * s;
|
|
13
|
+
return 32 * s;
|
|
14
|
+
}
|
|
15
|
+
if (words <= 2 && chars <= 12) return Math.max(36 * s, 96 * s);
|
|
16
|
+
if (words <= 3 && chars <= 20) return Math.max(36 * s, 80 * s);
|
|
17
|
+
if (words <= 5 && chars <= 35) return Math.max(36 * s, 64 * s);
|
|
18
|
+
if (words <= 8 && chars <= 60) return Math.max(36 * s, 48 * s);
|
|
19
|
+
return 36 * s;
|
|
20
|
+
}
|
|
21
|
+
function fitTextSize(text, baseFontSize, maxWidth, opts) {
|
|
22
|
+
if (!text) return baseFontSize;
|
|
23
|
+
const minScale = opts?.minScale ?? 0.5;
|
|
24
|
+
const charWidthRatio = opts?.charWidthRatio ?? 0.55;
|
|
25
|
+
const estWidth = text.length * baseFontSize * charWidthRatio;
|
|
26
|
+
if (estWidth <= maxWidth) return baseFontSize;
|
|
27
|
+
const scaled = maxWidth / estWidth * baseFontSize;
|
|
28
|
+
return Math.max(scaled, baseFontSize * minScale);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
stripPipe,
|
|
33
|
+
getResponsiveFontSize,
|
|
34
|
+
fitTextSize
|
|
35
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// src/protocol/timeline.ts
|
|
2
|
+
function resolveVideoTimeline(config) {
|
|
3
|
+
const ranges = [];
|
|
4
|
+
let cursor = 0;
|
|
5
|
+
for (const scene of config.scenes) {
|
|
6
|
+
const timing = scene.timing ?? {};
|
|
7
|
+
const beatStart = timing.beatStart == null ? void 0 : config.audio?.beatMarkers[timing.beatStart]?.time;
|
|
8
|
+
const beatEnd = timing.beatEnd == null ? void 0 : config.audio?.beatMarkers[timing.beatEnd]?.time;
|
|
9
|
+
const start = timing.startTime ?? beatStart ?? cursor;
|
|
10
|
+
const duration = Math.max(1e-3, timing.fixedDuration ?? 5);
|
|
11
|
+
const end = Math.max(start + 1e-3, timing.endTime ?? beatEnd ?? start + duration);
|
|
12
|
+
ranges.push({ scene, start, end });
|
|
13
|
+
cursor = end;
|
|
14
|
+
}
|
|
15
|
+
return ranges;
|
|
16
|
+
}
|
|
17
|
+
function getVideoDuration(config) {
|
|
18
|
+
return resolveVideoTimeline(config).at(-1)?.end ?? 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
resolveVideoTimeline,
|
|
23
|
+
getVideoDuration
|
|
24
|
+
};
|