Skip to content

Commit ff72498

Browse files
committed
deploy: d95709e
1 parent 8e13814 commit ff72498

File tree

11 files changed

+105
-97
lines changed

11 files changed

+105
-97
lines changed

.well-known/agent-card.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"name": "AnimaSync",
44
"description": "Voice-driven 3D avatar animation engine for the browser. Extracts emotion from speech and generates lip sync, facial expressions, and body motion in real time — entirely client-side via Rust/WASM and ONNX inference.",
55
"url": "https://animasync.quasar.ggls.dev/",
6-
"version": "0.4.3",
6+
"version": "0.4.4",
77
"provider": {
88
"organization": "GoodGang Labs",
99
"url": "https://goodganglabs.com"

.well-known/ai-catalog.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,15 @@
4646
],
4747
"packages": {
4848
"npm": [
49-
{
50-
"name": "@goodganglabs/lipsync-wasm-v2",
51-
"description": "V2 engine — 52-dim ARKit blendshapes via student distillation. Recommended for most projects.",
52-
"url": "https://www.npmjs.com/package/@goodganglabs/lipsync-wasm-v2"
53-
},
5449
{
5550
"name": "@goodganglabs/lipsync-wasm-v1",
56-
"description": "V1 engine — 111-dim ARKit blendshapes via phoneme classification. Full expression control.",
51+
"description": "V1 engine — 111-dim ARKit blendshapes via phoneme classification. Recommended for most projects.",
5752
"url": "https://www.npmjs.com/package/@goodganglabs/lipsync-wasm-v1"
53+
},
54+
{
55+
"name": "@goodganglabs/lipsync-wasm-v2",
56+
"description": "V2 engine — 52-dim ARKit blendshapes via student distillation. Lightweight alternative.",
57+
"url": "https://www.npmjs.com/package/@goodganglabs/lipsync-wasm-v2"
5858
}
5959
]
6060
},

README.md

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ Most lip sync engines stop at mouth shapes. AnimaSync goes further — it treats
8080
| **Lip Sync** | Mouth shapes matching phonemes | ONNX inference → ARKit blendshapes (jaw, mouth, tongue) |
8181
| **Facial Expression** | Emotion-driven brows, cheeks, eyes | Voice energy & pitch → expression mapping + anatomical constraints |
8282
| **Eye Animation** | Natural blinks, micro-movements | Stochastic blink injection (2.5–4.5s intervals, 15% double-blink) |
83-
| **Body Motion** | Idle breathing, speaking gestures | Embedded VRMA bone clips with automatic idle ↔ speaking crossfade |
83+
| **Body Motion** | Idle breathing, speaking gestures | Embedded VRMA bone clips with LoopPingPong idle + asymmetric crossfade (0.8s in, 1.0s out) |
8484

8585
One audio stream in → a fully animated 3D avatar out.
8686

@@ -91,11 +91,11 @@ One audio stream in → a fully animated 3D avatar out.
9191
### Install
9292

9393
```bash
94-
# V2 recommended for most use cases
95-
npm install @goodganglabs/lipsync-wasm-v2
96-
97-
# V1 for full 111-dim expression control
94+
# V1 recommended for most use cases
9895
npm install @goodganglabs/lipsync-wasm-v1
96+
97+
# V2 lightweight alternative
98+
npm install @goodganglabs/lipsync-wasm-v2
9999
```
100100

