@streamoji/aitwin 0.1.8 → 0.2.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/README.md +62 -4
- package/dist/aitwin.cjs +2 -2
- package/dist/aitwin.js +1759 -1176
- package/dist/assets/visemeDiffPreview.worker-C2ONr8XS.js +1 -0
- package/dist/index.d.ts +100 -3
- package/package.json +5 -1
- package/dist/assets/visemeDiffPreview.worker-Ctjq6W4x.js +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(){"use strict";function et(e,t,n){return{width:e.crop?.source_width??t,height:e.crop?.source_height??n}}function nt(e,t){const n=e.crop;return{x:t?.source_x??n?.x??0,y:t?.source_y??n?.y??0,width:t?.source_w??n?.width??e.frame_width??0,height:t?.source_h??n?.height??e.frame_height??0}}function rt(e){return e?(e.w??0)>0&&(e.h??0)>0:!1}function k(e){return e.includes("/custom-faces/")}function z(e){const t=e.binBase.trim().replace(/\/$/,""),n=e.twinBase.trim().replace(/\/$/,"");if(!(e.encrypted||k(t)||k(n)))return{twinBase:n,binBase:t,encrypted:!1};const s=k(t)?t:k(n)?n:t;return{twinBase:s,binBase:s,encrypted:!0}}function m(e){return z(e).encrypted}function st(e){const t=e.trim();if(t.length%2!==0)throw new Error("Invalid hex key");const n=new Uint8Array(t.length/2);for(let r=0;r<t.length;r+=2)n[r/2]=Number.parseInt(t.slice(r,r+2),16);return n}function P(e){return Uint8Array.from(e)}function at(e,t){const n=new Uint8Array(e.length+t.length);return n.set(e,0),n.set(t,e.length),n}async function ot(e,t){if(!t)throw new Error("Asset key is required for encrypted assets");if(e.byteLength<28)throw new Error("Encrypted payload too small");const n=new Uint8Array(e),r=P(n.subarray(0,12)),s=P(n.subarray(12,28)),a=P(n.subarray(28)),o=P(at(a,s)),l=await crypto.subtle.importKey("raw",st(t),{name:"AES-GCM"},!1,["decrypt"]);return crypto.subtle.decrypt({name:"AES-GCM",iv:r,tagLength:128},l,o)}async function T(e,t){const n=await fetch(e);if(!n.ok)throw new Error(`Failed to load encrypted asset: ${e}`);const r=await n.arrayBuffer();return ot(r,t)}async function it(e,t){if(!t)throw new Error("Missing asset key for encrypted JSON");const n=await T(e,t),r=new TextDecoder().decode(n);return JSON.parse(r)}async function ct(e){const t=new Blob([e],{type:"image/webp"});return createImageBitmap(t)}async function lt(e){const t=new Blob([e],{type:"image/png"});return createImageBitmap(t)}const W=5;function K(e){const t=e?.composite_threshold;if(typeof t=="number"&&t>0)return t;const n=e?.crop?.content_threshold;return typeof n=="number"&&n>0?n:W}const x=["t01_0.17","t02_0.33","t03_0.50","t04_0.67","t05_0.83"],_t=x.length,J=new Set(["aa__kk","aa__nn","aa__sil","CH__aa","CH__DD","CH__E","CH__FF","CH__I","CH__kk","CH__nn","CH__O","CH__PP","CH__RR","CH__sil","CH__SS","CH__TH","CH__U","DD__aa","DD__E","DD__FF","DD__I","DD__kk","DD__nn","DD__O","DD__PP","DD__RR","DD__sil","DD__SS","DD__TH","DD__U","E__aa","E__FF","E__I","E__kk","E__nn","E__O","E__PP","E__RR","E__sil","E__SS","E__TH","E__U","FF__aa","FF__I","FF__kk","FF__nn","FF__O","FF__PP","FF__RR","FF__sil","FF__SS","FF__TH","FF__U","I__aa","I__kk","I__nn","I__O","I__PP","I__RR","I__sil","I__SS","I__TH","I__U","kk__nn","kk__sil","nn__sil","O__aa","O__kk","O__nn","O__PP","O__RR","O__sil","O__SS","O__TH","O__U","PP__aa","PP__kk","PP__nn","PP__RR","PP__sil","PP__SS","PP__TH","PP__U","RR__aa","RR__kk","RR__nn","RR__sil","RR__SS","RR__TH","RR__U","SS__aa","SS__kk","SS__nn","SS__sil","SS__TH","SS__U","TH__aa","TH__kk","TH__nn","TH__sil","TH__U","U__aa","U__kk","U__nn","U__sil"]);function G(e,t){return`${e}__${t}`}function j(e,t){return`pairs/${e}/${t}_minus_sil.png`}function $(e){return`${e}_minus_sil.png`}function ft(e,t){if(t.has(e))return e;const n=/^pairs\/([^/]+)\//.exec(e);if(!n)return null;const r=n[1].split("__")[1];if(!r||r==="sil")return null;const s=$(r);return t.has(s)?s:null}function ut(e,t){return e.filter(n=>t.has(n))}function dt(e,t){if(e===t)return[];const n=[],r=G(e,t);if(J.has(r)){for(const a of x)n.push(j(r,a));return n}const s=G(t,e);if(J.has(s)){for(let a=_t-1;a>=0;a--)n.push(j(s,x[a]));return n}return[]}function wt(e){return e<16?[]:e<35?[2]:e<51?[0,4]:e<67?[0,2,4]:e<83?[0,1,2,3]:[0,1,2,3,4]}function ht(e,t){return wt(t).map(r=>e[r]).filter(r=>r!=null)}function pt(e,t,n){if(!n)return`${e}/${t}`;const r=t.split("/").pop()??t,s=r.includes(".")?r.slice(0,r.lastIndexOf(".")):r;return`${e}/${s}.bin`}function yt(e,t,n,r,s=W){const a=new OffscreenCanvas(n,r),o=a.getContext("2d",{willReadFrequently:!0});o.drawImage(e,0,0,n,r);const l=o.getImageData(0,0,n,r),i=o.createImageData(n,r);i.data.set(l.data);const u=new OffscreenCanvas(n,r).getContext("2d",{willReadFrequently:!0});for(const h of t){u.clearRect(0,0,n,r),u.drawImage(h,0,0,n,r);const _=u.getImageData(0,0,n,r);for(let f=0;f<i.data.length;f+=4){const y=_.data[f],F=_.data[f+1],O=_.data[f+2];Math.max(y,F,O)>s&&(i.data[f]=y,i.data[f+1]=F,i.data[f+2]=O,i.data[f+3]=255)}}return o.putImageData(i,0,0),a}const Q=-1;function mt(e){return e<0}let g=0,X=Promise.resolve(),D=!1;function v(e){const t=X.then(e);return X=t.then(()=>{},()=>{}),t}function I(e,t){const n=t>0?-t:Q,r={type:"renderDone",requestId:e,generation:n};self.postMessage(r)}async function gt(e){const t=typeof e.liveSeq=="number"?e.liveSeq:0;if(D){I(e.requestId,t);return}if(t>0&&(g=t),!d)throw new Error("Worker not initialized");if(t>0&&t!==g){I(e.requestId,t);return}if(await S(p??void 0),t>0&&t!==g){I(e.requestId,t);return}if(await tt(d,e.diffPath,e.expressionDiffPath??null,e.threshold),t>0&&t!==g){I(e.requestId,t);return}const n=t>0?-t:Q;await U(n),I(e.requestId,t)}let d=null,E=null,p=null,q=null,A="sil";const B=new Map;function b(){if(!q)throw new Error("Worker assets not configured");return q}function Y(){const e=b();return m(e)?`${e.binBase}/sil.bin`:`${e.twinBase}/sil.png`}function Dt(){const e=b();return m(e)?`${e.binBase}/atlas.bin`:`${e.twinBase}/atlas.json`}function It(){const e=b();return m(e)?`${e.binBase}/expression_atlas.bin`:`${e.twinBase}/expression_atlas.json`}function Et(){const e=b();return m(e)?e.binBase:e.twinBase}function R(){return m(b())}async function H(e){const t=await fetch(e);if(!t.ok)throw new Error(`Failed to load image: ${e}`);const n=await t.blob();return createImageBitmap(n)}async function bt(e){return R()?lt(await T(Y(),e??"")):H(Y())}async function Z(e,t,n){const r=R(),s=r?await it(e,n):await fetch(e).then(c=>{if(!c.ok)throw new Error(`Failed to load ${e}`);return c.json()}),a=s.sheets?.[0];if(!a?.path)throw new Error(`${e} has no sheets[0].path`);const o=r?await ct(await T(pt(t,a.path,r),n??"")):await H(`${t}/${a.path}`),l=new Map;for(const c of s.cells??[])c.path&&l.set(c.path,c);const i=new Map;for(const c of s.sheets??[])i.set(c.index,c);return{atlas:o,atlasMeta:s,cellByPath:l,sheetByIndex:i,diffBase:t}}async function S(e){if(R()&&!e)throw new Error("Encrypted assets enabled but no key provided");if(E&&(!R()||p===(e??null)))return E;p=e??null,B.clear();const t=Et();return E=(async()=>{const n=await bt(e),r=await Z(Dt(),t,e);let s=null;try{s=await Z(It(),t,e)}catch{s=null}return{sil:n,viseme:r,expression:s}})(),E}function M(e){return et(e.viseme.atlasMeta,e.sil.width,e.sil.height)}function St(e,t){if(!e.sheetByIndex.get(t.sheet??0))throw new Error(`Unknown sheet index: ${t.sheet}`);const r=new OffscreenCanvas(t.w,t.h);return r.getContext("2d",{willReadFrequently:!0}).drawImage(e.atlas,t.x,t.y,t.w,t.h,0,0,t.w,t.h),r}async function Ft(e){return createImageBitmap(e)}async function V(e,t,n){const r=`${t.diffBase}::${n}`,s=B.get(r);if(s)return s;const a=(async()=>{const o=t.cellByPath.get(n);if(!o)throw new Error(`No atlas cell for path: ${n}`);const l=nt(t.atlasMeta,o),i=M(e);if(rt(o)){const h=St(t,o),_=new OffscreenCanvas(i.width,i.height),f=_.getContext("2d");return f.fillStyle="#000",f.fillRect(0,0,i.width,i.height),f.drawImage(h,l.x,l.y),Ft(_)}const c=`${t.diffBase}/${n}`,u=await H(c);if(u.width===i.width&&u.height===i.height)return u;throw u.close(),new Error(`Diff PNG wrong size for ${n}`)})();return B.set(r,a),a}function C(e,t){const n={type:"status",label:e,generation:t};self.postMessage(n)}function kt(e,t){const n={type:"error",requestId:e,message:t};self.postMessage(n)}async function U(e){if(!d)return;const t=mt(e);if(!t&&e!==w)return;const n=await createImageBitmap(d);if(!t&&e!==w){n.close();return}const r={type:"frame",bitmap:n,generation:e};self.postMessage(r,{transfer:[n]})}function Pt(e,t,n){return new Promise((r,s)=>{setTimeout(()=>{t!==n?s(new DOMException("Aborted","AbortError")):r()},e)})}async function tt(e,t,n,r){const s=await S(p??void 0),a=r??K(s.viseme.atlasMeta),{width:o,height:l}=M(s);(e.width!==o||e.height!==l)&&(e.width=o,e.height=l);const i=[];if(t){const h=new Set(s.viseme.cellByPath.keys()),_=ft(t,h)??(h.has(t)?t:null);_&&i.push(await V(s,s.viseme,_))}if(n&&s.expression){if(!s.expression.cellByPath.get(n))throw new Error(`No expression atlas cell for path: ${n}`);i.push(await V(s,s.expression,n))}const c=e.getContext("2d");if(c.clearRect(0,0,o,l),i.length===0){c.drawImage(s.sil,0,0,o,l);return}const u=yt(s.sil,i,o,l,a);c.drawImage(u,0,0)}async function N(e,t,n,r,s){await tt(e,t,n,s),await U(r)}async function L(e,t,n){A="sil";const r=await S(p??void 0);if(t!==n)throw new DOMException("Aborted","AbortError");const{width:s,height:a}=M(r);e.width=s,e.height=a;const o=e.getContext("2d");o.clearRect(0,0,s,a),o.drawImage(r.sil,0,0,s,a),await U(t)}async function At(e,t,n,r,s={}){const a=s.from??A,o=s.transitionDurationMs??400,l=s.gapMs??o,i=await S(p??void 0),c=s.threshold??K(i.viseme.atlasMeta);if(a===t){if(C(t,n),t==="sil"?await L(e,n,r):await N(e,$(t),null,n,c),n!==r)throw new DOMException("Aborted","AbortError");A=t;return}const u=dt(a,t),h=new Set(i.viseme.cellByPath.keys());let _=ht(u,l);_=ut(_,h);const f=_.length>0?o/_.length:0;_.length===0&&C(`${a} → ${t} (no pair; final only)`,n);for(let y=0;y<_.length;y++){if(n!==r)throw new DOMException("Aborted","AbortError");const F=_[y],O=F.split("/").pop()?.replace("_minus_sil.png","")??`frame ${y}`;if(C(`${a} → ${t} (${O})`,n),await N(e,F,null,n,c),n!==r)throw new DOMException("Aborted","AbortError");f>0&&await Pt(f,n,r)}if(n!==r)throw new DOMException("Aborted","AbortError");if(C(t,n),t==="sil"?await L(e,n,r):await N(e,$(t),null,n,c),n!==r)throw new DOMException("Aborted","AbortError");A=t}let w=0;self.onmessage=e=>{const t=e.data;(async()=>{try{switch(t.type){case"init":{d=new OffscreenCanvas(1,1);const n={type:"ready",requestId:t.requestId};self.postMessage(n);break}case"loadAssets":{q=z(t.urls),E=null,p=null,B.clear();const n=await S(t.keyHex),{width:r,height:s}=M(n),a={type:"loadAssetsDone",requestId:t.requestId,width:r,height:s,visemePaths:[...n.viseme.cellByPath.keys()]};self.postMessage(a);break}case"cancelLiveDraws":{g+=1;const n={type:"renderDone",requestId:t.requestId,generation:w};self.postMessage(n);break}case"drawFrame":{await v(()=>gt(t));break}case"renderSil":{await v(async()=>{D=!0;try{if(w=t.generation,!d)throw new Error("Worker not initialized");await L(d,t.generation,w)}finally{D=!1}});const n={type:"renderDone",requestId:t.requestId,generation:t.generation};self.postMessage(n);break}case"renderViseme":{await v(async()=>{D=!0;try{if(w=t.generation,!d)throw new Error("Worker not initialized");await At(d,t.to,t.generation,w,{from:t.from,transitionDurationMs:t.transitionDurationMs,gapMs:t.gapMs,threshold:t.threshold})}finally{D=!1}});const n={type:"renderDone",requestId:t.requestId,generation:t.generation};self.postMessage(n);break}default:break}}catch(n){if(n instanceof DOMException&&n.name==="AbortError"){const r={type:"renderAborted",requestId:t.requestId,generation:"generation"in t?t.generation:w};self.postMessage(r);return}kt(t.requestId,n instanceof Error?n.message:String(n))}})()}})();
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,12 @@ export declare type AiTwinHandle = {
|
|
|
11
11
|
renderViseme: (toViseme: string, options?: AiTwinRenderVisemeOptions) => Promise<void>;
|
|
12
12
|
isReady: () => boolean;
|
|
13
13
|
getStatus: () => AvatarTtsLipsyncStatus;
|
|
14
|
+
/** Connect mic + WebSocket voice session over `/ws/voice`. */
|
|
15
|
+
connect: (options?: VoiceConnectOptions) => Promise<void>;
|
|
16
|
+
/** Disconnect voice session. */
|
|
17
|
+
disconnect: () => Promise<void>;
|
|
18
|
+
/** Whether a voice WebSocket session is active. */
|
|
19
|
+
isConnected: () => boolean;
|
|
14
20
|
};
|
|
15
21
|
|
|
16
22
|
export declare class AiTwinNotFoundError extends Error {
|
|
@@ -18,11 +24,21 @@ export declare class AiTwinNotFoundError extends Error {
|
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
export declare type AiTwinProps = {
|
|
21
|
-
/** Twin id for getAiTwin (e.g. `olivia`). Omit when `assets` is set. */
|
|
27
|
+
/** Twin id for getAiTwin (e.g. `olivia`). Omit when `assets` or `avatarId`/`faceId` is set. */
|
|
22
28
|
id?: string;
|
|
29
|
+
/**
|
|
30
|
+
* 64-char hex R2 custom-faces id (pipeline `avatarId`).
|
|
31
|
+
* Loads encrypted CDN assets directly; skips getAiTwin.
|
|
32
|
+
* Alias: `faceId`.
|
|
33
|
+
*/
|
|
34
|
+
avatarId?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Same as `avatarId` — R2 custom-faces/{faceId}/ bundle id from getAiTwin or the pipeline.
|
|
37
|
+
*/
|
|
38
|
+
faceId?: string;
|
|
23
39
|
/**
|
|
24
40
|
* Preconfigured asset URLs (e.g. local `twins/blondelady`).
|
|
25
|
-
* When set, skips getAiTwin
|
|
41
|
+
* When set, skips getAiTwin and `avatarId`/`faceId`.
|
|
26
42
|
*/
|
|
27
43
|
assets?: TwinAssetUrls;
|
|
28
44
|
/**
|
|
@@ -30,7 +46,7 @@ export declare type AiTwinProps = {
|
|
|
30
46
|
* When omitted, a dev token is fetched via getAuthToken.
|
|
31
47
|
*/
|
|
32
48
|
authToken?: string;
|
|
33
|
-
/** TTS engine when using `assets` (default:
|
|
49
|
+
/** TTS engine when using `assets` or `avatarId`/`faceId` (default: Cartesia). */
|
|
34
50
|
ttsEngineId?: TtsEngineId;
|
|
35
51
|
/** TTS voice id override (engine-specific). */
|
|
36
52
|
voiceId?: string;
|
|
@@ -48,6 +64,12 @@ export declare type AiTwinProps = {
|
|
|
48
64
|
onReady?: () => void;
|
|
49
65
|
/** When false, errors only go to `onError` (no overlay on the canvas). Default: true. */
|
|
50
66
|
showErrorOverlay?: boolean;
|
|
67
|
+
/** User speech transcript from voice STT (partial and final). */
|
|
68
|
+
onUserTranscript?: (text: string, final: boolean) => void;
|
|
69
|
+
/** Bot LLM text from voice session. */
|
|
70
|
+
onBotOutput?: (text: string) => void;
|
|
71
|
+
/** Voice lipsync debug (viseme segments, clock, queue). */
|
|
72
|
+
onRealtimeLipsyncDebug?: (snapshot: RealtimeLipsyncDebugSnapshot) => void;
|
|
51
73
|
};
|
|
52
74
|
|
|
53
75
|
export declare type AiTwinRecord = {
|
|
@@ -93,21 +115,78 @@ export declare type AvatarTtsSpeakOptions = {
|
|
|
93
115
|
ttsEngineId?: TtsEngineId;
|
|
94
116
|
};
|
|
95
117
|
|
|
118
|
+
export declare function buildVoiceWebSocketUrl(options: RealtimeVoiceOptions): string;
|
|
119
|
+
|
|
96
120
|
export declare function createAvatarTtsLipsyncController(onStatus: (status: AvatarTtsLipsyncStatus) => void, apiBase?: string): AvatarTtsLipsyncController;
|
|
97
121
|
|
|
122
|
+
export declare function createRealtimeVoiceLipsyncController(onStatus: (status: AvatarTtsLipsyncStatus) => void, initialOptions?: RealtimeVoiceOptions): RealtimeVoiceLipsyncController;
|
|
123
|
+
|
|
98
124
|
export declare const DEFAULT_TTS_ENGINE_ID: TtsEngineId;
|
|
99
125
|
|
|
126
|
+
export declare function faceAssetUrls(faceId: string, cdnBase?: string): TwinAssetUrls;
|
|
127
|
+
|
|
100
128
|
export declare function fetchAiTwin(id: string, baseUrl?: string): Promise<AiTwinRecord>;
|
|
101
129
|
|
|
102
130
|
/** Bearer token for /avatar_ttsWithPoses and /api/session-value. */
|
|
103
131
|
export declare function fetchDevAuthToken(): Promise<string>;
|
|
104
132
|
|
|
133
|
+
export declare const MIC_SAMPLE_RATE = 16000;
|
|
134
|
+
|
|
105
135
|
/** Google TTS streams Oculus viseme symbols; normalize casing and unknown ids. */
|
|
106
136
|
export declare function normalizeOculusViseme(symbol: string | null | undefined): string;
|
|
107
137
|
|
|
108
138
|
/** Oculus viseme ids for Talking Lady stills and pair transitions (15 standard). */
|
|
109
139
|
export declare const OCULUS_VISEME_IDS: readonly ["aa", "CH", "DD", "E", "FF", "I", "O", "PP", "RR", "SS", "TH", "U", "kk", "nn", "sil"];
|
|
110
140
|
|
|
141
|
+
export declare const PLAYBACK_SAMPLE_RATE = 24000;
|
|
142
|
+
|
|
143
|
+
export declare type RealtimeLipsyncDebugSnapshot = {
|
|
144
|
+
event: "metadata" | "audio_chunk" | "lipsync" | "bot_started" | "bot_stopped" | "user_started" | "reset";
|
|
145
|
+
spokenText: string;
|
|
146
|
+
mood: string;
|
|
147
|
+
/** performance.now() anchor — elapsed = performance.now() - playbackAnchorPerf */
|
|
148
|
+
playbackAnchorPerf: number;
|
|
149
|
+
playbackElapsedMs: number;
|
|
150
|
+
queueLength: number;
|
|
151
|
+
activeViseme: string;
|
|
152
|
+
segments: RealtimeLipsyncSegmentDebug[];
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export declare type RealtimeLipsyncSegmentDebug = {
|
|
156
|
+
spokenText: string;
|
|
157
|
+
audioOffsetMs: number;
|
|
158
|
+
isNewSegment: boolean;
|
|
159
|
+
chunkDurationMs: number | null;
|
|
160
|
+
visemes: RealtimeVisemeDebugEntry[];
|
|
161
|
+
receivedAtMs: number;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export declare type RealtimeVisemeDebugEntry = {
|
|
165
|
+
rawSymbol: string;
|
|
166
|
+
viseme: string;
|
|
167
|
+
startSec: number;
|
|
168
|
+
durationSec: number;
|
|
169
|
+
vtimeMs: number;
|
|
170
|
+
vdurationMs: number;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export declare type RealtimeVoiceLipsyncController = AvatarTtsLipsyncController & {
|
|
174
|
+
connect: (connectOptions?: VoiceConnectOptions) => Promise<void>;
|
|
175
|
+
disconnect: () => Promise<void>;
|
|
176
|
+
isConnected: () => boolean;
|
|
177
|
+
/** Resume AudioContext — call from a user gesture before connect. */
|
|
178
|
+
unlockAudio: () => Promise<void>;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export declare type RealtimeVoiceOptions = VoiceConnectOptions & {
|
|
182
|
+
onUserTranscript?: (text: string, final: boolean) => void;
|
|
183
|
+
onBotOutput?: (text: string) => void;
|
|
184
|
+
onLipsyncDebug?: (snapshot: RealtimeLipsyncDebugSnapshot) => void;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/** Resolve optional `avatarId` / `faceId` props (same R2 custom-faces hex id). */
|
|
188
|
+
export declare function resolveDirectFaceId(avatarId?: string, faceId?: string): string | undefined;
|
|
189
|
+
|
|
111
190
|
/** Pick the active viseme at playback offset (latest vtime wins on overlap). */
|
|
112
191
|
export declare function resolveVisemeAtTime(queue: VisemeQueueItem[], elapsedMs: number): string;
|
|
113
192
|
|
|
@@ -123,6 +202,8 @@ export declare interface SentenceEmotion {
|
|
|
123
202
|
end_word: number;
|
|
124
203
|
}
|
|
125
204
|
|
|
205
|
+
export declare function setActiveTwinAssets(urls: TwinAssetUrls): void;
|
|
206
|
+
|
|
126
207
|
export declare const SPEAKING_RATE_MAX = 1.5;
|
|
127
208
|
|
|
128
209
|
export declare const SPEAKING_RATE_MIN = 0.5;
|
|
@@ -162,6 +243,20 @@ export declare interface VisemeQueueItem {
|
|
|
162
243
|
vduration: number;
|
|
163
244
|
}
|
|
164
245
|
|
|
246
|
+
export declare type VoiceConnectOptions = {
|
|
247
|
+
/** Same JWT as SSE `Authorization: Bearer` (same as `authToken` prop on `<AiTwin>`). */
|
|
248
|
+
authToken?: string;
|
|
249
|
+
/** Cartesia voice UUID override (optional). */
|
|
250
|
+
voiceId?: string;
|
|
251
|
+
speakingRate?: number;
|
|
252
|
+
/** Default true — server streams TTS + avatar messages. */
|
|
253
|
+
ttsStream?: boolean;
|
|
254
|
+
/** HTTP API base; default `https://ai.streamoji.com`. */
|
|
255
|
+
apiBase?: string;
|
|
256
|
+
/** Full WebSocket URL override. Query params must be included. */
|
|
257
|
+
wsUrl?: string;
|
|
258
|
+
};
|
|
259
|
+
|
|
165
260
|
export declare interface WordQueueItem {
|
|
166
261
|
word: string;
|
|
167
262
|
wtime: number;
|
|
@@ -170,6 +265,8 @@ export declare interface WordQueueItem {
|
|
|
170
265
|
queueIndex: number;
|
|
171
266
|
gesture?: string | null;
|
|
172
267
|
emotion?: string;
|
|
268
|
+
/** When true, raise eyebrows +6 (capped at +20) for this word's duration. */
|
|
269
|
+
emphasis?: boolean;
|
|
173
270
|
}
|
|
174
271
|
|
|
175
272
|
export { }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@streamoji/aitwin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Embeddable React AI twin face with TTS lipsync",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/aitwin.cjs",
|
|
@@ -46,5 +46,9 @@
|
|
|
46
46
|
"vite": "^7.2.4",
|
|
47
47
|
"vite-plugin-dts": "^4.5.4",
|
|
48
48
|
"vitest": "^4.1.8"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@pipecat-ai/client-js": "^1.11.0",
|
|
52
|
+
"@pipecat-ai/websocket-transport": "^1.6.7"
|
|
49
53
|
}
|
|
50
54
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
(function(){"use strict";function Y(e,t,n){return{width:e.crop?.source_width??t,height:e.crop?.source_height??n}}function Z(e,t){const n=e.crop;return{x:t?.source_x??n?.x??0,y:t?.source_y??n?.y??0,width:t?.source_w??n?.width??e.frame_width??0,height:t?.source_h??n?.height??e.frame_height??0}}function V(e){return e?(e.w??0)>0&&(e.h??0)>0:!1}function tt(e){const t=e.trim();if(t.length%2!==0)throw new Error("Invalid hex key");const n=new Uint8Array(t.length/2);for(let r=0;r<t.length;r+=2)n[r/2]=Number.parseInt(t.slice(r,r+2),16);return n}function b(e){return Uint8Array.from(e)}function et(e,t){const n=new Uint8Array(e.length+t.length);return n.set(e,0),n.set(t,e.length),n}async function nt(e,t){if(!t)throw new Error("Asset key is required for encrypted assets");if(e.byteLength<28)throw new Error("Encrypted payload too small");const n=new Uint8Array(e),r=b(n.subarray(0,12)),s=b(n.subarray(12,28)),a=b(n.subarray(28)),o=b(et(a,s)),c=await crypto.subtle.importKey("raw",tt(t),{name:"AES-GCM"},!1,["decrypt"]);return crypto.subtle.decrypt({name:"AES-GCM",iv:r,tagLength:128},c,o)}async function O(e,t){const n=await fetch(e);if(!n.ok)throw new Error(`Failed to load encrypted asset: ${e}`);const r=await n.arrayBuffer();return nt(r,t)}async function rt(e,t){if(!t)throw new Error("Missing asset key for encrypted JSON");const n=await O(e,t),r=new TextDecoder().decode(n);return JSON.parse(r)}async function st(e){const t=new Blob([e],{type:"image/webp"});return createImageBitmap(t)}async function at(e){const t=new Blob([e],{type:"image/png"});return createImageBitmap(t)}const y=15,C=["t01_0.17","t02_0.33","t03_0.50","t04_0.67","t05_0.83"],ot=C.length,U=new Set(["aa__kk","aa__nn","aa__sil","CH__aa","CH__DD","CH__E","CH__FF","CH__I","CH__kk","CH__nn","CH__O","CH__PP","CH__RR","CH__sil","CH__SS","CH__TH","CH__U","DD__aa","DD__E","DD__FF","DD__I","DD__kk","DD__nn","DD__O","DD__PP","DD__RR","DD__sil","DD__SS","DD__TH","DD__U","E__aa","E__FF","E__I","E__kk","E__nn","E__O","E__PP","E__RR","E__sil","E__SS","E__TH","E__U","FF__aa","FF__I","FF__kk","FF__nn","FF__O","FF__PP","FF__RR","FF__sil","FF__SS","FF__TH","FF__U","I__aa","I__kk","I__nn","I__O","I__PP","I__RR","I__sil","I__SS","I__TH","I__U","kk__nn","kk__sil","nn__sil","O__aa","O__kk","O__nn","O__PP","O__RR","O__sil","O__SS","O__TH","O__U","PP__aa","PP__kk","PP__nn","PP__RR","PP__sil","PP__SS","PP__TH","PP__U","RR__aa","RR__kk","RR__nn","RR__sil","RR__SS","RR__TH","RR__U","SS__aa","SS__kk","SS__nn","SS__sil","SS__TH","SS__U","TH__aa","TH__kk","TH__nn","TH__sil","TH__U","U__aa","U__kk","U__nn","U__sil"]);function v(e,t){return`${e}__${t}`}function N(e,t){return`pairs/${e}/${t}_minus_sil.png`}function L(e){return`${e}_minus_sil.png`}function it(e,t){if(e===t)return[];const n=[],r=v(e,t);if(U.has(r)){for(const a of C)n.push(N(r,a));return n}const s=v(t,e);if(U.has(s)){for(let a=ot-1;a>=0;a--)n.push(N(s,C[a]));return n}return[]}function ct(e){return e<16?[]:e<35?[2]:e<51?[0,4]:e<67?[0,2,4]:e<83?[0,1,2,3]:[0,1,2,3,4]}function _t(e,t){return ct(t).map(r=>e[r]).filter(r=>r!=null)}function lt(e,t,n){if(!n)return`${e}/${t}`;const r=t.split("/").pop()??t,s=r.includes(".")?r.slice(0,r.lastIndexOf(".")):r;return`${e}/${s}.bin`}function ft(e,t,n,r,s=y){const a=new OffscreenCanvas(n,r),o=a.getContext("2d",{willReadFrequently:!0});o.drawImage(e,0,0,n,r);const c=o.getImageData(0,0,n,r),i=o.createImageData(n,r);i.data.set(c.data);const f=new OffscreenCanvas(n,r).getContext("2d",{willReadFrequently:!0});for(const p of t){f.clearRect(0,0,n,r),f.drawImage(p,0,0,n,r);const u=f.getImageData(0,0,n,r);for(let l=0;l<i.data.length;l+=4){const M=u.data[l],Q=u.data[l+1],X=u.data[l+2];Math.max(M,Q,X)>s&&(i.data[l]=M,i.data[l+1]=Q,i.data[l+2]=X,i.data[l+3]=255)}}return o.putImageData(i,0,0),a}const W=-1;function ut(e){return e<0}let g=0,z=Promise.resolve(),m=!1;function x(e){const t=z.then(e);return z=t.then(()=>{},()=>{}),t}function D(e,t){const n=t>0?-t:W,r={type:"renderDone",requestId:e,generation:n};self.postMessage(r)}async function dt(e){const t=typeof e.liveSeq=="number"?e.liveSeq:0;if(m){D(e.requestId,t);return}if(t>0&&(g=t),!d)throw new Error("Worker not initialized");if(t>0&&t!==g){D(e.requestId,t);return}if(await S(h??void 0),t>0&&t!==g){D(e.requestId,t);return}if(await j(d,e.diffPath,e.expressionDiffPath??null,e.threshold??y),t>0&&t!==g){D(e.requestId,t);return}const n=t>0?-t:W;await T(n),D(e.requestId,t)}let d=null,I=null,h=null,$=null,F="sil";const k=new Map;function E(){if(!$)throw new Error("Worker assets not configured");return $}function K(){const e=E();return e.encrypted?`${e.binBase}/sil.bin`:`${e.twinBase}/sil.png`}function wt(){const e=E();return e.encrypted?`${e.binBase}/atlas.bin`:`${e.twinBase}/atlas.json`}function ht(){const e=E();return e.encrypted?`${e.binBase}/expression_atlas.bin`:`${e.twinBase}/expression_atlas.json`}function pt(){const e=E();return e.encrypted?e.binBase:e.twinBase}function R(){return E().encrypted}async function B(e){const t=await fetch(e);if(!t.ok)throw new Error(`Failed to load image: ${e}`);const n=await t.blob();return createImageBitmap(n)}async function yt(e){return R()?at(await O(K(),e??"")):B(K())}async function J(e,t,n){const r=R(),s=r?await rt(e,n):await fetch(e).then(_=>{if(!_.ok)throw new Error(`Failed to load ${e}`);return _.json()}),a=s.sheets?.[0];if(!a?.path)throw new Error(`${e} has no sheets[0].path`);const o=r?await st(await O(lt(t,a.path,r),n??"")):await B(`${t}/${a.path}`),c=new Map;for(const _ of s.cells??[])_.path&&c.set(_.path,_);const i=new Map;for(const _ of s.sheets??[])i.set(_.index,_);return{atlas:o,atlasMeta:s,cellByPath:c,sheetByIndex:i,diffBase:t}}async function S(e){if(R()&&!e)throw new Error("Encrypted assets enabled but no key provided");if(I&&(!R()||h===(e??null)))return I;h=e??null,k.clear();const t=pt();return I=(async()=>{const n=await yt(e),r=await J(wt(),t,e);let s=null;try{s=await J(ht(),t,e)}catch{s=null}return{sil:n,viseme:r,expression:s}})(),I}function A(e){return Y(e.viseme.atlasMeta,e.sil.width,e.sil.height)}function gt(e,t){if(!e.sheetByIndex.get(t.sheet??0))throw new Error(`Unknown sheet index: ${t.sheet}`);const r=new OffscreenCanvas(t.w,t.h);return r.getContext("2d",{willReadFrequently:!0}).drawImage(e.atlas,t.x,t.y,t.w,t.h,0,0,t.w,t.h),r}async function mt(e){return createImageBitmap(e)}async function G(e,t,n){const r=`${t.diffBase}::${n}`,s=k.get(r);if(s)return s;const a=(async()=>{const o=t.cellByPath.get(n);if(!o)throw new Error(`No atlas cell for path: ${n}`);const c=Z(t.atlasMeta,o),i=A(e);if(V(o)){const p=gt(t,o),u=new OffscreenCanvas(i.width,i.height),l=u.getContext("2d");return l.fillStyle="#000",l.fillRect(0,0,i.width,i.height),l.drawImage(p,c.x,c.y),mt(u)}const _=`${t.diffBase}/${n}`,f=await B(_);if(f.width===i.width&&f.height===i.height)return f;throw f.close(),new Error(`Diff PNG wrong size for ${n}`)})();return k.set(r,a),a}function P(e,t){const n={type:"status",label:e,generation:t};self.postMessage(n)}function Dt(e,t){const n={type:"error",requestId:e,message:t};self.postMessage(n)}async function T(e){if(!d)return;const t=ut(e);if(!t&&e!==w)return;const n=await createImageBitmap(d);if(!t&&e!==w){n.close();return}const r={type:"frame",bitmap:n,generation:e};self.postMessage(r,{transfer:[n]})}function It(e,t,n){return new Promise((r,s)=>{setTimeout(()=>{t!==n?s(new DOMException("Aborted","AbortError")):r()},e)})}async function j(e,t,n,r=y){const s=await S(h??void 0),{width:a,height:o}=A(s);(e.width!==a||e.height!==o)&&(e.width=a,e.height=o);const c=[];if(t){if(!s.viseme.cellByPath.get(t))throw new Error(`No viseme atlas cell for path: ${t}`);c.push(await G(s,s.viseme,t))}if(n&&s.expression){if(!s.expression.cellByPath.get(n))throw new Error(`No expression atlas cell for path: ${n}`);c.push(await G(s,s.expression,n))}const i=e.getContext("2d");if(i.clearRect(0,0,a,o),c.length===0){i.drawImage(s.sil,0,0,a,o);return}const _=ft(s.sil,c,a,o,r);i.drawImage(_,0,0)}async function q(e,t,n,r,s=y){await j(e,t,n,s),await T(r)}async function H(e,t,n){F="sil";const r=await S(h??void 0);if(t!==n)throw new DOMException("Aborted","AbortError");const{width:s,height:a}=A(r);e.width=s,e.height=a;const o=e.getContext("2d");o.clearRect(0,0,s,a),o.drawImage(r.sil,0,0,s,a),await T(t)}async function Et(e,t,n,r,s={}){const a=s.from??F,o=s.transitionDurationMs??400,c=s.gapMs??o,i=s.threshold??y;if(await S(h??void 0),a===t){if(P(t,n),t==="sil"?await H(e,n,r):await q(e,L(t),null,n,i),n!==r)throw new DOMException("Aborted","AbortError");F=t;return}const _=it(a,t),f=_t(_,c),p=f.length>0?o/f.length:0;f.length===0&&P(`${a} → ${t} (no pair; final only)`,n);for(let u=0;u<f.length;u++){if(n!==r)throw new DOMException("Aborted","AbortError");const l=f[u],M=l.split("/").pop()?.replace("_minus_sil.png","")??`frame ${u}`;if(P(`${a} → ${t} (${M})`,n),await q(e,l,null,n,i),n!==r)throw new DOMException("Aborted","AbortError");p>0&&await It(p,n,r)}if(n!==r)throw new DOMException("Aborted","AbortError");if(P(t,n),t==="sil"?await H(e,n,r):await q(e,L(t),null,n,i),n!==r)throw new DOMException("Aborted","AbortError");F=t}let w=0;self.onmessage=e=>{const t=e.data;(async()=>{try{switch(t.type){case"init":{d=new OffscreenCanvas(1,1);const n={type:"ready",requestId:t.requestId};self.postMessage(n);break}case"loadAssets":{$=t.urls,I=null,h=null,k.clear();const n=await S(t.keyHex),{width:r,height:s}=A(n),a={type:"loadAssetsDone",requestId:t.requestId,width:r,height:s};self.postMessage(a);break}case"cancelLiveDraws":{g+=1;const n={type:"renderDone",requestId:t.requestId,generation:w};self.postMessage(n);break}case"drawFrame":{await x(()=>dt(t));break}case"renderSil":{await x(async()=>{m=!0;try{if(w=t.generation,!d)throw new Error("Worker not initialized");await H(d,t.generation,w)}finally{m=!1}});const n={type:"renderDone",requestId:t.requestId,generation:t.generation};self.postMessage(n);break}case"renderViseme":{await x(async()=>{m=!0;try{if(w=t.generation,!d)throw new Error("Worker not initialized");await Et(d,t.to,t.generation,w,{from:t.from,transitionDurationMs:t.transitionDurationMs,gapMs:t.gapMs,threshold:t.threshold})}finally{m=!1}});const n={type:"renderDone",requestId:t.requestId,generation:t.generation};self.postMessage(n);break}default:break}}catch(n){if(n instanceof DOMException&&n.name==="AbortError"){const r={type:"renderAborted",requestId:t.requestId,generation:"generation"in t?t.generation:w};self.postMessage(r);return}Dt(t.requestId,n instanceof Error?n.message:String(n))}})()}})();
|