@vanillaskyai/video 0.7.0 → 0.8.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 +66 -0
- package/PUBLIC-API.md +83 -79
- package/README.md +16 -15
- package/dist/{builtin-server-KB7FKF6A.js → builtin-server-CUYPIDE7.js} +2 -2
- package/dist/check-runtime.js +1 -1
- package/dist/{chunk-2PYO6VAC.js → chunk-2DOLF4QN.js} +1 -43
- package/dist/{chunk-GAIOHGNR.js → chunk-2OKB27TG.js} +42 -45
- package/dist/{chunk-K746NUIP.js → chunk-2USVB3CY.js} +80 -29
- package/dist/{chunk-RE4IMWJR.js → chunk-3VRMKFM7.js} +87 -14
- package/dist/{chunk-JNN3EYVP.js → chunk-4UNKZEAT.js} +46 -46
- package/dist/{chunk-2XT4MZ76.js → chunk-APQKY6P3.js} +36 -3
- package/dist/{chunk-RXHW4EP4.js → chunk-L7QGGKJ2.js} +71 -40
- package/dist/{chunk-RGF452LL.js → chunk-OWBT45EN.js} +1 -1
- package/dist/{chunk-LKBZX7GV.js → chunk-PP3MCPN4.js} +4 -4
- package/dist/chunk-ZD2VTUYR.js +28 -0
- package/dist/cli.js +131 -24
- package/dist/{compose-video-PD6LKKRK.js → compose-video-2NFH4DZY.js} +3 -3
- package/dist/{events-B6qS1Lsb.d.ts → events-BQ6z264n.d.ts} +1 -1
- package/dist/index.d.ts +5 -67
- package/dist/index.js +2 -4
- package/dist/{kit-8g46H2RZ.d.ts → kit-tQnCPMOM.d.ts} +1 -1
- package/dist/react.d.ts +33 -53
- package/dist/react.js +642 -536
- package/dist/server.d.ts +11 -13
- package/dist/server.js +170 -81
- package/dist/template-catalog.js +1 -1
- package/dist/templates.d.ts +3 -3
- package/dist/test.d.ts +2 -2
- package/dist/test.js +3 -3
- package/dist/{text-stream-FW4BLBAL.js → text-stream-54LXABJW.js} +1 -1
- package/dist/{types-2wHBqtg8.d.ts → types-CEh9eUFu.d.ts} +1 -1
- package/dist/{types-DrABlRa7.d.ts → types-DWFO6qca.d.ts} +1 -1
- package/docs/agent-integration.md +9 -3
- package/docs/architecture.md +8 -10
- package/docs/concepts.md +16 -40
- package/docs/custom-templates.md +27 -23
- package/docs/customization.md +12 -49
- package/docs/errors.md +58 -68
- package/docs/getting-started.md +21 -14
- package/docs/media-and-audio.md +2 -2
- package/docs/performance.md +69 -0
- package/docs/persistence.md +12 -27
- package/docs/production.md +2 -2
- package/docs/provider-integration.md +11 -5
- package/docs/reference/provider-adapters.md +37 -89
- package/docs/security.md +1 -1
- package/docs/streaming-protocol.md +2 -2
- package/examples/custom-template/README.md +6 -6
- package/package.json +6 -7
- package/registry/items/barChart.json +2 -2
- package/registry/items/bigNumber.json +2 -2
- package/registry/items/brandMessage.json +2 -2
- package/registry/items/cardList.json +2 -2
- package/registry/items/codeEditor.json +2 -2
- package/registry/items/ctaLogo.json +2 -2
- package/registry/items/ctaMedia.json +2 -2
- package/registry/items/incomingCall.json +2 -2
- package/registry/items/media.json +2 -2
- package/registry/items/milestone.json +2 -2
- package/registry/items/notification.json +2 -2
- package/registry/items/phoneMockup.json +2 -2
- package/registry/items/problemSolution.json +2 -2
- package/registry/items/progressRing.json +2 -2
- package/registry/items/promptInput.json +2 -2
- package/registry/items/reaction.json +2 -2
- package/registry/items/reviewStack.json +2 -2
- package/registry/items/steps.json +2 -2
- package/registry/items/terminal.json +2 -2
- package/registry/items/testimonial.json +2 -2
- package/registry/items/tripleStats.json +2 -2
- package/registry/items/tweet.json +2 -2
- package/registry/items/webMockup.json +2 -2
- package/starters/video-chat/.env.example +4 -2
- package/starters/video-chat/README.md +17 -5
- package/starters/video-chat/package.json +2 -4
- package/starters/video-chat/providers/speech.ts +20 -0
- package/starters/video-chat/providers/video.ts +66 -0
- package/starters/video-chat/providers.ts +3 -0
- package/starters/video-chat/server.ts +4 -80
- package/starters/video-chat/stock.ts +114 -99
- package/dist/chunk-SKRGRKHY.js +0 -142
- package/dist/state-DZcKuS32.d.ts +0 -3
- package/docs/branding-and-personalization.md +0 -87
- package/examples/custom-template/supplied-media.tsx +0 -92
package/dist/react.js
CHANGED
|
@@ -1,31 +1,29 @@
|
|
|
1
1
|
import {
|
|
2
|
-
createSceneTimeline,
|
|
3
2
|
getSceneDuration
|
|
4
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-ZD2VTUYR.js";
|
|
5
4
|
import {
|
|
6
|
-
VIDEO_STREAM_CONTENT_TYPE,
|
|
7
|
-
VIDEO_STREAM_HEADER,
|
|
8
5
|
decodeVideoSse,
|
|
9
|
-
sanitizeVideoChatMedia
|
|
10
|
-
|
|
6
|
+
sanitizeVideoChatMedia,
|
|
7
|
+
withDeadline
|
|
8
|
+
} from "./chunk-APQKY6P3.js";
|
|
11
9
|
import {
|
|
12
10
|
applyVideoEvent,
|
|
13
|
-
|
|
11
|
+
checksumVideo,
|
|
12
|
+
createVideoEventFactory,
|
|
14
13
|
createVideoState
|
|
15
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-2OKB27TG.js";
|
|
16
15
|
import {
|
|
17
16
|
safePublicDiagnostic
|
|
18
17
|
} from "./chunk-MMUXVA47.js";
|
|
19
18
|
import {
|
|
20
19
|
parseVideo
|
|
21
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-2DOLF4QN.js";
|
|
22
21
|
import {
|
|
23
|
-
|
|
24
|
-
createVideoRequest
|
|
22
|
+
VIDEO_SCHEMA_VERSION
|
|
25
23
|
} from "./chunk-AOWSXU2K.js";
|
|
26
24
|
import {
|
|
27
25
|
getBuiltinTemplateMetadata
|
|
28
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-4UNKZEAT.js";
|
|
29
27
|
import "./chunk-3O7OMMMF.js";
|
|
30
28
|
import {
|
|
31
29
|
assertTemplateTransitionMetadata
|
|
@@ -33,7 +31,7 @@ import {
|
|
|
33
31
|
import {
|
|
34
32
|
VideoFrame,
|
|
35
33
|
getDimensions
|
|
36
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-2USVB3CY.js";
|
|
37
35
|
import "./chunk-224QNWRA.js";
|
|
38
36
|
import "./chunk-5CDAM24P.js";
|
|
39
37
|
import {
|
|
@@ -43,7 +41,9 @@ import {
|
|
|
43
41
|
getVideoDuration,
|
|
44
42
|
resolveVideoTimeline
|
|
45
43
|
} from "./chunk-SPVTJH3F.js";
|
|
46
|
-
import
|
|
44
|
+
import {
|
|
45
|
+
resolveVideoBrand
|
|
46
|
+
} from "./chunk-XGRA2MUV.js";
|
|
47
47
|
import "./chunk-WJ6FLKNC.js";
|
|
48
48
|
import "./chunk-73NTSFFI.js";
|
|
49
49
|
|
|
@@ -1018,6 +1018,15 @@ function usePlaybackClock({
|
|
|
1018
1018
|
}) {
|
|
1019
1019
|
useEffect(() => {
|
|
1020
1020
|
if (!isPlaying) return;
|
|
1021
|
+
let stalled = false;
|
|
1022
|
+
const reportStall = (next) => {
|
|
1023
|
+
if (stalled === next) return;
|
|
1024
|
+
stalled = next;
|
|
1025
|
+
try {
|
|
1026
|
+
void Promise.resolve(callbacksRef.current.onStallChange?.(next)).catch(() => void 0);
|
|
1027
|
+
} catch {
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1021
1030
|
let frame = 0;
|
|
1022
1031
|
let previous = performance.now();
|
|
1023
1032
|
const tick = (now) => {
|
|
@@ -1067,11 +1076,15 @@ function usePlaybackClock({
|
|
|
1067
1076
|
}
|
|
1068
1077
|
}
|
|
1069
1078
|
const duration = current.config ? getVideoDuration(current.config) : 0;
|
|
1079
|
+
reportStall(!settled && Boolean(current.config?.scenes.length) && duration > 0 && timeRef.current >= duration);
|
|
1070
1080
|
if (!settled || looping || timeRef.current < duration) frame = requestAnimationFrame(tick);
|
|
1071
1081
|
else setIsPlaying(false);
|
|
1072
1082
|
};
|
|
1073
1083
|
frame = requestAnimationFrame(tick);
|
|
1074
|
-
return () =>
|
|
1084
|
+
return () => {
|
|
1085
|
+
cancelAnimationFrame(frame);
|
|
1086
|
+
reportStall(false);
|
|
1087
|
+
};
|
|
1075
1088
|
}, [isPlaying]);
|
|
1076
1089
|
}
|
|
1077
1090
|
|
|
@@ -1106,6 +1119,8 @@ function VideoPlayerRuntime({
|
|
|
1106
1119
|
onComplete,
|
|
1107
1120
|
onError,
|
|
1108
1121
|
onSceneChange,
|
|
1122
|
+
onFramePresented,
|
|
1123
|
+
onStallChange,
|
|
1109
1124
|
onStateChange
|
|
1110
1125
|
}) {
|
|
1111
1126
|
const nativeMediaVolume = Math.max(0, Math.min(
|
|
@@ -1140,14 +1155,25 @@ function VideoPlayerRuntime({
|
|
|
1140
1155
|
const timeRef = useRef(currentTime);
|
|
1141
1156
|
const audioRef = useRef(null);
|
|
1142
1157
|
const introStartedAtRef = useRef(autoStartGeneration ? performance.now() : null);
|
|
1143
|
-
const callbacksRef = useRef({ onComplete, onPlaybackEnd, onError, onSceneChange, onStateChange });
|
|
1158
|
+
const callbacksRef = useRef({ onComplete, onPlaybackEnd, onError, onSceneChange, onFramePresented, onStallChange, onStateChange });
|
|
1144
1159
|
const loopRef = useRef(loop);
|
|
1145
1160
|
const sceneIndexRef = useRef(-1);
|
|
1146
1161
|
const playbackEndedRef = useRef(false);
|
|
1147
1162
|
stateRef.current = state;
|
|
1148
1163
|
timeRef.current = currentTime;
|
|
1149
|
-
callbacksRef.current = { onComplete, onPlaybackEnd, onError, onSceneChange, onStateChange };
|
|
1164
|
+
callbacksRef.current = { onComplete, onPlaybackEnd, onError, onSceneChange, onFramePresented, onStallChange, onStateChange };
|
|
1150
1165
|
loopRef.current = loop;
|
|
1166
|
+
const reportFramePresented = useMemo(() => {
|
|
1167
|
+
let reported = false;
|
|
1168
|
+
return () => {
|
|
1169
|
+
if (reported) return;
|
|
1170
|
+
reported = true;
|
|
1171
|
+
try {
|
|
1172
|
+
void Promise.resolve(callbacksRef.current.onFramePresented?.()).catch(() => void 0);
|
|
1173
|
+
} catch {
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
}, [stream, video]);
|
|
1151
1177
|
const primeSoundtrack = () => {
|
|
1152
1178
|
const audio = audioRef.current;
|
|
1153
1179
|
if (!audio || audio.dataset.audioOutput) return;
|
|
@@ -1499,6 +1525,7 @@ function VideoPlayerRuntime({
|
|
|
1499
1525
|
VideoFrame,
|
|
1500
1526
|
{
|
|
1501
1527
|
kit,
|
|
1528
|
+
onFramePresented: !showStartPoster && onFramePresented ? reportFramePresented : void 0,
|
|
1502
1529
|
config: displayConfig,
|
|
1503
1530
|
time: showStartPoster ? posterTime : currentTime,
|
|
1504
1531
|
width: dimensions.width,
|
|
@@ -1611,292 +1638,134 @@ var VideoError = class extends Error {
|
|
|
1611
1638
|
}
|
|
1612
1639
|
};
|
|
1613
1640
|
|
|
1614
|
-
// src/
|
|
1615
|
-
import { useCallback
|
|
1641
|
+
// src/video-chat/use-video-chat.ts
|
|
1642
|
+
import { useCallback as useCallback2, useEffect as useEffect4, useReducer, useRef as useRef3 } from "react";
|
|
1616
1643
|
|
|
1617
|
-
// src/
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
let message = `Video response endpoint failed (${response.status})`;
|
|
1635
|
-
try {
|
|
1636
|
-
const body = await response.json();
|
|
1637
|
-
if (body && typeof body === "object" && "error" in body) {
|
|
1638
|
-
const error = body.error;
|
|
1639
|
-
if (error && typeof error === "object") {
|
|
1640
|
-
if ("code" in error && typeof error.code === "string" && HTTP_ERROR_CODES.has(error.code)) {
|
|
1641
|
-
code = error.code;
|
|
1642
|
-
}
|
|
1643
|
-
if ("message" in error && typeof error.message === "string") {
|
|
1644
|
-
message = safePublicDiagnostic(error.message, message);
|
|
1645
|
-
}
|
|
1646
|
-
}
|
|
1647
|
-
}
|
|
1648
|
-
} catch {
|
|
1649
|
-
}
|
|
1650
|
-
return new VideoError(message, { code, status: response.status, requestId });
|
|
1651
|
-
}
|
|
1652
|
-
function normalizeStreamError(cause, requestId, state, aborted) {
|
|
1653
|
-
if (cause instanceof VideoError) return cause;
|
|
1654
|
-
const causeError = cause instanceof Error ? cause : void 0;
|
|
1655
|
-
const safeMessage = aborted ? causeError?.name === "TimeoutError" ? "Request timed out" : safePublicDiagnostic(causeError?.message, "Video generation was aborted") : causeError?.message.startsWith("Video response protocol") || causeError?.message.startsWith("Video response endpoint omitted protocol") ? safePublicDiagnostic(causeError.message, "Video response protocol failed") : "Video response stream failed";
|
|
1656
|
-
return new VideoError(safeMessage, {
|
|
1657
|
-
code: aborted ? "aborted" : "stream_failed",
|
|
1658
|
-
requestId: state.requestId ?? requestId,
|
|
1659
|
-
runId: state.runId,
|
|
1660
|
-
recoverable: false
|
|
1661
|
-
});
|
|
1662
|
-
}
|
|
1663
|
-
function streamVideo(options) {
|
|
1664
|
-
const request = createVideoRequest(options.input, {
|
|
1665
|
-
requestId: options.requestId,
|
|
1666
|
-
capabilities: options.capabilities,
|
|
1667
|
-
...options.resume?.state.runId != null && options.resume.state.lastSequence >= 0 ? {
|
|
1668
|
-
resume: {
|
|
1669
|
-
runId: options.resume.state.runId,
|
|
1670
|
-
afterSequence: options.resume.state.lastSequence
|
|
1671
|
-
}
|
|
1672
|
-
} : {}
|
|
1673
|
-
});
|
|
1674
|
-
const controller = new AbortController();
|
|
1675
|
-
const forwardAbort = () => controller.abort(options.signal?.reason);
|
|
1676
|
-
if (options.signal?.aborted) forwardAbort();
|
|
1677
|
-
else options.signal?.addEventListener("abort", forwardAbort, { once: true });
|
|
1678
|
-
let resolveResult;
|
|
1679
|
-
let rejectResult;
|
|
1680
|
-
let settled = false;
|
|
1681
|
-
const result = new Promise((resolve, reject) => {
|
|
1682
|
-
resolveResult = resolve;
|
|
1683
|
-
rejectResult = reject;
|
|
1684
|
-
});
|
|
1685
|
-
const source = (async function* consume() {
|
|
1686
|
-
let state = options.resume?.state ?? createVideoState();
|
|
1687
|
-
try {
|
|
1688
|
-
const fetcher = options.fetcher ?? fetch;
|
|
1689
|
-
const headers = new Headers(options.headers);
|
|
1690
|
-
headers.set("Accept", VIDEO_STREAM_CONTENT_TYPE);
|
|
1691
|
-
headers.set("Content-Type", "application/json");
|
|
1692
|
-
if (request.resume) headers.set("Last-Event-ID", `${request.resume.runId}:${request.resume.afterSequence}`);
|
|
1693
|
-
const response = await fetcher(options.endpoint, {
|
|
1694
|
-
method: "POST",
|
|
1695
|
-
headers,
|
|
1696
|
-
body: JSON.stringify(request),
|
|
1697
|
-
signal: controller.signal,
|
|
1698
|
-
credentials: options.credentials
|
|
1699
|
-
});
|
|
1700
|
-
if (!response.ok) throw await responseError(response, request.requestId);
|
|
1701
|
-
if (!response.body || !response.headers.get("content-type")?.includes("text/event-stream")) {
|
|
1702
|
-
throw new Error("Video response endpoint did not return an SSE stream");
|
|
1703
|
-
}
|
|
1704
|
-
const protocolVersion = response.headers.get(VIDEO_STREAM_HEADER);
|
|
1705
|
-
if (protocolVersion == null) {
|
|
1706
|
-
throw new Error(`Video response endpoint omitted protocol version header ${VIDEO_STREAM_HEADER}`);
|
|
1707
|
-
}
|
|
1708
|
-
if (protocolVersion !== VIDEO_PROTOCOL_VERSION) {
|
|
1709
|
-
throw new Error(
|
|
1710
|
-
`Video response protocol mismatch: expected ${VIDEO_PROTOCOL_VERSION}, received ${protocolVersion}`
|
|
1711
|
-
);
|
|
1712
|
-
}
|
|
1713
|
-
for await (const event of decodeVideoSse(response.body)) {
|
|
1714
|
-
state = applyVideoEvent(state, event);
|
|
1715
|
-
options.onEvent?.(event, state);
|
|
1716
|
-
if (state.status === "complete" && !settled) {
|
|
1717
|
-
settled = true;
|
|
1718
|
-
resolveResult(state);
|
|
1719
|
-
} else if (state.status === "error" && !settled) {
|
|
1720
|
-
settled = true;
|
|
1721
|
-
const error = state.errors.at(-1);
|
|
1722
|
-
rejectResult(new VideoError(error?.message ?? "Video response could not finish", {
|
|
1723
|
-
code: error?.code ?? "generation_failed",
|
|
1724
|
-
status: response.status,
|
|
1725
|
-
requestId: state.requestId ?? request.requestId,
|
|
1726
|
-
runId: state.runId,
|
|
1727
|
-
recoverable: error?.recoverable
|
|
1728
|
-
}));
|
|
1729
|
-
} else if (state.status === "aborted" && !settled) {
|
|
1730
|
-
settled = true;
|
|
1731
|
-
rejectResult(new VideoError(state.abortReason ?? "Video generation was aborted", {
|
|
1732
|
-
code: "aborted",
|
|
1733
|
-
status: response.status,
|
|
1734
|
-
requestId: state.requestId ?? request.requestId,
|
|
1735
|
-
runId: state.runId,
|
|
1736
|
-
recoverable: false
|
|
1737
|
-
}));
|
|
1738
|
-
}
|
|
1739
|
-
yield event;
|
|
1740
|
-
}
|
|
1741
|
-
if (!["complete", "error", "aborted"].includes(state.status)) {
|
|
1742
|
-
throw new Error("Video response stream ended without a terminal event");
|
|
1743
|
-
}
|
|
1744
|
-
if (!settled && state.status === "complete") {
|
|
1745
|
-
settled = true;
|
|
1746
|
-
resolveResult(state);
|
|
1644
|
+
// src/protocol/scene-timeline.ts
|
|
1645
|
+
function createQueue() {
|
|
1646
|
+
const buffered = [];
|
|
1647
|
+
const waiting = [];
|
|
1648
|
+
let closed = false;
|
|
1649
|
+
return {
|
|
1650
|
+
push(value) {
|
|
1651
|
+
const next = waiting.shift();
|
|
1652
|
+
if (next) next({ value, done: false });
|
|
1653
|
+
else buffered.push(value);
|
|
1654
|
+
},
|
|
1655
|
+
close() {
|
|
1656
|
+
closed = true;
|
|
1657
|
+
let next = waiting.shift();
|
|
1658
|
+
while (next) {
|
|
1659
|
+
next({ value: void 0, done: true });
|
|
1660
|
+
next = waiting.shift();
|
|
1747
1661
|
}
|
|
1748
|
-
}
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1662
|
+
},
|
|
1663
|
+
iterable: {
|
|
1664
|
+
[Symbol.asyncIterator]() {
|
|
1665
|
+
return {
|
|
1666
|
+
next() {
|
|
1667
|
+
const buffer = buffered.shift();
|
|
1668
|
+
if (buffer !== void 0) return Promise.resolve({ value: buffer, done: false });
|
|
1669
|
+
if (closed) return Promise.resolve({ value: void 0, done: true });
|
|
1670
|
+
return new Promise((resolve) => waiting.push(resolve));
|
|
1671
|
+
}
|
|
1672
|
+
};
|
|
1753
1673
|
}
|
|
1754
|
-
throw error;
|
|
1755
|
-
} finally {
|
|
1756
|
-
options.signal?.removeEventListener("abort", forwardAbort);
|
|
1757
|
-
}
|
|
1758
|
-
})();
|
|
1759
|
-
const stream = createReplayableStream(source, () => void 0);
|
|
1760
|
-
return {
|
|
1761
|
-
request,
|
|
1762
|
-
stream,
|
|
1763
|
-
result,
|
|
1764
|
-
abort(reason = "user cancelled") {
|
|
1765
|
-
controller.abort(new Error(reason));
|
|
1766
1674
|
}
|
|
1767
1675
|
};
|
|
1768
1676
|
}
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
const
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
const
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
requestId: current.request.requestId,
|
|
1804
|
-
recoverable: false
|
|
1805
|
-
}));
|
|
1806
|
-
setState((previous) => ({ ...previous, status: "aborted", abortReason: reason }));
|
|
1677
|
+
var runCounter = 0;
|
|
1678
|
+
function createSceneTimeline(options) {
|
|
1679
|
+
const queue = createQueue();
|
|
1680
|
+
const runId = options.runId ?? `composed-${runCounter += 1}`;
|
|
1681
|
+
const events = createVideoEventFactory({ runId });
|
|
1682
|
+
const orientation = options.orientation ?? "landscape";
|
|
1683
|
+
const snapshot = {
|
|
1684
|
+
schemaVersion: VIDEO_SCHEMA_VERSION,
|
|
1685
|
+
orientation,
|
|
1686
|
+
scenes: [],
|
|
1687
|
+
style: options.style
|
|
1688
|
+
};
|
|
1689
|
+
let position = 0;
|
|
1690
|
+
let finished = false;
|
|
1691
|
+
const held = [];
|
|
1692
|
+
let openForScenes = !options.awaitAudio;
|
|
1693
|
+
const emit = (event) => {
|
|
1694
|
+
if (finished) return;
|
|
1695
|
+
queue.push(event);
|
|
1696
|
+
};
|
|
1697
|
+
emit(events.create("response.start", {
|
|
1698
|
+
requestId: options.requestId ?? runId,
|
|
1699
|
+
format: { orientation },
|
|
1700
|
+
style: options.style
|
|
1701
|
+
}));
|
|
1702
|
+
if (options.audio) {
|
|
1703
|
+
snapshot.audio = options.audio;
|
|
1704
|
+
emit(events.create("audio.set", { audio: options.audio }));
|
|
1705
|
+
}
|
|
1706
|
+
const release = () => {
|
|
1707
|
+
const pending = held.splice(0, held.length);
|
|
1708
|
+
for (const scene of pending) {
|
|
1709
|
+
snapshot.scenes.push(scene);
|
|
1710
|
+
emit(events.create("scene.add", { scene, position: position++ }));
|
|
1807
1711
|
}
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
setError(void 0);
|
|
1817
|
-
const nextRun = streamVideo({
|
|
1818
|
-
endpoint: currentOptions.endpoint ?? "/api/video",
|
|
1819
|
-
input,
|
|
1820
|
-
requestId: (currentOptions.createRequestId ?? defaultRequestId)(),
|
|
1821
|
-
capabilities: currentOptions.templateIds ? { ...templatesRef.current.capabilities, templates: [...new Set(currentOptions.templateIds)] } : templatesRef.current.capabilities,
|
|
1822
|
-
headers: currentOptions.headers,
|
|
1823
|
-
credentials: currentOptions.credentials,
|
|
1824
|
-
fetcher: currentOptions.fetcher,
|
|
1825
|
-
onEvent: (_event, nextState) => {
|
|
1826
|
-
if (_event.type === "scene.add") {
|
|
1827
|
-
if (!currentOptions.templates?.getTemplate(_event.data.scene.templateId)) {
|
|
1828
|
-
preloadBuiltinTemplate(_event.data.scene.templateId);
|
|
1829
|
-
}
|
|
1830
|
-
warmSceneMedia(_event.data.scene.variables);
|
|
1831
|
-
}
|
|
1832
|
-
if (mounted.current && generation.current === currentGeneration) setState(nextState);
|
|
1712
|
+
};
|
|
1713
|
+
return {
|
|
1714
|
+
stream: queue.iterable,
|
|
1715
|
+
add(scene) {
|
|
1716
|
+
if (finished) return;
|
|
1717
|
+
if (!openForScenes) {
|
|
1718
|
+
held.push(scene);
|
|
1719
|
+
return;
|
|
1833
1720
|
}
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
if (
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
const error2 = errorFrom(cause);
|
|
1843
|
-
setError(error2);
|
|
1844
|
-
setState((previous) => ({
|
|
1845
|
-
...previous,
|
|
1846
|
-
status: error2.code === "aborted" ? "aborted" : "error",
|
|
1847
|
-
...error2.code === "aborted" && previous.abortReason == null ? { abortReason: error2.message } : {}
|
|
1848
|
-
}));
|
|
1721
|
+
snapshot.scenes.push(scene);
|
|
1722
|
+
emit(events.create("scene.add", { scene, position: position++ }));
|
|
1723
|
+
},
|
|
1724
|
+
setAudio(audio) {
|
|
1725
|
+
if (finished || openForScenes) return;
|
|
1726
|
+
if (audio) {
|
|
1727
|
+
snapshot.audio = audio;
|
|
1728
|
+
emit(events.create("audio.set", { audio }));
|
|
1849
1729
|
}
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
}, []);
|
|
1867
|
-
return {
|
|
1868
|
-
generate,
|
|
1869
|
-
abort,
|
|
1870
|
-
video: state.config,
|
|
1871
|
-
status: state.status,
|
|
1872
|
-
error,
|
|
1873
|
-
warnings: state.warnings,
|
|
1874
|
-
playerProps: {
|
|
1875
|
-
...options.templates ? { templates: options.templates } : {},
|
|
1876
|
-
...run ? { stream: run.stream } : {}
|
|
1730
|
+
openForScenes = true;
|
|
1731
|
+
release();
|
|
1732
|
+
},
|
|
1733
|
+
complete() {
|
|
1734
|
+
if (finished) return;
|
|
1735
|
+
if (!openForScenes) {
|
|
1736
|
+
openForScenes = true;
|
|
1737
|
+
release();
|
|
1738
|
+
}
|
|
1739
|
+
emit(events.create("response.complete", {
|
|
1740
|
+
finishReason: "stop",
|
|
1741
|
+
snapshot,
|
|
1742
|
+
checksum: checksumVideo(snapshot)
|
|
1743
|
+
}));
|
|
1744
|
+
finished = true;
|
|
1745
|
+
queue.close();
|
|
1877
1746
|
}
|
|
1878
1747
|
};
|
|
1879
1748
|
}
|
|
1880
1749
|
|
|
1881
1750
|
// src/player/use-narration.ts
|
|
1882
|
-
import { useCallback
|
|
1751
|
+
import { useCallback, useEffect as useEffect3, useRef as useRef2, useState as useState2 } from "react";
|
|
1883
1752
|
function useNarration(options) {
|
|
1884
|
-
const [speaking, setSpeaking] =
|
|
1885
|
-
const optionsRef =
|
|
1753
|
+
const [speaking, setSpeaking] = useState2(false);
|
|
1754
|
+
const optionsRef = useRef2(options);
|
|
1886
1755
|
optionsRef.current = options;
|
|
1887
|
-
const currentRef =
|
|
1888
|
-
const spokenIndexRef =
|
|
1889
|
-
const stop =
|
|
1756
|
+
const currentRef = useRef2(void 0);
|
|
1757
|
+
const spokenIndexRef = useRef2(void 0);
|
|
1758
|
+
const stop = useCallback(() => {
|
|
1890
1759
|
currentRef.current?.abort();
|
|
1891
1760
|
currentRef.current = void 0;
|
|
1892
1761
|
setSpeaking(false);
|
|
1893
1762
|
}, []);
|
|
1894
|
-
const interrupt =
|
|
1763
|
+
const interrupt = useCallback(() => {
|
|
1895
1764
|
spokenIndexRef.current = void 0;
|
|
1896
1765
|
stop();
|
|
1897
1766
|
}, [stop]);
|
|
1898
|
-
|
|
1899
|
-
const onSceneChange =
|
|
1767
|
+
useEffect3(() => () => currentRef.current?.abort(), []);
|
|
1768
|
+
const onSceneChange = useCallback((scene, index) => {
|
|
1900
1769
|
const { voice, enabled = true } = optionsRef.current;
|
|
1901
1770
|
if (!enabled) return;
|
|
1902
1771
|
if (spokenIndexRef.current === index) return;
|
|
@@ -1909,7 +1778,18 @@ function useNarration(options) {
|
|
|
1909
1778
|
setSpeaking(true);
|
|
1910
1779
|
void (async () => {
|
|
1911
1780
|
try {
|
|
1912
|
-
|
|
1781
|
+
let started = false;
|
|
1782
|
+
await voice.speak(line, {
|
|
1783
|
+
signal: controller.signal,
|
|
1784
|
+
onStart: (source) => {
|
|
1785
|
+
if (started || controller.signal.aborted || currentRef.current !== controller || spokenIndexRef.current !== index || optionsRef.current.enabled === false) return;
|
|
1786
|
+
started = true;
|
|
1787
|
+
try {
|
|
1788
|
+
void Promise.resolve(optionsRef.current.onSpeechStart?.(source)).catch(() => void 0);
|
|
1789
|
+
} catch {
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
});
|
|
1913
1793
|
} catch {
|
|
1914
1794
|
} finally {
|
|
1915
1795
|
if (currentRef.current === controller) {
|
|
@@ -1922,11 +1802,9 @@ function useNarration(options) {
|
|
|
1922
1802
|
return { onSceneChange, interrupt, speaking };
|
|
1923
1803
|
}
|
|
1924
1804
|
|
|
1925
|
-
// src/video-chat/use-video-chat.ts
|
|
1926
|
-
import { useCallback as useCallback3, useEffect as useEffect5, useReducer, useRef as useRef4 } from "react";
|
|
1927
|
-
|
|
1928
1805
|
// src/video-chat/voice.ts
|
|
1929
1806
|
var DEFAULT_MAX_CACHED_LINES = 60;
|
|
1807
|
+
var SPEECH_PREPARATION_TIMEOUT_MS = 3e3;
|
|
1930
1808
|
var FALLBACK_BITS_PER_SECOND = 128e3;
|
|
1931
1809
|
var sharedContext;
|
|
1932
1810
|
function actionEndpoint(endpoint, action) {
|
|
@@ -1959,6 +1837,13 @@ function createVideoChatVoice(options = {}) {
|
|
|
1959
1837
|
let silent = false;
|
|
1960
1838
|
let disposed = false;
|
|
1961
1839
|
let generatedSpeechUnavailable = false;
|
|
1840
|
+
let playbackFailure;
|
|
1841
|
+
const notifyFallback = () => {
|
|
1842
|
+
try {
|
|
1843
|
+
void Promise.resolve(options.onFallback?.()).catch(() => void 0);
|
|
1844
|
+
} catch {
|
|
1845
|
+
}
|
|
1846
|
+
};
|
|
1962
1847
|
const forgetOldest = () => {
|
|
1963
1848
|
while (lines.size > maximum) {
|
|
1964
1849
|
const oldest = lines.keys().next();
|
|
@@ -1991,38 +1876,34 @@ function createVideoChatVoice(options = {}) {
|
|
|
1991
1876
|
if (generatedSpeechUnavailable) {
|
|
1992
1877
|
prepared = { source: "browser", seconds: estimatedBrowserSeconds(normalized) };
|
|
1993
1878
|
} else {
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
prepared = { source: "browser", seconds: estimatedBrowserSeconds(normalized) };
|
|
2007
|
-
} else if (!response.ok) {
|
|
2008
|
-
prepared = { source: "browser", seconds: estimatedBrowserSeconds(normalized) };
|
|
2009
|
-
} else {
|
|
2010
|
-
const bytes = await response.arrayBuffer();
|
|
2011
|
-
if (controller.signal.aborted) {
|
|
2012
|
-
throw controller.signal.reason ?? new DOMException("Speech preparation cancelled", "AbortError");
|
|
1879
|
+
prepared = await withDeadline(async (preparationSignal) => {
|
|
1880
|
+
const response = await fetcher(actionEndpoint(endpoint, "speech"), {
|
|
1881
|
+
method: "POST",
|
|
1882
|
+
headers,
|
|
1883
|
+
credentials: options.credentials,
|
|
1884
|
+
signal: preparationSignal,
|
|
1885
|
+
body: JSON.stringify({ text: normalized })
|
|
1886
|
+
});
|
|
1887
|
+
preparationSignal.throwIfAborted();
|
|
1888
|
+
if (response.status === 204 || response.status === 404) {
|
|
1889
|
+
generatedSpeechUnavailable = true;
|
|
1890
|
+
return { source: "browser", seconds: estimatedBrowserSeconds(normalized) };
|
|
2013
1891
|
}
|
|
1892
|
+
if (!response.ok) throw new Error("Speech is unavailable");
|
|
1893
|
+
const bytes = await response.arrayBuffer();
|
|
1894
|
+
preparationSignal.throwIfAborted();
|
|
1895
|
+
const seconds = await measureSeconds(bytes);
|
|
1896
|
+
preparationSignal.throwIfAborted();
|
|
2014
1897
|
createdSrc = URL.createObjectURL(new Blob([bytes], {
|
|
2015
1898
|
type: response.headers.get("content-type") || "audio/mpeg"
|
|
2016
1899
|
}));
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
throw controller.signal.reason ?? new DOMException("Speech preparation cancelled", "AbortError");
|
|
2020
|
-
}
|
|
2021
|
-
}
|
|
1900
|
+
return { source: "generated", src: createdSrc, seconds };
|
|
1901
|
+
}, SPEECH_PREPARATION_TIMEOUT_MS, controller.signal);
|
|
2022
1902
|
}
|
|
2023
1903
|
} catch (cause) {
|
|
2024
1904
|
if (createdSrc) URL.revokeObjectURL(createdSrc);
|
|
2025
1905
|
if (controller.signal.aborted) throw controller.signal.reason ?? cause;
|
|
1906
|
+
notifyFallback();
|
|
2026
1907
|
prepared = { source: "browser", seconds: estimatedBrowserSeconds(normalized) };
|
|
2027
1908
|
}
|
|
2028
1909
|
if (disposed || controller.signal.aborted) {
|
|
@@ -2044,6 +1925,19 @@ function createVideoChatVoice(options = {}) {
|
|
|
2044
1925
|
signal?.removeEventListener("abort", forwardAbort);
|
|
2045
1926
|
}
|
|
2046
1927
|
};
|
|
1928
|
+
const speechStops = /* @__PURE__ */ new Set();
|
|
1929
|
+
const watchSpeech = (seconds, expire) => {
|
|
1930
|
+
let remaining = Math.max(1, seconds) * 2e3 + 5e3;
|
|
1931
|
+
const timer = setInterval(() => {
|
|
1932
|
+
if (held) return;
|
|
1933
|
+
remaining -= 250;
|
|
1934
|
+
if (remaining <= 0) {
|
|
1935
|
+
clearInterval(timer);
|
|
1936
|
+
expire();
|
|
1937
|
+
}
|
|
1938
|
+
}, 250);
|
|
1939
|
+
return () => clearInterval(timer);
|
|
1940
|
+
};
|
|
2047
1941
|
const stopBrowser = () => {
|
|
2048
1942
|
globalThis.speechSynthesis?.cancel();
|
|
2049
1943
|
browserFinish?.();
|
|
@@ -2060,7 +1954,7 @@ function createVideoChatVoice(options = {}) {
|
|
|
2060
1954
|
},
|
|
2061
1955
|
resume() {
|
|
2062
1956
|
held = false;
|
|
2063
|
-
if (sounding && !silent) void sounding.play().catch(() =>
|
|
1957
|
+
if (sounding && !silent) void sounding.play().catch(() => playbackFailure?.());
|
|
2064
1958
|
if (!silent) globalThis.speechSynthesis?.resume();
|
|
2065
1959
|
},
|
|
2066
1960
|
setMuted(muted) {
|
|
@@ -2068,7 +1962,16 @@ function createVideoChatVoice(options = {}) {
|
|
|
2068
1962
|
if (sounding) sounding.muted = muted;
|
|
2069
1963
|
if (muted) stopBrowser();
|
|
2070
1964
|
},
|
|
2071
|
-
async speak(text, { signal }) {
|
|
1965
|
+
async speak(text, { signal, onStart }) {
|
|
1966
|
+
let started = false;
|
|
1967
|
+
const notifyStart = (source) => {
|
|
1968
|
+
if (started || disposed || signal.aborted || silent || held) return;
|
|
1969
|
+
started = true;
|
|
1970
|
+
try {
|
|
1971
|
+
void Promise.resolve(onStart?.(source)).catch(() => void 0);
|
|
1972
|
+
} catch {
|
|
1973
|
+
}
|
|
1974
|
+
};
|
|
2072
1975
|
const line = await load(text, signal);
|
|
2073
1976
|
if (disposed || signal.aborted || silent) return;
|
|
2074
1977
|
if (line.source === "browser") {
|
|
@@ -2081,40 +1984,91 @@ function createVideoChatVoice(options = {}) {
|
|
|
2081
1984
|
const finish = () => {
|
|
2082
1985
|
if (finished) return;
|
|
2083
1986
|
finished = true;
|
|
1987
|
+
clearWatchdog();
|
|
1988
|
+
signal.removeEventListener("abort", stop);
|
|
1989
|
+
speechStops.delete(stop);
|
|
1990
|
+
utterance.onstart = null;
|
|
1991
|
+
utterance.onend = null;
|
|
1992
|
+
utterance.onerror = null;
|
|
2084
1993
|
if (browserFinish === finish) browserFinish = void 0;
|
|
2085
1994
|
resolve();
|
|
2086
1995
|
};
|
|
1996
|
+
const stop = () => {
|
|
1997
|
+
synthesis.cancel();
|
|
1998
|
+
finish();
|
|
1999
|
+
};
|
|
2000
|
+
const clearWatchdog = watchSpeech(estimatedBrowserSeconds(text), stop);
|
|
2001
|
+
speechStops.add(stop);
|
|
2087
2002
|
browserFinish = finish;
|
|
2003
|
+
utterance.onstart = () => {
|
|
2004
|
+
if (!finished) notifyStart("browser");
|
|
2005
|
+
};
|
|
2088
2006
|
utterance.onend = finish;
|
|
2089
2007
|
utterance.onerror = finish;
|
|
2090
|
-
signal.addEventListener("abort",
|
|
2091
|
-
|
|
2008
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
2009
|
+
try {
|
|
2010
|
+
synthesis.speak(utterance);
|
|
2011
|
+
if (held) synthesis.pause();
|
|
2012
|
+
} catch {
|
|
2092
2013
|
finish();
|
|
2093
|
-
}
|
|
2094
|
-
synthesis.speak(utterance);
|
|
2095
|
-
if (held) synthesis.pause();
|
|
2014
|
+
}
|
|
2096
2015
|
});
|
|
2097
2016
|
return;
|
|
2098
2017
|
}
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2018
|
+
let playbackFailed = false;
|
|
2019
|
+
try {
|
|
2020
|
+
const element = new Audio(line.src);
|
|
2021
|
+
element.muted = silent;
|
|
2022
|
+
sounding = element;
|
|
2023
|
+
await new Promise((resolve) => {
|
|
2024
|
+
let finished = false;
|
|
2025
|
+
const finish = () => {
|
|
2026
|
+
if (finished) return;
|
|
2027
|
+
finished = true;
|
|
2028
|
+
clearWatchdog();
|
|
2029
|
+
signal.removeEventListener("abort", stop);
|
|
2030
|
+
speechStops.delete(stop);
|
|
2031
|
+
element.onplaying = null;
|
|
2032
|
+
element.onended = null;
|
|
2033
|
+
element.onerror = null;
|
|
2034
|
+
if (sounding === element) {
|
|
2035
|
+
sounding = void 0;
|
|
2036
|
+
playbackFailure = void 0;
|
|
2037
|
+
}
|
|
2038
|
+
resolve();
|
|
2039
|
+
};
|
|
2040
|
+
const stop = () => {
|
|
2041
|
+
element.pause();
|
|
2042
|
+
finish();
|
|
2043
|
+
};
|
|
2044
|
+
const fail = () => {
|
|
2045
|
+
playbackFailed = true;
|
|
2046
|
+
stop();
|
|
2047
|
+
};
|
|
2048
|
+
const clearWatchdog = watchSpeech(Math.max(line.seconds, estimatedBrowserSeconds(text)), fail);
|
|
2049
|
+
speechStops.add(stop);
|
|
2050
|
+
playbackFailure = fail;
|
|
2051
|
+
element.onplaying = () => {
|
|
2052
|
+
if (!finished) notifyStart("generated");
|
|
2053
|
+
};
|
|
2054
|
+
element.onended = finish;
|
|
2055
|
+
element.onerror = fail;
|
|
2056
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
2057
|
+
try {
|
|
2058
|
+
if (!held) void element.play().catch(fail);
|
|
2059
|
+
} catch {
|
|
2060
|
+
fail();
|
|
2061
|
+
}
|
|
2062
|
+
});
|
|
2063
|
+
} catch {
|
|
2064
|
+
playbackFailed = true;
|
|
2065
|
+
}
|
|
2066
|
+
if (playbackFailed && !disposed && !signal.aborted && !silent) {
|
|
2067
|
+
notifyFallback();
|
|
2068
|
+
URL.revokeObjectURL(line.src);
|
|
2069
|
+
lines.set(text.trim(), { source: "browser", seconds: estimatedBrowserSeconds(text) });
|
|
2070
|
+
await this.speak(text, { signal, onStart: notifyStart });
|
|
2071
|
+
}
|
|
2118
2072
|
},
|
|
2119
2073
|
dispose() {
|
|
2120
2074
|
disposed = true;
|
|
@@ -2122,6 +2076,8 @@ function createVideoChatVoice(options = {}) {
|
|
|
2122
2076
|
controller.abort(new DOMException("Video chat voice was disposed", "AbortError"));
|
|
2123
2077
|
}
|
|
2124
2078
|
pendingLoads.clear();
|
|
2079
|
+
for (const stop of speechStops) stop();
|
|
2080
|
+
speechStops.clear();
|
|
2125
2081
|
sounding?.pause();
|
|
2126
2082
|
sounding = void 0;
|
|
2127
2083
|
stopBrowser();
|
|
@@ -2135,6 +2091,12 @@ function createVideoChatVoice(options = {}) {
|
|
|
2135
2091
|
|
|
2136
2092
|
// src/video-chat/use-video-chat.ts
|
|
2137
2093
|
var DEFAULT_TIMEOUT_MS = 18e4;
|
|
2094
|
+
function observe(callback) {
|
|
2095
|
+
try {
|
|
2096
|
+
void Promise.resolve(callback?.()).catch(() => void 0);
|
|
2097
|
+
} catch {
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2138
2100
|
function initialState(muted) {
|
|
2139
2101
|
return {
|
|
2140
2102
|
turns: [],
|
|
@@ -2160,6 +2122,7 @@ function reducer(state, action) {
|
|
|
2160
2122
|
return {
|
|
2161
2123
|
...state,
|
|
2162
2124
|
turns: [...state.turns, action.turn],
|
|
2125
|
+
playerKey: state.playerKey + 1,
|
|
2163
2126
|
shownTurnId: action.turn.id,
|
|
2164
2127
|
status: "composing",
|
|
2165
2128
|
resumeStatus: "composing",
|
|
@@ -2219,6 +2182,8 @@ function reducer(state, action) {
|
|
|
2219
2182
|
suggestions: action.suggestions
|
|
2220
2183
|
}))
|
|
2221
2184
|
};
|
|
2185
|
+
case "suggestions":
|
|
2186
|
+
return { ...state, turns: replaceTurn(state.turns, action.id, (turn) => ({ ...turn, suggestions: action.suggestions })) };
|
|
2222
2187
|
case "scene":
|
|
2223
2188
|
if (state.playerKey !== action.key) return state;
|
|
2224
2189
|
return {
|
|
@@ -2229,6 +2194,11 @@ function reducer(state, action) {
|
|
|
2229
2194
|
case "playback-end":
|
|
2230
2195
|
if (state.playerKey !== action.key) return state;
|
|
2231
2196
|
return { ...state, status: "ended", resumeStatus: "ended", playbackEnded: true, openingSpeaking: false };
|
|
2197
|
+
case "warning":
|
|
2198
|
+
return { ...state, turns: replaceTurn(state.turns, action.id, (turn) => ({
|
|
2199
|
+
...turn,
|
|
2200
|
+
warnings: [.../* @__PURE__ */ new Set([...turn.warnings ?? [], action.message])]
|
|
2201
|
+
})) };
|
|
2232
2202
|
case "error":
|
|
2233
2203
|
if (state.turns.at(-1)?.id !== action.id) return state;
|
|
2234
2204
|
return {
|
|
@@ -2311,28 +2281,35 @@ function conversationFor(turns) {
|
|
|
2311
2281
|
response: [...transcriptFor(turn).join(" ")].slice(0, 8e3).join("")
|
|
2312
2282
|
}));
|
|
2313
2283
|
}
|
|
2314
|
-
function
|
|
2284
|
+
async function untilAborted(work, signal) {
|
|
2285
|
+
let abort = () => void 0;
|
|
2286
|
+
const cancelled = new Promise((_resolve, reject) => {
|
|
2287
|
+
abort = () => reject(signal.reason ?? new DOMException("Cancelled", "AbortError"));
|
|
2288
|
+
if (signal.aborted) abort();
|
|
2289
|
+
else signal.addEventListener("abort", abort, { once: true });
|
|
2290
|
+
});
|
|
2291
|
+
try {
|
|
2292
|
+
return await Promise.race([work, cancelled]);
|
|
2293
|
+
} finally {
|
|
2294
|
+
signal.removeEventListener("abort", abort);
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
function errorFrom(cause) {
|
|
2315
2298
|
if (cause instanceof VideoError) return cause;
|
|
2316
2299
|
if (cause instanceof DOMException && cause.name === "AbortError") {
|
|
2317
2300
|
return new VideoError("Video chat was cancelled", { code: "aborted", recoverable: false });
|
|
2318
2301
|
}
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
"Video chat failed"
|
|
2322
|
-
), { code: "video_chat_failed", recoverable: false });
|
|
2323
|
-
}
|
|
2324
|
-
async function responseError2(response) {
|
|
2325
|
-
let code = "http_error";
|
|
2326
|
-
let message = `Video chat endpoint failed (${response.status})`;
|
|
2327
|
-
try {
|
|
2328
|
-
const body = await response.json();
|
|
2329
|
-
if (typeof body.error?.code === "string") code = body.error.code.slice(0, 80);
|
|
2330
|
-
if (typeof body.error?.message === "string") {
|
|
2331
|
-
message = safePublicDiagnostic(body.error.message, message);
|
|
2332
|
-
}
|
|
2333
|
-
} catch {
|
|
2302
|
+
if (cause instanceof DOMException && cause.name === "TimeoutError") {
|
|
2303
|
+
return new VideoError("Video chat timed out", { code: "timeout", recoverable: false });
|
|
2334
2304
|
}
|
|
2335
|
-
return new VideoError(
|
|
2305
|
+
return new VideoError("Video chat could not produce a playable response", { code: "video_chat_failed", recoverable: false });
|
|
2306
|
+
}
|
|
2307
|
+
async function responseError(response) {
|
|
2308
|
+
return new VideoError("Video chat could not produce a playable response", {
|
|
2309
|
+
code: "http_error",
|
|
2310
|
+
status: response.status,
|
|
2311
|
+
recoverable: false
|
|
2312
|
+
});
|
|
2336
2313
|
}
|
|
2337
2314
|
function pacedScene(scene, spokenSeconds, templates) {
|
|
2338
2315
|
const held = spokenSeconds && spokenSeconds > 0 ? spokenSeconds + 0.8 : getSceneDuration(scene, templates?.getTemplateMetadata(scene.templateId) ?? getBuiltinTemplateMetadata(scene.templateId));
|
|
@@ -2361,35 +2338,76 @@ function prepareVisualScene(scene, mode) {
|
|
|
2361
2338
|
return overMedia ? { ...shown, textArchetype: "subtle" } : shown;
|
|
2362
2339
|
}
|
|
2363
2340
|
function useVideoChat(options = {}) {
|
|
2364
|
-
const optionsRef =
|
|
2341
|
+
const optionsRef = useRef3(options);
|
|
2365
2342
|
optionsRef.current = options;
|
|
2366
|
-
const
|
|
2343
|
+
const voiceWarningRef = useRef3(() => void 0);
|
|
2344
|
+
const ownedVoiceRef = useRef3(void 0);
|
|
2367
2345
|
if (!options.voice && !ownedVoiceRef.current) {
|
|
2368
2346
|
ownedVoiceRef.current = createVideoChatVoice({
|
|
2369
2347
|
endpoint: options.endpoint,
|
|
2370
2348
|
headers: options.headers,
|
|
2371
2349
|
credentials: options.credentials,
|
|
2372
|
-
fetcher: options.fetcher
|
|
2350
|
+
fetcher: options.fetcher,
|
|
2351
|
+
onFallback: () => voiceWarningRef.current()
|
|
2373
2352
|
});
|
|
2374
2353
|
}
|
|
2375
2354
|
const voice = options.voice ?? ownedVoiceRef.current;
|
|
2376
|
-
const voiceRef =
|
|
2355
|
+
const voiceRef = useRef3(voice);
|
|
2377
2356
|
voiceRef.current = voice;
|
|
2378
|
-
const
|
|
2379
|
-
const
|
|
2357
|
+
const unavailableVoiceLines = useRef3(/* @__PURE__ */ new Set());
|
|
2358
|
+
const speechStartRef = useRef3(() => void 0);
|
|
2359
|
+
const narration = useNarration({ onSpeechStart: (source) => speechStartRef.current(source), voice: {
|
|
2360
|
+
speak: (text, options2) => unavailableVoiceLines.current.has(text) ? void 0 : voice.speak(text, options2)
|
|
2361
|
+
} });
|
|
2362
|
+
const narrationRef = useRef3(narration);
|
|
2380
2363
|
narrationRef.current = narration;
|
|
2381
2364
|
const [state, dispatch] = useReducer(reducer, options.initialMuted ?? false, initialState);
|
|
2382
|
-
const stateRef =
|
|
2365
|
+
const stateRef = useRef3(state);
|
|
2383
2366
|
stateRef.current = state;
|
|
2384
|
-
const runRef =
|
|
2385
|
-
const inFlightRef =
|
|
2386
|
-
const
|
|
2387
|
-
const
|
|
2388
|
-
const
|
|
2389
|
-
const
|
|
2390
|
-
const
|
|
2391
|
-
const
|
|
2392
|
-
const
|
|
2367
|
+
const runRef = useRef3(0);
|
|
2368
|
+
const inFlightRef = useRef3(void 0);
|
|
2369
|
+
const suggestionsRef = useRef3(void 0);
|
|
2370
|
+
const openingRef = useRef3(void 0);
|
|
2371
|
+
const timelineRef = useRef3(void 0);
|
|
2372
|
+
const heldRef = useRef3(false);
|
|
2373
|
+
const flushRef = useRef3(void 0);
|
|
2374
|
+
const mountedRef = useRef3(true);
|
|
2375
|
+
const firstFrameRef = useRef3(void 0);
|
|
2376
|
+
const reportMetric = useCallback2((metric) => {
|
|
2377
|
+
observe(() => optionsRef.current.onPlaybackMetric?.(metric));
|
|
2378
|
+
}, []);
|
|
2379
|
+
const finishStall = useCallback2(() => {
|
|
2380
|
+
const timing = firstFrameRef.current;
|
|
2381
|
+
if (!timing?.active || timing.stallStartedAt == null) return;
|
|
2382
|
+
const now = monotonicNow();
|
|
2383
|
+
const started = timing.stallStartedAt;
|
|
2384
|
+
timing.stallStartedAt = void 0;
|
|
2385
|
+
reportMetric({
|
|
2386
|
+
type: "stall",
|
|
2387
|
+
turnId: timing.turnId,
|
|
2388
|
+
mode: timing.mode,
|
|
2389
|
+
elapsedMs: Math.max(0, Math.round(now - timing.startedAt)),
|
|
2390
|
+
durationMs: Math.max(0, Math.round(now - started)),
|
|
2391
|
+
reason: "scene-generation"
|
|
2392
|
+
});
|
|
2393
|
+
}, [reportMetric]);
|
|
2394
|
+
const endTiming = useCallback2(() => {
|
|
2395
|
+
finishStall();
|
|
2396
|
+
if (firstFrameRef.current) firstFrameRef.current.active = false;
|
|
2397
|
+
}, [finishStall]);
|
|
2398
|
+
speechStartRef.current = (source) => {
|
|
2399
|
+
const timing = firstFrameRef.current;
|
|
2400
|
+
if (!mountedRef.current || !timing?.active || timing.speechReported || heldRef.current || stateRef.current.muted) return;
|
|
2401
|
+
timing.speechReported = true;
|
|
2402
|
+
reportMetric({
|
|
2403
|
+
type: "first-speech",
|
|
2404
|
+
turnId: timing.turnId,
|
|
2405
|
+
mode: timing.mode,
|
|
2406
|
+
elapsedMs: Math.max(0, Math.round(monotonicNow() - timing.startedAt)),
|
|
2407
|
+
source: optionsRef.current.voice ? "custom" : source === "generated" ? "generated" : "browser"
|
|
2408
|
+
});
|
|
2409
|
+
};
|
|
2410
|
+
const request = useCallback2(async (action, init = {}, signal) => {
|
|
2393
2411
|
const current = optionsRef.current;
|
|
2394
2412
|
const headers = new Headers(current.headers);
|
|
2395
2413
|
new Headers(init.headers).forEach((header, name) => headers.set(name, header));
|
|
@@ -2403,27 +2421,29 @@ function useVideoChat(options = {}) {
|
|
|
2403
2421
|
signal
|
|
2404
2422
|
});
|
|
2405
2423
|
}, []);
|
|
2406
|
-
|
|
2424
|
+
useEffect4(() => {
|
|
2407
2425
|
mountedRef.current = true;
|
|
2408
2426
|
const controller = new AbortController();
|
|
2409
2427
|
void request("capabilities", {}, controller.signal).then(async (response) => {
|
|
2410
|
-
if (!response.ok) throw await
|
|
2428
|
+
if (!response.ok) throw await responseError(response);
|
|
2411
2429
|
return response.json();
|
|
2412
2430
|
}).then((value) => {
|
|
2413
2431
|
if (mountedRef.current) dispatch({ type: "capabilities", value });
|
|
2414
2432
|
}).catch(() => void 0);
|
|
2415
2433
|
void request("welcome", {}, controller.signal).then(async (response) => {
|
|
2416
|
-
if (!response.ok) throw await
|
|
2434
|
+
if (!response.ok) throw await responseError(response);
|
|
2417
2435
|
return response.json();
|
|
2418
2436
|
}).then((value) => {
|
|
2419
2437
|
if (mountedRef.current) dispatch({ type: "welcome", value });
|
|
2420
2438
|
}).catch(() => void 0);
|
|
2421
2439
|
return () => {
|
|
2440
|
+
endTiming();
|
|
2422
2441
|
mountedRef.current = false;
|
|
2423
2442
|
controller.abort();
|
|
2424
2443
|
runRef.current += 1;
|
|
2425
2444
|
inFlightRef.current?.abort(new DOMException("Component unmounted", "AbortError"));
|
|
2426
2445
|
openingRef.current?.abort(new DOMException("Component unmounted", "AbortError"));
|
|
2446
|
+
suggestionsRef.current?.abort();
|
|
2427
2447
|
timelineRef.current?.complete();
|
|
2428
2448
|
timelineRef.current = void 0;
|
|
2429
2449
|
narrationRef.current.interrupt();
|
|
@@ -2431,12 +2451,14 @@ function useVideoChat(options = {}) {
|
|
|
2431
2451
|
if (!mountedRef.current) ownedVoiceRef.current?.dispose?.();
|
|
2432
2452
|
});
|
|
2433
2453
|
};
|
|
2434
|
-
}, [request]);
|
|
2435
|
-
|
|
2436
|
-
const cancel =
|
|
2454
|
+
}, [request, endTiming]);
|
|
2455
|
+
useEffect4(() => voice.setMuted(state.muted), [state.muted, voice]);
|
|
2456
|
+
const cancel = useCallback2((reason = "Video chat was cancelled") => {
|
|
2457
|
+
endTiming();
|
|
2437
2458
|
runRef.current += 1;
|
|
2438
2459
|
inFlightRef.current?.abort(new DOMException(reason, "AbortError"));
|
|
2439
2460
|
openingRef.current?.abort(new DOMException(reason, "AbortError"));
|
|
2461
|
+
suggestionsRef.current?.abort();
|
|
2440
2462
|
inFlightRef.current = void 0;
|
|
2441
2463
|
openingRef.current = void 0;
|
|
2442
2464
|
timelineRef.current?.complete();
|
|
@@ -2444,16 +2466,19 @@ function useVideoChat(options = {}) {
|
|
|
2444
2466
|
flushRef.current = void 0;
|
|
2445
2467
|
narrationRef.current.interrupt();
|
|
2446
2468
|
dispatch({ type: "cancelled" });
|
|
2447
|
-
}, []);
|
|
2448
|
-
const ask =
|
|
2469
|
+
}, [endTiming]);
|
|
2470
|
+
const ask = useCallback2(async (value, askOptions = {}) => {
|
|
2449
2471
|
const prompt = value.trim();
|
|
2450
2472
|
if (!prompt) return void 0;
|
|
2473
|
+
endTiming();
|
|
2451
2474
|
inFlightRef.current?.abort(new DOMException("Replaced by a new prompt", "AbortError"));
|
|
2452
2475
|
openingRef.current?.abort(new DOMException("Replaced by a new prompt", "AbortError"));
|
|
2476
|
+
suggestionsRef.current?.abort();
|
|
2453
2477
|
timelineRef.current?.complete();
|
|
2454
2478
|
timelineRef.current = void 0;
|
|
2455
2479
|
narrationRef.current.interrupt();
|
|
2456
2480
|
voiceRef.current.resume();
|
|
2481
|
+
unavailableVoiceLines.current.clear();
|
|
2457
2482
|
heldRef.current = false;
|
|
2458
2483
|
const controller = new AbortController();
|
|
2459
2484
|
const openingController = new AbortController();
|
|
@@ -2488,7 +2513,7 @@ function useVideoChat(options = {}) {
|
|
|
2488
2513
|
suggestions: [],
|
|
2489
2514
|
...openingMedia ? { openingMedia } : {}
|
|
2490
2515
|
};
|
|
2491
|
-
firstFrameRef.current = { turnId: id, mode, startedAt: monotonicNow(), reported: false };
|
|
2516
|
+
firstFrameRef.current = { turnId: id, mode, startedAt: monotonicNow(), reported: false, speechReported: false, active: true };
|
|
2492
2517
|
dispatch({ type: "start", turn });
|
|
2493
2518
|
let timeline;
|
|
2494
2519
|
let style;
|
|
@@ -2500,9 +2525,14 @@ function useVideoChat(options = {}) {
|
|
|
2500
2525
|
let terminal = false;
|
|
2501
2526
|
let attempt = 0;
|
|
2502
2527
|
let ready = [];
|
|
2528
|
+
let received = [];
|
|
2503
2529
|
let appended = 0;
|
|
2504
|
-
const isCurrent = () => mountedRef.current && runRef.current === run && !controller.signal.aborted;
|
|
2530
|
+
const isCurrent = () => mountedRef.current && runRef.current === run && !controller.signal.aborted && !terminal;
|
|
2505
2531
|
const isOpeningCurrent = () => isCurrent() && !terminal && openingRef.current === openingController && !openingController.signal.aborted;
|
|
2532
|
+
const warn = (message) => {
|
|
2533
|
+
if (isCurrent()) dispatch({ type: "warning", id, message });
|
|
2534
|
+
};
|
|
2535
|
+
voiceWarningRef.current = () => warn("Using browser voice for this response.");
|
|
2506
2536
|
const flush = () => {
|
|
2507
2537
|
if (!isCurrent()) return;
|
|
2508
2538
|
let available = appended;
|
|
@@ -2536,11 +2566,11 @@ function useVideoChat(options = {}) {
|
|
|
2536
2566
|
}
|
|
2537
2567
|
};
|
|
2538
2568
|
flushRef.current = flush;
|
|
2539
|
-
const resolveOpeningMedia = async (keyword) => {
|
|
2569
|
+
const resolveOpeningMedia = async (keyword, fallbackKeyword) => {
|
|
2540
2570
|
try {
|
|
2541
2571
|
const response = await request("opening-media", {
|
|
2542
2572
|
method: "POST",
|
|
2543
|
-
body: JSON.stringify({ keyword, orientation })
|
|
2573
|
+
body: JSON.stringify({ keyword, ...fallbackKeyword ? { fallbackKeyword } : {}, orientation })
|
|
2544
2574
|
}, openingController.signal);
|
|
2545
2575
|
if (!response.ok || !isOpeningCurrent() || timeline) return;
|
|
2546
2576
|
const payload = await response.json();
|
|
@@ -2549,13 +2579,25 @@ function useVideoChat(options = {}) {
|
|
|
2549
2579
|
} catch {
|
|
2550
2580
|
}
|
|
2551
2581
|
};
|
|
2582
|
+
const prepareSpeech = async (text, signal) => {
|
|
2583
|
+
try {
|
|
2584
|
+
return currentOptions.voice ? await withDeadline((child) => voiceRef.current.prepare(text, { signal: child }), 3e3, signal) : await voiceRef.current.prepare(text, { signal });
|
|
2585
|
+
} catch (cause) {
|
|
2586
|
+
if (currentOptions.voice && !signal.aborted && isCurrent()) unavailableVoiceLines.current.add(text);
|
|
2587
|
+
throw cause;
|
|
2588
|
+
}
|
|
2589
|
+
};
|
|
2552
2590
|
const speakOpening = async (text) => {
|
|
2553
2591
|
try {
|
|
2554
|
-
await voiceRef.current.prepare(text, { signal: openingController.signal });
|
|
2555
2592
|
if (!isOpeningCurrent()) return;
|
|
2556
2593
|
dispatch({ type: "opening-start", id, line: text });
|
|
2557
|
-
await
|
|
2594
|
+
await prepareSpeech(text, openingController.signal);
|
|
2595
|
+
if (!isOpeningCurrent()) return;
|
|
2596
|
+
await voiceRef.current.speak(text, { signal: openingController.signal, onStart: (source) => {
|
|
2597
|
+
if (isOpeningCurrent()) speechStartRef.current(source);
|
|
2598
|
+
} });
|
|
2558
2599
|
} catch {
|
|
2600
|
+
if (isOpeningCurrent()) warn("Some narration is unavailable; the response will continue.");
|
|
2559
2601
|
} finally {
|
|
2560
2602
|
openingActive = false;
|
|
2561
2603
|
if (isOpeningCurrent()) dispatch({ type: "opening-end", id });
|
|
@@ -2581,7 +2623,7 @@ function useVideoChat(options = {}) {
|
|
|
2581
2623
|
...currentOptions.style ? { style: currentOptions.style } : {}
|
|
2582
2624
|
})
|
|
2583
2625
|
}, controller.signal);
|
|
2584
|
-
if (!response.ok) throw await
|
|
2626
|
+
if (!response.ok) throw await responseError(response);
|
|
2585
2627
|
if (!response.body || !response.headers.get("content-type")?.includes("text/event-stream")) {
|
|
2586
2628
|
throw new VideoError("Video chat endpoint did not return a video stream", { code: "invalid_response" });
|
|
2587
2629
|
}
|
|
@@ -2590,83 +2632,100 @@ function useVideoChat(options = {}) {
|
|
|
2590
2632
|
const pending = [];
|
|
2591
2633
|
let narrating = Promise.resolve();
|
|
2592
2634
|
let terminalError;
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
const keyword = typeof payload.keyword === "string" ? payload.keyword.trim().slice(0, 80) : "";
|
|
2600
|
-
if (!openingMedia && keyword) void resolveOpeningMedia(keyword);
|
|
2601
|
-
if (!openingRequested && line) {
|
|
2602
|
-
spokenHook = line;
|
|
2603
|
-
lines.push(line);
|
|
2604
|
-
openingRequested = true;
|
|
2605
|
-
openingActive = true;
|
|
2606
|
-
void speakOpening(line);
|
|
2635
|
+
try {
|
|
2636
|
+
for await (const event of decodeVideoSse(response.body)) {
|
|
2637
|
+
if (!isCurrent() || currentAttempt !== attempt) return { video: { schemaVersion: "0.1", orientation, scenes: [], style }, lines: [] };
|
|
2638
|
+
if (event.type === "response.start") style = event.data.style;
|
|
2639
|
+
if (event.type === "response.warning" || event.type === "response.error" && !event.data.terminal) {
|
|
2640
|
+
warn("Some parts were simplified so the response could continue.");
|
|
2607
2641
|
}
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2642
|
+
if (event.type === "data.video-chat-opening") {
|
|
2643
|
+
const payload = event.data && typeof event.data === "object" && !Array.isArray(event.data) ? event.data : {};
|
|
2644
|
+
const line = typeof payload.line === "string" ? payload.line.trim().slice(0, 300) : "";
|
|
2645
|
+
const keyword = typeof payload.keyword === "string" ? payload.keyword.trim().slice(0, 80) : "";
|
|
2646
|
+
const fallbackKeyword = typeof payload.fallbackKeyword === "string" ? payload.fallbackKeyword.trim().slice(0, 80) : void 0;
|
|
2647
|
+
if (!openingMedia && keyword) void resolveOpeningMedia(keyword, fallbackKeyword);
|
|
2648
|
+
if (!openingRequested && line) {
|
|
2649
|
+
spokenHook = line;
|
|
2650
|
+
lines.push(line);
|
|
2651
|
+
openingRequested = true;
|
|
2652
|
+
openingActive = true;
|
|
2653
|
+
void speakOpening(line);
|
|
2654
|
+
}
|
|
2655
|
+
continue;
|
|
2656
|
+
}
|
|
2657
|
+
if (event.type === "response.error" && event.data.terminal) {
|
|
2658
|
+
terminalError = new VideoError("Video chat could not finish this response", {
|
|
2659
|
+
code: event.data.error.code,
|
|
2660
|
+
requestId: event.data.snapshot ? void 0 : id,
|
|
2661
|
+
recoverable: event.data.error.recoverable
|
|
2662
|
+
});
|
|
2663
|
+
}
|
|
2664
|
+
if (event.type === "response.abort") {
|
|
2665
|
+
terminalError = new VideoError("Video chat was interrupted", { code: "aborted", recoverable: false });
|
|
2666
|
+
}
|
|
2667
|
+
if (event.type !== "scene.add") continue;
|
|
2668
|
+
const position = event.data.position;
|
|
2669
|
+
const plannedScene = event.data.scene;
|
|
2670
|
+
planned[position] = plannedScene;
|
|
2671
|
+
received[position] = prepareVisualScene(plannedScene, mode);
|
|
2672
|
+
if (!currentOptions.templates?.getTemplate(plannedScene.templateId)) {
|
|
2673
|
+
preloadBuiltinTemplate(plannedScene.templateId);
|
|
2674
|
+
}
|
|
2675
|
+
warmSceneMedia(plannedScene.variables);
|
|
2676
|
+
const narrated = narrating.then(async () => {
|
|
2677
|
+
const supplied = plannedScene.narration?.trim();
|
|
2678
|
+
if (supplied) return supplied;
|
|
2679
|
+
return withDeadline(async (signal) => {
|
|
2680
|
+
const narrationResponse = await request("narration", {
|
|
2681
|
+
method: "POST",
|
|
2682
|
+
body: JSON.stringify({ prompt, scene: plannedScene, earlier: [...lines] })
|
|
2683
|
+
}, signal);
|
|
2684
|
+
if (!narrationResponse.ok) throw new Error("Narration unavailable");
|
|
2685
|
+
const payload = await narrationResponse.json();
|
|
2686
|
+
const line = typeof payload.line === "string" ? payload.line.trim() : "";
|
|
2687
|
+
if (!line) throw new Error("Narration unavailable");
|
|
2688
|
+
return line;
|
|
2689
|
+
}, 3e3, controller.signal);
|
|
2690
|
+
}).catch((cause) => {
|
|
2691
|
+
if (controller.signal.aborted) throw cause;
|
|
2692
|
+
warn("Some narration was simplified so the response could continue.");
|
|
2693
|
+
return Object.values(plannedScene.variables).filter((value2) => typeof value2 === "string" && !/^https?:/i.test(value2)).join(" ").slice(0, 500);
|
|
2694
|
+
}).then((line) => {
|
|
2695
|
+
if (line) lines.push(line);
|
|
2696
|
+
return line;
|
|
2615
2697
|
});
|
|
2698
|
+
narrating = narrated.catch(() => "");
|
|
2699
|
+
pending.push(narrated.then(async (line) => {
|
|
2700
|
+
if (!isCurrent() || currentAttempt !== attempt) return;
|
|
2701
|
+
const visual = prepareVisualScene(plannedScene, mode);
|
|
2702
|
+
const withNarration = line ? { ...visual, narration: line } : visual;
|
|
2703
|
+
const spoken = line ? await prepareSpeech(line, controller.signal).catch((cause) => {
|
|
2704
|
+
if (controller.signal.aborted) throw cause;
|
|
2705
|
+
warn("Some narration is unavailable; the response will continue.");
|
|
2706
|
+
return void 0;
|
|
2707
|
+
}) : void 0;
|
|
2708
|
+
if (!isCurrent() || currentAttempt !== attempt) return;
|
|
2709
|
+
ready[position] = pacedScene(withNarration, spoken?.seconds, currentOptions.templates);
|
|
2710
|
+
flush();
|
|
2711
|
+
}).catch(() => {
|
|
2712
|
+
if (!isCurrent() || currentAttempt !== attempt) return;
|
|
2713
|
+
ready[position] = { ...received[position], timing: { fixedDuration: 5 } };
|
|
2714
|
+
warn("Some parts were simplified so the response could continue.");
|
|
2715
|
+
flush();
|
|
2716
|
+
}));
|
|
2616
2717
|
}
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
if (event.type !== "scene.add") continue;
|
|
2621
|
-
const position = event.data.position;
|
|
2622
|
-
const plannedScene = event.data.scene;
|
|
2623
|
-
planned[position] = plannedScene;
|
|
2624
|
-
if (!currentOptions.templates?.getTemplate(plannedScene.templateId)) {
|
|
2625
|
-
preloadBuiltinTemplate(plannedScene.templateId);
|
|
2626
|
-
}
|
|
2627
|
-
warmSceneMedia(plannedScene.variables);
|
|
2628
|
-
const narrated = narrating.then(async () => {
|
|
2629
|
-
const supplied = plannedScene.narration?.trim();
|
|
2630
|
-
if (supplied) return supplied;
|
|
2631
|
-
const narrationResponse = await request("narration", {
|
|
2632
|
-
method: "POST",
|
|
2633
|
-
body: JSON.stringify({ prompt, scene: plannedScene, earlier: [...lines] })
|
|
2634
|
-
}, controller.signal);
|
|
2635
|
-
if (!narrationResponse.ok) return "";
|
|
2636
|
-
const payload = await narrationResponse.json();
|
|
2637
|
-
return typeof payload.line === "string" ? payload.line.trim() : "";
|
|
2638
|
-
}).then((line) => {
|
|
2639
|
-
if (line) lines.push(line);
|
|
2640
|
-
return line;
|
|
2641
|
-
});
|
|
2642
|
-
narrating = narrated.catch(() => "");
|
|
2643
|
-
pending.push(narrated.then(async (line) => {
|
|
2644
|
-
if (!isCurrent() || currentAttempt !== attempt) return;
|
|
2645
|
-
const visual = prepareVisualScene(plannedScene, mode);
|
|
2646
|
-
const withNarration = line ? { ...visual, narration: line } : visual;
|
|
2647
|
-
const spoken = line ? await voiceRef.current.prepare(line, { signal: controller.signal }) : void 0;
|
|
2648
|
-
if (!isCurrent() || currentAttempt !== attempt) return;
|
|
2649
|
-
ready[position] = pacedScene(withNarration, spoken?.seconds, currentOptions.templates);
|
|
2650
|
-
flush();
|
|
2651
|
-
}));
|
|
2718
|
+
} catch (cause) {
|
|
2719
|
+
if (controller.signal.aborted) throw cause;
|
|
2720
|
+
terminalError = errorFrom(cause);
|
|
2652
2721
|
}
|
|
2653
2722
|
await Promise.all(pending);
|
|
2654
|
-
if (terminalError)
|
|
2723
|
+
if (terminalError && ready.some(Boolean) && style) {
|
|
2724
|
+
warn("The response was interrupted; completed scenes are still available.");
|
|
2725
|
+
} else if (terminalError) throw terminalError;
|
|
2655
2726
|
if (planned.length === 0 || ready.filter(Boolean).length === 0 || !style) {
|
|
2656
2727
|
throw new VideoError("The video response contained no playable scenes", { code: "empty_response" });
|
|
2657
2728
|
}
|
|
2658
|
-
let suggestions2 = [];
|
|
2659
|
-
try {
|
|
2660
|
-
const suggestionsResponse = await request("suggestions", {
|
|
2661
|
-
method: "POST",
|
|
2662
|
-
body: JSON.stringify({ prompt, lines })
|
|
2663
|
-
}, controller.signal);
|
|
2664
|
-
if (suggestionsResponse.ok) {
|
|
2665
|
-
suggestions2 = (await suggestionsResponse.json()).suggestions ?? [];
|
|
2666
|
-
}
|
|
2667
|
-
} catch (cause) {
|
|
2668
|
-
if (controller.signal.aborted) throw cause;
|
|
2669
|
-
}
|
|
2670
2729
|
return {
|
|
2671
2730
|
video: {
|
|
2672
2731
|
schemaVersion: "0.1",
|
|
@@ -2674,25 +2733,43 @@ function useVideoChat(options = {}) {
|
|
|
2674
2733
|
scenes: ready.filter((entry) => entry != null),
|
|
2675
2734
|
style
|
|
2676
2735
|
},
|
|
2677
|
-
|
|
2736
|
+
lines
|
|
2678
2737
|
};
|
|
2679
2738
|
};
|
|
2680
2739
|
try {
|
|
2681
2740
|
let response;
|
|
2682
2741
|
try {
|
|
2683
|
-
response = await runAttempt(attempt);
|
|
2742
|
+
response = await untilAborted(runAttempt(attempt), controller.signal);
|
|
2684
2743
|
} catch (cause) {
|
|
2685
|
-
if (controller.signal.aborted || timeline) throw cause;
|
|
2744
|
+
if (controller.signal.aborted || timeline || spokenHook) throw cause;
|
|
2686
2745
|
attempt += 1;
|
|
2687
2746
|
ready = [];
|
|
2747
|
+
received = [];
|
|
2688
2748
|
appended = 0;
|
|
2689
2749
|
style = void 0;
|
|
2690
|
-
response = await runAttempt(attempt);
|
|
2750
|
+
response = await untilAborted(runAttempt(attempt), controller.signal);
|
|
2691
2751
|
}
|
|
2692
2752
|
if (!isCurrent()) return void 0;
|
|
2693
2753
|
planDone = true;
|
|
2694
2754
|
flush();
|
|
2695
|
-
dispatch({ type: "complete", id, video: response.video, suggestions:
|
|
2755
|
+
dispatch({ type: "complete", id, video: response.video, suggestions: [] });
|
|
2756
|
+
const suggestionsController = new AbortController();
|
|
2757
|
+
suggestionsRef.current = suggestionsController;
|
|
2758
|
+
void withDeadline(async (signal) => {
|
|
2759
|
+
const result = await request("suggestions", {
|
|
2760
|
+
method: "POST",
|
|
2761
|
+
body: JSON.stringify({ prompt, lines: response.lines })
|
|
2762
|
+
}, signal);
|
|
2763
|
+
if (!result.ok) return [];
|
|
2764
|
+
const payload = await result.json();
|
|
2765
|
+
return Array.isArray(payload.suggestions) ? payload.suggestions : [];
|
|
2766
|
+
}, 7e3, suggestionsController.signal).then((suggestions2) => {
|
|
2767
|
+
if (mountedRef.current && runRef.current === run && !suggestionsController.signal.aborted) {
|
|
2768
|
+
dispatch({ type: "suggestions", id, suggestions: suggestions2 });
|
|
2769
|
+
}
|
|
2770
|
+
}).catch(() => void 0).finally(() => {
|
|
2771
|
+
if (suggestionsRef.current === suggestionsController) suggestionsRef.current = void 0;
|
|
2772
|
+
});
|
|
2696
2773
|
return response.video;
|
|
2697
2774
|
} catch (cause) {
|
|
2698
2775
|
if (runRef.current !== run) return void 0;
|
|
@@ -2700,31 +2777,52 @@ function useVideoChat(options = {}) {
|
|
|
2700
2777
|
return void 0;
|
|
2701
2778
|
}
|
|
2702
2779
|
terminal = true;
|
|
2780
|
+
const recovered = received.flatMap((scene, index) => scene ? [ready[index] ?? { ...scene, timing: { fixedDuration: 5 } }] : []);
|
|
2781
|
+
if (recovered.length === 0 && spokenHook) {
|
|
2782
|
+
recovered.push({ id: `${id}-opening`, templateId: "media", variables: { texts: spokenHook, mediaType: "gradient" }, narration: spokenHook, timing: { fixedDuration: Math.max(4, spokenHook.split(/\s+/u).length / 2.5) } });
|
|
2783
|
+
}
|
|
2784
|
+
if (recovered.length > 0) {
|
|
2785
|
+
style ??= { brand: resolveVideoBrand(currentOptions.brand), density: "normal", motion: "normal", defaultBackgroundEffect: "static", defaultTextArchetype: "subtle", defaultTransition: "crossfade" };
|
|
2786
|
+
openingController.abort(new DOMException("Continuing completed response", "AbortError"));
|
|
2787
|
+
if (!timeline) {
|
|
2788
|
+
timeline = createSceneTimeline({ style, orientation });
|
|
2789
|
+
dispatch({ type: "player", id, stream: timeline.stream });
|
|
2790
|
+
}
|
|
2791
|
+
for (const scene of recovered.slice(appended)) timeline.add(scene);
|
|
2792
|
+
timeline.complete();
|
|
2793
|
+
if (timelineRef.current === timeline) timelineRef.current = void 0;
|
|
2794
|
+
const video = { schemaVersion: "0.1", orientation, style, scenes: recovered };
|
|
2795
|
+
dispatch({ type: "warning", id, message: "The response was interrupted; completed scenes are still available." });
|
|
2796
|
+
dispatch({ type: "complete", id, video, suggestions: [] });
|
|
2797
|
+
return video;
|
|
2798
|
+
}
|
|
2703
2799
|
openingController.abort(new DOMException("Response failed", "AbortError"));
|
|
2704
2800
|
timeline?.complete();
|
|
2705
2801
|
if (timelineRef.current === timeline) timelineRef.current = void 0;
|
|
2706
|
-
const error =
|
|
2802
|
+
const error = errorFrom(cause);
|
|
2707
2803
|
dispatch({ type: "error", id, error });
|
|
2708
2804
|
return void 0;
|
|
2709
2805
|
} finally {
|
|
2710
2806
|
clearTimeout(timeout);
|
|
2711
2807
|
if (runRef.current === run) inFlightRef.current = void 0;
|
|
2712
2808
|
}
|
|
2713
|
-
}, [request]);
|
|
2714
|
-
const pause =
|
|
2809
|
+
}, [request, endTiming]);
|
|
2810
|
+
const pause = useCallback2(() => {
|
|
2811
|
+
finishStall();
|
|
2715
2812
|
heldRef.current = true;
|
|
2716
2813
|
voiceRef.current.pause();
|
|
2717
2814
|
dispatch({ type: "pause" });
|
|
2718
|
-
}, []);
|
|
2719
|
-
const resume =
|
|
2815
|
+
}, [finishStall]);
|
|
2816
|
+
const resume = useCallback2(() => {
|
|
2720
2817
|
heldRef.current = false;
|
|
2721
2818
|
voiceRef.current.resume();
|
|
2722
2819
|
dispatch({ type: "resume" });
|
|
2723
2820
|
flushRef.current?.();
|
|
2724
2821
|
}, []);
|
|
2725
|
-
const replay =
|
|
2822
|
+
const replay = useCallback2(() => {
|
|
2726
2823
|
const turn = stateRef.current.turns.find((entry) => entry.id === stateRef.current.shownTurnId);
|
|
2727
2824
|
if (!turn?.completed || !turn.video) return;
|
|
2825
|
+
endTiming();
|
|
2728
2826
|
if (inFlightRef.current) {
|
|
2729
2827
|
runRef.current += 1;
|
|
2730
2828
|
inFlightRef.current.abort(new DOMException("Replaying a saved response", "AbortError"));
|
|
@@ -2738,10 +2836,11 @@ function useVideoChat(options = {}) {
|
|
|
2738
2836
|
heldRef.current = false;
|
|
2739
2837
|
voiceRef.current.resume();
|
|
2740
2838
|
dispatch({ type: "replay" });
|
|
2741
|
-
}, []);
|
|
2742
|
-
const selectTurn =
|
|
2839
|
+
}, [endTiming]);
|
|
2840
|
+
const selectTurn = useCallback2((id) => {
|
|
2743
2841
|
const turn = stateRef.current.turns.find((entry) => entry.id === id);
|
|
2744
2842
|
if (!turn?.completed || !turn.video) return;
|
|
2843
|
+
endTiming();
|
|
2745
2844
|
if (inFlightRef.current) {
|
|
2746
2845
|
runRef.current += 1;
|
|
2747
2846
|
inFlightRef.current.abort(new DOMException("Viewing a saved response", "AbortError"));
|
|
@@ -2755,14 +2854,14 @@ function useVideoChat(options = {}) {
|
|
|
2755
2854
|
heldRef.current = false;
|
|
2756
2855
|
voiceRef.current.resume();
|
|
2757
2856
|
dispatch({ type: "select", id });
|
|
2758
|
-
}, []);
|
|
2759
|
-
const reset =
|
|
2857
|
+
}, [endTiming]);
|
|
2858
|
+
const reset = useCallback2(() => {
|
|
2760
2859
|
cancel("Session reset");
|
|
2761
2860
|
heldRef.current = false;
|
|
2762
2861
|
voiceRef.current.resume();
|
|
2763
2862
|
dispatch({ type: "reset" });
|
|
2764
2863
|
}, [cancel]);
|
|
2765
|
-
const setMuted =
|
|
2864
|
+
const setMuted = useCallback2((muted) => dispatch({ type: "mute", value: muted }), []);
|
|
2766
2865
|
const currentTurn = state.turns.at(-1);
|
|
2767
2866
|
const shownTurn = state.turns.find((turn) => turn.id === state.shownTurnId) ?? currentTurn;
|
|
2768
2867
|
const availableModes = state.capabilities?.modes ?? ["templates"];
|
|
@@ -2777,27 +2876,35 @@ function useVideoChat(options = {}) {
|
|
|
2777
2876
|
paused: state.status === "paused",
|
|
2778
2877
|
controls: false,
|
|
2779
2878
|
orientation: shownTurn?.fixedOrientation ? shownTurn.orientation : "auto",
|
|
2879
|
+
onFramePresented: () => {
|
|
2880
|
+
const timing = firstFrameRef.current;
|
|
2881
|
+
if (stateRef.current.playerKey !== playbackKey || state.playback?.kind !== "stream" || !timing?.active || timing.reported) return;
|
|
2882
|
+
timing.reported = true;
|
|
2883
|
+
const elapsedMs = Math.max(0, Math.round(monotonicNow() - timing.startedAt));
|
|
2884
|
+
observe(() => optionsRef.current.onFirstFrame?.({ turnId: timing.turnId, mode: timing.mode, timeToFirstFrameMs: elapsedMs }));
|
|
2885
|
+
reportMetric({ type: "first-frame", turnId: timing.turnId, mode: timing.mode, elapsedMs });
|
|
2886
|
+
},
|
|
2887
|
+
onStallChange: (stalled) => {
|
|
2888
|
+
if (stateRef.current.playerKey !== playbackKey || state.playback?.kind !== "stream") return;
|
|
2889
|
+
const timing = firstFrameRef.current;
|
|
2890
|
+
if (!timing?.active || !timing.reported) return;
|
|
2891
|
+
if (!stalled) finishStall();
|
|
2892
|
+
else if (!heldRef.current && timing.stallStartedAt == null) timing.stallStartedAt = monotonicNow();
|
|
2893
|
+
},
|
|
2780
2894
|
onSceneChange: (scene, index) => {
|
|
2781
2895
|
if (stateRef.current.playerKey !== playbackKey) return;
|
|
2782
|
-
const firstFrame = firstFrameRef.current;
|
|
2783
|
-
if (index === 0 && firstFrame && !firstFrame.reported && state.playback?.kind === "stream") {
|
|
2784
|
-
firstFrame.reported = true;
|
|
2785
|
-
try {
|
|
2786
|
-
void optionsRef.current.onFirstFrame?.({
|
|
2787
|
-
turnId: firstFrame.turnId,
|
|
2788
|
-
mode: firstFrame.mode,
|
|
2789
|
-
timeToFirstFrameMs: Math.max(0, Math.round(monotonicNow() - firstFrame.startedAt))
|
|
2790
|
-
});
|
|
2791
|
-
} catch {
|
|
2792
|
-
}
|
|
2793
|
-
}
|
|
2794
2896
|
dispatch({ type: "scene", key: playbackKey, scene, index });
|
|
2795
2897
|
narrationRef.current.onSceneChange(scene, index);
|
|
2796
2898
|
},
|
|
2797
|
-
onPlaybackEnd: () =>
|
|
2899
|
+
onPlaybackEnd: () => {
|
|
2900
|
+
if (stateRef.current.playerKey !== playbackKey) return;
|
|
2901
|
+
endTiming();
|
|
2902
|
+
dispatch({ type: "playback-end", key: playbackKey });
|
|
2903
|
+
},
|
|
2798
2904
|
onError: (cause) => {
|
|
2905
|
+
if (stateRef.current.playerKey !== playbackKey) return;
|
|
2799
2906
|
const id = stateRef.current.turns.at(-1)?.id;
|
|
2800
|
-
if (id) dispatch({ type: "error", id, error:
|
|
2907
|
+
if (id) dispatch({ type: "error", id, error: errorFrom(cause) });
|
|
2801
2908
|
}
|
|
2802
2909
|
} : void 0;
|
|
2803
2910
|
return {
|
|
@@ -2815,6 +2922,7 @@ function useVideoChat(options = {}) {
|
|
|
2815
2922
|
welcome: state.welcome,
|
|
2816
2923
|
availableModes,
|
|
2817
2924
|
status: state.status,
|
|
2925
|
+
warnings: shownTurn?.warnings ?? [],
|
|
2818
2926
|
error: state.error,
|
|
2819
2927
|
suggestions,
|
|
2820
2928
|
caption: state.caption,
|
|
@@ -2829,7 +2937,7 @@ function useVideoChat(options = {}) {
|
|
|
2829
2937
|
}
|
|
2830
2938
|
|
|
2831
2939
|
// src/video-chat/video-chat.tsx
|
|
2832
|
-
import { useCallback as
|
|
2940
|
+
import { useCallback as useCallback5, useEffect as useEffect8, useId, useMemo as useMemo2, useRef as useRef6, useState as useState5 } from "react";
|
|
2833
2941
|
|
|
2834
2942
|
// src/video-chat/modes.ts
|
|
2835
2943
|
var visualModes = [
|
|
@@ -2939,9 +3047,9 @@ var Warning = () => /* @__PURE__ */ jsxs3(Glyph, { children: [
|
|
|
2939
3047
|
] });
|
|
2940
3048
|
|
|
2941
3049
|
// src/video-chat/use-dismiss.ts
|
|
2942
|
-
import { useEffect as
|
|
3050
|
+
import { useEffect as useEffect5 } from "react";
|
|
2943
3051
|
function useDismiss(open, close, surfaces) {
|
|
2944
|
-
|
|
3052
|
+
useEffect5(() => {
|
|
2945
3053
|
if (!open) return;
|
|
2946
3054
|
const onKeyDown = (event) => {
|
|
2947
3055
|
if (event.key !== "Escape") return;
|
|
@@ -2963,7 +3071,7 @@ function useDismiss(open, close, surfaces) {
|
|
|
2963
3071
|
}, [open, close, surfaces]);
|
|
2964
3072
|
}
|
|
2965
3073
|
function useFocusTrap(active, surface) {
|
|
2966
|
-
|
|
3074
|
+
useEffect5(() => {
|
|
2967
3075
|
if (!active) return;
|
|
2968
3076
|
const returnTo = document.activeElement;
|
|
2969
3077
|
const focusable = () => Array.from(
|
|
@@ -2996,11 +3104,11 @@ function useFocusTrap(active, surface) {
|
|
|
2996
3104
|
}
|
|
2997
3105
|
|
|
2998
3106
|
// src/video-chat/suggestion-cards.tsx
|
|
2999
|
-
import { useCallback as
|
|
3107
|
+
import { useCallback as useCallback3, useEffect as useEffect6, useRef as useRef4, useState as useState3 } from "react";
|
|
3000
3108
|
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
3001
3109
|
function Frame({ media, poster, playing }) {
|
|
3002
|
-
const video =
|
|
3003
|
-
|
|
3110
|
+
const video = useRef4(null);
|
|
3111
|
+
useEffect6(() => {
|
|
3004
3112
|
const element = video.current;
|
|
3005
3113
|
if (!element || playing === void 0) return;
|
|
3006
3114
|
if (playing) void element.play().catch(() => void 0);
|
|
@@ -3025,20 +3133,20 @@ function Frame({ media, poster, playing }) {
|
|
|
3025
3133
|
);
|
|
3026
3134
|
}
|
|
3027
3135
|
function SuggestionCards({ suggestions, label, onAsk }) {
|
|
3028
|
-
const railRef =
|
|
3029
|
-
const [at, setAt] =
|
|
3030
|
-
const [taken, setTaken] =
|
|
3031
|
-
const take =
|
|
3136
|
+
const railRef = useRef4(null);
|
|
3137
|
+
const [at, setAt] = useState3(0);
|
|
3138
|
+
const [taken, setTaken] = useState3(false);
|
|
3139
|
+
const take = useCallback3((index) => {
|
|
3032
3140
|
setTaken(true);
|
|
3033
3141
|
setAt(index);
|
|
3034
3142
|
}, []);
|
|
3035
|
-
|
|
3143
|
+
useEffect6(() => {
|
|
3036
3144
|
if (taken || suggestions.length < 2) return;
|
|
3037
3145
|
if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return;
|
|
3038
3146
|
const tour = window.setInterval(() => setAt((index) => (index + 1) % suggestions.length), 5e3);
|
|
3039
3147
|
return () => window.clearInterval(tour);
|
|
3040
3148
|
}, [taken, suggestions.length]);
|
|
3041
|
-
|
|
3149
|
+
useEffect6(() => {
|
|
3042
3150
|
const rail = railRef.current;
|
|
3043
3151
|
const card = rail?.children[at];
|
|
3044
3152
|
if (!rail || !card) return;
|
|
@@ -3095,7 +3203,7 @@ function Welcome({ data, onAsk, title }) {
|
|
|
3095
3203
|
}
|
|
3096
3204
|
|
|
3097
3205
|
// src/video-chat/use-voice-input.ts
|
|
3098
|
-
import { useCallback as
|
|
3206
|
+
import { useCallback as useCallback4, useEffect as useEffect7, useRef as useRef5, useState as useState4 } from "react";
|
|
3099
3207
|
function recognitionConstructor() {
|
|
3100
3208
|
if (typeof window === "undefined") return void 0;
|
|
3101
3209
|
const holder = window;
|
|
@@ -3165,19 +3273,19 @@ async function recordAndTranscribe(signal, ready, captured, options) {
|
|
|
3165
3273
|
return clip ? transcribeRecording(clip, signal, options) : "";
|
|
3166
3274
|
}
|
|
3167
3275
|
function useVoiceInput(onTranscript, transcriptionAvailable = false, options = {}) {
|
|
3168
|
-
const [supported, setSupported] =
|
|
3169
|
-
const [listening, setListening] =
|
|
3170
|
-
const [error, setError] =
|
|
3171
|
-
const [thinking, setThinking] =
|
|
3172
|
-
const recognitionRef =
|
|
3173
|
-
const useRecorderRef =
|
|
3174
|
-
const operationRef =
|
|
3175
|
-
const handlerRef =
|
|
3276
|
+
const [supported, setSupported] = useState4(false);
|
|
3277
|
+
const [listening, setListening] = useState4(false);
|
|
3278
|
+
const [error, setError] = useState4();
|
|
3279
|
+
const [thinking, setThinking] = useState4(false);
|
|
3280
|
+
const recognitionRef = useRef5(void 0);
|
|
3281
|
+
const useRecorderRef = useRef5(false);
|
|
3282
|
+
const operationRef = useRef5(void 0);
|
|
3283
|
+
const handlerRef = useRef5(onTranscript);
|
|
3176
3284
|
handlerRef.current = onTranscript;
|
|
3177
|
-
|
|
3285
|
+
useEffect7(() => {
|
|
3178
3286
|
setSupported(supportsVoiceInput(transcriptionAvailable));
|
|
3179
3287
|
}, [transcriptionAvailable]);
|
|
3180
|
-
const stop =
|
|
3288
|
+
const stop = useCallback4(() => {
|
|
3181
3289
|
recognitionRef.current?.abort();
|
|
3182
3290
|
recognitionRef.current = void 0;
|
|
3183
3291
|
const operation = operationRef.current;
|
|
@@ -3187,7 +3295,7 @@ function useVoiceInput(onTranscript, transcriptionAvailable = false, options = {
|
|
|
3187
3295
|
setListening(false);
|
|
3188
3296
|
setThinking(false);
|
|
3189
3297
|
}, []);
|
|
3190
|
-
|
|
3298
|
+
useEffect7(() => () => {
|
|
3191
3299
|
recognitionRef.current?.abort();
|
|
3192
3300
|
recognitionRef.current = void 0;
|
|
3193
3301
|
const operation = operationRef.current;
|
|
@@ -3195,7 +3303,7 @@ function useVoiceInput(onTranscript, transcriptionAvailable = false, options = {
|
|
|
3195
3303
|
operation?.controller.abort();
|
|
3196
3304
|
operation?.finish?.();
|
|
3197
3305
|
}, []);
|
|
3198
|
-
const finish =
|
|
3306
|
+
const finish = useCallback4(() => {
|
|
3199
3307
|
const recognition = recognitionRef.current;
|
|
3200
3308
|
if (recognition) {
|
|
3201
3309
|
recognition.stop();
|
|
@@ -3209,7 +3317,7 @@ function useVoiceInput(onTranscript, transcriptionAvailable = false, options = {
|
|
|
3209
3317
|
}
|
|
3210
3318
|
stop();
|
|
3211
3319
|
}, [stop]);
|
|
3212
|
-
const record =
|
|
3320
|
+
const record = useCallback4(async () => {
|
|
3213
3321
|
if (!transcriptionAvailable || !recorderAvailable()) {
|
|
3214
3322
|
setError("Server transcription is not configured.");
|
|
3215
3323
|
return;
|
|
@@ -3247,7 +3355,7 @@ function useVoiceInput(onTranscript, transcriptionAvailable = false, options = {
|
|
|
3247
3355
|
}
|
|
3248
3356
|
}
|
|
3249
3357
|
}, [transcriptionAvailable, options.endpoint, options.headers, options.credentials, options.fetcher]);
|
|
3250
|
-
const toggle =
|
|
3358
|
+
const toggle = useCallback4(() => {
|
|
3251
3359
|
if (listening) {
|
|
3252
3360
|
finish();
|
|
3253
3361
|
return;
|
|
@@ -3302,8 +3410,8 @@ function useVoiceInput(onTranscript, transcriptionAvailable = false, options = {
|
|
|
3302
3410
|
import { Fragment as Fragment4, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
3303
3411
|
var DESKTOP_WIDTH = 900;
|
|
3304
3412
|
function useViewportOrientation() {
|
|
3305
|
-
const [portrait, setPortrait] =
|
|
3306
|
-
|
|
3413
|
+
const [portrait, setPortrait] = useState5(() => typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia(`(max-width: ${DESKTOP_WIDTH - 1}px)`).matches : false);
|
|
3414
|
+
useEffect8(() => {
|
|
3307
3415
|
if (typeof window.matchMedia !== "function") return;
|
|
3308
3416
|
const query = window.matchMedia(`(max-width: ${DESKTOP_WIDTH - 1}px)`);
|
|
3309
3417
|
const update = () => setPortrait(query.matches);
|
|
@@ -3320,8 +3428,8 @@ var STEPS = [
|
|
|
3320
3428
|
"Still filming\u2026"
|
|
3321
3429
|
];
|
|
3322
3430
|
function useFilmingStep(active, key) {
|
|
3323
|
-
const [step, setStep] =
|
|
3324
|
-
|
|
3431
|
+
const [step, setStep] = useState5(0);
|
|
3432
|
+
useEffect8(() => {
|
|
3325
3433
|
if (!active) return;
|
|
3326
3434
|
setStep(0);
|
|
3327
3435
|
const stepper = window.setInterval(() => setStep((current) => current + 1), 3e3);
|
|
@@ -3333,14 +3441,14 @@ function Waveform({ active, listening }) {
|
|
|
3333
3441
|
return /* @__PURE__ */ jsx6("span", { className: `waveform${active ? " on" : ""}${listening ? " hearing" : ""}`, "aria-hidden": "true", children: [0, 1, 2, 3, 4].map((bar) => /* @__PURE__ */ jsx6("span", { style: { animationDelay: `${bar * 140}ms`, animationDuration: `${900 + bar * 130}ms` } }, bar)) });
|
|
3334
3442
|
}
|
|
3335
3443
|
function VideoChat({ options = {}, className, welcomeTitle }) {
|
|
3336
|
-
const [draft, setDraft] =
|
|
3337
|
-
const [themeId, setThemeId] =
|
|
3338
|
-
const [modeId, setModeId] =
|
|
3339
|
-
const [historyOpen, setHistoryOpen] =
|
|
3340
|
-
const [settingsOpen, setSettingsOpen] =
|
|
3341
|
-
const [sheetOpen, setSheetOpen] =
|
|
3444
|
+
const [draft, setDraft] = useState5("");
|
|
3445
|
+
const [themeId, setThemeId] = useState5(defaultTheme.id);
|
|
3446
|
+
const [modeId, setModeId] = useState5(options.mode ?? defaultMode.id);
|
|
3447
|
+
const [historyOpen, setHistoryOpen] = useState5(false);
|
|
3448
|
+
const [settingsOpen, setSettingsOpen] = useState5(false);
|
|
3449
|
+
const [sheetOpen, setSheetOpen] = useState5(false);
|
|
3342
3450
|
const viewportOrientation = useViewportOrientation();
|
|
3343
|
-
const [themeChoice, setThemeChoice] =
|
|
3451
|
+
const [themeChoice, setThemeChoice] = useState5("system");
|
|
3344
3452
|
const theme = themeById(themeId);
|
|
3345
3453
|
const sessionOrientation = options.orientation ?? viewportOrientation;
|
|
3346
3454
|
const { brand: configuredBrand, style: configuredStyle, ...sessionOptions } = options;
|
|
@@ -3360,29 +3468,29 @@ function VideoChat({ options = {}, className, welcomeTitle }) {
|
|
|
3360
3468
|
const appearanceName = `${instanceId}-appearance`;
|
|
3361
3469
|
const visualsName = `${instanceId}-visuals`;
|
|
3362
3470
|
const styleName = `${instanceId}-style`;
|
|
3363
|
-
const inputRef =
|
|
3364
|
-
const historyRef =
|
|
3365
|
-
const historyButtonRef =
|
|
3366
|
-
const settingsRef =
|
|
3367
|
-
const settingsButtonRef =
|
|
3368
|
-
const sheetRef =
|
|
3471
|
+
const inputRef = useRef6(null);
|
|
3472
|
+
const historyRef = useRef6(null);
|
|
3473
|
+
const historyButtonRef = useRef6(null);
|
|
3474
|
+
const settingsRef = useRef6(null);
|
|
3475
|
+
const settingsButtonRef = useRef6(null);
|
|
3476
|
+
const sheetRef = useRef6(null);
|
|
3369
3477
|
const listen = useVoiceInput(setDraft, chat.capabilities?.transcription ?? false, {
|
|
3370
3478
|
endpoint: options.endpoint,
|
|
3371
3479
|
headers: options.headers,
|
|
3372
3480
|
credentials: options.credentials,
|
|
3373
3481
|
fetcher: options.fetcher
|
|
3374
3482
|
});
|
|
3375
|
-
const historySurfaces =
|
|
3376
|
-
const settingsSurfaces =
|
|
3377
|
-
const noSurfaces =
|
|
3378
|
-
const closeHistory =
|
|
3379
|
-
const closeSettings =
|
|
3380
|
-
const closeSheet =
|
|
3483
|
+
const historySurfaces = useMemo2(() => [historyRef, historyButtonRef], []);
|
|
3484
|
+
const settingsSurfaces = useMemo2(() => [settingsRef, settingsButtonRef], []);
|
|
3485
|
+
const noSurfaces = useMemo2(() => [], []);
|
|
3486
|
+
const closeHistory = useCallback5(() => setHistoryOpen(false), []);
|
|
3487
|
+
const closeSettings = useCallback5(() => setSettingsOpen(false), []);
|
|
3488
|
+
const closeSheet = useCallback5(() => setSheetOpen(false), []);
|
|
3381
3489
|
useDismiss(historyOpen, closeHistory, historySurfaces);
|
|
3382
3490
|
useDismiss(settingsOpen, closeSettings, settingsSurfaces);
|
|
3383
3491
|
useDismiss(sheetOpen, closeSheet, noSurfaces);
|
|
3384
3492
|
useFocusTrap(sheetOpen, sheetRef);
|
|
3385
|
-
const ask =
|
|
3493
|
+
const ask = useCallback5((value) => {
|
|
3386
3494
|
const prompt = (typeof value === "string" ? value : value.prompt).trim();
|
|
3387
3495
|
if (!prompt) return;
|
|
3388
3496
|
setDraft("");
|
|
@@ -3395,7 +3503,7 @@ function VideoChat({ options = {}, className, welcomeTitle }) {
|
|
|
3395
3503
|
opening: value.opening
|
|
3396
3504
|
});
|
|
3397
3505
|
}, [chat, listen]);
|
|
3398
|
-
const newSession =
|
|
3506
|
+
const newSession = useCallback5(() => {
|
|
3399
3507
|
if (chat.turns.length === 0) {
|
|
3400
3508
|
inputRef.current?.focus();
|
|
3401
3509
|
return;
|
|
@@ -3643,7 +3751,5 @@ export {
|
|
|
3643
3751
|
VideoError,
|
|
3644
3752
|
VideoPlayer,
|
|
3645
3753
|
createVideoChatVoice,
|
|
3646
|
-
useNarration,
|
|
3647
|
-
useVideo,
|
|
3648
3754
|
useVideoChat
|
|
3649
3755
|
};
|