101101
> Peer dependency: [`onnxruntime-web`](https://www.npmjs.com/package/onnxruntime-web) >= 1.17.0
@@ -163,17 +163,17 @@ The production site is available at **[animasync.quasar.ggls.dev](https://animas
163163

164164
## V1 vs V2
165165

166-
| | V2 (Recommended) | V1 (Full Control) |
166+
| | V1 (Recommended) | V2 (Lightweight) |
167167
|---|---|---|
168-
| **npm** | `@goodganglabs/lipsync-wasm-v2` | `@goodganglabs/lipsync-wasm-v1` |
169-
| **Output** | 52-dim ARKit blendshapes | 111-dim ARKit blendshapes |
170-
| **Model** | Student distillation (direct prediction) | Phoneme classification → viseme mapping |
171-
| **Post-processing** | crisp_mouth + fade + auto-blink | OneEuroFilter + anatomical constraints |
172-
| **Expression generation** | Blink injection in post-process | Built-in `IdleExpressionGenerator` (blinks + micro-expressions) |
173-
| **Voice activity** | Not included | Built-in `VoiceActivityDetector` (body pose switching) |
174-
| **ONNX fallback** | None (ONNX required) | Heuristic mode (energy-based) |
175-
| **Body motion** | VRMA idle/speaking (both versions) | VRMA idle/speaking + VAD auto-switch |
176-
| **Best for** | Most projects, quick integration | Full expression control, custom avatars |
168+
| **npm** | `@goodganglabs/lipsync-wasm-v1` | `@goodganglabs/lipsync-wasm-v2` |
169+
| **Output** | 111-dim ARKit blendshapes | 52-dim ARKit blendshapes |
170+
| **Model** | Phoneme classification → viseme mapping | Student distillation (direct prediction) |
171+
| **Post-processing** | OneEuroFilter + anatomical constraints | crisp_mouth + fade + auto-blink |
172+
| **Expression generation** | Built-in `IdleExpressionGenerator` (blinks + micro-expressions) | Blink injection in post-process |
173+
| **Voice activity** | Built-in `VoiceActivityDetector` (body pose switching) | Not included |
174+
| **ONNX fallback** | Heuristic mode (energy-based) | None (ONNX required) |
175+
| **Body motion** | VRMA idle/speaking + VAD auto-switch (LoopPingPong, asymmetric crossfade) | VRMA idle/speaking (LoopPingPong, asymmetric crossfade) |
176+
| **Best for** | Full expression control, custom avatars | Quick integration, lightweight |
177177

178178
---
179179

agents.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"id": "quick-start-v2",
3030
"name": "Quick Start with V2 Engine",
3131
"description": "Install AnimaSync V2, initialize the engine, and process an audio file to get 52 ARKit blendshape animation frames at 30fps.",
32-
"agent_instructions": "Use this flow when a developer wants to add lip sync animation to a web app. V2 is recommended for most use cases — simpler integration, direct 52-dim prediction.",
32+
"agent_instructions": "Use this flow when a developer wants to add lip sync animation to a web app. V1 is recommended for most use cases — full expression control with built-in VAD and idle expressions.",
3333
"steps": [
3434
{
3535
"action": "install",
@@ -150,7 +150,7 @@
150150
},
151151
{
152152
"action": "init-animasync",
153-
"code": "<script type=\"module\">\nconst CDN = 'https://cdn.jsdelivr.net/npm/@goodganglabs/lipsync-wasm-v2@0.4.3';\nconst { LipSyncWasmWrapper } = await import(`${CDN}/lipsync-wasm-wrapper.js`);\nconst lipsync = new LipSyncWasmWrapper({ wasmPath: `${CDN}/lipsync_wasm_v2.js` });\nawait lipsync.init();\n</script>",
153+
"code": "<script type=\"module\">\nconst CDN = 'https://cdn.jsdelivr.net/npm/@goodganglabs/lipsync-wasm-v2@0.4.4';\nconst { LipSyncWasmWrapper } = await import(`${CDN}/lipsync-wasm-wrapper.js`);\nconst lipsync = new LipSyncWasmWrapper({ wasmPath: `${CDN}/lipsync_wasm_v2.js` });\nawait lipsync.init();\n</script>",
154154
"description": "Import and initialize AnimaSync V2 from CDN"
155155
}
156156
]

examples/guide/index.html

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -603,7 +603,7 @@ <h2 class="step-title">Initialize AnimaSync</h2>
603603
<button data-action="fold">Fold</button>
604604
</div>
605605
</div>
606-
<div class="code-block"><span class="kw">const</span> CDN = <span class="str">'https://cdn.jsdelivr.net/npm/&#64;goodganglabs/lipsync-wasm-v1&#64;0.4.3'</span>;
606+
<div class="code-block"><span class="kw">const</span> CDN = <span class="str">'https://cdn.jsdelivr.net/npm/&#64;goodganglabs/lipsync-wasm-v1&#64;0.4.4'</span>;
607607

608608
<span class="kw">const</span> { <span class="fn">LipSyncWasmWrapper</span> } = <span class="kw">await</span> <span class="fn">import</span>(<span class="str">`${CDN}/lipsync-wasm-wrapper.js`</span>);
609609
<span class="kw">const</span> lipsync = <span class="kw">new</span> <span class="fn">LipSyncWasmWrapper</span>({ <span class="attr">wasmPath</span>: <span class="str">`${CDN}/lipsync_wasm_v1.js`</span> });
@@ -895,7 +895,7 @@ <h2 class="step-title">Add Real-time Microphone</h2>
895895
// ════════════════════════════════════════
896896
// Config
897897
// ════════════════════════════════════════
898-
const VERSION = '0.4.3';
898+
const VERSION = '0.4.4';
899899
const CDN = `https://cdn.jsdelivr.net/npm/@goodganglabs/lipsync-wasm-v1@${VERSION}`;
900900
const ARKIT_52 = [
901901
'browDownLeft','browDownRight','browInnerUp','browOuterUpLeft','browOuterUpRight',
@@ -1012,11 +1012,11 @@ <h2 class="step-title">Add Real-time Microphone</h2>
10121012
const vrmaBytes = lipsync.getVrmaBytes();
10131013
if (vrmaBytes?.idle?.length) {
10141014
const a = await loadVRMAFromBytes(vrmaBytes.idle);
1015-
if (a) { idleAction = mixer.clipAction(createVRMAnimationClip(a, vrm)); idleAction.play(); }
1015+
if (a) { idleAction = mixer.clipAction(createVRMAnimationClip(a, vrm)); idleAction.setLoop(THREE.LoopPingPong); idleAction.play(); }
10161016
}
10171017
if (vrmaBytes?.speaking?.length) {
10181018
const a = await loadVRMAFromBytes(vrmaBytes.speaking);
1019-
if (a) { speakingAction = mixer.clipAction(createVRMAnimationClip(a, vrm)); speakingAction.play(); speakingAction.setEffectiveWeight(0); }
1019+
if (a) { speakingAction = mixer.clipAction(createVRMAnimationClip(a, vrm)); speakingAction.setLoop(THREE.LoopPingPong); speakingAction.play(); speakingAction.setEffectiveWeight(0); }
10201020
}
10211021
} catch (e) { console.warn('VRMA skip:', e.message); }
10221022
}
@@ -1251,12 +1251,15 @@ <h2 class="step-title">Add Real-time Microphone</h2>
12511251
for (const n of names) vrm.expressionManager.setValue(n, 0);
12521252
prevFrame = null;
12531253
}
1254-
function transitionToSpeaking() { isSpeaking = true; crossFadeProgress = 1; }
1254+
function transitionToSpeaking() { isSpeaking = true; }
12551255
function transitionToIdle() { isSpeaking = false; }
12561256
function updateBoneWeights(dt) {
1257-
const speed = 3.0;
1258-
if (isSpeaking && crossFadeProgress < 1) crossFadeProgress = Math.min(1, crossFadeProgress + dt * speed);
1259-
else if (!isSpeaking && crossFadeProgress > 0) crossFadeProgress = Math.max(0, crossFadeProgress - dt * speed);
1257+
const target = isSpeaking ? 1 : 0;
1258+
const duration = isSpeaking ? 0.8 : 1.0;
1259+
const step = dt / duration;
1260+
crossFadeProgress = target > crossFadeProgress
1261+
? Math.min(crossFadeProgress + step, 1)
1262+
: Math.max(crossFadeProgress - step, 0);
12601263
const w = crossFadeProgress * crossFadeProgress * (3 - 2 * crossFadeProgress);
12611264
if (idleAction) idleAction.setEffectiveWeight(1 - w);
12621265
if (speakingAction) speakingAction.setEffectiveWeight(w);
@@ -1590,11 +1593,11 @@ <h2 class="step-title">Add Real-time Microphone</h2>
15901593
' const vrmaBytes = lipsync.getVrmaBytes();',
15911594
' if (vrmaBytes?.idle?.length) {',
15921595
' const a = await loadVRMAFromBytes(vrmaBytes.idle);',
1593-
' if (a) { idleAction = mixer.clipAction(createVRMAnimationClip(a, vrm)); idleAction.play(); }',
1596+
' if (a) { idleAction = mixer.clipAction(createVRMAnimationClip(a, vrm)); idleAction.setLoop(THREE.LoopPingPong); idleAction.play(); }',
15941597
' }',
15951598
' if (vrmaBytes?.speaking?.length) {',
15961599
' const a = await loadVRMAFromBytes(vrmaBytes.speaking);',
1597-
' if (a) { speakingAction = mixer.clipAction(createVRMAnimationClip(a, vrm)); speakingAction.play(); speakingAction.setEffectiveWeight(0); }',
1600+
' if (a) { speakingAction = mixer.clipAction(createVRMAnimationClip(a, vrm)); speakingAction.setLoop(THREE.LoopPingPong); speakingAction.play(); speakingAction.setEffectiveWeight(0); }',
15981601
' }',
15991602
" } catch (e) { console.warn('VRMA skip:', e.message); }",
16001603
' }',
@@ -1776,12 +1779,15 @@ <h2 class="step-title">Add Real-time Microphone</h2>
17761779
' for (const n of names) vrm.expressionManager.setValue(n, 0);',
17771780
' prevFrame = null;',
17781781
'}',
1779-
'function transitionToSpeaking() { isSpeaking = true; crossFadeProgress = 1; }',
1782+
'function transitionToSpeaking() { isSpeaking = true; }',
17801783
'function transitionToIdle() { isSpeaking = false; }',
17811784
'function updateBoneWeights(dt) {',
1782-
' const speed = 3.0;',
1783-
' if (isSpeaking && crossFadeProgress < 1) crossFadeProgress = Math.min(1, crossFadeProgress + dt * speed);',
1784-
' else if (!isSpeaking && crossFadeProgress > 0) crossFadeProgress = Math.max(0, crossFadeProgress - dt * speed);',
1785+
' const target = isSpeaking ? 1 : 0;',
1786+
' const duration = isSpeaking ? 0.8 : 1.0;',
1787+
' const step = dt / duration;',
1788+
' crossFadeProgress = target > crossFadeProgress',
1789+
' ? Math.min(crossFadeProgress + step, 1)',
1790+
' : Math.max(crossFadeProgress - step, 0);',
17851791
' const w = crossFadeProgress * crossFadeProgress * (3 - 2 * crossFadeProgress);',
17861792
' if (idleAction) idleAction.setEffectiveWeight(1 - w);',
17871793
' if (speakingAction) speakingAction.setEffectiveWeight(w);',

examples/vanilla-avatar/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ <h2>52 ARKit Blendshapes — V2 Student</h2>
199199
// No 3D avatar, no Three.js. Pure audio → lip sync data (52-dim).
200200
// ================================================================
201201

202-
const VERSION = '0.4.3';
202+
const VERSION = '0.4.4';
203203
const CDN = `https://cdn.jsdelivr.net/npm/@goodganglabs/lipsync-wasm-v2@${VERSION}`;
204204

205205
// ── All 52 ARKit blendshape channels ──

examples/vanilla-basic/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ <h2>52 ARKit Blendshapes — V1 Phoneme</h2>
199199
// No 3D avatar, no Three.js. Pure audio → lip sync + expression + blink data.
200200
// ================================================================
201201

202-
const VERSION = '0.4.3';
202+
const VERSION = '0.4.4';
203203
const CDN = `https://cdn.jsdelivr.net/npm/@goodganglabs/lipsync-wasm-v1@${VERSION}`;
204204

205205
// ── All 52 ARKit blendshape channels ──

examples/vanilla-comparison/index.html

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ <h1>Anima<span>Sync</span></h1>
253253
// ================================================================
254254
// Config
255255
// ================================================================
256-
const VERSION = '0.4.3';
256+
const VERSION = '0.4.4';
257257
const CDN_V1 = `https://cdn.jsdelivr.net/npm/@goodganglabs/lipsync-wasm-v1@${VERSION}`;
258258
const CDN_V2 = `https://cdn.jsdelivr.net/npm/@goodganglabs/lipsync-wasm-v2@${VERSION}`;
259259

@@ -372,26 +372,28 @@ <h1>Anima<span>Sync</span></h1>
372372
const vrmaBytes = lipsyncInstance.getVrmaBytes();
373373
if (vrmaBytes?.idle?.length) {
374374
const anim = await loadVRMAFromBytes(vrmaBytes.idle);
375-
if (anim) { idleAct = mixer.clipAction(createVRMAnimationClip(anim, vrm)); idleAct.play(); }
375+
if (anim) { idleAct = mixer.clipAction(createVRMAnimationClip(anim, vrm)); idleAct.setLoop(THREE.LoopPingPong); idleAct.play(); }
376376
}
377377
if (vrmaBytes?.speaking?.length) {
378378
const anim = await loadVRMAFromBytes(vrmaBytes.speaking);
379-
if (anim) { speakAct = mixer.clipAction(createVRMAnimationClip(anim, vrm)); speakAct.play(); speakAct.setEffectiveWeight(0); }
379+
if (anim) { speakAct = mixer.clipAction(createVRMAnimationClip(anim, vrm)); speakAct.setLoop(THREE.LoopPingPong); speakAct.play(); speakAct.setEffectiveWeight(0); }
380380
}
381381
} catch (e) { console.warn('VRMA skip:', e.message); }
382382

