@convai/embody 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +59 -7
- package/dist/react/useCharacter.d.ts +6 -0
- package/dist/react/useCharacter.js +16 -4
- package/dist/react/useWidgetClient.d.ts +26 -0
- package/dist/react/useWidgetClient.js +44 -0
- package/dist/resolve/resolver.d.ts +1 -1
- package/dist/resolve/resolver.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# @convai/embody
|
|
2
2
|
|
|
3
|
+
## 0.4.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 532c49e: Swapping the body under a fixed character id now reaches the screen.
|
|
8
|
+
|
|
9
|
+
`useCharacter` rebuilt the character only when the character id, credentials
|
|
10
|
+
or base url changed. The **resolver** was not among them, and the effect read
|
|
11
|
+
its options from a ref — so an authoring tool that holds one avatar id and
|
|
12
|
+
swaps the block underneath (preview release 3, then release 4) got nothing:
|
|
13
|
+
the old GLB stayed on the canvas and it read as a stubborn cache. Nothing was
|
|
14
|
+
cached. The loader was never asked for the new url.
|
|
15
|
+
|
|
16
|
+
The resolver decides WHICH body a character id resolves to, so it belongs in
|
|
17
|
+
the rebuild set, and now is.
|
|
18
|
+
|
|
19
|
+
This makes the resolver's identity part of the contract: **memoize it**, and
|
|
20
|
+
change it exactly when the body should change. A resolver rebuilt inline on
|
|
21
|
+
every render would rebuild the character on every render. The README's new
|
|
22
|
+
"Swapping the body under a fixed character id" section shows the shape.
|
|
23
|
+
|
|
24
|
+
Also documented, unchanged in behaviour: the `orbit` prop (shipped in 0.4.0
|
|
25
|
+
and previously absent from the README), which backend each npm tag talks to,
|
|
26
|
+
and the correct release-branch names — the table named three branches that do
|
|
27
|
+
not exist.
|
|
28
|
+
|
|
29
|
+
## 0.4.1
|
|
30
|
+
|
|
31
|
+
### Patch Changes
|
|
32
|
+
|
|
33
|
+
- 62deba9: `useWidgetClient` hides the server's `user-transcription` echo of a typed message, so `<ConvaiWidget/>` shows each sent message once instead of twice.
|
|
34
|
+
|
|
3
35
|
## 0.4.0
|
|
4
36
|
|
|
5
37
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Give a Convai character a body.**
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/@convai/embody)
|
|
6
6
|
[](https://www.typescriptlang.org/)
|
|
7
7
|
[](./LICENSE)
|
|
8
8
|
|
|
@@ -12,13 +12,21 @@ lipsync, gaze, breathing and blink. Layered on
|
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
-
> **Branch: `
|
|
16
|
-
>
|
|
15
|
+
> **Branch: `main`** — this branch defaults to the production backend and
|
|
16
|
+
> publishes the `latest` tag. `stg` targets staging, which has its own
|
|
17
|
+
> character database.
|
|
17
18
|
>
|
|
18
19
|
> **Status: early release.** The front door — a credential plus a
|
|
19
20
|
> `characterId` — is settled; what is still moving is listed in
|
|
20
21
|
> [Roadmap](#roadmap).
|
|
21
22
|
|
|
23
|
+
> **Which backend a build talks to is fixed at publish time.** The `latest`
|
|
24
|
+
> and `beta` tags point at `https://api.convai.com`; the `stg` tag points at
|
|
25
|
+
> `https://api-stg.convai.com`. Character ids are per-environment and not
|
|
26
|
+
> interchangeable — a staging id against production returns *"Character not
|
|
27
|
+
> found or doesn't belong to user"*, which reads like a permissions problem
|
|
28
|
+
> and is a wrong-host one. Override with `baseUrl` only if you know why.
|
|
29
|
+
|
|
22
30
|
## Install
|
|
23
31
|
|
|
24
32
|
```bash
|
|
@@ -58,6 +66,32 @@ For anything user-facing, mint a short-lived auth token server-side and pass
|
|
|
58
66
|
<ConvaiCharacter token={token} characterId={characterId} />
|
|
59
67
|
```
|
|
60
68
|
|
|
69
|
+
### Moving the camera
|
|
70
|
+
|
|
71
|
+
Drag to turn the character, scroll to move in and out. On by default — a 3D
|
|
72
|
+
character you cannot walk around is a picture of one.
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
<ConvaiCharacter apiKey={apiKey} characterId={characterId} orbit={false} />
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Pass an object to tune the limits instead of turning it off:
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
<ConvaiCharacter
|
|
82
|
+
apiKey={apiKey}
|
|
83
|
+
characterId={characterId}
|
|
84
|
+
orbit={{ maxDistance: 6, autoRotate: true, autoRotateSpeed: 0.8 }}
|
|
85
|
+
/>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`minDistance`, `maxDistance`, `minPolarAngle`, `maxPolarAngle`, `enablePan`,
|
|
89
|
+
`enableZoom`, `autoRotate`, `autoRotateSpeed`. The defaults are tuned for one
|
|
90
|
+
subject — a standing human — not for a general scene viewer: panning is off
|
|
91
|
+
because it only ever slides the character out of frame, and the vertical sweep
|
|
92
|
+
stops just below eye level because orbiting under the floor shows the inside
|
|
93
|
+
of a head.
|
|
94
|
+
|
|
61
95
|
### In a scene you already have
|
|
62
96
|
|
|
63
97
|
`ConvaiCharacter` brings its own canvas. When the character should join a
|
|
@@ -174,6 +208,24 @@ The defaults cover the normal case; each of these exists for a specific one.
|
|
|
174
208
|
| `baseUrl` | A different backend environment. Defaults per release channel. |
|
|
175
209
|
| `queue` on `update()` | One client driving several bodies, when you are deciding which owns the current turn. |
|
|
176
210
|
|
|
211
|
+
### Swapping the body under a fixed character id
|
|
212
|
+
|
|
213
|
+
`resolver` is how an authoring tool previews a body that is not published — or
|
|
214
|
+
not even saved. Its **identity is part of the rebuild contract**: memoize it,
|
|
215
|
+
and change it exactly when the body should change.
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
const resolver = useMemo(
|
|
219
|
+
() => ({ async resolve() { return bundle } }),
|
|
220
|
+
[bundle],
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
<ConvaiCharacter characterId={avatarId} resolver={resolver} connect={false} />
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Changing the resolver rebuilds the character; keeping it stable does not. A
|
|
227
|
+
resolver rebuilt inline on every render would reload the GLB on every render.
|
|
228
|
+
|
|
177
229
|
## Docs
|
|
178
230
|
|
|
179
231
|
Design, plan and decision history live in [`docs/`](./docs):
|
|
@@ -194,9 +246,9 @@ The backend host is a source constant, `DEFAULT_BASE_URL` in
|
|
|
194
246
|
|
|
195
247
|
| Branch | Host | npm tag |
|
|
196
248
|
|---|---|---|
|
|
197
|
-
| `
|
|
198
|
-
| `beta
|
|
199
|
-
| `stg
|
|
249
|
+
| `main` / `prod` | `https://api.convai.com` | `latest` |
|
|
250
|
+
| `beta` | `https://api.convai.com` | `beta` |
|
|
251
|
+
| `stg` | `https://api-stg.convai.com` | `stg` |
|
|
200
252
|
|
|
201
253
|
Beta points at **production** — it is the first line of testing against real
|
|
202
254
|
infrastructure, not a staging channel. Only `stg-release` targets staging,
|
|
@@ -217,7 +269,7 @@ pnpm check:hosts # auto-detects the current branch
|
|
|
217
269
|
|
|
218
270
|
```bash
|
|
219
271
|
pnpm install
|
|
220
|
-
pnpm test #
|
|
272
|
+
pnpm test # 220 tests
|
|
221
273
|
pnpm typecheck
|
|
222
274
|
pnpm check:tests # every src/ module has a named test — see docs/testing-spec.md
|
|
223
275
|
pnpm build # emits dist/ with .d.ts
|
|
@@ -7,6 +7,12 @@ export interface UseCharacterOptions extends CreateCharacterOptions {
|
|
|
7
7
|
*/
|
|
8
8
|
queue?: BlendshapeQueue;
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* NOTE ON `resolver` (inherited from CreateCharacterOptions): its identity is
|
|
12
|
+
* part of this hook's dependency set. Memoize it, and change it precisely
|
|
13
|
+
* when the body should change — an inline `{ resolve() {...} }` is a new
|
|
14
|
+
* object every render and would rebuild the character every render.
|
|
15
|
+
*/
|
|
10
16
|
export interface UseCharacterResult {
|
|
11
17
|
character: CharacterHandle | null;
|
|
12
18
|
/**
|
|
@@ -35,11 +35,23 @@ export function useCharacter(opts) {
|
|
|
35
35
|
setError(null);
|
|
36
36
|
}, (c) => setCharacter(c), (e) => setError(e));
|
|
37
37
|
return () => lifecycle.cancel();
|
|
38
|
-
// Re-create when the identity of the character
|
|
39
|
-
//
|
|
40
|
-
//
|
|
38
|
+
// Re-create when the identity of the character, the credentials, OR the
|
|
39
|
+
// resolver change.
|
|
40
|
+
//
|
|
41
|
+
// The resolver belongs here because it decides WHICH BODY this character
|
|
42
|
+
// id resolves to. An authoring tool holds the character id fixed and
|
|
43
|
+
// swaps the block underneath — preview release 3, then release 4 — and
|
|
44
|
+
// without this dependency none of that reached the screen: the effect
|
|
45
|
+
// never re-ran, so the old GLB stayed on the canvas and the change looked
|
|
46
|
+
// like a stubborn cache. Nothing was cached; the loader was simply never
|
|
47
|
+
// asked for the new url.
|
|
48
|
+
//
|
|
49
|
+
// This makes the resolver's IDENTITY part of the contract: pass a stable
|
|
50
|
+
// one (`useMemo`) and change it exactly when the body should change. A
|
|
51
|
+
// resolver rebuilt inline on every render would rebuild the character on
|
|
52
|
+
// every render.
|
|
41
53
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
42
|
-
}, [opts.characterId, opts.apiKey, opts.authToken, opts.token, opts.baseUrl]);
|
|
54
|
+
}, [opts.characterId, opts.apiKey, opts.authToken, opts.token, opts.baseUrl, opts.resolver]);
|
|
43
55
|
useEffect(() => {
|
|
44
56
|
if (character)
|
|
45
57
|
character.camera = camera;
|
|
@@ -6,6 +6,31 @@ export type WidgetClient = ConvaiClient & {
|
|
|
6
6
|
isVideoEnabled: boolean;
|
|
7
7
|
isScreenShareActive: boolean;
|
|
8
8
|
};
|
|
9
|
+
type ChatMessage = ConvaiClient['chatMessages'][number];
|
|
10
|
+
/**
|
|
11
|
+
* How long after a typed message its server echo can still arrive. Measured
|
|
12
|
+
* at ~2 s on staging; the window is generous because a slow turn is still an
|
|
13
|
+
* echo, while the same words said aloud half a minute later are a new message.
|
|
14
|
+
*/
|
|
15
|
+
export declare const TYPED_ECHO_WINDOW_MS = 30000;
|
|
16
|
+
/**
|
|
17
|
+
* The transcript without the server's echo of typed messages.
|
|
18
|
+
*
|
|
19
|
+
* `sendUserTextMessage` appends the text locally as `user-llm-text`, and the
|
|
20
|
+
* SDK drops the server's `user-llm-text` echo so the message appears once.
|
|
21
|
+
* But the server ALSO answers typed text with a final `user-transcription`
|
|
22
|
+
* carrying the same words — with no `logicalTurnId` to correlate on — which
|
|
23
|
+
* the SDK stores as a new message and `<ConvaiWidget/>` renders as a second
|
|
24
|
+
* "You" bubble. Every typed message showed twice; every reply once. Verified
|
|
25
|
+
* on web-sdk 1.8.0 and 1.9.0-beta.3, which filter identically.
|
|
26
|
+
*
|
|
27
|
+
* A transcription is an echo when an unmatched typed message with the same
|
|
28
|
+
* trimmed text precedes it within the window; each typed message cancels at
|
|
29
|
+
* most one. Spoken turns never produce a `user-llm-text`, so a real
|
|
30
|
+
* transcription is never dropped. An echo that streams in chunks can show
|
|
31
|
+
* its partial text until the final chunk matches — a flicker, not a duplicate.
|
|
32
|
+
*/
|
|
33
|
+
export declare function withoutTypedEchoes(messages: readonly ChatMessage[]): ChatMessage[];
|
|
9
34
|
/**
|
|
10
35
|
* ```tsx
|
|
11
36
|
* const { client } = useCharacter({ apiKey, characterId })
|
|
@@ -16,3 +41,4 @@ export type WidgetClient = ConvaiClient & {
|
|
|
16
41
|
* Returns `null` until the character's connection exists.
|
|
17
42
|
*/
|
|
18
43
|
export declare function useWidgetClient(client: ConvaiClient | null): WidgetClient | null;
|
|
44
|
+
export {};
|
|
@@ -12,6 +12,47 @@
|
|
|
12
12
|
* for activity, and the three control emitters for the toggles.
|
|
13
13
|
*/
|
|
14
14
|
import { useEffect, useMemo, useState } from 'react';
|
|
15
|
+
/**
|
|
16
|
+
* How long after a typed message its server echo can still arrive. Measured
|
|
17
|
+
* at ~2 s on staging; the window is generous because a slow turn is still an
|
|
18
|
+
* echo, while the same words said aloud half a minute later are a new message.
|
|
19
|
+
*/
|
|
20
|
+
export const TYPED_ECHO_WINDOW_MS = 30_000;
|
|
21
|
+
/**
|
|
22
|
+
* The transcript without the server's echo of typed messages.
|
|
23
|
+
*
|
|
24
|
+
* `sendUserTextMessage` appends the text locally as `user-llm-text`, and the
|
|
25
|
+
* SDK drops the server's `user-llm-text` echo so the message appears once.
|
|
26
|
+
* But the server ALSO answers typed text with a final `user-transcription`
|
|
27
|
+
* carrying the same words — with no `logicalTurnId` to correlate on — which
|
|
28
|
+
* the SDK stores as a new message and `<ConvaiWidget/>` renders as a second
|
|
29
|
+
* "You" bubble. Every typed message showed twice; every reply once. Verified
|
|
30
|
+
* on web-sdk 1.8.0 and 1.9.0-beta.3, which filter identically.
|
|
31
|
+
*
|
|
32
|
+
* A transcription is an echo when an unmatched typed message with the same
|
|
33
|
+
* trimmed text precedes it within the window; each typed message cancels at
|
|
34
|
+
* most one. Spoken turns never produce a `user-llm-text`, so a real
|
|
35
|
+
* transcription is never dropped. An echo that streams in chunks can show
|
|
36
|
+
* its partial text until the final chunk matches — a flicker, not a duplicate.
|
|
37
|
+
*/
|
|
38
|
+
export function withoutTypedEchoes(messages) {
|
|
39
|
+
const pending = [];
|
|
40
|
+
return messages.filter((m) => {
|
|
41
|
+
const content = m.content.trim();
|
|
42
|
+
const at = Date.parse(m.timestamp);
|
|
43
|
+
if (m.type === 'user-llm-text') {
|
|
44
|
+
pending.push({ content, at });
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
if (m.type !== 'user-transcription')
|
|
48
|
+
return true;
|
|
49
|
+
const i = pending.findIndex((t) => t.content === content && at - t.at >= 0 && at - t.at <= TYPED_ECHO_WINDOW_MS);
|
|
50
|
+
if (i === -1)
|
|
51
|
+
return true;
|
|
52
|
+
pending.splice(i, 1);
|
|
53
|
+
return false;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
15
56
|
/** Priority order matches the SDK's: speaking > listening > thinking > connected. */
|
|
16
57
|
function activityOf(state) {
|
|
17
58
|
if (state.isSpeaking)
|
|
@@ -89,6 +130,9 @@ export function useWidgetClient(client) {
|
|
|
89
130
|
};
|
|
90
131
|
return new Proxy(client, {
|
|
91
132
|
get(target, prop, receiver) {
|
|
133
|
+
// The widget renders straight from this; see withoutTypedEchoes.
|
|
134
|
+
if (prop === 'chatMessages')
|
|
135
|
+
return withoutTypedEchoes(Reflect.get(target, prop, target));
|
|
92
136
|
if (typeof prop === 'string' && prop in overrides)
|
|
93
137
|
return overrides[prop];
|
|
94
138
|
// Receiver is the TARGET, not the proxy, so getters and methods run
|
|
@@ -53,7 +53,7 @@ export interface CharacterResolver {
|
|
|
53
53
|
* not pass `baseUrl` to the wrong backend, which is why it is checked rather
|
|
54
54
|
* than trusted.
|
|
55
55
|
*/
|
|
56
|
-
export declare const DEFAULT_BASE_URL = "https://api
|
|
56
|
+
export declare const DEFAULT_BASE_URL = "https://api.convai.com";
|
|
57
57
|
export interface HttpResolverOptions {
|
|
58
58
|
/** Base url of the embodiment backend. Defaults to the release branch's host. */
|
|
59
59
|
baseUrl?: string;
|
package/dist/resolve/resolver.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* not pass `baseUrl` to the wrong backend, which is why it is checked rather
|
|
21
21
|
* than trusted.
|
|
22
22
|
*/
|
|
23
|
-
export const DEFAULT_BASE_URL = 'https://api
|
|
23
|
+
export const DEFAULT_BASE_URL = 'https://api.convai.com';
|
|
24
24
|
/** Shared so `createCharacter` and the resolver cannot drift apart. */
|
|
25
25
|
export const MISSING_CREDENTIAL = '@convai/embody: a credential is required — pass `token` (recommended for ' +
|
|
26
26
|
'browsers) or `apiKey`. There is no anonymous access to Convai.';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@convai/embody",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Give a Convai character a body: resolve 3D characters by characterId and drive them with lipsync and animation. Layered on @convai/web-sdk.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Convai <support@convai.com>",
|