@convai/embody 0.1.0 → 0.2.1
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 +93 -0
- package/README.md +114 -30
- package/dist/character/createCharacter.d.ts +108 -3
- package/dist/character/createCharacter.js +118 -7
- package/dist/connect/session.d.ts +51 -0
- package/dist/connect/session.js +62 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +23 -0
- package/dist/motion/breathe.d.ts +1 -1
- package/dist/motion/breathe.js +1 -1
- package/dist/react/CharacterModel.d.ts +8 -0
- package/dist/react/CharacterModel.js +39 -0
- package/dist/react/ConvaiCharacter.d.ts +20 -6
- package/dist/react/ConvaiCharacter.js +37 -22
- package/dist/react/index.d.ts +35 -1
- package/dist/react/index.js +33 -1
- package/dist/react/useCharacter.d.ts +13 -2
- package/dist/react/useCharacter.js +17 -8
- package/dist/react/useWidgetClient.d.ts +18 -0
- package/dist/react/useWidgetClient.js +105 -0
- package/dist/resolve/resolver.d.ts +14 -1
- package/dist/resolve/resolver.js +33 -3
- package/package.json +8 -4
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MIND half of a character: the @convai/web-sdk connection.
|
|
3
|
+
*
|
|
4
|
+
* embody is a wrapper package, so an app should never have to stand up its
|
|
5
|
+
* own `ConvaiClient` alongside ours — that is two connections, two copies of
|
|
6
|
+
* the credentials, and a `blendshapeQueue` the app has to hand-plumb into
|
|
7
|
+
* every frame. This module owns that connection so `createCharacter` can
|
|
8
|
+
* hand back one object that is both body and mind.
|
|
9
|
+
*
|
|
10
|
+
* Nothing here is embody-specific: it is the exact three-step dance the SDK
|
|
11
|
+
* asks for (construct, connect, attach audio), plus teardown in the reverse
|
|
12
|
+
* order. Apps that already own a client pass it in as `client` and this
|
|
13
|
+
* module steps aside entirely.
|
|
14
|
+
*/
|
|
15
|
+
import { ConvaiClient, type BlendshapeQueue, type ConvaiConfig } from '@convai/web-sdk/core';
|
|
16
|
+
export interface SessionOptions extends ConvaiConfig {
|
|
17
|
+
/**
|
|
18
|
+
* Attach an `AudioRenderer` to the room so the character is audible.
|
|
19
|
+
* Default `true`. Set `false` when the app renders SDK audio itself —
|
|
20
|
+
* React apps using the SDK's `<AudioRenderer/>` component, for instance,
|
|
21
|
+
* or anything doing its own WebAudio routing.
|
|
22
|
+
*/
|
|
23
|
+
audio?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface SessionHandle {
|
|
26
|
+
/**
|
|
27
|
+
* The live SDK client. Everything the SDK can do — `sendUserTextMessage`,
|
|
28
|
+
* `on('transcript', …)`, mic control, memory — is reached through here;
|
|
29
|
+
* embody deliberately does not re-wrap that surface.
|
|
30
|
+
*/
|
|
31
|
+
client: ConvaiClient;
|
|
32
|
+
/** Lipsync frames for the turn currently being spoken. */
|
|
33
|
+
readonly queue: BlendshapeQueue;
|
|
34
|
+
/** Whether this session owns the client (and so should close it). */
|
|
35
|
+
readonly owned: boolean;
|
|
36
|
+
dispose(): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Connect a client and make it audible.
|
|
40
|
+
*
|
|
41
|
+
* `audio` is attached only after `connect()` resolves: `AudioRenderer` takes
|
|
42
|
+
* the LiveKit room and subscribes to its track events, so it needs a room
|
|
43
|
+
* that exists. Attaching earlier silently produces a mute character.
|
|
44
|
+
*/
|
|
45
|
+
export declare function createSession(opts: SessionOptions): Promise<SessionHandle>;
|
|
46
|
+
/**
|
|
47
|
+
* Adopt a client the app already created and connected. embody will drive
|
|
48
|
+
* lipsync from its queue but will not disconnect it on teardown — the app
|
|
49
|
+
* owns its own client's lifetime.
|
|
50
|
+
*/
|
|
51
|
+
export declare function adoptSession(client: ConvaiClient): SessionHandle;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MIND half of a character: the @convai/web-sdk connection.
|
|
3
|
+
*
|
|
4
|
+
* embody is a wrapper package, so an app should never have to stand up its
|
|
5
|
+
* own `ConvaiClient` alongside ours — that is two connections, two copies of
|
|
6
|
+
* the credentials, and a `blendshapeQueue` the app has to hand-plumb into
|
|
7
|
+
* every frame. This module owns that connection so `createCharacter` can
|
|
8
|
+
* hand back one object that is both body and mind.
|
|
9
|
+
*
|
|
10
|
+
* Nothing here is embody-specific: it is the exact three-step dance the SDK
|
|
11
|
+
* asks for (construct, connect, attach audio), plus teardown in the reverse
|
|
12
|
+
* order. Apps that already own a client pass it in as `client` and this
|
|
13
|
+
* module steps aside entirely.
|
|
14
|
+
*/
|
|
15
|
+
import { ConvaiClient } from '@convai/web-sdk/core';
|
|
16
|
+
import { AudioRenderer } from '@convai/web-sdk/vanilla';
|
|
17
|
+
/**
|
|
18
|
+
* Connect a client and make it audible.
|
|
19
|
+
*
|
|
20
|
+
* `audio` is attached only after `connect()` resolves: `AudioRenderer` takes
|
|
21
|
+
* the LiveKit room and subscribes to its track events, so it needs a room
|
|
22
|
+
* that exists. Attaching earlier silently produces a mute character.
|
|
23
|
+
*/
|
|
24
|
+
export async function createSession(opts) {
|
|
25
|
+
const { audio = true, ...config } = opts;
|
|
26
|
+
const client = new ConvaiClient(config);
|
|
27
|
+
await client.connect();
|
|
28
|
+
const renderer = audio ? new AudioRenderer(client.room) : null;
|
|
29
|
+
return wrapSession(client, renderer, true);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Adopt a client the app already created and connected. embody will drive
|
|
33
|
+
* lipsync from its queue but will not disconnect it on teardown — the app
|
|
34
|
+
* owns its own client's lifetime.
|
|
35
|
+
*/
|
|
36
|
+
export function adoptSession(client) {
|
|
37
|
+
return wrapSession(client, null, false);
|
|
38
|
+
}
|
|
39
|
+
function wrapSession(client, renderer, owned) {
|
|
40
|
+
let disposed = false;
|
|
41
|
+
return {
|
|
42
|
+
client,
|
|
43
|
+
owned,
|
|
44
|
+
// A getter, not a snapshot: the SDK replaces the queue across
|
|
45
|
+
// reconnects, and a captured reference would go quietly stale — the
|
|
46
|
+
// character would keep rendering frames from a dead turn.
|
|
47
|
+
get queue() {
|
|
48
|
+
return client.blendshapeQueue;
|
|
49
|
+
},
|
|
50
|
+
async dispose() {
|
|
51
|
+
// Idempotent: React StrictMode double-invokes cleanup, and a second
|
|
52
|
+
// disconnect on an already-closed client throws.
|
|
53
|
+
if (disposed)
|
|
54
|
+
return;
|
|
55
|
+
disposed = true;
|
|
56
|
+
renderer?.destroy();
|
|
57
|
+
// Only close what we opened.
|
|
58
|
+
if (owned)
|
|
59
|
+
await client.disconnect();
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @convai/embody — 3D bodies for Convai characters.
|
|
3
|
+
*
|
|
4
|
+
* embody wraps @convai/web-sdk rather than sitting beside it: one credential,
|
|
5
|
+
* one character id, one connection. Apps should not need to depend on the SDK
|
|
6
|
+
* directly, so its conversational surface is re-exported below.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { createCharacter } from '@convai/embody'
|
|
10
|
+
*
|
|
11
|
+
* const character = await createCharacter({ apiKey, characterId })
|
|
12
|
+
* scene.add(character.root)
|
|
13
|
+
* character.client?.sendUserTextMessage('hello')
|
|
14
|
+
* // per frame:
|
|
15
|
+
* character.update(delta)
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export { ConvaiClient, BlendshapeQueue, AudioManager, VideoManager, ScreenShareManager, MemoryManager, getDisconnectReasonMessage, } from '@convai/web-sdk/core';
|
|
19
|
+
export type { ConvaiConfig, ConvaiClientType, TurnStats } from '@convai/web-sdk/core';
|
|
20
|
+
export { createSession, adoptSession } from './connect/session.js';
|
|
21
|
+
export type { SessionOptions, SessionHandle } from './connect/session.js';
|
|
1
22
|
export { httpResolver, pickRepresentation, DEFAULT_BASE_URL } from './resolve/resolver.js';
|
|
2
23
|
export type { HttpResolverOptions } from './resolve/resolver.js';
|
|
3
24
|
export type { CharacterResolver, CharacterBundle, Representation, ModelVariant } from './resolve/resolver.js';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @convai/embody — 3D bodies for Convai characters.
|
|
3
|
+
*
|
|
4
|
+
* embody wraps @convai/web-sdk rather than sitting beside it: one credential,
|
|
5
|
+
* one character id, one connection. Apps should not need to depend on the SDK
|
|
6
|
+
* directly, so its conversational surface is re-exported below.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { createCharacter } from '@convai/embody'
|
|
10
|
+
*
|
|
11
|
+
* const character = await createCharacter({ apiKey, characterId })
|
|
12
|
+
* scene.add(character.root)
|
|
13
|
+
* character.client?.sendUserTextMessage('hello')
|
|
14
|
+
* // per frame:
|
|
15
|
+
* character.update(delta)
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
// ── The conversational half, re-exported from @convai/web-sdk ──────────────
|
|
19
|
+
// Everything an app needs to drive the conversation, so `@convai/embody` is
|
|
20
|
+
// the only import. These are the SDK's own types — not wrappers — so they
|
|
21
|
+
// stay in step with it and interop with an SDK client the app already has.
|
|
22
|
+
export { ConvaiClient, BlendshapeQueue, AudioManager, VideoManager, ScreenShareManager, MemoryManager, getDisconnectReasonMessage, } from '@convai/web-sdk/core';
|
|
23
|
+
export { createSession, adoptSession } from './connect/session.js';
|
|
1
24
|
export { httpResolver, pickRepresentation, DEFAULT_BASE_URL } from './resolve/resolver.js';
|
|
2
25
|
export { createCharacter } from './character/createCharacter.js';
|
|
3
26
|
export { AnimationController } from './clips/animations.js';
|
package/dist/motion/breathe.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ export interface BreathingConfig {
|
|
|
22
22
|
* A FIRST fix composed `rest * breath` absolutely every frame, using the
|
|
23
23
|
* bone's bind pose captured once at `resolveRig` time. That bounded the
|
|
24
24
|
* no-clip case but silently broke the WITH-clip case: every clip in
|
|
25
|
-
* `
|
|
25
|
+
* `convai-playhouse/pagoda-act/public/japanese-f-animations.glb` (Idle included, which the demo
|
|
26
26
|
* actually plays) animates `spine_02`/`spine_04` — the exact two bones this
|
|
27
27
|
* system drives — so composing onto a STATIC captured rest pose discarded
|
|
28
28
|
* the clip's own spine motion outright, every frame, on the normal
|
package/dist/motion/breathe.js
CHANGED
|
@@ -18,7 +18,7 @@ function freshAnchor() {
|
|
|
18
18
|
* A FIRST fix composed `rest * breath` absolutely every frame, using the
|
|
19
19
|
* bone's bind pose captured once at `resolveRig` time. That bounded the
|
|
20
20
|
* no-clip case but silently broke the WITH-clip case: every clip in
|
|
21
|
-
* `
|
|
21
|
+
* `convai-playhouse/pagoda-act/public/japanese-f-animations.glb` (Idle included, which the demo
|
|
22
22
|
* actually plays) animates `spine_02`/`spine_04` — the exact two bones this
|
|
23
23
|
* system drives — so composing onto a STATIC captured rest pose discarded
|
|
24
24
|
* the clip's own spine motion outright, every frame, on the normal
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type UseCharacterOptions } from './useCharacter.js';
|
|
2
|
+
import type { CharacterHandle } from '../character/createCharacter.js';
|
|
3
|
+
export interface CharacterModelProps extends UseCharacterOptions {
|
|
4
|
+
/** Called once the body is loaded and the connection is live. */
|
|
5
|
+
onReady?: (character: CharacterHandle) => void;
|
|
6
|
+
onError?: (error: Error) => void;
|
|
7
|
+
}
|
|
8
|
+
export declare function CharacterModel({ onReady, onError, ...opts }: CharacterModelProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The character as a scene object, for an app that already has its own
|
|
4
|
+
* `<Canvas>`, lighting and camera. Must be rendered INSIDE a `<Canvas>`.
|
|
5
|
+
*
|
|
6
|
+
* Most apps want the default export of `@convai/embody/react` instead, which
|
|
7
|
+
* brings its own canvas and needs nothing around it. Reach for this one when
|
|
8
|
+
* the character joins a scene you are already composing.
|
|
9
|
+
*
|
|
10
|
+
* ```tsx
|
|
11
|
+
* <Canvas>
|
|
12
|
+
* <MyLights />
|
|
13
|
+
* <CharacterModel token={token} characterId={characterId} />
|
|
14
|
+
* <MyProps />
|
|
15
|
+
* </Canvas>
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* Thin wiring only — all lifecycle/tick behaviour lives in `useCharacter`
|
|
19
|
+
* (which delegates cancellation to `../character/lifecycle`). This adds
|
|
20
|
+
* nothing but the `<primitive>` render and the onReady/onError plumbing.
|
|
21
|
+
*/
|
|
22
|
+
import { useEffect } from 'react';
|
|
23
|
+
import { useCharacter } from './useCharacter.js';
|
|
24
|
+
export function CharacterModel({ onReady, onError, ...opts }) {
|
|
25
|
+
const { character, error } = useCharacter(opts);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (character && onReady)
|
|
28
|
+
onReady(character);
|
|
29
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
30
|
+
}, [character, onReady]);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (error && onError)
|
|
33
|
+
onError(error);
|
|
34
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
35
|
+
}, [error, onError]);
|
|
36
|
+
if (!character)
|
|
37
|
+
return null;
|
|
38
|
+
return _jsx("primitive", { object: character.root });
|
|
39
|
+
}
|
|
@@ -1,7 +1,21 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { type CharacterModelProps } from './CharacterModel.js';
|
|
2
|
+
export interface ConvaiCharacterProps extends CharacterModelProps {
|
|
3
|
+
/** Wrapper style. Defaults to filling the parent. */
|
|
4
|
+
style?: React.CSSProperties;
|
|
5
|
+
className?: string;
|
|
6
|
+
/** Scene background. `null` renders transparent, to composite over a page. */
|
|
7
|
+
background?: string | null;
|
|
8
|
+
/**
|
|
9
|
+
* Camera framing, in metres, tuned for a standing human-scale character:
|
|
10
|
+
* eye-level and about three and a half metres back.
|
|
11
|
+
*/
|
|
12
|
+
camera?: {
|
|
13
|
+
position?: [number, number, number];
|
|
14
|
+
fov?: number;
|
|
15
|
+
target?: [number, number, number];
|
|
16
|
+
};
|
|
17
|
+
/** Rendered while the body is still loading. */
|
|
18
|
+
fallback?: React.ReactNode;
|
|
6
19
|
}
|
|
7
|
-
export declare function ConvaiCharacter({
|
|
20
|
+
export declare function ConvaiCharacter({ style, className, background, camera, fallback, ...character }: ConvaiCharacterProps): import("react").JSX.Element;
|
|
21
|
+
export default ConvaiCharacter;
|
|
@@ -1,25 +1,40 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* The whole package in one component.
|
|
4
|
+
*
|
|
5
|
+
* ```tsx
|
|
6
|
+
* import ConvaiCharacter from '@convai/embody/react'
|
|
7
|
+
*
|
|
8
|
+
* export default function App() {
|
|
9
|
+
* return <ConvaiCharacter token={token} characterId={characterId} />
|
|
10
|
+
* }
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* That renders the character. Nothing else is needed — no `<Canvas>`, no
|
|
14
|
+
* lights, no camera, no `ConvaiClient`, no resolver, no frame loop. embody
|
|
15
|
+
* resolves the body, opens the @convai/web-sdk connection, renders its audio
|
|
16
|
+
* and drives lipsync, gaze, breathing and blink.
|
|
17
|
+
*
|
|
18
|
+
* It fills its parent, so give that parent a size (the default is a full
|
|
19
|
+
* viewport when the parent has no height of its own).
|
|
20
|
+
*
|
|
21
|
+
* When the character should join a scene you are already composing, use the
|
|
22
|
+
* named `CharacterModel` export inside your own `<Canvas>` instead.
|
|
7
23
|
*/
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}, [
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return null;
|
|
24
|
-
return _jsx("primitive", { object: character.root });
|
|
24
|
+
import { Suspense } from 'react';
|
|
25
|
+
import { Canvas } from '@react-three/fiber';
|
|
26
|
+
import { CharacterModel } from './CharacterModel.js';
|
|
27
|
+
const DEFAULT_POSITION = [0, 1.45, 3.6];
|
|
28
|
+
const DEFAULT_TARGET = [0, 0.92, 0];
|
|
29
|
+
export function ConvaiCharacter({ style, className, background = '#17181c', camera, fallback = null, ...character }) {
|
|
30
|
+
const position = camera?.position ?? DEFAULT_POSITION;
|
|
31
|
+
const target = camera?.target ?? DEFAULT_TARGET;
|
|
32
|
+
return (_jsxs("div", { className: className, style: { width: '100%', height: '100%', minHeight: '100vh', ...style }, children: [_jsxs(Canvas, { camera: { position, fov: camera?.fov ?? 35, near: 0.05, far: 100 },
|
|
33
|
+
// Transparent when background is null, so the character can sit over
|
|
34
|
+
// existing page content rather than in a box of its own.
|
|
35
|
+
gl: { antialias: true, alpha: background === null },
|
|
36
|
+
// Aim at the character's head rather than the origin at its feet;
|
|
37
|
+
// r3f points the camera at [0,0,0] otherwise.
|
|
38
|
+
onCreated: ({ camera: cam }) => cam.lookAt(...target), children: [background !== null && _jsx("color", { attach: "background", args: [background] }), _jsx("hemisphereLight", { args: [0xffffff, 0x334455, 1.4] }), _jsx("directionalLight", { position: [2, 4, 3], intensity: 2.2 }), _jsx(Suspense, { fallback: null, children: _jsx(CharacterModel, { ...character }) })] }), fallback] }));
|
|
25
39
|
}
|
|
40
|
+
export default ConvaiCharacter;
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,4 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React entry point.
|
|
3
|
+
*
|
|
4
|
+
* ```tsx
|
|
5
|
+
* import { ConvaiCharacter } from '@convai/embody/react'
|
|
6
|
+
*
|
|
7
|
+
* <Canvas>
|
|
8
|
+
* <ConvaiCharacter apiKey={apiKey} characterId={characterId} />
|
|
9
|
+
* </Canvas>
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* The SDK's own React surface is re-exported so an app can build chat UI
|
|
13
|
+
* without adding a second dependency — or a second connection. `ConvaiWidget`
|
|
14
|
+
* takes a client, and `character.client` is one.
|
|
15
|
+
*
|
|
16
|
+
* Chat UI is DOM and the character is a `<Canvas>` child, so lift the client
|
|
17
|
+
* out rather than rendering the widget from inside the scene:
|
|
18
|
+
*
|
|
19
|
+
* ```tsx
|
|
20
|
+
* const [client, setClient] = useState<ConvaiClient | null>(null)
|
|
21
|
+
* <Canvas>
|
|
22
|
+
* <ConvaiCharacter apiKey={apiKey} characterId={characterId}
|
|
23
|
+
* onReady={(c) => setClient(c.client)} />
|
|
24
|
+
* </Canvas>
|
|
25
|
+
* {client && <ConvaiWidget convaiClient={client} />}
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
1
28
|
export { useCharacter } from './useCharacter.js';
|
|
2
29
|
export type { UseCharacterOptions, UseCharacterResult } from './useCharacter.js';
|
|
3
|
-
export { ConvaiCharacter } from './ConvaiCharacter.js';
|
|
30
|
+
export { ConvaiCharacter, default } from './ConvaiCharacter.js';
|
|
4
31
|
export type { ConvaiCharacterProps } from './ConvaiCharacter.js';
|
|
32
|
+
export { CharacterModel } from './CharacterModel.js';
|
|
33
|
+
export type { CharacterModelProps } from './CharacterModel.js';
|
|
34
|
+
export { useWidgetClient } from './useWidgetClient.js';
|
|
35
|
+
export type { WidgetClient } from './useWidgetClient.js';
|
|
36
|
+
export { ConvaiWidget, useConvaiClient, AudioRenderer, AudioContext } from '@convai/web-sdk/react';
|
|
37
|
+
export { ConvaiClient } from '@convai/web-sdk/core';
|
|
38
|
+
export type { ConvaiConfig } from '@convai/web-sdk/core';
|
package/dist/react/index.js
CHANGED
|
@@ -1,2 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React entry point.
|
|
3
|
+
*
|
|
4
|
+
* ```tsx
|
|
5
|
+
* import { ConvaiCharacter } from '@convai/embody/react'
|
|
6
|
+
*
|
|
7
|
+
* <Canvas>
|
|
8
|
+
* <ConvaiCharacter apiKey={apiKey} characterId={characterId} />
|
|
9
|
+
* </Canvas>
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* The SDK's own React surface is re-exported so an app can build chat UI
|
|
13
|
+
* without adding a second dependency — or a second connection. `ConvaiWidget`
|
|
14
|
+
* takes a client, and `character.client` is one.
|
|
15
|
+
*
|
|
16
|
+
* Chat UI is DOM and the character is a `<Canvas>` child, so lift the client
|
|
17
|
+
* out rather than rendering the widget from inside the scene:
|
|
18
|
+
*
|
|
19
|
+
* ```tsx
|
|
20
|
+
* const [client, setClient] = useState<ConvaiClient | null>(null)
|
|
21
|
+
* <Canvas>
|
|
22
|
+
* <ConvaiCharacter apiKey={apiKey} characterId={characterId}
|
|
23
|
+
* onReady={(c) => setClient(c.client)} />
|
|
24
|
+
* </Canvas>
|
|
25
|
+
* {client && <ConvaiWidget convaiClient={client} />}
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
1
28
|
export { useCharacter } from './useCharacter.js';
|
|
2
|
-
export { ConvaiCharacter } from './ConvaiCharacter.js';
|
|
29
|
+
export { ConvaiCharacter, default } from './ConvaiCharacter.js';
|
|
30
|
+
export { CharacterModel } from './CharacterModel.js';
|
|
31
|
+
export { useWidgetClient } from './useWidgetClient.js';
|
|
32
|
+
// ── Chat / conversation surface, re-exported from @convai/web-sdk/react ────
|
|
33
|
+
export { ConvaiWidget, useConvaiClient, AudioRenderer, AudioContext } from '@convai/web-sdk/react';
|
|
34
|
+
export { ConvaiClient } from '@convai/web-sdk/core';
|
|
@@ -1,11 +1,22 @@
|
|
|
1
|
-
import type { BlendshapeQueue } from '@convai/web-sdk/core';
|
|
1
|
+
import type { BlendshapeQueue, ConvaiClient } from '@convai/web-sdk/core';
|
|
2
2
|
import { type CharacterHandle, type CreateCharacterOptions } from '../character/createCharacter.js';
|
|
3
3
|
export interface UseCharacterOptions extends CreateCharacterOptions {
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Override which turn this body lipsyncs to. Only needed when one client
|
|
6
|
+
* drives several characters; otherwise the character uses its own session.
|
|
7
|
+
*/
|
|
5
8
|
queue?: BlendshapeQueue;
|
|
6
9
|
}
|
|
7
10
|
export interface UseCharacterResult {
|
|
8
11
|
character: CharacterHandle | null;
|
|
12
|
+
/**
|
|
13
|
+
* The connected SDK client, once ready — the same object as
|
|
14
|
+
* `character.client`, lifted out so chat UI can be written without
|
|
15
|
+
* null-checking the body first.
|
|
16
|
+
*/
|
|
17
|
+
client: ConvaiClient | null;
|
|
9
18
|
error: Error | null;
|
|
19
|
+
/** `loading` until body and mind are both up, then `ready` or `error`. */
|
|
20
|
+
status: 'loading' | 'ready' | 'error';
|
|
10
21
|
}
|
|
11
22
|
export declare function useCharacter(opts: UseCharacterOptions): UseCharacterResult;
|
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
* Thin wiring only — see the module doc on `../character/lifecycle` for why
|
|
3
3
|
* the cancel/dispose behaviour lives there and not here. This hook does
|
|
4
4
|
* nothing `createCharacterLifecycle` doesn't already own: it creates a
|
|
5
|
-
* lifecycle per
|
|
6
|
-
* unmount/re-run, and forwards the
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* lifecycle per identity, feeds it `createCharacter`, cancels it on
|
|
6
|
+
* unmount/re-run, and forwards the state React needs. Ticking and camera
|
|
7
|
+
* assignment are one-line pass-throughs to `CharacterHandle.update`/`.camera`.
|
|
8
|
+
*
|
|
9
|
+
* The lipsync queue is NOT a prop: the character owns its own connection, so
|
|
10
|
+
* it already knows which frames are its own. Apps driving several bodies from
|
|
11
|
+
* one client pass `queue` to take that decision back.
|
|
9
12
|
*/
|
|
10
13
|
import { useEffect, useRef, useState } from 'react';
|
|
11
14
|
import { useFrame, useThree } from '@react-three/fiber';
|
|
@@ -32,14 +35,20 @@ export function useCharacter(opts) {
|
|
|
32
35
|
setError(null);
|
|
33
36
|
}, (c) => setCharacter(c), (e) => setError(e));
|
|
34
37
|
return () => lifecycle.cancel();
|
|
35
|
-
// Re-create
|
|
36
|
-
//
|
|
38
|
+
// Re-create when the identity of the character OR the credentials change
|
|
39
|
+
// — a new key/token means a different session, and reusing the old one
|
|
40
|
+
// would leave the body talking over a connection the app has revoked.
|
|
37
41
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
38
|
-
}, [opts.characterId]);
|
|
42
|
+
}, [opts.characterId, opts.apiKey, opts.authToken, opts.token, opts.baseUrl]);
|
|
39
43
|
useEffect(() => {
|
|
40
44
|
if (character)
|
|
41
45
|
character.camera = camera;
|
|
42
46
|
}, [character, camera]);
|
|
43
47
|
useFrame((_, delta) => character?.update(delta, queueRef.current));
|
|
44
|
-
return {
|
|
48
|
+
return {
|
|
49
|
+
character,
|
|
50
|
+
client: character?.client ?? null,
|
|
51
|
+
error,
|
|
52
|
+
status: error ? 'error' : character ? 'ready' : 'loading',
|
|
53
|
+
};
|
|
45
54
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ConvaiClient } from '@convai/web-sdk/core';
|
|
2
|
+
/** The shape `<ConvaiWidget convaiClient={…} />` requires. */
|
|
3
|
+
export type WidgetClient = ConvaiClient & {
|
|
4
|
+
activity: string;
|
|
5
|
+
isAudioMuted: boolean;
|
|
6
|
+
isVideoEnabled: boolean;
|
|
7
|
+
isScreenShareActive: boolean;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* ```tsx
|
|
11
|
+
* const { client } = useCharacter({ apiKey, characterId })
|
|
12
|
+
* const widgetClient = useWidgetClient(client)
|
|
13
|
+
* return widgetClient && <ConvaiWidget convaiClient={widgetClient} />
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* Returns `null` until the character's connection exists.
|
|
17
|
+
*/
|
|
18
|
+
export declare function useWidgetClient(client: ConvaiClient | null): WidgetClient | null;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Makes embody's client usable by the SDK's `<ConvaiWidget/>`.
|
|
3
|
+
*
|
|
4
|
+
* The widget wants `IConvaiClient` plus four pieces of reactive UI state
|
|
5
|
+
* (`activity`, `isAudioMuted`, `isVideoEnabled`, `isScreenShareActive`). The
|
|
6
|
+
* SDK supplies those through `useConvaiClient`, but that hook CONSTRUCTS its
|
|
7
|
+
* own client — using it next to embody would open a second connection, which
|
|
8
|
+
* is the whole thing embody exists to avoid.
|
|
9
|
+
*
|
|
10
|
+
* So this hook does the same composition against a client that already
|
|
11
|
+
* exists. It mirrors `useConvaiClient`'s subscriptions exactly: `stateChange`
|
|
12
|
+
* for activity, and the three control emitters for the toggles.
|
|
13
|
+
*/
|
|
14
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
15
|
+
/** Priority order matches the SDK's: speaking > listening > thinking > connected. */
|
|
16
|
+
function activityOf(state) {
|
|
17
|
+
if (state.isSpeaking)
|
|
18
|
+
return 'Speaking';
|
|
19
|
+
if (state.isListening)
|
|
20
|
+
return 'Listening';
|
|
21
|
+
if (state.isThinking)
|
|
22
|
+
return 'Thinking';
|
|
23
|
+
if (state.isConnected)
|
|
24
|
+
return 'Connected';
|
|
25
|
+
return 'Idle';
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* ```tsx
|
|
29
|
+
* const { client } = useCharacter({ apiKey, characterId })
|
|
30
|
+
* const widgetClient = useWidgetClient(client)
|
|
31
|
+
* return widgetClient && <ConvaiWidget convaiClient={widgetClient} />
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* Returns `null` until the character's connection exists.
|
|
35
|
+
*/
|
|
36
|
+
export function useWidgetClient(client) {
|
|
37
|
+
const c = client;
|
|
38
|
+
const [activity, setActivity] = useState('Idle');
|
|
39
|
+
const [isAudioMuted, setAudioMuted] = useState(false);
|
|
40
|
+
const [isVideoEnabled, setVideoEnabled] = useState(false);
|
|
41
|
+
const [isScreenShareActive, setScreenShareActive] = useState(false);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (!c)
|
|
44
|
+
return;
|
|
45
|
+
// Seed from the current values: the client may already be connected and
|
|
46
|
+
// mid-turn by the time this hook mounts, and waiting for the next event
|
|
47
|
+
// would show a stale "Idle" until the user did something.
|
|
48
|
+
setActivity(activityOf(c.state));
|
|
49
|
+
setAudioMuted(c.audioControls.isAudioMuted);
|
|
50
|
+
setVideoEnabled(c.videoControls.isVideoEnabled);
|
|
51
|
+
setScreenShareActive(c.screenShareControls.isScreenShareActive);
|
|
52
|
+
const unsubs = [
|
|
53
|
+
c.on('stateChange', (s) => setActivity(activityOf(s))),
|
|
54
|
+
c.audioControls.on('audioStateChange', (s) => {
|
|
55
|
+
if (s.isAudioMuted !== undefined)
|
|
56
|
+
setAudioMuted(s.isAudioMuted);
|
|
57
|
+
}),
|
|
58
|
+
c.videoControls.on('videoStateChange', (s) => {
|
|
59
|
+
if (s.isVideoEnabled !== undefined)
|
|
60
|
+
setVideoEnabled(s.isVideoEnabled);
|
|
61
|
+
}),
|
|
62
|
+
c.screenShareControls.on('screenShareStateChange', (s) => {
|
|
63
|
+
if (s.isScreenShareActive !== undefined)
|
|
64
|
+
setScreenShareActive(s.isScreenShareActive);
|
|
65
|
+
}),
|
|
66
|
+
];
|
|
67
|
+
return () => unsubs.forEach((u) => u());
|
|
68
|
+
}, [c]);
|
|
69
|
+
// A view over the client, never a mutation of it: the same client drives
|
|
70
|
+
// the 3D character and anything else the app holds, and stamping React
|
|
71
|
+
// state onto it would make those views disagree.
|
|
72
|
+
//
|
|
73
|
+
// A Proxy rather than `Object.create` + assign, because the widget calls
|
|
74
|
+
// methods on this object. Under `Object.create` any `this.x = y` inside a
|
|
75
|
+
// method writes to the derived object instead of the client, silently
|
|
76
|
+
// losing the mutation. Here reads, writes and `this` all reach the real
|
|
77
|
+
// client; only the four UI fields are intercepted.
|
|
78
|
+
//
|
|
79
|
+
// Re-created when those four change so a memoised widget re-renders, and
|
|
80
|
+
// stable between changes so it does not re-render for nothing.
|
|
81
|
+
return useMemo(() => {
|
|
82
|
+
if (!client)
|
|
83
|
+
return null;
|
|
84
|
+
const overrides = {
|
|
85
|
+
activity,
|
|
86
|
+
isAudioMuted,
|
|
87
|
+
isVideoEnabled,
|
|
88
|
+
isScreenShareActive,
|
|
89
|
+
};
|
|
90
|
+
return new Proxy(client, {
|
|
91
|
+
get(target, prop, receiver) {
|
|
92
|
+
if (typeof prop === 'string' && prop in overrides)
|
|
93
|
+
return overrides[prop];
|
|
94
|
+
// Receiver is the TARGET, not the proxy, so getters and methods run
|
|
95
|
+
// with `this` bound to the real client.
|
|
96
|
+
const value = Reflect.get(target, prop, target);
|
|
97
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
98
|
+
},
|
|
99
|
+
set(target, prop, value) {
|
|
100
|
+
return Reflect.set(target, prop, value, target);
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
104
|
+
}, [client, activity, isAudioMuted, isVideoEnabled, isScreenShareActive]);
|
|
105
|
+
}
|
|
@@ -26,6 +26,15 @@ export interface CharacterBundle {
|
|
|
26
26
|
release?: number;
|
|
27
27
|
representations: Representation[];
|
|
28
28
|
thumbnail?: string;
|
|
29
|
+
/**
|
|
30
|
+
* `true` when this is a stand-in body, not the character's own: the
|
|
31
|
+
* character exists and is viewable, but has no finished embodiment, so the
|
|
32
|
+
* backend served the configured default for its gender.
|
|
33
|
+
*
|
|
34
|
+
* Worth surfacing. A placeholder that nobody notices is a character
|
|
35
|
+
* shipping as visibly the wrong person.
|
|
36
|
+
*/
|
|
37
|
+
fallback?: boolean;
|
|
29
38
|
}
|
|
30
39
|
export interface CharacterResolver {
|
|
31
40
|
resolve(characterId: string): Promise<CharacterBundle>;
|
|
@@ -48,9 +57,13 @@ export declare const DEFAULT_BASE_URL = "https://api-stg.convai.com";
|
|
|
48
57
|
export interface HttpResolverOptions {
|
|
49
58
|
/** Base url of the embodiment backend. Defaults to the release branch's host. */
|
|
50
59
|
baseUrl?: string;
|
|
51
|
-
/**
|
|
60
|
+
/** CONVAI-API-KEY. Supply this or `authToken`. */
|
|
52
61
|
apiKey?: string;
|
|
62
|
+
/** Convai auth token. Takes precedence over `apiKey` when both are set. */
|
|
63
|
+
authToken?: string;
|
|
53
64
|
}
|
|
65
|
+
/** Shared so `createCharacter` and the resolver cannot drift apart. */
|
|
66
|
+
export declare const MISSING_CREDENTIAL: string;
|
|
54
67
|
export declare function httpResolver(opts?: HttpResolverOptions): CharacterResolver;
|
|
55
68
|
/** Pick the representation for a rig profile this build of embody understands. */
|
|
56
69
|
export declare function pickRepresentation(bundle: CharacterBundle, supported: string[]): Representation;
|