@convai/embody 0.4.2 → 0.6.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 +13 -19
- package/README.md +50 -68
- package/dist/character/createCharacter.d.ts +6 -0
- package/dist/character/createCharacter.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/react/index.d.ts +8 -5
- package/dist/react/index.js +10 -6
- package/dist/react/useCharacter.d.ts +0 -6
- package/dist/react/useCharacter.js +11 -16
- package/dist/react/useWidgetClient.js +1 -1
- package/dist/resolve/resolver.d.ts +1 -1
- package/dist/resolve/resolver.js +1 -1
- package/dist/speech/correctiveMap.d.ts +75 -0
- package/dist/speech/correctiveMap.js +125 -0
- package/dist/speech/lipsync.d.ts +10 -1
- package/dist/speech/lipsync.js +13 -2
- package/package.json +7 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,30 +1,24 @@
|
|
|
1
1
|
# @convai/embody
|
|
2
2
|
|
|
3
|
-
## 0.
|
|
3
|
+
## 0.6.0
|
|
4
4
|
|
|
5
|
-
###
|
|
5
|
+
### Minor Changes
|
|
6
6
|
|
|
7
|
-
-
|
|
7
|
+
- b9b1e4c: **embody is the 3D character only.** `@convai/embody/react` no longer re-exports `ConvaiWidget`, `AudioRenderer` or `AudioContext`: `@convai/web-sdk` 2.0 moved its chat UI to `@convai/web-widget`, so import them from `@convai/web-widget/react` and install it next to embody. `useWidgetClient` stays: hand its result to the widget so the chat and the character share one connection.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
cached. The loader was never asked for the new url.
|
|
9
|
+
```diff
|
|
10
|
+
- import { ConvaiCharacter, ConvaiWidget, useWidgetClient } from '@convai/embody/react'
|
|
11
|
+
+ import { ConvaiCharacter, useWidgetClient } from '@convai/embody/react'
|
|
12
|
+
+ import { ConvaiWidget } from '@convai/web-widget/react'
|
|
13
|
+
```
|
|
15
14
|
|
|
16
|
-
The
|
|
17
|
-
the rebuild set, and now is.
|
|
15
|
+
The `@convai/web-sdk` peer range now also admits the 2.0 prereleases (`>=1.8.0-beta.4 || >=2.0.0-beta.0`); `>=1.8.0-beta.4` alone made npm refuse `2.0.0-beta.x` with ERESOLVE.
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"Swapping the body under a fixed character id" section shows the shape.
|
|
17
|
+
## 0.5.0
|
|
18
|
+
|
|
19
|
+
### Minor Changes
|
|
23
20
|
|
|
24
|
-
|
|
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.
|
|
21
|
+
- 249a96f: Optional corrective map for one-morph-per-control bodies. A table fitted from the body's MetaHuman DNA re-weights each lipsync frame toward the full rig (PSD correctives, joint arcs). Pass `correctiveMap` (from `compileCorrectiveMap`) to `createCharacter` or the React components; it is off by default and swaps live through the prop or `character.lipsync.correctiveMap`.
|
|
28
22
|
|
|
29
23
|
## 0.4.1
|
|
30
24
|
|
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,31 +12,28 @@ lipsync, gaze, breathing and blink. Layered on
|
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
-
> **Branch: `
|
|
16
|
-
>
|
|
17
|
-
> character database.
|
|
15
|
+
> **Branch: `stg`** — this branch defaults to the staging backend, which has
|
|
16
|
+
> its own character database. Use `main` or `prod` for production.
|
|
18
17
|
>
|
|
19
18
|
> **Status: early release.** The front door — a credential plus a
|
|
20
19
|
> `characterId` — is settled; what is still moving is listed in
|
|
21
20
|
> [Roadmap](#roadmap).
|
|
22
21
|
|
|
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
|
-
|
|
30
22
|
## Install
|
|
31
23
|
|
|
32
24
|
```bash
|
|
33
|
-
pnpm add @convai/embody three
|
|
25
|
+
pnpm add @convai/embody @convai/web-sdk three
|
|
34
26
|
# React consumers also need:
|
|
35
27
|
pnpm add @react-three/fiber react
|
|
36
28
|
```
|
|
37
29
|
|
|
38
|
-
|
|
39
|
-
`@react-three/fiber` are optional peers — the
|
|
30
|
+
`@convai/web-sdk` and `three` are peer dependencies. `react` and
|
|
31
|
+
`@react-three/fiber` are optional peers — the root `@convai/embody` entry
|
|
32
|
+
point needs neither. Node.js 18 or later.
|
|
33
|
+
|
|
34
|
+
A character renders only if its avatar has a published web body, or a default
|
|
35
|
+
body applies to it. Otherwise loading fails with *"Character has no 3D
|
|
36
|
+
embodiment configured"* — a data problem, not a code one.
|
|
40
37
|
|
|
41
38
|
## Quick start
|
|
42
39
|
|
|
@@ -66,32 +63,6 @@ For anything user-facing, mint a short-lived auth token server-side and pass
|
|
|
66
63
|
<ConvaiCharacter token={token} characterId={characterId} />
|
|
67
64
|
```
|
|
68
65
|
|
|
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
|
-
|
|
95
66
|
### In a scene you already have
|
|
96
67
|
|
|
97
68
|
`ConvaiCharacter` brings its own canvas. When the character should join a
|
|
@@ -114,9 +85,8 @@ a second one. Lift it out with `onReady`; chat UI is DOM, so it lives outside
|
|
|
114
85
|
the canvas:
|
|
115
86
|
|
|
116
87
|
```tsx
|
|
117
|
-
import {
|
|
118
|
-
|
|
119
|
-
} from '@convai/embody/react'
|
|
88
|
+
import { ConvaiCharacter, useWidgetClient, type ConvaiClient } from '@convai/embody/react'
|
|
89
|
+
import { ConvaiWidget } from '@convai/web-widget/react'
|
|
120
90
|
|
|
121
91
|
function App() {
|
|
122
92
|
const [client, setClient] = useState<ConvaiClient | null>(null)
|
|
@@ -134,8 +104,9 @@ function App() {
|
|
|
134
104
|
}
|
|
135
105
|
```
|
|
136
106
|
|
|
137
|
-
|
|
138
|
-
`@convai/
|
|
107
|
+
embody is the 3D character only; the chat panel is
|
|
108
|
+
[`@convai/web-widget`](https://www.npmjs.com/package/@convai/web-widget)
|
|
109
|
+
(`npm install @convai/web-widget`). `useWidgetClient` adapts
|
|
139
110
|
embody's client to the widget's props — the SDK's own `useConvaiClient` would
|
|
140
111
|
*construct a second client*, and so open a second connection, which is exactly
|
|
141
112
|
what embody exists to avoid.
|
|
@@ -207,28 +178,33 @@ The defaults cover the normal case; each of these exists for a specific one.
|
|
|
207
178
|
| `session` | Extra SDK options (voice, memory, transport, `endUserId`). Credentials and `characterId` are not repeated here. |
|
|
208
179
|
| `baseUrl` | A different backend environment. Defaults per release channel. |
|
|
209
180
|
| `queue` on `update()` | One client driving several bodies, when you are deciding which owns the current turn. |
|
|
181
|
+
| `correctiveMap` | A table fitted from the body's MetaHuman DNA (`compileCorrectiveMap(json)`) that re-weights each lipsync frame toward what the full rig shows: PSD correctives and joint arcs that one-morph-per-control bodies lack. Off by default. Changing the prop, or `character.lipsync.correctiveMap`, swaps it live. In `pnpm dev`, the demo lists `*.map.json` tables from `../open-rig-dna/maps` (or `$EMBODY_CORRECTIVE_MAPS`) for A/B: **C** toggles, **F** frames the face. |
|
|
210
182
|
|
|
211
|
-
|
|
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.
|
|
183
|
+
## Documentation
|
|
228
184
|
|
|
229
|
-
|
|
185
|
+
**User documentation** is published on docs.convai.com under *Web Plugins →
|
|
186
|
+
Convai Embody*. Its source is [`docs/site/`](./docs/site) in this repository:
|
|
230
187
|
|
|
231
|
-
|
|
188
|
+
| | |
|
|
189
|
+
|---|---|
|
|
190
|
+
| [Add a 3D character to a React app](./docs/site/convai-embody/add-a-3d-character-to-a-react-app.md) | install, render, verify, troubleshoot |
|
|
191
|
+
| [Add a 3D character to a three.js scene](./docs/site/convai-embody/add-a-3d-character-to-a-three-js-scene.md) | the framework-free API |
|
|
192
|
+
| [Add a chat widget to a character](./docs/site/convai-embody/add-a-chat-widget-to-a-character.md) | one connection for body and chat |
|
|
193
|
+
| [React API reference](./docs/site/convai-embody/react-api-reference.md) | every prop and default |
|
|
194
|
+
| [createCharacter reference](./docs/site/convai-embody/createcharacter-reference.md) | every option, the handle, the errors |
|
|
195
|
+
| [How Convai Embody works](./docs/site/convai-embody/how-convai-embody-works.md) | resolution, the frame pipeline, environments |
|
|
196
|
+
|
|
197
|
+
Edit those pages here, not in the documentation repository. **Docs are part of every change:** CI runs the same sync into a throwaway checkout of Convai-Documentation and lints every page with that repository's `check_page.py`, so a page that would fail the docs gate fails the PR. Section path, version variable and sidebar placement live in [`docs/site/docs.config.json`](./docs/site/docs.config.json); page order, including nested groups, in [`docs/site/sidebar.json`](./docs/site/sidebar.json). Every publish to
|
|
198
|
+
npm `latest` runs [`docs-sync.yml`](./.github/workflows/docs-sync.yml), which
|
|
199
|
+
opens a pull request against `Conv-AI/Convai-Documentation` (`staging`) with
|
|
200
|
+
the pages, their sidebar entries, `last_reviewed`, and the `embody_version`
|
|
201
|
+
GitBook variable. Merging that pull request publishes to the GitBook **Staging**
|
|
202
|
+
Space for review; the pages reach docs.convai.com only when a human then merges
|
|
203
|
+
`staging` into `main` in that repository. Run the workflow by hand for a
|
|
204
|
+
docs-only fix. The pages follow that repository's authoring rules, recorded for
|
|
205
|
+
this package in [`docs/site/pack.md`](./docs/site/pack.md).
|
|
206
|
+
|
|
207
|
+
**Design, plan and decision history** live in [`docs/`](./docs):
|
|
232
208
|
|
|
233
209
|
| | |
|
|
234
210
|
|---|---|
|
|
@@ -251,8 +227,14 @@ The backend host is a source constant, `DEFAULT_BASE_URL` in
|
|
|
251
227
|
| `stg` | `https://api-stg.convai.com` | `stg` |
|
|
252
228
|
|
|
253
229
|
Beta points at **production** — it is the first line of testing against real
|
|
254
|
-
infrastructure, not a staging channel. Only `stg
|
|
255
|
-
|
|
230
|
+
infrastructure, not a staging channel. Only `stg` targets staging, which has
|
|
231
|
+
its own character database. Character IDs belong to one environment: a staging
|
|
232
|
+
ID against production returns a not-found error that reads like a permissions
|
|
233
|
+
problem.
|
|
234
|
+
|
|
235
|
+
Promotion has a silent failure mode: merging `stg` into `main` carries the
|
|
236
|
+
staging `DEFAULT_BASE_URL` across with no conflict, because only `stg` ever
|
|
237
|
+
edits that line. `pnpm check:hosts` is what catches it.
|
|
256
238
|
|
|
257
239
|
`scripts/check-hosts.mjs` enforces this. Each channel build asserts the policy
|
|
258
240
|
for the release it claims to be, so a staging artifact cannot be cut from
|
|
@@ -269,7 +251,7 @@ pnpm check:hosts # auto-detects the current branch
|
|
|
269
251
|
|
|
270
252
|
```bash
|
|
271
253
|
pnpm install
|
|
272
|
-
pnpm test #
|
|
254
|
+
pnpm test # vitest
|
|
273
255
|
pnpm typecheck
|
|
274
256
|
pnpm check:tests # every src/ module has a named test — see docs/testing-spec.md
|
|
275
257
|
pnpm build # emits dist/ with .d.ts
|
|
@@ -298,4 +280,4 @@ feature and adding its test are the same commit.
|
|
|
298
280
|
|
|
299
281
|
## License
|
|
300
282
|
|
|
301
|
-
Apache
|
|
283
|
+
Licensed under the [Apache License 2.0](./LICENSE). Copyright 2026 Convai.
|
|
@@ -9,6 +9,7 @@ import type { ConvaiClient } from '@convai/web-sdk/core';
|
|
|
9
9
|
import { type UnitNormalizer } from '../rig/units.js';
|
|
10
10
|
import { AnimationController } from '../clips/animations.js';
|
|
11
11
|
import { LipsyncSystem, type MouthTuning } from '../speech/lipsync.js';
|
|
12
|
+
import type { CorrectiveMap } from '../speech/correctiveMap.js';
|
|
12
13
|
import { type CharacterDecorator } from '../decorate/types.js';
|
|
13
14
|
import type { BlendshapeQueue } from '@convai/web-sdk/core';
|
|
14
15
|
import { Pipeline } from './pipeline.js';
|
|
@@ -87,6 +88,11 @@ export interface CreateCharacterOptions {
|
|
|
87
88
|
targetHeight?: number;
|
|
88
89
|
groundY?: number;
|
|
89
90
|
mouth?: MouthTuning;
|
|
91
|
+
/**
|
|
92
|
+
* A corrective map fitted from this body's DNA (`compileCorrectiveMap`).
|
|
93
|
+
* Off by default. Swap it later through `handle.lipsync.correctiveMap`.
|
|
94
|
+
*/
|
|
95
|
+
correctiveMap?: CorrectiveMap | null;
|
|
90
96
|
/** Tier-2 customization: run once after load, before first frame. */
|
|
91
97
|
decorators?: CharacterDecorator[];
|
|
92
98
|
/** Extra local clips (name → url), merged OVER manifest clips by name. */
|
|
@@ -223,7 +223,7 @@ export async function createCharacter(opts) {
|
|
|
223
223
|
const idle = pickIdleClip(animations.names, opts.idleClip);
|
|
224
224
|
if (idle)
|
|
225
225
|
animations.play(idle);
|
|
226
|
-
const lipsync = new LipsyncSystem(opts.mouth);
|
|
226
|
+
const lipsync = new LipsyncSystem(opts.mouth, opts.correctiveMap ?? null);
|
|
227
227
|
lipsync.bind(root);
|
|
228
228
|
for (const d of opts.decorators ?? []) {
|
|
229
229
|
d({ root, bundle, representation: rep, materials: collectMaterials(root) });
|
package/dist/index.d.ts
CHANGED
|
@@ -28,6 +28,8 @@ export { AnimationController } from './clips/animations.js';
|
|
|
28
28
|
export type { ClipTransition } from './clips/animations.js';
|
|
29
29
|
export { LipsyncSystem } from './speech/lipsync.js';
|
|
30
30
|
export type { MouthTuning } from './speech/lipsync.js';
|
|
31
|
+
export { compileCorrectiveMap, applyCorrectiveMap, CORRECTIVE_MAP_FORMAT } from './speech/correctiveMap.js';
|
|
32
|
+
export type { CorrectiveMap, CorrectiveMapTable } from './speech/correctiveMap.js';
|
|
31
33
|
export { createUnitNormalizer } from './rig/units.js';
|
|
32
34
|
export { collectMaterials } from './decorate/types.js';
|
|
33
35
|
export type { CharacterDecorator, DecoratorContext } from './decorate/types.js';
|
package/dist/index.js
CHANGED
|
@@ -25,6 +25,7 @@ export { httpResolver, pickRepresentation, DEFAULT_BASE_URL } from './resolve/re
|
|
|
25
25
|
export { createCharacter } from './character/createCharacter.js';
|
|
26
26
|
export { AnimationController } from './clips/animations.js';
|
|
27
27
|
export { LipsyncSystem } from './speech/lipsync.js';
|
|
28
|
+
export { compileCorrectiveMap, applyCorrectiveMap, CORRECTIVE_MAP_FORMAT } from './speech/correctiveMap.js';
|
|
28
29
|
export { createUnitNormalizer } from './rig/units.js';
|
|
29
30
|
export { collectMaterials } from './decorate/types.js';
|
|
30
31
|
export { Pipeline } from './character/pipeline.js';
|
package/dist/react/index.d.ts
CHANGED
|
@@ -9,20 +9,23 @@
|
|
|
9
9
|
* </Canvas>
|
|
10
10
|
* ```
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* embody is the 3D character only. For a chat panel, install
|
|
13
|
+
* `@convai/web-widget` and hand it the character's own client — one session,
|
|
14
|
+
* not two. `useWidgetClient` adapts that client to the widget's props.
|
|
15
15
|
*
|
|
16
16
|
* Chat UI is DOM and the character is a `<Canvas>` child, so lift the client
|
|
17
17
|
* out rather than rendering the widget from inside the scene:
|
|
18
18
|
*
|
|
19
19
|
* ```tsx
|
|
20
|
+
* import { ConvaiWidget } from '@convai/web-widget/react'
|
|
21
|
+
*
|
|
20
22
|
* const [client, setClient] = useState<ConvaiClient | null>(null)
|
|
23
|
+
* const widgetClient = useWidgetClient(client)
|
|
21
24
|
* <Canvas>
|
|
22
25
|
* <ConvaiCharacter apiKey={apiKey} characterId={characterId}
|
|
23
26
|
* onReady={(c) => setClient(c.client)} />
|
|
24
27
|
* </Canvas>
|
|
25
|
-
* {
|
|
28
|
+
* {widgetClient && <ConvaiWidget convaiClient={widgetClient} />}
|
|
26
29
|
* ```
|
|
27
30
|
*/
|
|
28
31
|
export { useCharacter } from './useCharacter.js';
|
|
@@ -35,6 +38,6 @@ export { OrbitRig } from './OrbitRig.js';
|
|
|
35
38
|
export type { OrbitOptions, OrbitRigProps } from './OrbitRig.js';
|
|
36
39
|
export { useWidgetClient } from './useWidgetClient.js';
|
|
37
40
|
export type { WidgetClient } from './useWidgetClient.js';
|
|
38
|
-
export {
|
|
41
|
+
export { useConvaiClient } from '@convai/web-sdk/react';
|
|
39
42
|
export { ConvaiClient } from '@convai/web-sdk/core';
|
|
40
43
|
export type { ConvaiConfig } from '@convai/web-sdk/core';
|
package/dist/react/index.js
CHANGED
|
@@ -9,20 +9,23 @@
|
|
|
9
9
|
* </Canvas>
|
|
10
10
|
* ```
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* embody is the 3D character only. For a chat panel, install
|
|
13
|
+
* `@convai/web-widget` and hand it the character's own client — one session,
|
|
14
|
+
* not two. `useWidgetClient` adapts that client to the widget's props.
|
|
15
15
|
*
|
|
16
16
|
* Chat UI is DOM and the character is a `<Canvas>` child, so lift the client
|
|
17
17
|
* out rather than rendering the widget from inside the scene:
|
|
18
18
|
*
|
|
19
19
|
* ```tsx
|
|
20
|
+
* import { ConvaiWidget } from '@convai/web-widget/react'
|
|
21
|
+
*
|
|
20
22
|
* const [client, setClient] = useState<ConvaiClient | null>(null)
|
|
23
|
+
* const widgetClient = useWidgetClient(client)
|
|
21
24
|
* <Canvas>
|
|
22
25
|
* <ConvaiCharacter apiKey={apiKey} characterId={characterId}
|
|
23
26
|
* onReady={(c) => setClient(c.client)} />
|
|
24
27
|
* </Canvas>
|
|
25
|
-
* {
|
|
28
|
+
* {widgetClient && <ConvaiWidget convaiClient={widgetClient} />}
|
|
26
29
|
* ```
|
|
27
30
|
*/
|
|
28
31
|
export { useCharacter } from './useCharacter.js';
|
|
@@ -32,6 +35,7 @@ export { CharacterModel } from './CharacterModel.js';
|
|
|
32
35
|
// still want the orbit behaviour ConvaiCharacter gives for free.
|
|
33
36
|
export { OrbitRig } from './OrbitRig.js';
|
|
34
37
|
export { useWidgetClient } from './useWidgetClient.js';
|
|
35
|
-
// ──
|
|
36
|
-
|
|
38
|
+
// ── The client, re-exported from @convai/web-sdk ──────────────────────────
|
|
39
|
+
// No UI: ConvaiWidget, AudioRenderer and AudioContext are @convai/web-widget's.
|
|
40
|
+
export { useConvaiClient } from '@convai/web-sdk/react';
|
|
37
41
|
export { ConvaiClient } from '@convai/web-sdk/core';
|
|
@@ -7,12 +7,6 @@ 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
|
-
*/
|
|
16
10
|
export interface UseCharacterResult {
|
|
17
11
|
character: CharacterHandle | null;
|
|
18
12
|
/**
|
|
@@ -35,27 +35,22 @@ 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
|
-
//
|
|
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.
|
|
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.
|
|
53
41
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
54
|
-
}, [opts.characterId, opts.apiKey, opts.authToken, opts.token, opts.baseUrl
|
|
42
|
+
}, [opts.characterId, opts.apiKey, opts.authToken, opts.token, opts.baseUrl]);
|
|
55
43
|
useEffect(() => {
|
|
56
44
|
if (character)
|
|
57
45
|
character.camera = camera;
|
|
58
46
|
}, [character, camera]);
|
|
47
|
+
// Live, unlike the props above: swapping the map must not reload the body,
|
|
48
|
+
// or an A/B on live speech would drop the turn it is comparing.
|
|
49
|
+
const correctiveMap = opts.correctiveMap ?? null;
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (character)
|
|
52
|
+
character.lipsync.correctiveMap = correctiveMap;
|
|
53
|
+
}, [character, correctiveMap]);
|
|
59
54
|
useFrame((_, delta) => character?.update(delta, queueRef.current));
|
|
60
55
|
return {
|
|
61
56
|
character,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Makes embody's client usable by
|
|
2
|
+
* Makes embody's client usable by `<ConvaiWidget/>` from `@convai/web-widget`.
|
|
3
3
|
*
|
|
4
4
|
* The widget wants `IConvaiClient` plus four pieces of reactive UI state
|
|
5
5
|
* (`activity`, `isAudioMuted`, `isVideoEnabled`, `isScreenShareActive`). The
|
|
@@ -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.convai.com";
|
|
56
|
+
export declare const DEFAULT_BASE_URL = "https://api-stg.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.convai.com';
|
|
23
|
+
export const DEFAULT_BASE_URL = 'https://api-stg.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.';
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Corrective map — re-weights Convai's 251 face controls so that a body with
|
|
3
|
+
* one baked morph per control lands closer to what the full MetaHuman rig
|
|
4
|
+
* shows.
|
|
5
|
+
*
|
|
6
|
+
* RigLogic fires correctives (PSDs) when controls combine, and its face joints
|
|
7
|
+
* rotate, so the skin moves along arcs; a sum of straight-line morphs follows
|
|
8
|
+
* neither. A table fitted from the character's DNA (convai-embody-console's
|
|
9
|
+
* tools/corrective-mapping/fit_mapping.py) corrects each frame before the
|
|
10
|
+
* mouth layer sees it:
|
|
11
|
+
*
|
|
12
|
+
* w = x + clamp(A · φ(x), ±limit)
|
|
13
|
+
*
|
|
14
|
+
* φ(x) is one activation per corrective (Π clamp01(x_i), weighted and capped
|
|
15
|
+
* at 1, as RigLogic computes it) and, in version 2 tables, each x_i² and each
|
|
16
|
+
* x_i · jawOpen. The table holds no geometry: feature definitions and
|
|
17
|
+
* coefficients only. Format and maths match the console's
|
|
18
|
+
* corrective-mapping.mjs, so a table fitted there loads here unchanged.
|
|
19
|
+
*/
|
|
20
|
+
export declare const CORRECTIVE_MAP_FORMAT = "convai-corrective-map";
|
|
21
|
+
/** One corrective's inputs (channel indices) and per-input weights. */
|
|
22
|
+
interface PsdDef {
|
|
23
|
+
inputs: number[];
|
|
24
|
+
weights: number[];
|
|
25
|
+
}
|
|
26
|
+
/** The JSON fit_mapping.py writes. */
|
|
27
|
+
export type CorrectiveMapTable = {
|
|
28
|
+
format: typeof CORRECTIVE_MAP_FORMAT;
|
|
29
|
+
version: 1;
|
|
30
|
+
channels: number;
|
|
31
|
+
/** One row per corrective: its definition and the channels it corrects. */
|
|
32
|
+
rows: (PsdDef & {
|
|
33
|
+
a: [number, number][];
|
|
34
|
+
})[];
|
|
35
|
+
} | {
|
|
36
|
+
format: typeof CORRECTIVE_MAP_FORMAT;
|
|
37
|
+
version: 2;
|
|
38
|
+
channels: number;
|
|
39
|
+
features: {
|
|
40
|
+
psd: PsdDef[];
|
|
41
|
+
squares?: boolean;
|
|
42
|
+
jaw?: number;
|
|
43
|
+
};
|
|
44
|
+
/** [feature index, [[channel, coefficient], …]] */
|
|
45
|
+
coefficients: [number, [number, number][]][];
|
|
46
|
+
limit?: number;
|
|
47
|
+
};
|
|
48
|
+
interface Column {
|
|
49
|
+
targets: Int16Array;
|
|
50
|
+
coeffs: Float32Array;
|
|
51
|
+
}
|
|
52
|
+
/** A validated table packed into typed arrays for the per-frame loop. */
|
|
53
|
+
export interface CorrectiveMap {
|
|
54
|
+
readonly channels: number;
|
|
55
|
+
readonly psd: {
|
|
56
|
+
inputs: Int16Array;
|
|
57
|
+
weights: Float32Array;
|
|
58
|
+
}[];
|
|
59
|
+
readonly squares: boolean;
|
|
60
|
+
/** jawOpen's channel index, or -1 when the table has no jaw features. */
|
|
61
|
+
readonly jaw: number;
|
|
62
|
+
readonly columns: (Column | null)[];
|
|
63
|
+
readonly limit: number;
|
|
64
|
+
/** Scratch for the per-frame delta; one map serves one frame at a time. */
|
|
65
|
+
readonly delta: Float32Array;
|
|
66
|
+
}
|
|
67
|
+
/** Validate a table and pack it. Throws on anything malformed, at load rather than mid-speech. */
|
|
68
|
+
export declare function compileCorrectiveMap(table: CorrectiveMapTable): CorrectiveMap;
|
|
69
|
+
/**
|
|
70
|
+
* Map one frame into `out` (allocated when omitted) and return it. `frame` is
|
|
71
|
+
* not modified. A frame of the wrong width is returned unmapped: a table
|
|
72
|
+
* fitted for another channel layout must not scribble over this one.
|
|
73
|
+
*/
|
|
74
|
+
export declare function applyCorrectiveMap(frame: Float32Array, map: CorrectiveMap, out?: Float32Array): Float32Array;
|
|
75
|
+
export {};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Corrective map — re-weights Convai's 251 face controls so that a body with
|
|
3
|
+
* one baked morph per control lands closer to what the full MetaHuman rig
|
|
4
|
+
* shows.
|
|
5
|
+
*
|
|
6
|
+
* RigLogic fires correctives (PSDs) when controls combine, and its face joints
|
|
7
|
+
* rotate, so the skin moves along arcs; a sum of straight-line morphs follows
|
|
8
|
+
* neither. A table fitted from the character's DNA (convai-embody-console's
|
|
9
|
+
* tools/corrective-mapping/fit_mapping.py) corrects each frame before the
|
|
10
|
+
* mouth layer sees it:
|
|
11
|
+
*
|
|
12
|
+
* w = x + clamp(A · φ(x), ±limit)
|
|
13
|
+
*
|
|
14
|
+
* φ(x) is one activation per corrective (Π clamp01(x_i), weighted and capped
|
|
15
|
+
* at 1, as RigLogic computes it) and, in version 2 tables, each x_i² and each
|
|
16
|
+
* x_i · jawOpen. The table holds no geometry: feature definitions and
|
|
17
|
+
* coefficients only. Format and maths match the console's
|
|
18
|
+
* corrective-mapping.mjs, so a table fitted there loads here unchanged.
|
|
19
|
+
*/
|
|
20
|
+
export const CORRECTIVE_MAP_FORMAT = 'convai-corrective-map';
|
|
21
|
+
const clamp01 = (v) => (v > 0 ? (v < 1 ? v : 1) : 0);
|
|
22
|
+
function checkIndex(i, channels, what) {
|
|
23
|
+
if (!Number.isInteger(i) || i < 0 || i >= channels)
|
|
24
|
+
throw new Error(`${what} ${i} is outside 0..${channels - 1}.`);
|
|
25
|
+
}
|
|
26
|
+
function compilePsd(defs, channels) {
|
|
27
|
+
return defs.map((d) => {
|
|
28
|
+
d.inputs.forEach((i) => checkIndex(i, channels, 'A corrective input'));
|
|
29
|
+
return { inputs: Int16Array.from(d.inputs), weights: Float32Array.from(d.weights) };
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function column(entries, channels) {
|
|
33
|
+
entries.forEach(([i]) => checkIndex(i, channels, 'A target channel'));
|
|
34
|
+
return { targets: Int16Array.from(entries.map(([i]) => i)), coeffs: Float32Array.from(entries.map(([, c]) => c)) };
|
|
35
|
+
}
|
|
36
|
+
/** Validate a table and pack it. Throws on anything malformed, at load rather than mid-speech. */
|
|
37
|
+
export function compileCorrectiveMap(table) {
|
|
38
|
+
if (!table || table.format !== CORRECTIVE_MAP_FORMAT)
|
|
39
|
+
throw new Error(`Not a ${CORRECTIVE_MAP_FORMAT} table.`);
|
|
40
|
+
const channels = table.channels;
|
|
41
|
+
const delta = new Float32Array(channels);
|
|
42
|
+
if (table.version === 1) {
|
|
43
|
+
const psd = compilePsd(table.rows, channels);
|
|
44
|
+
const columns = table.rows.map((row) => column(row.a, channels));
|
|
45
|
+
return { channels, psd, squares: false, jaw: -1, columns, limit: Infinity, delta };
|
|
46
|
+
}
|
|
47
|
+
if (table.version === 2) {
|
|
48
|
+
const f = table.features;
|
|
49
|
+
const psd = compilePsd(f.psd, channels);
|
|
50
|
+
const jaw = f.jaw ?? -1;
|
|
51
|
+
if (jaw >= 0)
|
|
52
|
+
checkIndex(jaw, channels, 'The jaw channel');
|
|
53
|
+
const size = psd.length + (f.squares ? channels : 0) + (jaw >= 0 ? channels : 0);
|
|
54
|
+
const columns = Array.from({ length: size }, () => null);
|
|
55
|
+
for (const [feature, entries] of table.coefficients) {
|
|
56
|
+
checkIndex(feature, size, 'Feature');
|
|
57
|
+
columns[feature] = column(entries, channels);
|
|
58
|
+
}
|
|
59
|
+
return { channels, psd, squares: Boolean(f.squares), jaw, columns, limit: table.limit ?? Infinity, delta };
|
|
60
|
+
}
|
|
61
|
+
throw new Error(`Unsupported ${CORRECTIVE_MAP_FORMAT} version ${table.version}.`);
|
|
62
|
+
}
|
|
63
|
+
function addColumn(delta, col, value) {
|
|
64
|
+
if (!col)
|
|
65
|
+
return;
|
|
66
|
+
for (let k = 0; k < col.targets.length; k++)
|
|
67
|
+
delta[col.targets[k]] += value * col.coeffs[k];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Map one frame into `out` (allocated when omitted) and return it. `frame` is
|
|
71
|
+
* not modified. A frame of the wrong width is returned unmapped: a table
|
|
72
|
+
* fitted for another channel layout must not scribble over this one.
|
|
73
|
+
*/
|
|
74
|
+
export function applyCorrectiveMap(frame, map, out = new Float32Array(frame.length)) {
|
|
75
|
+
const n = map.channels;
|
|
76
|
+
if (frame.length !== n) {
|
|
77
|
+
out.set(frame);
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
const delta = map.delta;
|
|
81
|
+
delta.fill(0);
|
|
82
|
+
let f = 0;
|
|
83
|
+
for (const psd of map.psd) {
|
|
84
|
+
let prod = 1;
|
|
85
|
+
for (let k = 0; k < psd.inputs.length; k++) {
|
|
86
|
+
const v = frame[psd.inputs[k]];
|
|
87
|
+
if (!(v > 0)) {
|
|
88
|
+
prod = 0;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
prod *= v < 1 ? v : 1;
|
|
92
|
+
}
|
|
93
|
+
if (prod !== 0) {
|
|
94
|
+
let p = 0;
|
|
95
|
+
for (let k = 0; k < psd.weights.length; k++)
|
|
96
|
+
p += Math.min(1, psd.weights[k] * prod);
|
|
97
|
+
addColumn(delta, map.columns[f], p);
|
|
98
|
+
}
|
|
99
|
+
f++;
|
|
100
|
+
}
|
|
101
|
+
if (map.squares) {
|
|
102
|
+
for (let i = 0; i < n; i++) {
|
|
103
|
+
const v = clamp01(frame[i]);
|
|
104
|
+
if (v !== 0)
|
|
105
|
+
addColumn(delta, map.columns[f + i], v * v);
|
|
106
|
+
}
|
|
107
|
+
f += n;
|
|
108
|
+
}
|
|
109
|
+
if (map.jaw >= 0) {
|
|
110
|
+
const jaw = clamp01(frame[map.jaw]);
|
|
111
|
+
if (jaw !== 0) {
|
|
112
|
+
for (let i = 0; i < n; i++) {
|
|
113
|
+
const v = clamp01(frame[i]);
|
|
114
|
+
if (v !== 0)
|
|
115
|
+
addColumn(delta, map.columns[f + i], v * jaw);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const limit = map.limit;
|
|
120
|
+
for (let i = 0; i < n; i++) {
|
|
121
|
+
const d = delta[i] > limit ? limit : delta[i] < -limit ? -limit : delta[i];
|
|
122
|
+
out[i] = frame[i] + d;
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
package/dist/speech/lipsync.d.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import type * as THREE from 'three';
|
|
17
17
|
import type { BlendshapeQueue } from '@convai/web-sdk/core';
|
|
18
|
+
import { type CorrectiveMap } from './correctiveMap.js';
|
|
18
19
|
/** Tier-1 customization: the mouth knobs, per character. Defaults = pagoda-act's tuned values. */
|
|
19
20
|
export interface MouthTuning {
|
|
20
21
|
/** Hard cap on jawOpen; peak excursions read exaggerated on camera. */
|
|
@@ -46,10 +47,18 @@ export declare class LipsyncSystem {
|
|
|
46
47
|
energy: number;
|
|
47
48
|
/** True while the bot owns the mouth; body layers read it. */
|
|
48
49
|
speaking: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Corrective map for this body, or null to play the stream as sent. Applied
|
|
52
|
+
* to each frame as it comes off the queue, before the SDK's shaping and the
|
|
53
|
+
* mouth layer. Mutable so a viewer can A/B it on live speech.
|
|
54
|
+
*/
|
|
55
|
+
correctiveMap: CorrectiveMap | null;
|
|
56
|
+
private mapped;
|
|
49
57
|
private readonly ch;
|
|
50
|
-
constructor(tuning?: MouthTuning);
|
|
58
|
+
constructor(tuning?: MouthTuning, correctiveMap?: CorrectiveMap | null);
|
|
51
59
|
/** Bind every morph-bearing mesh under root. Returns matched channel count. */
|
|
52
60
|
bind(root: THREE.Object3D): number;
|
|
61
|
+
private correct;
|
|
53
62
|
/** Tick each frame. Pass the queue only if this character owns the turn. */
|
|
54
63
|
update(delta: number, q: BlendshapeQueue | undefined): void;
|
|
55
64
|
}
|
package/dist/speech/lipsync.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createLipsyncProcessor, mhaChannelIndex, } from '@convai/web-sdk/lipsync-helpers';
|
|
2
|
+
import { applyCorrectiveMap } from './correctiveMap.js';
|
|
2
3
|
const DEFAULTS = {
|
|
3
4
|
jawCap: 0.55,
|
|
4
5
|
sealCeiling: 0.5,
|
|
@@ -10,7 +11,7 @@ const DEFAULTS = {
|
|
|
10
11
|
gainOverrides: { mouthLowerLipDepressL: 0.28, mouthLowerLipDepressR: 0.28 },
|
|
11
12
|
};
|
|
12
13
|
export class LipsyncSystem {
|
|
13
|
-
constructor(tuning = {}) {
|
|
14
|
+
constructor(tuning = {}, correctiveMap = null) {
|
|
14
15
|
this.bindings = [];
|
|
15
16
|
this.accum = 0;
|
|
16
17
|
this.lastFrame = null;
|
|
@@ -20,6 +21,8 @@ export class LipsyncSystem {
|
|
|
20
21
|
this.energy = 0;
|
|
21
22
|
/** True while the bot owns the mouth; body layers read it. */
|
|
22
23
|
this.speaking = false;
|
|
24
|
+
this.mapped = null;
|
|
25
|
+
this.correctiveMap = correctiveMap;
|
|
23
26
|
this.t = { ...DEFAULTS, ...tuning, gainOverrides: { ...DEFAULTS.gainOverrides, ...tuning.gainOverrides } };
|
|
24
27
|
this.processor = createLipsyncProcessor('mha', {
|
|
25
28
|
skipEyeChannels: true,
|
|
@@ -43,6 +46,14 @@ export class LipsyncSystem {
|
|
|
43
46
|
this.bindings = this.processor.bindAll(root);
|
|
44
47
|
return this.bindings.reduce((s, b) => s + b.matched, 0);
|
|
45
48
|
}
|
|
49
|
+
correct(frame) {
|
|
50
|
+
const map = this.correctiveMap;
|
|
51
|
+
if (!map)
|
|
52
|
+
return frame;
|
|
53
|
+
if (this.mapped?.length !== frame.length)
|
|
54
|
+
this.mapped = new Float32Array(frame.length);
|
|
55
|
+
return applyCorrectiveMap(frame, map, this.mapped);
|
|
56
|
+
}
|
|
46
57
|
/** Tick each frame. Pass the queue only if this character owns the turn. */
|
|
47
58
|
update(delta, q) {
|
|
48
59
|
if (!this.bindings.length)
|
|
@@ -74,7 +85,7 @@ export class LipsyncSystem {
|
|
|
74
85
|
if (this.accum > 0.25)
|
|
75
86
|
this.accum = 0.25; // a starved queue banks no debt
|
|
76
87
|
if (fresh)
|
|
77
|
-
this.lastFrame = fresh;
|
|
88
|
+
this.lastFrame = this.correct(fresh);
|
|
78
89
|
}
|
|
79
90
|
else {
|
|
80
91
|
this.accum = 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@convai/embody",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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>",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"access": "public"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"@convai/web-sdk": ">=1.8.0-beta.4",
|
|
61
|
+
"@convai/web-sdk": ">=1.8.0-beta.4 || >=2.0.0-beta.0",
|
|
62
62
|
"@react-three/fiber": ">=8",
|
|
63
63
|
"react": ">=18",
|
|
64
64
|
"three": ">=0.160"
|
|
@@ -73,7 +73,8 @@
|
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
75
|
"@changesets/cli": "^2.27.0",
|
|
76
|
-
"@convai/web-sdk": "
|
|
76
|
+
"@convai/web-sdk": "2.0.0-beta.0",
|
|
77
|
+
"@convai/web-widget": "0.2.1",
|
|
77
78
|
"@react-three/fiber": "^8.18.0",
|
|
78
79
|
"@types/react": "^18.3.31",
|
|
79
80
|
"@types/react-dom": "^19.2.7",
|
|
@@ -99,7 +100,7 @@
|
|
|
99
100
|
"typecheck": "tsc --noEmit",
|
|
100
101
|
"test": "vitest run",
|
|
101
102
|
"test:watch": "vitest",
|
|
102
|
-
"verify": "pnpm check:hosts && pnpm check:tests && pnpm typecheck && pnpm test && pnpm build",
|
|
103
|
+
"verify": "pnpm check:hosts && pnpm check:tests && pnpm typecheck && pnpm test && pnpm test:scripts && pnpm build",
|
|
103
104
|
"changeset": "changeset",
|
|
104
105
|
"version": "changeset version",
|
|
105
106
|
"release": "pnpm build:prod && changeset publish",
|
|
@@ -109,6 +110,7 @@
|
|
|
109
110
|
"build:stg": "pnpm clean && BRANCH=stg node scripts/check-hosts.mjs && pnpm build",
|
|
110
111
|
"release:stg": "pnpm build:stg && changeset publish --tag stg",
|
|
111
112
|
"docs": "typedoc",
|
|
112
|
-
"docs:watch": "typedoc --watch"
|
|
113
|
+
"docs:watch": "typedoc --watch",
|
|
114
|
+
"test:scripts": "node --test scripts/sync-docs.test.mjs"
|
|
113
115
|
}
|
|
114
116
|
}
|