@vanillaskyai/video 0.10.20 → 0.10.22
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 +9 -0
- package/dist/{chunk-HKUI5BTB.js → chunk-44WND2VP.js} +44 -0
- package/dist/react.js +69 -47
- package/dist/server.js +41 -30
- package/docs/media-and-audio.md +37 -3
- package/package.json +1 -1
- package/starters/video-chat/package.json +1 -1
- package/styles/video-chat.css +14 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ VanillaSky follows semantic versioning. This changelog begins with the 0.1 beta.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.10.22
|
|
8
|
+
|
|
9
|
+
- Offer eight diverse homepage prompts with curated footage, a balanced fresh-page shuffle, and stable ordering when returning Home.
|
|
10
|
+
- Browse four cards on desktop and two on mobile, with only the active card loading video.
|
|
11
|
+
|
|
12
|
+
## 0.10.21
|
|
13
|
+
|
|
14
|
+
- Choose illustrated, realistic, or cinematic generated-video direction within the existing chat brief, with consistent treatment across shots and caller style overrides.
|
|
15
|
+
|
|
7
16
|
## 0.10.20
|
|
8
17
|
|
|
9
18
|
- Collapse completed-response subtitles to an expandable transcript and remove the Pexels header link.
|
|
@@ -8,6 +8,47 @@ import {
|
|
|
8
8
|
VIDEO_PROTOCOL_VERSION
|
|
9
9
|
} from "./chunk-7NZOMTAL.js";
|
|
10
10
|
|
|
11
|
+
// src/video-chat/welcome-cards.ts
|
|
12
|
+
var WELCOME_CARDS = [
|
|
13
|
+
{ prompt: "Why do cats stare at us?", category: "curiosity", media: { "url": "https://videos.pexels.com/video-files/6131490/6131490-sd_640_360_25fps.mp4", "type": "video", "posterUrl": "https://images.pexels.com/videos/6131490/pictures/preview-0.jpg?auto=compress&fit=crop&w=640" } },
|
|
14
|
+
{ prompt: "Tell me a joke about office life", category: "entertainment", media: { "url": "https://videos.pexels.com/video-files/7438239/7438239-sd_640_338_25fps.mp4", "type": "video", "posterUrl": "https://images.pexels.com/videos/7438239/pictures/preview-0.jpg?auto=compress&fit=crop&w=640" } },
|
|
15
|
+
{ prompt: "Where would you take me in Japan?", category: "explore", media: { "url": "https://videos.pexels.com/video-files/35972506/15252635_360_640_30fps.mp4", "type": "video", "posterUrl": "https://images.pexels.com/videos/35972506/pictures/preview-0.jpg?auto=compress&fit=crop&w=640" } },
|
|
16
|
+
{ prompt: "How do I make better coffee?", category: "practical", media: { "url": "https://videos.pexels.com/video-files/5564283/5564283-sd_640_360_24fps.mp4", "type": "video", "posterUrl": "https://images.pexels.com/videos/5564283/pictures/preview-0.jpg?auto=compress&fit=crop&w=640" } },
|
|
17
|
+
{ prompt: "What would Earth look like without humans?", category: "explore", media: { "url": "https://videos.pexels.com/video-files/28732001/12464538_640_360_30fps.mp4", "type": "video", "posterUrl": "https://images.pexels.com/videos/28732001/pictures/preview-0.jpg?auto=compress&fit=crop&w=640" } },
|
|
18
|
+
{ prompt: "Tell me a short story with a twist", category: "entertainment", media: { "url": "https://videos.pexels.com/video-files/5683159/5683159-sd_640_360_30fps.mp4", "type": "video", "posterUrl": "https://images.pexels.com/videos/5683159/pictures/preview-0.jpg?auto=compress&fit=crop&w=640" } },
|
|
19
|
+
{ prompt: "Why does music give us goosebumps?", category: "curiosity", media: { "url": "https://videos.pexels.com/video-files/6870450/6870450-sd_640_360_30fps.mp4", "type": "video", "posterUrl": "https://images.pexels.com/videos/6870450/pictures/preview-0.jpg?auto=compress&fit=crop&w=640" } },
|
|
20
|
+
{ prompt: "How can I make my room feel bigger?", category: "practical", media: { "url": "https://videos.pexels.com/video-files/37479012/15875787_360_640_30fps.mp4", "type": "video", "posterUrl": "https://images.pexels.com/videos/37479012/pictures/preview-0.jpg?auto=compress&fit=crop&w=640" } }
|
|
21
|
+
];
|
|
22
|
+
function orderWelcomeCards(cards, seed) {
|
|
23
|
+
if (cards.length !== WELCOME_CARDS.length || new Set(cards.map((card) => card.prompt)).size !== cards.length || cards.some((card) => !WELCOME_CARDS.some((entry) => entry.prompt === card.prompt))) return [...cards];
|
|
24
|
+
let state = seed >>> 0;
|
|
25
|
+
const random = () => {
|
|
26
|
+
state = Math.imul(state, 1664525) + 1013904223 >>> 0;
|
|
27
|
+
return state / 4294967296;
|
|
28
|
+
};
|
|
29
|
+
const shuffle = (values) => {
|
|
30
|
+
const copy = [...values];
|
|
31
|
+
for (let i = copy.length - 1; i > 0; i--) {
|
|
32
|
+
const j = Math.floor(random() * (i + 1));
|
|
33
|
+
[copy[i], copy[j]] = [copy[j], copy[i]];
|
|
34
|
+
}
|
|
35
|
+
return copy;
|
|
36
|
+
};
|
|
37
|
+
const groups = /* @__PURE__ */ new Map();
|
|
38
|
+
for (const card of shuffle(cards)) {
|
|
39
|
+
const category = WELCOME_CARDS.find((entry) => entry.prompt === card.prompt).category;
|
|
40
|
+
const group = groups.get(category) ?? [];
|
|
41
|
+
group.push(card);
|
|
42
|
+
groups.set(category, group);
|
|
43
|
+
}
|
|
44
|
+
const categories = shuffle([...groups.values()]);
|
|
45
|
+
return [...categories.map((group) => group[0]), ...shuffle(categories.flatMap((group) => group.slice(1)))];
|
|
46
|
+
}
|
|
47
|
+
var pageSeed;
|
|
48
|
+
function welcomeVisitSeed() {
|
|
49
|
+
return pageSeed ??= Math.floor(Math.random() * 4294967296);
|
|
50
|
+
}
|
|
51
|
+
|
|
11
52
|
// src/video-chat/recovery.ts
|
|
12
53
|
var MEDIA_RECOVERY_NOTICE = "Some visuals were replaced so your response can continue.";
|
|
13
54
|
|
|
@@ -160,6 +201,9 @@ function safeMediaUrl(value) {
|
|
|
160
201
|
}
|
|
161
202
|
|
|
162
203
|
export {
|
|
204
|
+
WELCOME_CARDS,
|
|
205
|
+
orderWelcomeCards,
|
|
206
|
+
welcomeVisitSeed,
|
|
163
207
|
MEDIA_RECOVERY_NOTICE,
|
|
164
208
|
encodeVideoSseEvent,
|
|
165
209
|
decodeVideoSse,
|
package/dist/react.js
CHANGED
|
@@ -9,9 +9,11 @@ import {
|
|
|
9
9
|
import {
|
|
10
10
|
MEDIA_RECOVERY_NOTICE,
|
|
11
11
|
decodeVideoSse,
|
|
12
|
+
orderWelcomeCards,
|
|
12
13
|
sanitizeVideoChatMedia,
|
|
14
|
+
welcomeVisitSeed,
|
|
13
15
|
withDeadline
|
|
14
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-44WND2VP.js";
|
|
15
17
|
import {
|
|
16
18
|
applyVideoEvent,
|
|
17
19
|
checksumVideo,
|
|
@@ -2311,7 +2313,7 @@ function useVideoChatSession(options = {}) {
|
|
|
2311
2313
|
if (!response.ok) throw await responseError(response);
|
|
2312
2314
|
return response.json();
|
|
2313
2315
|
}).then((value) => {
|
|
2314
|
-
if (mountedRef.current) dispatch({ type: "welcome", value });
|
|
2316
|
+
if (mountedRef.current) dispatch({ type: "welcome", value: { ...value, cards: orderWelcomeCards(value.cards, welcomeVisitSeed()) } });
|
|
2315
2317
|
}).catch(() => void 0);
|
|
2316
2318
|
return () => {
|
|
2317
2319
|
endTiming();
|
|
@@ -3008,7 +3010,7 @@ function CaptionPages({ text, getProgress }) {
|
|
|
3008
3010
|
}
|
|
3009
3011
|
|
|
3010
3012
|
// src/video-chat/video-chat.tsx
|
|
3011
|
-
import { useCallback as
|
|
3013
|
+
import { useCallback as useCallback5, useEffect as useEffect10, useId as useId2, useLayoutEffect as useLayoutEffect3, useMemo as useMemo3, useRef as useRef9, useState as useState8 } from "react";
|
|
3012
3014
|
|
|
3013
3015
|
// src/video-chat/icons.tsx
|
|
3014
3016
|
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
@@ -3118,7 +3120,7 @@ function useFocusTrap(active, surface) {
|
|
|
3118
3120
|
}
|
|
3119
3121
|
|
|
3120
3122
|
// src/video-chat/suggestion-cards.tsx
|
|
3121
|
-
import {
|
|
3123
|
+
import { useEffect as useEffect7, useId, useRef as useRef5, useState as useState4 } from "react";
|
|
3122
3124
|
import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
3123
3125
|
function Frame({ media, poster, playing, onReady, onError, revealWhenReady = false }) {
|
|
3124
3126
|
const video = useRef5(null);
|
|
@@ -3130,6 +3132,9 @@ function Frame({ media, poster, playing, onReady, onError, revealWhenReady = fal
|
|
|
3130
3132
|
else element.pause();
|
|
3131
3133
|
}, [playing]);
|
|
3132
3134
|
if (!media) return null;
|
|
3135
|
+
if (playing === false && media.type === "video" && media.posterUrl) {
|
|
3136
|
+
return /* @__PURE__ */ jsx5("img", { className: "frame-media frame-poster", src: media.posterUrl, alt: "", onLoad: onReady, onError });
|
|
3137
|
+
}
|
|
3133
3138
|
const ready = () => {
|
|
3134
3139
|
setPlayingUrl(media.url);
|
|
3135
3140
|
onReady?.();
|
|
@@ -3161,37 +3166,38 @@ function Frame({ media, poster, playing, onReady, onError, revealWhenReady = fal
|
|
|
3161
3166
|
poster && media.posterUrl && playingUrl !== media.url && /* @__PURE__ */ jsx5("img", { className: "frame-media frame-poster", src: media.posterUrl, alt: "", onLoad: onReady })
|
|
3162
3167
|
] });
|
|
3163
3168
|
}
|
|
3164
|
-
function SuggestionCards({ suggestions, label, onAsk }) {
|
|
3169
|
+
function SuggestionCards({ suggestions, label, onAsk, browse = false }) {
|
|
3165
3170
|
const railRef = useRef5(null);
|
|
3171
|
+
const railId = useId();
|
|
3166
3172
|
const [at, setAt] = useState4(0);
|
|
3167
|
-
const [
|
|
3168
|
-
const take = useCallback3((index) => {
|
|
3169
|
-
setTaken(true);
|
|
3170
|
-
setAt(index);
|
|
3171
|
-
}, []);
|
|
3172
|
-
useEffect7(() => {
|
|
3173
|
-
if (taken || suggestions.length < 2) return;
|
|
3174
|
-
if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return;
|
|
3175
|
-
const tour = window.setInterval(() => setAt((index) => (index + 1) % suggestions.length), 5e3);
|
|
3176
|
-
return () => window.clearInterval(tour);
|
|
3177
|
-
}, [taken, suggestions.length]);
|
|
3173
|
+
const [overflow, setOverflow] = useState4({ before: false, after: false });
|
|
3178
3174
|
useEffect7(() => {
|
|
3179
3175
|
const rail = railRef.current;
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3176
|
+
if (!browse || !rail) return;
|
|
3177
|
+
const update = () => setOverflow({ before: rail.scrollLeft > 2, after: rail.scrollLeft + rail.clientWidth < rail.scrollWidth - 2 });
|
|
3178
|
+
update();
|
|
3179
|
+
rail.addEventListener("scroll", update, { passive: true });
|
|
3180
|
+
const observer = typeof ResizeObserver === "undefined" ? void 0 : new ResizeObserver(update);
|
|
3181
|
+
observer?.observe(rail);
|
|
3182
|
+
return () => {
|
|
3183
|
+
rail.removeEventListener("scroll", update);
|
|
3184
|
+
observer?.disconnect();
|
|
3185
|
+
};
|
|
3186
|
+
}, [browse, suggestions.length]);
|
|
3187
|
+
const move = (direction) => {
|
|
3188
|
+
const rail = railRef.current;
|
|
3189
|
+
if (!rail) return;
|
|
3190
|
+
rail.scrollBy({ left: direction * rail.clientWidth, behavior: window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth" });
|
|
3191
|
+
};
|
|
3186
3192
|
if (suggestions.length === 0) return null;
|
|
3187
|
-
|
|
3188
|
-
/* @__PURE__ */ jsx5("ul", { className: "cards", ref: railRef, "aria-label": label, children: suggestions.map((card, index) => /* @__PURE__ */ jsx5("li", { children: /* @__PURE__ */ jsxs4(
|
|
3193
|
+
const cards = /* @__PURE__ */ jsxs4(Fragment2, { children: [
|
|
3194
|
+
/* @__PURE__ */ jsx5("ul", { className: "cards", id: railId, ref: railRef, "aria-label": label, children: suggestions.map((card, index) => /* @__PURE__ */ jsx5("li", { children: /* @__PURE__ */ jsxs4(
|
|
3189
3195
|
"button",
|
|
3190
3196
|
{
|
|
3191
3197
|
type: "button",
|
|
3192
3198
|
"data-active": index === at ? "" : void 0,
|
|
3193
|
-
onFocus: () =>
|
|
3194
|
-
onPointerEnter: () =>
|
|
3199
|
+
onFocus: () => setAt(index),
|
|
3200
|
+
onPointerEnter: () => setAt(index),
|
|
3195
3201
|
onClick: () => onAsk(card),
|
|
3196
3202
|
children: [
|
|
3197
3203
|
/* @__PURE__ */ jsx5(Frame, { media: card.media, poster: true, playing: index === at }),
|
|
@@ -3207,11 +3213,23 @@ function SuggestionCards({ suggestions, label, onAsk }) {
|
|
|
3207
3213
|
"aria-pressed": index === at,
|
|
3208
3214
|
"aria-label": `Show suggestion ${index + 1}: ${card.prompt}`,
|
|
3209
3215
|
className: index === at ? "on" : void 0,
|
|
3210
|
-
onClick: () =>
|
|
3216
|
+
onClick: () => {
|
|
3217
|
+
setAt(index);
|
|
3218
|
+
const rail = railRef.current;
|
|
3219
|
+
const card2 = rail?.children[index];
|
|
3220
|
+
if (rail && card2) rail.scrollTo({ left: card2.offsetLeft, behavior: "smooth" });
|
|
3221
|
+
}
|
|
3211
3222
|
},
|
|
3212
3223
|
card.prompt
|
|
3213
3224
|
)) })
|
|
3214
3225
|
] });
|
|
3226
|
+
return browse ? /* @__PURE__ */ jsxs4("div", { className: "suggestion-rail", children: [
|
|
3227
|
+
cards,
|
|
3228
|
+
(overflow.before || overflow.after) && /* @__PURE__ */ jsxs4("div", { className: "rail-arrows", children: [
|
|
3229
|
+
/* @__PURE__ */ jsx5("button", { type: "button", "aria-label": "Previous suggestions", "aria-controls": railId, disabled: !overflow.before, onClick: () => move(-1), children: "\u2039" }),
|
|
3230
|
+
/* @__PURE__ */ jsx5("button", { type: "button", "aria-label": "Next suggestions", "aria-controls": railId, disabled: !overflow.after, onClick: () => move(1), children: "\u203A" })
|
|
3231
|
+
] })
|
|
3232
|
+
] }) : cards;
|
|
3215
3233
|
}
|
|
3216
3234
|
|
|
3217
3235
|
// src/video-chat/welcome.tsx
|
|
@@ -3226,7 +3244,7 @@ function Welcome({ data, onAsk, title }) {
|
|
|
3226
3244
|
/* @__PURE__ */ jsx6("br", {}),
|
|
3227
3245
|
/* @__PURE__ */ jsx6("em", { children: "in video, not text." })
|
|
3228
3246
|
] }) }),
|
|
3229
|
-
/* @__PURE__ */ jsx6(SuggestionCards, { suggestions: data?.cards ?? [], label: "Suggested prompts", onAsk })
|
|
3247
|
+
/* @__PURE__ */ jsx6(SuggestionCards, { browse: true, suggestions: data?.cards ?? [], label: "Suggested prompts", onAsk })
|
|
3230
3248
|
] })
|
|
3231
3249
|
] });
|
|
3232
3250
|
}
|
|
@@ -3276,7 +3294,7 @@ function OpeningChapter({ title, preparing = false }) {
|
|
|
3276
3294
|
}
|
|
3277
3295
|
|
|
3278
3296
|
// src/video-chat/use-voice-input.ts
|
|
3279
|
-
import { useCallback as
|
|
3297
|
+
import { useCallback as useCallback3, useEffect as useEffect8, useRef as useRef7, useState as useState6 } from "react";
|
|
3280
3298
|
function recognitionConstructor() {
|
|
3281
3299
|
if (typeof window === "undefined") return void 0;
|
|
3282
3300
|
const holder = window;
|
|
@@ -3358,7 +3376,7 @@ function useVoiceInput(onTranscript, transcriptionAvailable = false, options = {
|
|
|
3358
3376
|
useEffect8(() => {
|
|
3359
3377
|
setSupported(supportsVoiceInput(transcriptionAvailable));
|
|
3360
3378
|
}, [transcriptionAvailable]);
|
|
3361
|
-
const stop =
|
|
3379
|
+
const stop = useCallback3(() => {
|
|
3362
3380
|
recognitionRef.current?.abort();
|
|
3363
3381
|
recognitionRef.current = void 0;
|
|
3364
3382
|
const operation = operationRef.current;
|
|
@@ -3376,7 +3394,7 @@ function useVoiceInput(onTranscript, transcriptionAvailable = false, options = {
|
|
|
3376
3394
|
operation?.controller.abort();
|
|
3377
3395
|
operation?.finish?.();
|
|
3378
3396
|
}, []);
|
|
3379
|
-
const finish =
|
|
3397
|
+
const finish = useCallback3(() => {
|
|
3380
3398
|
const recognition = recognitionRef.current;
|
|
3381
3399
|
if (recognition) {
|
|
3382
3400
|
recognition.stop();
|
|
@@ -3390,7 +3408,7 @@ function useVoiceInput(onTranscript, transcriptionAvailable = false, options = {
|
|
|
3390
3408
|
}
|
|
3391
3409
|
stop();
|
|
3392
3410
|
}, [stop]);
|
|
3393
|
-
const record =
|
|
3411
|
+
const record = useCallback3(async () => {
|
|
3394
3412
|
if (!transcriptionAvailable || !recorderAvailable()) {
|
|
3395
3413
|
setError("Server transcription is not configured.");
|
|
3396
3414
|
return;
|
|
@@ -3428,7 +3446,7 @@ function useVoiceInput(onTranscript, transcriptionAvailable = false, options = {
|
|
|
3428
3446
|
}
|
|
3429
3447
|
}
|
|
3430
3448
|
}, [transcriptionAvailable, options.endpoint, options.headers, options.credentials, options.fetcher]);
|
|
3431
|
-
const toggle =
|
|
3449
|
+
const toggle = useCallback3(() => {
|
|
3432
3450
|
if (listening) {
|
|
3433
3451
|
finish();
|
|
3434
3452
|
return;
|
|
@@ -3480,18 +3498,18 @@ function useVoiceInput(onTranscript, transcriptionAvailable = false, options = {
|
|
|
3480
3498
|
}
|
|
3481
3499
|
|
|
3482
3500
|
// src/video-chat/use-immersive-controls.ts
|
|
3483
|
-
import { useCallback as
|
|
3501
|
+
import { useCallback as useCallback4, useEffect as useEffect9, useRef as useRef8, useState as useState7 } from "react";
|
|
3484
3502
|
function useImmersiveControls(playing, pinned, hasCaptions = false) {
|
|
3485
3503
|
const [visible, setVisible] = useState7(true);
|
|
3486
3504
|
const hovered = useRef8(false);
|
|
3487
3505
|
const focused = useRef8(false);
|
|
3488
3506
|
const previousCaptions = useRef8(false);
|
|
3489
3507
|
const timer = useRef8(null);
|
|
3490
|
-
const clearTimer =
|
|
3508
|
+
const clearTimer = useCallback4(() => {
|
|
3491
3509
|
if (timer.current !== null) clearTimeout(timer.current);
|
|
3492
3510
|
timer.current = null;
|
|
3493
3511
|
}, []);
|
|
3494
|
-
const reveal =
|
|
3512
|
+
const reveal = useCallback4(() => {
|
|
3495
3513
|
clearTimer();
|
|
3496
3514
|
setVisible(true);
|
|
3497
3515
|
if (playing && !pinned && !hovered.current && !focused.current) {
|
|
@@ -3513,21 +3531,21 @@ function useImmersiveControls(playing, pinned, hasCaptions = false) {
|
|
|
3513
3531
|
}
|
|
3514
3532
|
return clearTimer;
|
|
3515
3533
|
}, [hasCaptions, playing, pinned, reveal, clearTimer]);
|
|
3516
|
-
const onPointerEnter =
|
|
3534
|
+
const onPointerEnter = useCallback4((event) => {
|
|
3517
3535
|
if (event?.pointerType === "touch") return;
|
|
3518
3536
|
hovered.current = true;
|
|
3519
3537
|
reveal();
|
|
3520
3538
|
}, [reveal]);
|
|
3521
|
-
const onPointerLeave =
|
|
3539
|
+
const onPointerLeave = useCallback4(() => {
|
|
3522
3540
|
if (!hovered.current) return;
|
|
3523
3541
|
hovered.current = false;
|
|
3524
3542
|
reveal();
|
|
3525
3543
|
}, [reveal]);
|
|
3526
|
-
const onFocusCapture =
|
|
3544
|
+
const onFocusCapture = useCallback4((event) => {
|
|
3527
3545
|
focused.current = event.target.matches(":focus-visible");
|
|
3528
3546
|
reveal();
|
|
3529
3547
|
}, [reveal]);
|
|
3530
|
-
const onBlurCapture =
|
|
3548
|
+
const onBlurCapture = useCallback4((event) => {
|
|
3531
3549
|
if (focused.current && !event.currentTarget.contains(event.relatedTarget)) {
|
|
3532
3550
|
focused.current = false;
|
|
3533
3551
|
reveal();
|
|
@@ -3601,7 +3619,7 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3601
3619
|
if (turn.id !== chat.shownTurn?.id) return;
|
|
3602
3620
|
if (previous?.id !== turn.id || previous.mode !== turn.mode) setSelectedMode(turn.mode);
|
|
3603
3621
|
}, [chat.currentTurn, chat.shownTurn?.id]);
|
|
3604
|
-
const instanceId =
|
|
3622
|
+
const instanceId = useId2();
|
|
3605
3623
|
const historyId = `${instanceId}-history`;
|
|
3606
3624
|
const settingsId = `${instanceId}-settings`;
|
|
3607
3625
|
const promptId = `${instanceId}-prompt`;
|
|
@@ -3618,13 +3636,13 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3618
3636
|
});
|
|
3619
3637
|
const historySurfaces = useMemo3(() => [historyRef, historyButtonRef], []);
|
|
3620
3638
|
const settingsSurfaces = useMemo3(() => [settingsRef, settingsButtonRef], []);
|
|
3621
|
-
const closeHistory =
|
|
3622
|
-
const closeSettings =
|
|
3639
|
+
const closeHistory = useCallback5(() => setHistoryOpen(false), []);
|
|
3640
|
+
const closeSettings = useCallback5(() => setSettingsOpen(false), []);
|
|
3623
3641
|
useDismiss(historyOpen, closeHistory, historySurfaces);
|
|
3624
3642
|
useDismiss(settingsOpen, closeSettings, settingsSurfaces);
|
|
3625
3643
|
useFocusTrap(settingsOpen, settingsRef);
|
|
3626
3644
|
useFocusTrap(historyOpen, historyRef);
|
|
3627
|
-
const ask =
|
|
3645
|
+
const ask = useCallback5((value) => {
|
|
3628
3646
|
const prompt = (typeof value === "string" ? value : value.prompt).trim();
|
|
3629
3647
|
if (!prompt) return;
|
|
3630
3648
|
setDraft("");
|
|
@@ -3639,7 +3657,7 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3639
3657
|
opening: value.opening
|
|
3640
3658
|
});
|
|
3641
3659
|
}, [chat, listen]);
|
|
3642
|
-
const newSession =
|
|
3660
|
+
const newSession = useCallback5(() => {
|
|
3643
3661
|
if (chat.turns.length === 0) {
|
|
3644
3662
|
setHistoryOpen(false);
|
|
3645
3663
|
setSettingsOpen(false);
|
|
@@ -3712,7 +3730,7 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3712
3730
|
const line = chat.caption ?? "";
|
|
3713
3731
|
const fullTranscript = shown?.video ? [shown.opening, ...shown.video.scenes.map((scene) => scene.narration)].filter((entry) => Boolean(entry)) : chat.transcript;
|
|
3714
3732
|
const transport = status === "narrating" ? { label: "Pause", action: chat.pause, icon: /* @__PURE__ */ jsx9(Stop, {}) } : status === "paused" ? { label: "Continue", action: chat.resume, icon: /* @__PURE__ */ jsx9(Play, {}) } : status === "ended" && shown?.completed && shown.video ? { label: "Play again", action: chat.replay, icon: /* @__PURE__ */ jsx9(Replay, {}) } : void 0;
|
|
3715
|
-
const cancelInput =
|
|
3733
|
+
const cancelInput = useCallback5(() => {
|
|
3716
3734
|
listen.stop();
|
|
3717
3735
|
setDraft("");
|
|
3718
3736
|
setEditing(false);
|
|
@@ -3765,7 +3783,11 @@ function VideoChat({ options = {}, className, welcomeTitle, showRecoveryNotice =
|
|
|
3765
3783
|
onKeyDownCapture: controls.reveal,
|
|
3766
3784
|
children: [
|
|
3767
3785
|
/* @__PURE__ */ jsxs7("header", { className: "chrome", ...controlEvents, children: [
|
|
3768
|
-
/* @__PURE__ */ jsx9("div", { className: "session-brand", children: /* @__PURE__ */ jsx9("a", { className: "home-link", href: "/", "aria-label": "Home",
|
|
3786
|
+
/* @__PURE__ */ jsx9("div", { className: "session-brand", children: /* @__PURE__ */ jsx9("a", { className: "home-link", href: "/", "aria-label": "Home", onClick: (event) => {
|
|
3787
|
+
if (window.location.pathname !== "/" || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
3788
|
+
event.preventDefault();
|
|
3789
|
+
newSession();
|
|
3790
|
+
}, children: /* @__PURE__ */ jsx9(Logo, {}) }) }),
|
|
3769
3791
|
/* @__PURE__ */ jsxs7("div", { className: "group", children: [
|
|
3770
3792
|
/* @__PURE__ */ jsxs7(
|
|
3771
3793
|
"button",
|
package/dist/server.js
CHANGED
|
@@ -20,12 +20,13 @@ import {
|
|
|
20
20
|
} from "./chunk-IQMYK5DX.js";
|
|
21
21
|
import {
|
|
22
22
|
MEDIA_RECOVERY_NOTICE,
|
|
23
|
+
WELCOME_CARDS,
|
|
23
24
|
decodeVideoSse,
|
|
24
25
|
encodeVideoSseEvent,
|
|
25
26
|
sanitizeVideoChatMedia,
|
|
26
27
|
videoSseHeaders,
|
|
27
28
|
withDeadline
|
|
28
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-44WND2VP.js";
|
|
29
30
|
import "./chunk-RXTN2CW6.js";
|
|
30
31
|
import {
|
|
31
32
|
allowedKeys,
|
|
@@ -873,6 +874,27 @@ function createVideoHandler(options) {
|
|
|
873
874
|
});
|
|
874
875
|
}
|
|
875
876
|
|
|
877
|
+
// src/server/chat-visual-direction.ts
|
|
878
|
+
var defaults = {
|
|
879
|
+
explanation: "illustrated",
|
|
880
|
+
practical: "realistic",
|
|
881
|
+
story: "cinematic",
|
|
882
|
+
comedy: "cinematic",
|
|
883
|
+
imagination: "cinematic"
|
|
884
|
+
};
|
|
885
|
+
var bibles = {
|
|
886
|
+
illustrated: "Illustrated visual language: clear shaped forms, restrained texture and a coherent limited palette. Use readable spatial relationships, cutaways and purposeful motion to reveal the idea. Keep the same design of subjects and materials across shots.",
|
|
887
|
+
realistic: "Realistic visual language: natural light, credible materials, consistent colour and true physical proportions. Use unobstructed framing and meaningful close views so actions and results are easy to observe. Keep subjects, equipment and setting consistent.",
|
|
888
|
+
cinematic: "Cinematic visual language: intentional lighting, coherent colour and tactile detail. Use purposeful changes of shot scale and viewpoint, with clear action, consequence and a readable final frame. Preserve character appearance and the established world across cuts."
|
|
889
|
+
};
|
|
890
|
+
function compileVisualDirection(brief, callerLook) {
|
|
891
|
+
const intent = typeof brief.intent === "string" && Object.hasOwn(defaults, brief.intent) ? brief.intent : "explanation";
|
|
892
|
+
const visualStyle = typeof brief.visualStyle === "string" && Object.hasOwn(bibles, brief.visualStyle) ? brief.visualStyle : defaults[intent];
|
|
893
|
+
const visualDirection = typeof brief.visualDirection === "string" && brief.visualDirection.trim().length <= 600 ? brief.visualDirection.trim() : "";
|
|
894
|
+
const explicit = typeof callerLook === "string" && callerLook.trim().length <= 1e3 ? callerLook.trim() : "";
|
|
895
|
+
return { intent, visualStyle, visualDirection, generatedLook: explicit || bibles[visualStyle] };
|
|
896
|
+
}
|
|
897
|
+
|
|
876
898
|
// src/server/opening-continuity.ts
|
|
877
899
|
function continueAfterOpening(narration, earlier) {
|
|
878
900
|
const words = (value) => Array.from(value.matchAll(/[\p{L}\p{N}]+(?:['’][\p{L}\p{N}]+)*/gu));
|
|
@@ -960,6 +982,7 @@ function recoverFirstBrief(part, clipDurationSec) {
|
|
|
960
982
|
if (!ending || !bounded(ending.narration, 2e3) || !bounded(ending.subject, 80, true) || !bounded(ending.action, 600, true) || Object.hasOwn(ending, "title") && !bounded(ending.title, 65) || typeof ending.durationSec !== "number" || !Number.isFinite(ending.durationSec) || ending.continuity !== "cut" && ending.continuity !== "continue") return;
|
|
961
983
|
const subject = text(part.subject, 80);
|
|
962
984
|
return {
|
|
985
|
+
...compileVisualDirection(part),
|
|
963
986
|
opening: text(part.opening, 300),
|
|
964
987
|
subject,
|
|
965
988
|
development: text(part.development, 2e3),
|
|
@@ -978,12 +1001,14 @@ function replaceStream(source, textStream) {
|
|
|
978
1001
|
function createChatShotPlanner(options) {
|
|
979
1002
|
const clipDurationSec = options.generatedClipDurationSec ?? 5;
|
|
980
1003
|
const incomplete = /* @__PURE__ */ new WeakSet();
|
|
1004
|
+
const generatedLooks = /* @__PURE__ */ new WeakMap();
|
|
981
1005
|
const planner = createTextDeltaVideoPlanner({
|
|
982
1006
|
includeRawProviderData: options.includeRawProviderData,
|
|
983
1007
|
streamText(context) {
|
|
984
1008
|
const providerContext = { ...context, userPrompt: [
|
|
985
1009
|
`Create a complete answer within ${context.request.input.maxDurationSec ?? 40} seconds. Each generated clip has at most ${clipDurationSec} seconds; give each spoken beat room to finish.`,
|
|
986
1010
|
`Orientation: ${context.request.input.orientation ?? "landscape"}.`,
|
|
1011
|
+
...context.request.input.style?.generatedLook ? [`CALLER VISUAL DIRECTION (takes precedence over automatic style): ${context.request.input.style.generatedLook}`, "Preserve this requested visual language. The brief visualDirection must contain compatible subjects, setting and palette, never a contradictory rendering style."] : [],
|
|
987
1012
|
"USER REQUEST AND CONVERSATION",
|
|
988
1013
|
context.request.input.input
|
|
989
1014
|
].join("\n") };
|
|
@@ -1005,6 +1030,10 @@ function createChatShotPlanner(options) {
|
|
|
1005
1030
|
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
1006
1031
|
if (!getGenerationLifecycleSink(context)?.rejectPart?.(error)) throw error;
|
|
1007
1032
|
};
|
|
1033
|
+
const acceptDirection = (value) => {
|
|
1034
|
+
const direction = compileVisualDirection(value, context.request.input.style?.generatedLook);
|
|
1035
|
+
generatedLooks.set(context, direction.generatedLook);
|
|
1036
|
+
};
|
|
1008
1037
|
const scenePart = (shot, closer = false) => {
|
|
1009
1038
|
let narration = shot.narration;
|
|
1010
1039
|
if (firstBody && !closer) narration = continueAfterOpening(narration, [options.openingLine ?? brief?.opening ?? ""]);
|
|
@@ -1032,12 +1061,14 @@ function createChatShotPlanner(options) {
|
|
|
1032
1061
|
const recovered = firstRecord && !brief && index === 0 ? recoverFirstBrief(part, clipDurationSec) : void 0;
|
|
1033
1062
|
if (recovered) {
|
|
1034
1063
|
brief = recovered;
|
|
1064
|
+
acceptDirection(brief);
|
|
1035
1065
|
options.publishOpening({ line: brief.opening, keyword: brief.subject });
|
|
1036
1066
|
return;
|
|
1037
1067
|
}
|
|
1038
1068
|
if (part?.type === "answer") {
|
|
1039
1069
|
if (brief) throw new Error("Chat answer brief was emitted more than once");
|
|
1040
|
-
brief = { opening: text(part.opening, 300), subject: text(part.subject, 80), visualDirection: text(part.visualDirection, 600), development: text(part.development, 2e3) };
|
|
1070
|
+
brief = { ...compileVisualDirection(part), opening: text(part.opening, 300), subject: text(part.subject, 80), visualDirection: text(part.visualDirection, 600), development: text(part.development, 2e3) };
|
|
1071
|
+
acceptDirection(brief);
|
|
1041
1072
|
if (part.ending) {
|
|
1042
1073
|
try {
|
|
1043
1074
|
brief.ending = readShot(part.ending, clipDurationSec, brief.subject);
|
|
@@ -1144,7 +1175,7 @@ function createChatShotPlanner(options) {
|
|
|
1144
1175
|
});
|
|
1145
1176
|
return async function* (context) {
|
|
1146
1177
|
let completed = false;
|
|
1147
|
-
for await (const part of resolveShots(planner(context), context, options)) {
|
|
1178
|
+
for await (const part of resolveShots(planner(context), context, options, () => options.mode === "pexels" ? context.request.input.style?.generatedLook : generatedLooks.get(context))) {
|
|
1148
1179
|
if (part.type === "plan.complete") completed = true;
|
|
1149
1180
|
yield part;
|
|
1150
1181
|
}
|
|
@@ -1154,7 +1185,7 @@ function createChatShotPlanner(options) {
|
|
|
1154
1185
|
}
|
|
1155
1186
|
};
|
|
1156
1187
|
}
|
|
1157
|
-
async function* resolveShots(parts, context, options) {
|
|
1188
|
+
async function* resolveShots(parts, context, options, generatedLook) {
|
|
1158
1189
|
const queue = [];
|
|
1159
1190
|
const iterator = parts[Symbol.asyncIterator]();
|
|
1160
1191
|
const limit = Number.isFinite(options.mediaConcurrency) ? Math.min(5, Math.max(1, Math.floor(options.mediaConcurrency))) : 1;
|
|
@@ -1171,7 +1202,7 @@ async function* resolveShots(parts, context, options) {
|
|
|
1171
1202
|
scene: part.scene,
|
|
1172
1203
|
templateId: "cinemaMedia",
|
|
1173
1204
|
preferredType: "video",
|
|
1174
|
-
generatedLook: context.request.input.style?.generatedLook,
|
|
1205
|
+
generatedLook: generatedLook() ?? context.request.input.style?.generatedLook,
|
|
1175
1206
|
signal: context.signal
|
|
1176
1207
|
});
|
|
1177
1208
|
context.signal.throwIfAborted();
|
|
@@ -1229,7 +1260,7 @@ function createVideoChatResponseInstructions(generatedVideoAvailable, openingAlr
|
|
|
1229
1260
|
return [
|
|
1230
1261
|
'Use the exact record type "answer" for the first brief and "shot" for developing beats. Output JSON records only, with no prose outside them, including when explaining a limitation.',
|
|
1231
1262
|
"Write a complete, intentful video answer as newline-delimited JSON. Match the user's form and tone; mixed intents can combine directions.",
|
|
1232
|
-
`First write one brief: {"type":"answer","intent":"explanation|story|comedy|imagination|practical","opening":"one useful spoken line of 4\u20137 ordinary words","subject":"literal visual subject","development":"the essential development of this answer","visualDirection":"consistent subjects, appearance and visual approach","ending":{"title":"short meaningful chapter title, at most 65 characters","narration":"the authored payoff","subject":"literal subject","action":"visible action or change","durationSec":${clipDurationSec},"continuity":"cut|continue"}}.`,
|
|
1263
|
+
`First write one brief: {"type":"answer","intent":"explanation|story|comedy|imagination|practical","visualStyle":"illustrated|realistic|cinematic","opening":"one useful spoken line of 4\u20137 ordinary words","subject":"literal visual subject","development":"the essential development of this answer","visualDirection":"consistent subjects, appearance and visual approach","ending":{"title":"short meaningful chapter title, at most 65 characters","narration":"the authored payoff","subject":"literal subject","action":"visible action or change","durationSec":${clipDurationSec},"continuity":"cut|continue"}}.`,
|
|
1233
1264
|
"The opening should take roughly 2\u20133 seconds at a natural pace: give the core answer, a useful starting cue, or the story's immediate situation. No greeting, topic announcement, promise to explain, or description of loading. Never compress away an essential qualifier just to hit the word target.",
|
|
1234
1265
|
`Then stream each developing shot on its own line: {"type":"shot","title":"short meaningful chapter title, at most 65 characters","narration":"the exact spoken beat","subject":"2\u20138 literal filmable words, at most 80 characters","action":"concrete subject, action or visible change and useful framing","durationSec":${clipDurationSec},"continuity":"cut|continue"}.`,
|
|
1235
1266
|
`The selected footage mode is ${mode === "pexels" ? "Pexels stock search: use literal filmable subjects; never imply stock proves a mechanism or depicts fictional events exactly" : `AI video, with at most ${generatedVideoAvailable ? maxGeneratedVideos : 0} generation attempts`}. Missing footage becomes the authored chapter title, with complete narration. Never truncate already-authored narration when footage fails. The host selects providers; do not make source choices.`,
|
|
@@ -1237,6 +1268,8 @@ function createVideoChatResponseInstructions(generatedVideoAvailable, openingAlr
|
|
|
1237
1268
|
...mode === "pexels" ? ["Stock queries must retain the essential subject, activity and distinguishing equipment in the shot's subject field, within its word limit. That field alone is the search query; action and visualDirection do not refine it. Prefer common observable actions with usable framing. Do not replace the required actor or activity with scenery, a different sport or a loosely related setting. Preserve fictional or comic narration, but do not depend on stock showing an exact invented expression or sequence; choose an illustrative action that supports the beat."] : [],
|
|
1238
1269
|
...mode === "pexels" ? ["Choose a separate stock subject for each beat, describing footage that can realistically exist in a stock library. For historical, abstract or unseen events, use relevant present-day evidence, objects, environments or analogous visible processes as clearly illustrative support. Do not require literal footage of events or subjects that cannot realistically be filmed. Keep the causal explanation in narration; do not claim illustrative footage records the historical event or proves the mechanism. Make stockSelection describe the chosen visible subject, not the overall topic. Do not use illustrative freedom to replace a required practical action, person, sport or distinguishing equipment with unrelated scenery."] : [],
|
|
1239
1270
|
...mode === "pexels" ? ['Include stockSelection on every shot and the saved ending when the essential subject is known: "stockSelection":{"subject":"essential actor or object category","activity":"optional literal activity","equipment":"optional distinguishing equipment","exclude":["optional contradictory subject or activity"]}. Each phrase must be 1\u20134 words and at most 48 characters; exclude has at most 3 phrases. The essential subject is separate from the setting: do not use scenery, mood, camera framing or incidental appearance as the actor. Keep the search query broad enough to find footage; the optional hint helps select results without substituting a different actor or task. Use exclusions only for actual contradictions, not every detail absent from the story. Omit unknown fields or the whole hint rather than inventing an anchor. This is selection guidance, not verification that footage depicts the exact narration.'] : [],
|
|
1271
|
+
"Choose one of the five intents and one visualStyle in the first brief. Default explanation to illustrated, practical to realistic, and story, comedy or imagination to cinematic. An explicit visual-style request can choose any of the three. Put its specific medium, palette, character appearance and setting in visualDirection; keep those details consistent through the ending. A supplied caller visual direction takes precedence over these defaults and must not be contradicted.",
|
|
1272
|
+
"The visualStyle names describe generated footage only. Stock mode selects existing literal footage; it cannot redraw or restyle that footage.",
|
|
1240
1273
|
"Keep development to one concise sentence and visualDirection to the few details needed for consistency. Emit the complete brief, then the first developing shot immediately when developing shots are needed and allowed by the budget; otherwise end after the brief. Continue the same stream without an outline, recap or second planning pass. The saved ending must still contain the complete payoff before the brief is emitted.",
|
|
1241
1274
|
"For a very short answer whose ending alone fulfills the request, development may be empty and no developing shots are needed. Otherwise, develop the essential content before the ending.",
|
|
1242
1275
|
"The brief's ending is saved and played after your developing shots. Do not repeat it as a shot. Stop writing after the last developing shot. No technical events, identifiers, template choices, media providers, URLs or unlisted fields.",
|
|
@@ -1296,28 +1329,6 @@ var MAX_PROMPT_CHARACTERS = 8e3;
|
|
|
1296
1329
|
var MAX_CONVERSATION_TURNS = 12;
|
|
1297
1330
|
var MAX_CONVERSATION_RESPONSE_CHARACTERS = 8e3;
|
|
1298
1331
|
var VIDEO_CHAT_OPENING_EVENT_TYPE = "data.video-chat-opening";
|
|
1299
|
-
var DEFAULT_WELCOME_PROMPTS = [
|
|
1300
|
-
{
|
|
1301
|
-
prompt: "Why does the Moon always show one face?",
|
|
1302
|
-
opening: "The Moon turns, perfectly matching its orbit.",
|
|
1303
|
-
mediaQuery: "full moon night sky"
|
|
1304
|
-
},
|
|
1305
|
-
{
|
|
1306
|
-
prompt: "Tell me a tiny story about a robot growing a garden on Mars",
|
|
1307
|
-
opening: "One patient robot is about to make Mars bloom.",
|
|
1308
|
-
mediaQuery: "robot garden mars"
|
|
1309
|
-
},
|
|
1310
|
-
{
|
|
1311
|
-
prompt: "Recommend a perfect rainy afternoon in Amsterdam",
|
|
1312
|
-
opening: "Rain makes Amsterdam's best afternoons feel even warmer.",
|
|
1313
|
-
mediaQuery: "Amsterdam rain cafe"
|
|
1314
|
-
},
|
|
1315
|
-
{
|
|
1316
|
-
prompt: "Pitch a playful ad for a coffee mug that never spills",
|
|
1317
|
-
opening: "This mug makes gravity look completely optional.",
|
|
1318
|
-
mediaQuery: "coffee mug desk"
|
|
1319
|
-
}
|
|
1320
|
-
];
|
|
1321
1332
|
function jsonError2(status, code, message, headers) {
|
|
1322
1333
|
return Response.json({ error: { code, message } }, { status, headers });
|
|
1323
1334
|
}
|
|
@@ -1626,7 +1637,7 @@ function createVideoChatHandler(options) {
|
|
|
1626
1637
|
transcription: transcribe != null,
|
|
1627
1638
|
modes: searchMedia ? ["cinematic", "pexels"] : ["cinematic"]
|
|
1628
1639
|
};
|
|
1629
|
-
const welcomePrompts = (welcomeOptions?.prompts ??
|
|
1640
|
+
const welcomePrompts = (welcomeOptions?.prompts ?? WELCOME_CARDS).slice(0, 8);
|
|
1630
1641
|
const heroQuery = welcomeOptions?.heroQuery;
|
|
1631
1642
|
let welcomeResponse;
|
|
1632
1643
|
let requestSequence = 0;
|
|
@@ -1828,7 +1839,7 @@ function createVideoChatHandler(options) {
|
|
|
1828
1839
|
};
|
|
1829
1840
|
const [hero, ...cards] = await Promise.all([
|
|
1830
1841
|
heroQuery === void 0 ? DEFAULT_WELCOME_HERO : resolve(heroQuery),
|
|
1831
|
-
...welcomePrompts.map((entry) => resolve(entry.mediaQuery))
|
|
1842
|
+
...welcomePrompts.map((entry, index) => welcomeOptions?.prompts === void 0 ? WELCOME_CARDS[index]?.media ?? null : resolve("mediaQuery" in entry ? entry.mediaQuery : void 0))
|
|
1832
1843
|
]);
|
|
1833
1844
|
return {
|
|
1834
1845
|
cacheable: !failed,
|
package/docs/media-and-audio.md
CHANGED
|
@@ -19,8 +19,8 @@ The chapter retains narration and subtitles for the whole beat.
|
|
|
19
19
|
|
|
20
20
|
The starter performs bounded full-catalog Pexels video search with subject
|
|
21
21
|
matching, orientation-aware renditions and a bounded cache. Add `PEXELS_API_KEY`
|
|
22
|
-
on the server and choose Pexels in Settings. The
|
|
23
|
-
|
|
22
|
+
on the server and choose Pexels in Settings. The default header has no Pexels
|
|
23
|
+
link; the application owns any attribution required by its media provider.
|
|
24
24
|
|
|
25
25
|
Applications can replace `searchMedia` with their own licensed catalog:
|
|
26
26
|
|
|
@@ -50,9 +50,43 @@ default chat displays an authored chapter while retaining the spoken answer. An
|
|
|
50
50
|
validation, and fallback contracts.
|
|
51
51
|
|
|
52
52
|
For Pexels, keep `PEXELS_API_KEY` on the server, enforce a deadline, filter for
|
|
53
|
-
|
|
53
|
+
suitable renditions, and return only validated Pexels asset domains. Licensing,
|
|
54
54
|
attribution, caching, MIME checks, and byte limits remain application-owned.
|
|
55
55
|
|
|
56
|
+
## Automatic visual direction
|
|
57
|
+
|
|
58
|
+
Default chat uses the existing answer brief to choose the requested form:
|
|
59
|
+
explanation (including comparisons), practical instruction, story, comedy, or
|
|
60
|
+
imagination. These shape the content and pacing; they do not change knowledge
|
|
61
|
+
rules or provider allowances.
|
|
62
|
+
|
|
63
|
+
The same brief selects one of three generated-video treatments:
|
|
64
|
+
|
|
65
|
+
- **Illustrated:** clear drawn forms, consistent materials and a restrained palette;
|
|
66
|
+
the default for explanations.
|
|
67
|
+
- **Realistic:** believable lighting, proportions and movement with useful framing;
|
|
68
|
+
the default for practical instruction.
|
|
69
|
+
- **Cinematic:** deliberate composition, lighting and motivated camera movement;
|
|
70
|
+
the default for stories, comedy and imagined worlds.
|
|
71
|
+
|
|
72
|
+
An explicit style request in the prompt takes priority over the default. The
|
|
73
|
+
planner carries its response-specific subjects, palette and setting in the
|
|
74
|
+
brief's visual direction. Every body shot and ending receives the same selected
|
|
75
|
+
base treatment through `generatedLook`, alongside its individual `shotDirection`.
|
|
76
|
+
Adapters must pass both to their video provider. An explicit
|
|
77
|
+
`style.generatedLook` replaces the automatic base treatment.
|
|
78
|
+
|
|
79
|
+
There is no separate classification call or image-generation stage. Selecting a
|
|
80
|
+
look does not guarantee that independently generated clips preserve character
|
|
81
|
+
identity. Evaluate actual footage for subject consistency, useful action,
|
|
82
|
+
narration fit and completion; mocked responses only verify the integration.
|
|
83
|
+
|
|
84
|
+
Pexels retains literal footage queries; these instructions cannot restyle stock
|
|
85
|
+
assets. The opening chapter keeps its existing appearance. Automatic direction
|
|
86
|
+
is generation-time guidance, not a new persisted style field; saved media keeps
|
|
87
|
+
its rendered appearance and existing caller-supplied style persistence is
|
|
88
|
+
unchanged. Custom template planning keeps its existing behavior.
|
|
89
|
+
|
|
56
90
|
## Generated shots
|
|
57
91
|
|
|
58
92
|
Add `generateVideo` to enable generated shots within cinematic responses. It receives the planned visual
|
package/package.json
CHANGED
package/styles/video-chat.css
CHANGED
|
@@ -1127,3 +1127,17 @@
|
|
|
1127
1127
|
.vanillasky-video-chat .transcript-toggle { display: inline-flex; align-items: center; gap: 8px; min-height: 40px; padding: 8px 12px; border: 0; border-radius: 10px; background: var(--vs-media-glass); color: var(--vs-media-muted); font: inherit; font-size: 13px; cursor: pointer; pointer-events: auto; }
|
|
1128
1128
|
.vanillasky-video-chat .transcript-toggle svg { width: 16px; height: 16px; }
|
|
1129
1129
|
.vanillasky-video-chat .transcript-toggle:focus-visible { outline: 2px solid currentColor; outline-offset: 3px; }
|
|
1130
|
+
|
|
1131
|
+
/* Welcome shows a deliberate four-card window; the rest stays in the rail. */
|
|
1132
|
+
.vanillasky-video-chat .suggestion-rail { position: relative; flex-shrink: 0; width: calc(4 * clamp(160px, 18vw, 260px) + 66px); max-width: calc(100% + 24px); margin: auto -12px 0; }
|
|
1133
|
+
.vanillasky-video-chat .suggestion-rail .cards { width: 100%; max-width: 100%; margin: 0; }
|
|
1134
|
+
.vanillasky-video-chat .rail-arrows { position: absolute; inset: 50% -6px auto; display: flex; justify-content: space-between; transform: translateY(-50%); pointer-events: none; }
|
|
1135
|
+
.vanillasky-video-chat .rail-arrows button { width: 36px; height: 36px; border: 1px solid rgb(255 255 255 / 30%); border-radius: 50%; background: #202438; color: white; font: inherit; font-size: 25px; line-height: 1; cursor: pointer; pointer-events: auto; }
|
|
1136
|
+
.vanillasky-video-chat .rail-arrows button:disabled { visibility: hidden; pointer-events: none; }
|
|
1137
|
+
.vanillasky-video-chat .rail-arrows button:focus-visible { outline: 2px solid white; outline-offset: 3px; }
|
|
1138
|
+
@media (max-width: 700px) {
|
|
1139
|
+
.vanillasky-video-chat .suggestion-rail { width: calc(100% + 40px); max-width: calc(100% + 40px); margin-inline: -20px; }
|
|
1140
|
+
.vanillasky-video-chat .rail-arrows { display: none; }
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
@media (hover: none) { .vanillasky-video-chat .rail-arrows { display: none; } }
|