@vanillaskyai/video 0.11.1 → 0.11.2
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 +6 -0
- package/PUBLIC-API.md +1 -1
- package/README.md +3 -2
- package/dist/chunk-NEKYSCT6.js +39 -0
- package/dist/{chunk-666HGTVZ.js → chunk-R2RJIB6B.js} +15 -1
- package/dist/{chunk-RFXHLLYU.js → chunk-XJ36A5JD.js} +1 -39
- package/dist/{cinema-media-YZ2UANJG.js → cinema-media-DLIYPESB.js} +63 -8
- package/dist/react.js +50 -21
- package/dist/server.js +7 -5
- package/docs/architecture.md +5 -2
- package/docs/customization.md +4 -0
- package/docs/media-and-audio.md +14 -2
- package/docs/provider-integration.md +4 -2
- package/package.json +1 -1
- package/starters/video-chat/package.json +1 -1
- package/styles/video-chat.css +1 -7
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,12 @@ VanillaSky follows semantic versioning. This changelog begins with the 0.1 beta.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.11.2
|
|
8
|
+
|
|
9
|
+
- Show the spoken introduction in the normal subtitle line when subtitles are enabled. Keep the opening once in the full transcript, including replay and restored sessions.
|
|
10
|
+
- Keep “Ask next” directly above its follow-up thumbnails on desktop and phone layouts.
|
|
11
|
+
- Allow one native-speed repeat for a small measured speech overrun on healthy footage, ending when speech finishes. Never repeat solely for the quiet tail; retain chapter recovery for larger or unmeasured overruns and failed media. Planning still targets a 0.8-second tail and never buys an extra clip to fit narration.
|
|
12
|
+
|
|
7
13
|
## 0.11.1
|
|
8
14
|
|
|
9
15
|
- Plan single-idea narration with voice headroom against the configured video duration; keep the full repair budget for necessary meaning. Give the single rewrite explicit speech/word budgets and report content-free outcomes instead of silently collapsing timeout, empty, failed and oversized results.
|
package/PUBLIC-API.md
CHANGED
|
@@ -35,7 +35,7 @@ Two visual modes remain: `cinematic` (AI video) and `pexels` (application-owned
|
|
|
35
35
|
|
|
36
36
|
The video callback receives `requestedDurationSec`, `shotDirection` and an absolute `deadlineAt`, alongside its abort signal. Returned media may report `durationSec`. Configure the model, clip duration and timeout together in the application adapter; the server accepts timeouts up to ten minutes, while the client defaults to an eleven-minute response deadline. These are safety ceilings, not latency promises.
|
|
37
37
|
|
|
38
|
-
Planning
|
|
38
|
+
Planning targets at least 0.8 seconds of quiet footage after narration. Before buying a clip, the handler allows one bounded `generateText` call with task `narration-rewrite`. Oversized or failed rewrites retain the original narration on a chapter and do not buy a clip. Measured audio and decoded footage are checked again before playback; normal video runs at its native speed without repeats. A measured speech overrun of at most the smaller of one second or 25% of the actual clip may repeat healthy footage once, ending at speech completion. Footage never repeats just to fill a quiet tail; larger/unmeasured overruns and failed media recover to a chapter. Rewriting is model-assisted, not automatic fact verification.
|
|
39
39
|
|
|
40
40
|
`authorize` is required; use `authorize: "none"` only for intentionally local/test handlers. `invalidPartBehavior: "drop"` preserves valid scenes after malformed planner output; `"fail"` opts into strict failure. Interrupted plans preserve playable partial answers and emit a safe warning. The narration action is a fallback for missing narration, not a second model call in the normal path.
|
|
41
41
|
|
package/README.md
CHANGED
|
@@ -67,8 +67,9 @@ substitutes stock; stock mode never spends on generated video. A failed or late
|
|
|
67
67
|
clip becomes a narrated chapter. Narration targets a 0.8-second visual tail;
|
|
68
68
|
one short rewrite may fit an oversized beat before generation. If it still
|
|
69
69
|
does not fit, the complete original narration plays over a chapter. Footage
|
|
70
|
-
plays once at native speed
|
|
71
|
-
|
|
70
|
+
normally plays once at native speed. A small measured speech overrun can repeat
|
|
71
|
+
healthy footage once, only until speech finishes; larger or unmeasured overruns
|
|
72
|
+
recover to a chapter without cutting off the sentence.
|
|
72
73
|
|
|
73
74
|
This is progressive **scene** delivery, not real-time frames from every vendor.
|
|
74
75
|
Some generation APIs take minutes; preloading cannot remove that latency.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// src/protocol/clip-budget.ts
|
|
2
|
+
var CLIP_NARRATION_TAIL_SEC = 0.8;
|
|
3
|
+
function clipNarrationBudget(clipDurationSec) {
|
|
4
|
+
const maxSpeechSec = Math.max(0, clipDurationSec - CLIP_NARRATION_TAIL_SEC);
|
|
5
|
+
const maxWords = Math.floor(maxSpeechSec * 2);
|
|
6
|
+
const maxUnspacedCharacters = Math.floor(maxSpeechSec * 3);
|
|
7
|
+
return {
|
|
8
|
+
clipDurationSec,
|
|
9
|
+
maxSpeechSec,
|
|
10
|
+
targetWords: Math.floor(maxWords * 0.8),
|
|
11
|
+
targetUnspacedCharacters: Math.floor(maxUnspacedCharacters * 0.8),
|
|
12
|
+
maxWords,
|
|
13
|
+
maxUnspacedCharacters
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
var UNSPACED_SCRIPT = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}\p{Script=Thai}\p{Script=Lao}\p{Script=Khmer}\p{Script=Myanmar}]/gu;
|
|
17
|
+
function estimateNarrationSeconds(text) {
|
|
18
|
+
const normalized = text.trim();
|
|
19
|
+
if (!normalized) return 0;
|
|
20
|
+
const characters = normalized.match(UNSPACED_SCRIPT)?.length ?? 0;
|
|
21
|
+
const words = normalized.replace(UNSPACED_SCRIPT, " ").match(/[\p{L}\p{N}]+(?:['’][\p{L}\p{N}]+)*/gu)?.length ?? 0;
|
|
22
|
+
const numericExpansion = (normalized.match(/\p{Nd}+/gu) ?? []).reduce((sum, digits) => sum + Math.max(0, digits.length - 1) + (digits.length >= 3 ? 1 : 0), 0);
|
|
23
|
+
const symbols = normalized.match(/[%°\p{Sc}]/gu)?.length ?? 0;
|
|
24
|
+
const pauses = normalized.match(/[.!?。!?;;::]/gu)?.length ?? 0;
|
|
25
|
+
return (words + numericExpansion + symbols) / 2.2 + characters / 3.5 + pauses * 0.15;
|
|
26
|
+
}
|
|
27
|
+
function speechFitsClip(seconds, durationSec) {
|
|
28
|
+
return Number.isFinite(seconds) && seconds >= 0 && Number.isFinite(durationSec) && durationSec > 0 && seconds + CLIP_NARRATION_TAIL_SEC <= durationSec + 1e-6;
|
|
29
|
+
}
|
|
30
|
+
function narrationFitsClip(text, durationSec) {
|
|
31
|
+
return speechFitsClip(estimateNarrationSeconds(text), durationSec);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export {
|
|
35
|
+
CLIP_NARRATION_TAIL_SEC,
|
|
36
|
+
clipNarrationBudget,
|
|
37
|
+
estimateNarrationSeconds,
|
|
38
|
+
narrationFitsClip
|
|
39
|
+
};
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CLIP_NARRATION_TAIL_SEC
|
|
3
|
+
} from "./chunk-NEKYSCT6.js";
|
|
4
|
+
|
|
1
5
|
// src/visual-system/scene-templates/external-video-backdrop.tsx
|
|
2
6
|
import React from "react";
|
|
3
7
|
import { jsx } from "react/jsx-runtime";
|
|
@@ -32,10 +36,20 @@ function useMediaFailure() {
|
|
|
32
36
|
return React.useContext(BackdropContext).onMediaError;
|
|
33
37
|
}
|
|
34
38
|
|
|
39
|
+
// src/player/clip-repeat.ts
|
|
40
|
+
function measuredClipPlayback(spokenSeconds, clipDurationSec) {
|
|
41
|
+
if (typeof spokenSeconds !== "number" || !Number.isFinite(spokenSeconds) || spokenSeconds <= 0 || !Number.isFinite(clipDurationSec) || clipDurationSec <= 0) return void 0;
|
|
42
|
+
const overrun = spokenSeconds - clipDurationSec;
|
|
43
|
+
if (overrun <= 0) return { durationSec: Math.min(spokenSeconds + CLIP_NARRATION_TAIL_SEC, clipDurationSec), repeat: false };
|
|
44
|
+
if (overrun <= Math.min(1, clipDurationSec * 0.25) + 1e-6) return { durationSec: spokenSeconds, repeat: true };
|
|
45
|
+
return void 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
35
48
|
export {
|
|
36
49
|
ExternalVideoBackdropProvider,
|
|
37
50
|
useExternalVideoBackdrop,
|
|
38
51
|
useMediaAudio,
|
|
39
52
|
useNarrationPreroll,
|
|
40
|
-
useMediaFailure
|
|
53
|
+
useMediaFailure,
|
|
54
|
+
measuredClipPlayback
|
|
41
55
|
};
|
|
@@ -136,39 +136,6 @@ function videoSseHeaders(headers) {
|
|
|
136
136
|
return result;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
// src/protocol/clip-budget.ts
|
|
140
|
-
var CLIP_NARRATION_TAIL_SEC = 0.8;
|
|
141
|
-
function clipNarrationBudget(clipDurationSec) {
|
|
142
|
-
const maxSpeechSec = Math.max(0, clipDurationSec - CLIP_NARRATION_TAIL_SEC);
|
|
143
|
-
const maxWords = Math.floor(maxSpeechSec * 2);
|
|
144
|
-
const maxUnspacedCharacters = Math.floor(maxSpeechSec * 3);
|
|
145
|
-
return {
|
|
146
|
-
clipDurationSec,
|
|
147
|
-
maxSpeechSec,
|
|
148
|
-
targetWords: Math.floor(maxWords * 0.8),
|
|
149
|
-
targetUnspacedCharacters: Math.floor(maxUnspacedCharacters * 0.8),
|
|
150
|
-
maxWords,
|
|
151
|
-
maxUnspacedCharacters
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
var UNSPACED_SCRIPT = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}\p{Script=Thai}\p{Script=Lao}\p{Script=Khmer}\p{Script=Myanmar}]/gu;
|
|
155
|
-
function estimateNarrationSeconds(text) {
|
|
156
|
-
const normalized = text.trim();
|
|
157
|
-
if (!normalized) return 0;
|
|
158
|
-
const characters = normalized.match(UNSPACED_SCRIPT)?.length ?? 0;
|
|
159
|
-
const words = normalized.replace(UNSPACED_SCRIPT, " ").match(/[\p{L}\p{N}]+(?:['’][\p{L}\p{N}]+)*/gu)?.length ?? 0;
|
|
160
|
-
const numericExpansion = (normalized.match(/\p{Nd}+/gu) ?? []).reduce((sum, digits) => sum + Math.max(0, digits.length - 1) + (digits.length >= 3 ? 1 : 0), 0);
|
|
161
|
-
const symbols = normalized.match(/[%°\p{Sc}]/gu)?.length ?? 0;
|
|
162
|
-
const pauses = normalized.match(/[.!?。!?;;::]/gu)?.length ?? 0;
|
|
163
|
-
return (words + numericExpansion + symbols) / 2.2 + characters / 3.5 + pauses * 0.15;
|
|
164
|
-
}
|
|
165
|
-
function speechFitsClip(seconds, durationSec) {
|
|
166
|
-
return Number.isFinite(seconds) && seconds >= 0 && Number.isFinite(durationSec) && durationSec > 0 && seconds + CLIP_NARRATION_TAIL_SEC <= durationSec + 1e-6;
|
|
167
|
-
}
|
|
168
|
-
function narrationFitsClip(text, durationSec) {
|
|
169
|
-
return speechFitsClip(estimateNarrationSeconds(text), durationSec);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
139
|
export {
|
|
173
140
|
WELCOME_CARDS,
|
|
174
141
|
orderWelcomeCards,
|
|
@@ -176,10 +143,5 @@ export {
|
|
|
176
143
|
MEDIA_RECOVERY_NOTICE,
|
|
177
144
|
encodeVideoSseEvent,
|
|
178
145
|
decodeVideoSse,
|
|
179
|
-
videoSseHeaders
|
|
180
|
-
CLIP_NARRATION_TAIL_SEC,
|
|
181
|
-
clipNarrationBudget,
|
|
182
|
-
estimateNarrationSeconds,
|
|
183
|
-
speechFitsClip,
|
|
184
|
-
narrationFitsClip
|
|
146
|
+
videoSseHeaders
|
|
185
147
|
};
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
|
+
measuredClipPlayback,
|
|
2
3
|
useExternalVideoBackdrop,
|
|
3
4
|
useMediaAudio,
|
|
4
5
|
useMediaFailure,
|
|
5
6
|
useNarrationPreroll
|
|
6
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-R2RJIB6B.js";
|
|
7
8
|
import {
|
|
8
9
|
resolveMediaType
|
|
9
10
|
} from "./chunk-K5J7ESRO.js";
|
|
11
|
+
import "./chunk-NEKYSCT6.js";
|
|
10
12
|
|
|
11
13
|
// src/visual-system/scene-templates/scene-background.tsx
|
|
12
14
|
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
@@ -32,6 +34,7 @@ var SceneVideoBackdrop = ({
|
|
|
32
34
|
mediaPosition = "center",
|
|
33
35
|
progress,
|
|
34
36
|
sceneDuration,
|
|
37
|
+
measuredSpeechDurationSec,
|
|
35
38
|
preparingNarration = false,
|
|
36
39
|
isPlaying,
|
|
37
40
|
muted,
|
|
@@ -50,15 +53,18 @@ var SceneVideoBackdrop = ({
|
|
|
50
53
|
const [decodedVideoUrl, setDecodedVideoUrl] = useState();
|
|
51
54
|
const [waitingKey, setWaitingKey] = useState();
|
|
52
55
|
const [exhaustedKey, setExhaustedKey] = useState();
|
|
56
|
+
const [repeatingKey, setRepeatingKey] = useState();
|
|
57
|
+
const [endedKey, setEndedKey] = useState();
|
|
53
58
|
const videoRef = useRef(null);
|
|
54
59
|
const playableVideoUrl = useRef(void 0);
|
|
55
60
|
const startedVideoUrl = useRef(void 0);
|
|
56
61
|
const startedPlaybackId = useRef(void 0);
|
|
57
62
|
const videoPresentationKey = `${playbackId}\0${mediaUrl}`;
|
|
58
|
-
const presentationRef = useRef({ key: videoPresentationKey, playing: isPlaying });
|
|
63
|
+
const presentationRef = useRef({ key: videoPresentationKey, playing: isPlaying, progress });
|
|
59
64
|
const failedPresentationRef = useRef(void 0);
|
|
65
|
+
const repeatedPresentationRef = useRef(void 0);
|
|
60
66
|
const previousProgressRef = useRef({ key: videoPresentationKey, progress });
|
|
61
|
-
presentationRef.current = { key: videoPresentationKey, playing: isPlaying };
|
|
67
|
+
presentationRef.current = { key: videoPresentationKey, playing: isPlaying, progress };
|
|
62
68
|
const unavailable = (reason = "playback-error") => {
|
|
63
69
|
if (presentationRef.current.key === videoPresentationKey && failedPresentationRef.current !== videoPresentationKey && (presentationRef.current.playing || reason === "duration-mismatch")) {
|
|
64
70
|
failedPresentationRef.current = videoPresentationKey;
|
|
@@ -152,20 +158,60 @@ var SceneVideoBackdrop = ({
|
|
|
152
158
|
if (frame !== void 0) video.cancelVideoFrameCallback?.(frame);
|
|
153
159
|
};
|
|
154
160
|
}, [mediaUrl, videoPresentationKey]);
|
|
161
|
+
const allowsRepeat = (video) => {
|
|
162
|
+
const fit = measuredClipPlayback(measuredSpeechDurationSec, video.duration);
|
|
163
|
+
return fit?.repeat === true && sceneDuration !== void 0 && sceneDuration <= fit.durationSec + 1e-6;
|
|
164
|
+
};
|
|
155
165
|
const fitDuration = useCallback((video) => {
|
|
156
166
|
video.playbackRate = 1;
|
|
157
|
-
if (sceneDuration && Number.isFinite(video.duration) && video.duration > 0 && sceneDuration > video.duration + 0.05) {
|
|
167
|
+
if (sceneDuration && Number.isFinite(video.duration) && video.duration > 0 && sceneDuration > video.duration + 0.05 && !allowsRepeat(video)) {
|
|
158
168
|
video.pause();
|
|
159
169
|
unavailable("duration-mismatch");
|
|
160
170
|
return false;
|
|
161
171
|
}
|
|
162
172
|
return true;
|
|
163
|
-
}, [sceneDuration, videoPresentationKey]);
|
|
173
|
+
}, [sceneDuration, measuredSpeechDurationSec, videoPresentationKey]);
|
|
164
174
|
useEffect(() => {
|
|
165
175
|
if (videoRef.current) fitDuration(videoRef.current);
|
|
166
176
|
}, [fitDuration]);
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
if (!isPlaying || endedKey !== videoPresentationKey) return;
|
|
179
|
+
const timer = setTimeout(() => {
|
|
180
|
+
if (videoRef.current?.ended) unavailable();
|
|
181
|
+
}, 50);
|
|
182
|
+
return () => clearTimeout(timer);
|
|
183
|
+
}, [endedKey, videoPresentationKey, isPlaying]);
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
if (!isPlaying || repeatingKey !== videoPresentationKey) return;
|
|
186
|
+
let frame;
|
|
187
|
+
const observe = () => {
|
|
188
|
+
const video = videoRef.current;
|
|
189
|
+
const fit = video && measuredClipPlayback(measuredSpeechDurationSec, video.duration);
|
|
190
|
+
if (!video || presentationRef.current.progress >= 1) return;
|
|
191
|
+
if (!fit?.repeat || video.currentTime > fit.durationSec - video.duration + 0.05) {
|
|
192
|
+
unavailable("duration-mismatch");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
frame = requestAnimationFrame(observe);
|
|
196
|
+
};
|
|
197
|
+
frame = requestAnimationFrame(observe);
|
|
198
|
+
return () => cancelAnimationFrame(frame);
|
|
199
|
+
}, [repeatingKey, videoPresentationKey, isPlaying, measuredSpeechDurationSec]);
|
|
167
200
|
const finishMotion = () => {
|
|
168
201
|
if (!isPlaying) return;
|
|
202
|
+
const video = videoRef.current;
|
|
203
|
+
const fit = video && measuredClipPlayback(measuredSpeechDurationSec, video.duration);
|
|
204
|
+
if (video?.ended && fit && !fit.repeat && sceneDuration !== void 0 && sceneDuration <= video.duration + 0.05 && (1 - progress) * sceneDuration <= 0.05) {
|
|
205
|
+
setEndedKey(videoPresentationKey);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (video && video.ended && !video.error && video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && video.currentSrc === video.src && playableVideoUrl.current === mediaUrl && failedPresentationRef.current !== videoPresentationKey && waitingKey !== videoPresentationKey && repeatedPresentationRef.current !== videoPresentationKey && progress < 1 && allowsRepeat(video)) {
|
|
209
|
+
repeatedPresentationRef.current = videoPresentationKey;
|
|
210
|
+
setRepeatingKey(videoPresentationKey);
|
|
211
|
+
video.currentTime = 0;
|
|
212
|
+
void video.play().catch(() => unavailable());
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
169
215
|
unavailable();
|
|
170
216
|
};
|
|
171
217
|
useEffect(() => {
|
|
@@ -173,11 +219,17 @@ var SceneVideoBackdrop = ({
|
|
|
173
219
|
previousProgressRef.current = { key: videoPresentationKey, progress };
|
|
174
220
|
const video = videoRef.current;
|
|
175
221
|
if (!video || rewindPreroll || previous.key !== videoPresentationKey || progress >= previous.progress - 0.05 || !sceneDuration || !Number.isFinite(sceneDuration)) return;
|
|
176
|
-
|
|
222
|
+
const target = Math.max(0, progress * sceneDuration);
|
|
223
|
+
setEndedKey(void 0);
|
|
224
|
+
const seekingRepeated = allowsRepeat(video) && target >= video.duration;
|
|
225
|
+
setRepeatingKey(seekingRepeated ? videoPresentationKey : void 0);
|
|
226
|
+
if (seekingRepeated) repeatedPresentationRef.current = videoPresentationKey;
|
|
227
|
+
else if (progress <= 1e-3) repeatedPresentationRef.current = void 0;
|
|
228
|
+
video.currentTime = seekingRepeated ? target % video.duration : target;
|
|
177
229
|
failedPresentationRef.current = void 0;
|
|
178
230
|
setExhaustedKey(void 0);
|
|
179
231
|
if (isPlaying && video.paused) void video.play().catch(() => unavailable());
|
|
180
|
-
}, [progress, videoPresentationKey, sceneDuration, rewindPreroll, isPlaying]);
|
|
232
|
+
}, [progress, videoPresentationKey, sceneDuration, measuredSpeechDurationSec, rewindPreroll, isPlaying]);
|
|
181
233
|
useEffect(() => {
|
|
182
234
|
const video = videoRef.current;
|
|
183
235
|
if (!video) return;
|
|
@@ -280,12 +332,14 @@ function getMediaBackgroundProps(variables) {
|
|
|
280
332
|
mediaUrl: String(variables.mediaUrl || ""),
|
|
281
333
|
mediaType: String(variables.mediaType || "auto"),
|
|
282
334
|
mediaPoster: String(variables.mediaPoster || ""),
|
|
283
|
-
mediaPosition: String(variables.mediaPosition || "center")
|
|
335
|
+
mediaPosition: String(variables.mediaPosition || "center"),
|
|
336
|
+
measuredSpeechDurationSec: typeof variables.measuredSpeechDurationSec === "number" && Number.isFinite(variables.measuredSpeechDurationSec) && variables.measuredSpeechDurationSec > 0 ? variables.measuredSpeechDurationSec : void 0
|
|
284
337
|
};
|
|
285
338
|
}
|
|
286
339
|
function SceneBackground({
|
|
287
340
|
progress,
|
|
288
341
|
sceneDuration,
|
|
342
|
+
measuredSpeechDurationSec,
|
|
289
343
|
mediaUrl = "",
|
|
290
344
|
mediaType = "auto",
|
|
291
345
|
mediaPoster,
|
|
@@ -328,6 +382,7 @@ function SceneBackground({
|
|
|
328
382
|
mediaPosition,
|
|
329
383
|
progress,
|
|
330
384
|
sceneDuration,
|
|
385
|
+
measuredSpeechDurationSec,
|
|
331
386
|
isPlaying,
|
|
332
387
|
onError: () => setFailedUrl(mediaUrl)
|
|
333
388
|
}
|
package/dist/react.js
CHANGED
|
@@ -3,8 +3,9 @@ import {
|
|
|
3
3
|
getBuiltinSceneDefinition
|
|
4
4
|
} from "./chunk-NM4CXXZY.js";
|
|
5
5
|
import {
|
|
6
|
-
ExternalVideoBackdropProvider
|
|
7
|
-
|
|
6
|
+
ExternalVideoBackdropProvider,
|
|
7
|
+
measuredClipPlayback
|
|
8
|
+
} from "./chunk-R2RJIB6B.js";
|
|
8
9
|
import {
|
|
9
10
|
TitleSceneTemplate
|
|
10
11
|
} from "./chunk-5DOQTIMD.js";
|
|
@@ -18,14 +19,15 @@ import {
|
|
|
18
19
|
resolveVideoTimeline
|
|
19
20
|
} from "./chunk-AUN4S3YW.js";
|
|
20
21
|
import {
|
|
21
|
-
CLIP_NARRATION_TAIL_SEC,
|
|
22
22
|
MEDIA_RECOVERY_NOTICE,
|
|
23
23
|
decodeVideoSse,
|
|
24
|
-
estimateNarrationSeconds,
|
|
25
24
|
orderWelcomeCards,
|
|
26
|
-
speechFitsClip,
|
|
27
25
|
welcomeVisitSeed
|
|
28
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-XJ36A5JD.js";
|
|
27
|
+
import {
|
|
28
|
+
CLIP_NARRATION_TAIL_SEC,
|
|
29
|
+
estimateNarrationSeconds
|
|
30
|
+
} from "./chunk-NEKYSCT6.js";
|
|
29
31
|
import {
|
|
30
32
|
withDeadline
|
|
31
33
|
} from "./chunk-4G4JBMCM.js";
|
|
@@ -101,7 +103,7 @@ function MountedSceneReadiness({
|
|
|
101
103
|
consumed = true;
|
|
102
104
|
return valid;
|
|
103
105
|
} } : void 0);
|
|
104
|
-
} else report?.(key, error, actualVideoFrame);
|
|
106
|
+
} else report?.(key, error, actualVideoFrame, actualVideoFrame ? presented ?? observed : void 0);
|
|
105
107
|
};
|
|
106
108
|
const check = () => {
|
|
107
109
|
if (stopped) return;
|
|
@@ -122,6 +124,7 @@ function MountedSceneReadiness({
|
|
|
122
124
|
const video = layer.querySelector("video");
|
|
123
125
|
if (video && video.getAttribute("src") === mediaUrl && video.currentSrc === video.src && video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
|
|
124
126
|
if (preparedProof?.consume(video)) {
|
|
127
|
+
presented = video;
|
|
125
128
|
finish(void 0, true);
|
|
126
129
|
return;
|
|
127
130
|
}
|
|
@@ -248,7 +251,7 @@ function createRenderer(defaults, loader) {
|
|
|
248
251
|
return Object.freeze({ component, defaults, preload });
|
|
249
252
|
}
|
|
250
253
|
var loaders = {
|
|
251
|
-
cinemaMedia: () => import("./cinema-media-
|
|
254
|
+
cinemaMedia: () => import("./cinema-media-DLIYPESB.js").then((module) => ({ default: module.MediaSceneTemplate })),
|
|
252
255
|
chapterTitle: () => import("./chapter-title-2RCXX7SN.js").then((module) => ({ default: module.TitleSceneTemplate }))
|
|
253
256
|
};
|
|
254
257
|
var renderers = new Map(SCENE_DEFINITIONS.map((scene) => [
|
|
@@ -1044,6 +1047,7 @@ function usePlaybackClock({
|
|
|
1044
1047
|
loopRef,
|
|
1045
1048
|
sceneIndexRef,
|
|
1046
1049
|
visualReadyRef,
|
|
1050
|
+
activeMediaRef,
|
|
1047
1051
|
callbacksRef,
|
|
1048
1052
|
setCurrentTime,
|
|
1049
1053
|
setIsPlaying
|
|
@@ -1055,6 +1059,7 @@ function usePlaybackClock({
|
|
|
1055
1059
|
let onsetWaitSeconds = 0;
|
|
1056
1060
|
let clockWaitSeconds = 0;
|
|
1057
1061
|
let lastNarrationTime;
|
|
1062
|
+
let narratedKey;
|
|
1058
1063
|
let completionHold;
|
|
1059
1064
|
let committedTime = timeRef.current;
|
|
1060
1065
|
let groupHandoff;
|
|
@@ -1094,6 +1099,7 @@ function usePlaybackClock({
|
|
|
1094
1099
|
if (externallySeeked || replaced) {
|
|
1095
1100
|
groupHandoff = void 0;
|
|
1096
1101
|
completionHold = void 0;
|
|
1102
|
+
narratedKey = void 0;
|
|
1097
1103
|
}
|
|
1098
1104
|
let deferGroupStall = false;
|
|
1099
1105
|
let completionBlocked = false;
|
|
@@ -1132,6 +1138,8 @@ function usePlaybackClock({
|
|
|
1132
1138
|
}
|
|
1133
1139
|
clockWaitSeconds = narrationTime !== void 0 && narrationTime === lastNarrationTime && narrationReady && !stalled ? clockWaitSeconds + elapsed : 0;
|
|
1134
1140
|
const audioMovedBackwards = narrationTime !== void 0 && lastNarrationTime !== void 0 && narrationTime < lastNarrationTime;
|
|
1141
|
+
if (audioMovedBackwards) narratedKey = void 0;
|
|
1142
|
+
if (cued && narrationReady && narrationTime !== void 0 && narrationTime > 0) narratedKey = sceneReadinessKey(cued.scene);
|
|
1135
1143
|
lastNarrationTime = narrationTime;
|
|
1136
1144
|
if (clockWaitSeconds >= 8) {
|
|
1137
1145
|
failNarration(new Error("Narration audio clock did not advance within eight seconds"), current);
|
|
@@ -1142,6 +1150,8 @@ function usePlaybackClock({
|
|
|
1142
1150
|
raw = Math.max(timeRef.current, raw);
|
|
1143
1151
|
}
|
|
1144
1152
|
if (cued && !cued.scene.narrationGroup && narrationTime === void 0) {
|
|
1153
|
+
const measuredSpeech = cued.scene.variables.measuredSpeechDurationSec;
|
|
1154
|
+
const tailSeconds = cued.scene.templateId === "cinemaMedia" && typeof measuredSpeech === "number" && Number.isFinite(measuredSpeech) && measuredSpeech > 0 ? Math.min(CLIP_NARRATION_TAIL_SEC, Math.max(0, cued.end - cued.start - measuredSpeech)) : CLIP_NARRATION_TAIL_SEC;
|
|
1145
1155
|
let speaking = false;
|
|
1146
1156
|
try {
|
|
1147
1157
|
speaking = callbacksRef.current.narrationActive?.(cued.scene) === true;
|
|
@@ -1149,8 +1159,15 @@ function usePlaybackClock({
|
|
|
1149
1159
|
failNarration(cause instanceof Error ? cause : new Error("Narration completion failed"), current);
|
|
1150
1160
|
return;
|
|
1151
1161
|
}
|
|
1162
|
+
const key = sceneReadinessKey(cued.scene);
|
|
1163
|
+
if (speaking && narrationReady) narratedKey = key;
|
|
1164
|
+
const native = activeMediaRef?.current;
|
|
1165
|
+
const media = native?.video;
|
|
1166
|
+
if (!speaking && narrationReady && callbacksRef.current.narrationActive && narratedKey === key && typeof measuredSpeech === "number" && Number.isFinite(measuredSpeech) && measuredSpeech > 0 && native?.key === key && media?.isConnected && !media.error && !media.seeking && media.getAttribute("src") === String(cued.scene.variables.mediaUrl || "") && media.currentSrc === media.src && media.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && Number.isFinite(media.currentTime) && media.currentTime >= 0 && Number.isFinite(media.duration) && media.duration > 0 && measuredSpeech <= media.duration && cued.end - cued.start <= media.duration + 0.05) {
|
|
1167
|
+
raw = Math.max(raw, Math.min(cued.end, cued.start + media.currentTime));
|
|
1168
|
+
}
|
|
1152
1169
|
if (completionHold?.sceneId !== cued.scene.id) completionHold = void 0;
|
|
1153
|
-
if (raw >= cued.end -
|
|
1170
|
+
if (raw >= cued.end - tailSeconds && speaking) {
|
|
1154
1171
|
completionHold ??= { sceneId: cued.scene.id, wait: 0, tail: 0 };
|
|
1155
1172
|
if (raw >= cued.end) completionHold.wait += elapsed;
|
|
1156
1173
|
if (completionHold.wait >= 8) {
|
|
@@ -1163,10 +1180,10 @@ function usePlaybackClock({
|
|
|
1163
1180
|
}
|
|
1164
1181
|
} else if (completionHold && !speaking) {
|
|
1165
1182
|
completionHold.tail += elapsed;
|
|
1166
|
-
if (completionHold.tail <
|
|
1183
|
+
if (completionHold.tail < tailSeconds && raw >= cued.end) {
|
|
1167
1184
|
raw = Math.max(cued.start, cued.end - 0.01);
|
|
1168
1185
|
completionBlocked = true;
|
|
1169
|
-
} else if (completionHold.tail >=
|
|
1186
|
+
} else if (completionHold.tail >= tailSeconds) completionHold = void 0;
|
|
1170
1187
|
}
|
|
1171
1188
|
}
|
|
1172
1189
|
let nextTime;
|
|
@@ -1322,7 +1339,9 @@ function VideoPlayerRuntime({
|
|
|
1322
1339
|
const sceneIndexRef = useRef4(-1);
|
|
1323
1340
|
const mediaFrameReportedRef = useRef4(false);
|
|
1324
1341
|
const visualReadyRef = useRef4(void 0);
|
|
1325
|
-
const
|
|
1342
|
+
const activeMediaRef = useRef4(void 0);
|
|
1343
|
+
const reportVisualReady = useMemo(() => (key, error, actualVideoFrame = false, media) => {
|
|
1344
|
+
activeMediaRef.current = !error && actualVideoFrame && media ? { key, video: media } : void 0;
|
|
1326
1345
|
if (error) {
|
|
1327
1346
|
setIsPlaying(false);
|
|
1328
1347
|
callbacksRef.current.onError?.(error, stateRef.current);
|
|
@@ -1382,6 +1401,7 @@ function VideoPlayerRuntime({
|
|
|
1382
1401
|
setActiveStream(stream);
|
|
1383
1402
|
setActiveSavedVideo(video);
|
|
1384
1403
|
mediaFrameReportedRef.current = false;
|
|
1404
|
+
activeMediaRef.current = void 0;
|
|
1385
1405
|
sceneIndexRef.current = -1;
|
|
1386
1406
|
setReplacementPending(stream != null);
|
|
1387
1407
|
setState(video ? savedVideoState(video) : createVideoState());
|
|
@@ -1494,6 +1514,7 @@ function VideoPlayerRuntime({
|
|
|
1494
1514
|
loopRef,
|
|
1495
1515
|
sceneIndexRef,
|
|
1496
1516
|
visualReadyRef,
|
|
1517
|
+
activeMediaRef,
|
|
1497
1518
|
callbacksRef,
|
|
1498
1519
|
setCurrentTime,
|
|
1499
1520
|
setIsPlaying
|
|
@@ -1638,6 +1659,7 @@ function VideoPlayerRuntime({
|
|
|
1638
1659
|
return;
|
|
1639
1660
|
}
|
|
1640
1661
|
if (!isPlaying && ended) {
|
|
1662
|
+
activeMediaRef.current = void 0;
|
|
1641
1663
|
timeRef.current = 0;
|
|
1642
1664
|
setCurrentTime(0);
|
|
1643
1665
|
if (audioRef.current) audioRef.current.currentTime = 0;
|
|
@@ -1942,13 +1964,21 @@ function preparedSceneDuration(scene, spokenSeconds, metadata, clipDurationSec)
|
|
|
1942
1964
|
const speech = Number.isFinite(spokenSeconds) && spokenSeconds > 0 ? spokenSeconds + CLIP_NARRATION_TAIL_SEC : scene.narration ? getSpokenDuration(scene.narration) : 0;
|
|
1943
1965
|
return Math.max(floor, speech);
|
|
1944
1966
|
}
|
|
1945
|
-
function prepareNarratedScene(scene, spokenSeconds) {
|
|
1967
|
+
function prepareNarratedScene(scene, spokenSeconds, measured = false) {
|
|
1946
1968
|
const requested = scene.timing.fixedDuration;
|
|
1947
1969
|
const actual = scene.variables.mediaDurationSec;
|
|
1948
1970
|
const clipDurationSec = scene.templateId === "cinemaMedia" ? [actual, requested].find((value) => typeof value === "number" && Number.isFinite(value) && value > 0) : void 0;
|
|
1949
|
-
const
|
|
1950
|
-
const
|
|
1951
|
-
const
|
|
1971
|
+
const { measuredSpeechDurationSec: _incomingMeasurement, ...variables } = scene.variables;
|
|
1972
|
+
const measuredSeconds = measured && typeof spokenSeconds === "number" && Number.isFinite(spokenSeconds) && spokenSeconds > 0 ? spokenSeconds : void 0;
|
|
1973
|
+
const candidate = { ...scene, variables: {
|
|
1974
|
+
...variables,
|
|
1975
|
+
...scene.templateId === "cinemaMedia" && measuredSeconds !== void 0 ? { measuredSpeechDurationSec: measuredSeconds } : {}
|
|
1976
|
+
} };
|
|
1977
|
+
const fit = clipDurationSec === void 0 ? void 0 : measuredClipPlayback(measuredSeconds, clipDurationSec);
|
|
1978
|
+
const recovered = clipDurationSec !== void 0 && (measuredSeconds === void 0 ? preparedSceneDuration(candidate, spokenSeconds, getBuiltinSceneDefinition(scene.templateId), clipDurationSec) > clipDurationSec : fit === void 0);
|
|
1979
|
+
const visual = recovered ? recoverSceneMedia(candidate) : candidate;
|
|
1980
|
+
const prepared = preparedSceneDuration(visual, spokenSeconds, getBuiltinSceneDefinition(visual.templateId), clipDurationSec);
|
|
1981
|
+
const duration = fit && !recovered ? fit.repeat ? fit.durationSec : Math.min(prepared, clipDurationSec) : prepared;
|
|
1952
1982
|
const { startTime: _start, endTime: _end, beatStart: _beatStart, beatEnd: _beatEnd, ...timing } = visual.timing;
|
|
1953
1983
|
return { scene: { ...visual, timing: { ...timing, fixedDuration: duration } }, recovered, clipDurationSec };
|
|
1954
1984
|
}
|
|
@@ -2011,8 +2041,8 @@ function createScenePreparation(options) {
|
|
|
2011
2041
|
void prepared.catch(() => void 0);
|
|
2012
2042
|
return prepared;
|
|
2013
2043
|
};
|
|
2014
|
-
const pace = (scene, seconds) => {
|
|
2015
|
-
const prepared = prepareNarratedScene(scene, seconds);
|
|
2044
|
+
const pace = (scene, seconds, measured) => {
|
|
2045
|
+
const prepared = prepareNarratedScene(scene, seconds, measured);
|
|
2016
2046
|
if (prepared.recovered) options.warn(MEDIA_RECOVERY_NOTICE);
|
|
2017
2047
|
if (seconds !== void 0 && prepared.clipDurationSec !== void 0) {
|
|
2018
2048
|
try {
|
|
@@ -3219,7 +3249,7 @@ function useVideoChatSession(options = {}) {
|
|
|
3219
3249
|
const withNarration = line ? { ...visual, narration: line } : visual;
|
|
3220
3250
|
const group = plannedScene.narrationGroup;
|
|
3221
3251
|
if (group && (spoken?.supportsOffsets !== true || voiceRef.current.supportsOffsets !== true || Math.abs(spoken.seconds - group.totalSeconds) > 0.1)) throw new VideoError("Narration group requires matching measured audio with offset support", { code: "narration_group_invalid" });
|
|
3222
|
-
ready[position] = group ? withNarration : preparation.pace(withNarration, spoken?.seconds);
|
|
3252
|
+
ready[position] = group ? withNarration : preparation.pace(withNarration, spoken?.seconds, spoken?.supportsOffsets === true);
|
|
3223
3253
|
flush();
|
|
3224
3254
|
}).catch((cause) => {
|
|
3225
3255
|
if (!isCurrent() || currentAttempt !== attempt) return;
|
|
@@ -4612,7 +4642,6 @@ function VideoChat({ options = {}, className, welcomeTitle, branding, showRecove
|
|
|
4612
4642
|
"div",
|
|
4613
4643
|
{
|
|
4614
4644
|
className: "line-row",
|
|
4615
|
-
"data-opening-copy": openingChapter && line === shown?.opening && !captionsExpanded,
|
|
4616
4645
|
"data-expanded": captionsExpanded,
|
|
4617
4646
|
"data-actions-visible": captionControls.visible,
|
|
4618
4647
|
onPointerMove: captionControls.onPointerEnter,
|
|
@@ -4631,7 +4660,7 @@ function VideoChat({ options = {}, className, welcomeTitle, branding, showRecove
|
|
|
4631
4660
|
setCaptionsExpanded(false);
|
|
4632
4661
|
}, children: /* @__PURE__ */ jsx11(Close, {}) })
|
|
4633
4662
|
] }),
|
|
4634
|
-
captionsExpanded ? /* @__PURE__ */ jsx11("div", { className: "expanded-captions", role: "region", tabIndex: 0, "aria-label": "Expanded subtitles", children: fullTranscript.map((entry, index) => /* @__PURE__ */ jsx11("p", { children: entry }, index)) }) : /* @__PURE__ */ jsx11(CaptionPages, { text:
|
|
4663
|
+
captionsExpanded ? /* @__PURE__ */ jsx11("div", { className: "expanded-captions", role: "region", tabIndex: 0, "aria-label": "Expanded subtitles", children: fullTranscript.map((entry, index) => /* @__PURE__ */ jsx11("p", { children: entry }, index)) }) : /* @__PURE__ */ jsx11(CaptionPages, { text: line, getProgress: getCaptionProgress }, `${shown?.id}:${chat.playerKey}`)
|
|
4635
4664
|
]
|
|
4636
4665
|
}
|
|
4637
4666
|
) }) }),
|
package/dist/server.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
|
-
CLIP_NARRATION_TAIL_SEC,
|
|
3
2
|
MEDIA_RECOVERY_NOTICE,
|
|
4
3
|
WELCOME_CARDS,
|
|
5
|
-
clipNarrationBudget,
|
|
6
4
|
decodeVideoSse,
|
|
7
5
|
encodeVideoSseEvent,
|
|
8
|
-
estimateNarrationSeconds,
|
|
9
|
-
narrationFitsClip,
|
|
10
6
|
videoSseHeaders
|
|
11
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-XJ36A5JD.js";
|
|
8
|
+
import {
|
|
9
|
+
CLIP_NARRATION_TAIL_SEC,
|
|
10
|
+
clipNarrationBudget,
|
|
11
|
+
estimateNarrationSeconds,
|
|
12
|
+
narrationFitsClip
|
|
13
|
+
} from "./chunk-NEKYSCT6.js";
|
|
12
14
|
import {
|
|
13
15
|
withDeadline
|
|
14
16
|
} from "./chunk-4G4JBMCM.js";
|
package/docs/architecture.md
CHANGED
|
@@ -30,8 +30,11 @@ Narration is estimated against each clip with a 0.8-second tail. An oversized
|
|
|
30
30
|
beat gets at most one short rewrite before footage is requested. A failed or
|
|
31
31
|
still-oversized rewrite preserves the original speech in a chapter instead of
|
|
32
32
|
spending on unusable footage. Measured speech remains authoritative during
|
|
33
|
-
playback
|
|
34
|
-
|
|
33
|
+
playback. Normal footage plays once; a small measured speech overrun can repeat
|
|
34
|
+
healthy footage once until speech completes. Larger or unmeasured overruns
|
|
35
|
+
recover to a chapter. See the [playback bounds](media-and-audio.md#timing-and-recovery)
|
|
36
|
+
for the exceptional-repeat policy. Estimation is not a guarantee that every
|
|
37
|
+
voice/language finishes inside its clip.
|
|
35
38
|
|
|
36
39
|
## Where to work
|
|
37
40
|
|
package/docs/customization.md
CHANGED
|
@@ -72,6 +72,10 @@ await chat.ask(card.prompt, { opening: card.opening, openingMedia: card.media })
|
|
|
72
72
|
|
|
73
73
|
Typed prompts receive their opening from the same model stream as the answer.
|
|
74
74
|
The opening holds until its narration completes and the first scene is ready.
|
|
75
|
+
When subtitles are enabled, the spoken opening also appears in the standard
|
|
76
|
+
subtitle line. The full transcript includes that opening once, followed by the
|
|
77
|
+
scene narration, including on replay and when restoring an in-memory session.
|
|
78
|
+
At the end, the "Ask next" label stays directly above its follow-up cards.
|
|
75
79
|
|
|
76
80
|
`portrait` reserves a 9:16 response frame; `landscape` reserves 16:9.
|
|
77
81
|
The saved orientation stays stable. For responsive display without changing the
|
package/docs/media-and-audio.md
CHANGED
|
@@ -137,8 +137,20 @@ settings and host spending limits. The planner fits natural spoken beats to that
|
|
|
137
137
|
budget with a 0.8-second tail. One bounded rewrite may shorten an oversized
|
|
138
138
|
beat before generation; otherwise its complete narration stays on a chapter.
|
|
139
139
|
Measured audio and actual decoded footage are checked before playback. Normal
|
|
140
|
-
footage plays once at native speed
|
|
141
|
-
|
|
140
|
+
footage plays once at native speed. If measured speech itself exceeds a healthy
|
|
141
|
+
clip by at most the smaller of one second or 25% of its length, playback may
|
|
142
|
+
repeat that same clip once, stopping when speech finishes. It never repeats just
|
|
143
|
+
to fill the quiet tail: when speech already fits, a shorter available tail is
|
|
144
|
+
allowed. Larger or unmeasured speech overruns, missing media and failed/stalled
|
|
145
|
+
decoders still recover to a chapter without cutting off narration or buying
|
|
146
|
+
another clip. The mounted decoder rechecks the bound against actual footage;
|
|
147
|
+
pause/resume does not grant another repeat, and explicit replay starts a fresh
|
|
148
|
+
playback lifecycle.
|
|
149
|
+
|
|
150
|
+
Repeat eligibility uses the prepared audio's decoded duration, not a browser
|
|
151
|
+
speech estimate. A custom voice must return `supportsOffsets: true` from
|
|
152
|
+
`prepare` only when its audio is measured and seekable. In-memory replay keeps
|
|
153
|
+
that prepared timing; the mounted decoder still enforces the repeat bound.
|
|
142
154
|
|
|
143
155
|
`generateVideoTimeoutMs` sets the first-shot preparation budget (default 15 seconds).
|
|
144
156
|
Later deadlines account for their position in the answer rather than restarting
|
|
@@ -104,8 +104,10 @@ footage generation. If the rewrite fails or still cannot fit, no video job is
|
|
|
104
104
|
submitted for that beat: its complete original narration plays over a chapter.
|
|
105
105
|
Rewrites are instructed to preserve facts and qualifications; applications
|
|
106
106
|
should still evaluate meaning and timing with their actual models and voices.
|
|
107
|
-
Measured speech can overrun the estimate
|
|
108
|
-
|
|
107
|
+
Measured speech can overrun the estimate. A small measured overrun may repeat
|
|
108
|
+
healthy footage once until speech finishes; other overruns recover to a chapter
|
|
109
|
+
without cutting off the sentence. See the [playback bounds](media-and-audio.md#timing-and-recovery).
|
|
110
|
+
Requested duration constrains
|
|
109
111
|
the paid submission; a valid returned duration describes the footage actually
|
|
110
112
|
available for playback. The mounted decoder also checks its physical duration.
|
|
111
113
|
Without reported duration, generated footage keeps its requested budget.
|
package/package.json
CHANGED
package/styles/video-chat.css
CHANGED
|
@@ -887,7 +887,7 @@
|
|
|
887
887
|
}
|
|
888
888
|
|
|
889
889
|
.vanillasky-video-chat .ending { pointer-events: none; }
|
|
890
|
-
.vanillasky-video-chat .ending .cards { pointer-events: auto; }
|
|
890
|
+
.vanillasky-video-chat .ending .cards { pointer-events: auto; margin-block-start: 0; }
|
|
891
891
|
|
|
892
892
|
/* Respect viewers who prefer solid controls over translucent media surfaces. */
|
|
893
893
|
@media (prefers-reduced-transparency: reduce) {
|
|
@@ -961,12 +961,6 @@
|
|
|
961
961
|
.vanillasky-video-chat .opening-chapter [data-title-composition="centered"] {
|
|
962
962
|
animation: vanillasky-video-chat-fade-in 800ms ease both;
|
|
963
963
|
}
|
|
964
|
-
.vanillasky-video-chat .line-row[data-opening-copy="true"] {
|
|
965
|
-
background: transparent;
|
|
966
|
-
backdrop-filter: none;
|
|
967
|
-
-webkit-backdrop-filter: none;
|
|
968
|
-
}
|
|
969
|
-
|
|
970
964
|
.vanillasky-video-chat .opening-chapter { animation: vanillasky-chapter-enter 450ms ease-out both; }
|
|
971
965
|
@keyframes vanillasky-chapter-enter { from { opacity: 0; } to { opacity: 1; } }
|
|
972
966
|
@media (prefers-reduced-motion: reduce) { .vanillasky-video-chat .opening-chapter { animation: none; } }
|