383383
return { vrm, mixer, idleAction: idleAct, speakingAction: speakAct };
384384
}
385385

386-
function transitionToSpeaking() { isSpeaking = true; crossFadeProgress = 1; }
386+
function transitionToSpeaking() { isSpeaking = true; }
387387
function transitionToIdle() { isSpeaking = false; }
388388

389389
function updateBoneWeights(delta) {
390-
const speed = 3.0;
391-
if (isSpeaking && crossFadeProgress < 1) crossFadeProgress = Math.min(1, crossFadeProgress + delta * speed);
392-
else if (!isSpeaking && crossFadeProgress > 0) crossFadeProgress = Math.max(0, crossFadeProgress - delta * speed);
393-
const t = crossFadeProgress;
394-
const w = t * t * (3 - 2 * t);
390+
const target = isSpeaking ? 1 : 0;
391+
const duration = isSpeaking ? 0.8 : 1.0;
392+
const step = delta / duration;
393+
crossFadeProgress = target > crossFadeProgress
394+
? Math.min(crossFadeProgress + step, 1)
395+
: Math.max(crossFadeProgress - step, 0);
396+
const w = crossFadeProgress * crossFadeProgress * (3 - 2 * crossFadeProgress);
395397
if (idleActionV1) idleActionV1.setEffectiveWeight(1 - w);
396398
if (speakingActionV1) speakingActionV1.setEffectiveWeight(w);
397399
if (idleActionV2) idleActionV2.setEffectiveWeight(1 - w);

0 commit comments

Comments
 (0)