@streamoji/aitwin 0.1.7 → 0.1.9
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 +58 -2
- package/dist/aitwin.cjs +2 -2
- package/dist/aitwin.js +1792 -1111
- package/dist/assets/visemeDiffPreview.worker-C2ONr8XS.js +1 -0
- package/dist/index.d.ts +80 -0
- package/package.json +5 -1
- package/dist/assets/visemeDiffPreview.worker-zI0svOaa.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 {
|
|
@@ -48,6 +54,12 @@ export declare type AiTwinProps = {
|
|
|
48
54
|
onReady?: () => void;
|
|
49
55
|
/** When false, errors only go to `onError` (no overlay on the canvas). Default: true. */
|
|
50
56
|
showErrorOverlay?: boolean;
|
|
57
|
+
/** User speech transcript from voice STT (partial and final). */
|
|
58
|
+
onUserTranscript?: (text: string, final: boolean) => void;
|
|
59
|
+
/** Bot LLM text from voice session. */
|
|
60
|
+
onBotOutput?: (text: string) => void;
|
|
61
|
+
/** Voice lipsync debug (viseme segments, clock, queue). */
|
|
62
|
+
onRealtimeLipsyncDebug?: (snapshot: RealtimeLipsyncDebugSnapshot) => void;
|
|
51
63
|
};
|
|
52
64
|
|
|
53
65
|
export declare type AiTwinRecord = {
|
|
@@ -93,8 +105,12 @@ export declare type AvatarTtsSpeakOptions = {
|
|
|
93
105
|
ttsEngineId?: TtsEngineId;
|
|
94
106
|
};
|
|
95
107
|
|
|
108
|
+
export declare function buildVoiceWebSocketUrl(options: RealtimeVoiceOptions): string;
|
|
109
|
+
|
|
96
110
|
export declare function createAvatarTtsLipsyncController(onStatus: (status: AvatarTtsLipsyncStatus) => void, apiBase?: string): AvatarTtsLipsyncController;
|
|
97
111
|
|
|
112
|
+
export declare function createRealtimeVoiceLipsyncController(onStatus: (status: AvatarTtsLipsyncStatus) => void, initialOptions?: RealtimeVoiceOptions): RealtimeVoiceLipsyncController;
|
|
113
|
+
|
|
98
114
|
export declare const DEFAULT_TTS_ENGINE_ID: TtsEngineId;
|
|
99
115
|
|
|
100
116
|
export declare function fetchAiTwin(id: string, baseUrl?: string): Promise<AiTwinRecord>;
|
|
@@ -102,12 +118,60 @@ export declare function fetchAiTwin(id: string, baseUrl?: string): Promise<AiTwi
|
|
|
102
118
|
/** Bearer token for /avatar_ttsWithPoses and /api/session-value. */
|
|
103
119
|
export declare function fetchDevAuthToken(): Promise<string>;
|
|
104
120
|
|
|
121
|
+
export declare const MIC_SAMPLE_RATE = 16000;
|
|
122
|
+
|
|
105
123
|
/** Google TTS streams Oculus viseme symbols; normalize casing and unknown ids. */
|
|
106
124
|
export declare function normalizeOculusViseme(symbol: string | null | undefined): string;
|
|
107
125
|
|
|
108
126
|
/** Oculus viseme ids for Talking Lady stills and pair transitions (15 standard). */
|
|
109
127
|
export declare const OCULUS_VISEME_IDS: readonly ["aa", "CH", "DD", "E", "FF", "I", "O", "PP", "RR", "SS", "TH", "U", "kk", "nn", "sil"];
|
|
110
128
|
|
|
129
|
+
export declare const PLAYBACK_SAMPLE_RATE = 24000;
|
|
130
|
+
|
|
131
|
+
export declare type RealtimeLipsyncDebugSnapshot = {
|
|
132
|
+
event: "metadata" | "audio_chunk" | "lipsync" | "bot_started" | "bot_stopped" | "user_started" | "reset";
|
|
133
|
+
spokenText: string;
|
|
134
|
+
mood: string;
|
|
135
|
+
/** performance.now() anchor — elapsed = performance.now() - playbackAnchorPerf */
|
|
136
|
+
playbackAnchorPerf: number;
|
|
137
|
+
playbackElapsedMs: number;
|
|
138
|
+
queueLength: number;
|
|
139
|
+
activeViseme: string;
|
|
140
|
+
segments: RealtimeLipsyncSegmentDebug[];
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export declare type RealtimeLipsyncSegmentDebug = {
|
|
144
|
+
spokenText: string;
|
|
145
|
+
audioOffsetMs: number;
|
|
146
|
+
isNewSegment: boolean;
|
|
147
|
+
chunkDurationMs: number | null;
|
|
148
|
+
visemes: RealtimeVisemeDebugEntry[];
|
|
149
|
+
receivedAtMs: number;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export declare type RealtimeVisemeDebugEntry = {
|
|
153
|
+
rawSymbol: string;
|
|
154
|
+
viseme: string;
|
|
155
|
+
startSec: number;
|
|
156
|
+
durationSec: number;
|
|
157
|
+
vtimeMs: number;
|
|
158
|
+
vdurationMs: number;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export declare type RealtimeVoiceLipsyncController = AvatarTtsLipsyncController & {
|
|
162
|
+
connect: (connectOptions?: VoiceConnectOptions) => Promise<void>;
|
|
163
|
+
disconnect: () => Promise<void>;
|
|
164
|
+
isConnected: () => boolean;
|
|
165
|
+
/** Resume AudioContext — call from a user gesture before connect. */
|
|
166
|
+
unlockAudio: () => Promise<void>;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export declare type RealtimeVoiceOptions = VoiceConnectOptions & {
|
|
170
|
+
onUserTranscript?: (text: string, final: boolean) => void;
|
|
171
|
+
onBotOutput?: (text: string) => void;
|
|
172
|
+
onLipsyncDebug?: (snapshot: RealtimeLipsyncDebugSnapshot) => void;
|
|
173
|
+
};
|
|
174
|
+
|
|
111
175
|
/** Pick the active viseme at playback offset (latest vtime wins on overlap). */
|
|
112
176
|
export declare function resolveVisemeAtTime(queue: VisemeQueueItem[], elapsedMs: number): string;
|
|
113
177
|
|
|
@@ -162,6 +226,20 @@ export declare interface VisemeQueueItem {
|
|
|
162
226
|
vduration: number;
|
|
163
227
|
}
|
|
164
228
|
|
|
229
|
+
export declare type VoiceConnectOptions = {
|
|
230
|
+
/** Same JWT as SSE `Authorization: Bearer` (same as `authToken` prop on `<AiTwin>`). */
|
|
231
|
+
authToken?: string;
|
|
232
|
+
/** Cartesia voice UUID override (optional). */
|
|
233
|
+
voiceId?: string;
|
|
234
|
+
speakingRate?: number;
|
|
235
|
+
/** Default true — server streams TTS + avatar messages. */
|
|
236
|
+
ttsStream?: boolean;
|
|
237
|
+
/** HTTP API base; default `https://ai.streamoji.com`. */
|
|
238
|
+
apiBase?: string;
|
|
239
|
+
/** Full WebSocket URL override. Query params must be included. */
|
|
240
|
+
wsUrl?: string;
|
|
241
|
+
};
|
|
242
|
+
|
|
165
243
|
export declare interface WordQueueItem {
|
|
166
244
|
word: string;
|
|
167
245
|
wtime: number;
|
|
@@ -170,6 +248,8 @@ export declare interface WordQueueItem {
|
|
|
170
248
|
queueIndex: number;
|
|
171
249
|
gesture?: string | null;
|
|
172
250
|
emotion?: string;
|
|
251
|
+
/** When true, raise eyebrows +6 (capped at +20) for this word's duration. */
|
|
252
|
+
emphasis?: boolean;
|
|
173
253
|
}
|
|
174
254
|
|
|
175
255
|
export { }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@streamoji/aitwin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
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 j(e,t,n){return{width:e.crop?.source_width??t,height:e.crop?.source_height??n}}function Q(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 X(e){return e?(e.w??0)>0&&(e.h??0)>0:!1}function Y(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 I(e){return Uint8Array.from(e)}function Z(e,t){const n=new Uint8Array(e.length+t.length);return n.set(e,0),n.set(t,e.length),n}async function V(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=I(n.subarray(0,12)),s=I(n.subarray(12,28)),a=I(n.subarray(28)),o=I(Z(a,s)),_=await crypto.subtle.importKey("raw",Y(t),{name:"AES-GCM"},!1,["decrypt"]);return crypto.subtle.decrypt({name:"AES-GCM",iv:r,tagLength:128},_,o)}async function M(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 V(r,t)}async function tt(e,t){if(!t)throw new Error("Missing asset key for encrypted JSON");const n=await M(e,t),r=new TextDecoder().decode(n);return JSON.parse(r)}async function et(e){const t=new Blob([e],{type:"image/webp"});return createImageBitmap(t)}async function nt(e){const t=new Blob([e],{type:"image/png"});return createImageBitmap(t)}const p=15,C=["t01_0.17","t02_0.33","t03_0.50","t04_0.67","t05_0.83"],rt=C.length,H=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 U(e,t){return`pairs/${e}/${t}_minus_sil.png`}function q(e){return`${e}_minus_sil.png`}function st(e,t){if(e===t)return[];const n=[],r=v(e,t);if(H.has(r)){for(const a of C)n.push(U(r,a));return n}const s=v(t,e);if(H.has(s)){for(let a=rt-1;a>=0;a--)n.push(U(s,C[a]));return n}return[]}function at(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 ot(e,t){return at(t).map(r=>e[r]).filter(r=>r!=null)}function it(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 ct(e,t,n,r,s=p){const a=new OffscreenCanvas(n,r),o=a.getContext("2d",{willReadFrequently:!0});o.drawImage(e,0,0,n,r);const _=o.getImageData(0,0,n,r),i=o.createImageData(n,r);i.data.set(_.data);const f=new OffscreenCanvas(n,r).getContext("2d",{willReadFrequently:!0});for(const D of t){f.clearRect(0,0,n,r),f.drawImage(D,0,0,n,r);const d=f.getImageData(0,0,n,r);for(let c=0;c<i.data.length;c+=4){const E=d.data[c],O=d.data[c+1],G=d.data[c+2];Math.max(E,O,G)>s&&(i.data[c]=E,i.data[c+1]=O,i.data[c+2]=G,i.data[c+3]=255)}}return o.putImageData(i,0,0),a}const N=-1;function _t(e){return e<0}let S=0,L=Promise.resolve();function b(e,t){const n=t>0?-t:N,r={type:"renderDone",requestId:e,generation:n};self.postMessage(r)}async function lt(e){const t=typeof e.liveSeq=="number"?e.liveSeq:0;if(t>0&&(S=t),!u)throw new Error("Worker not initialized");if(t>0&&t!==S){b(e.requestId,t);return}if(await m(w??void 0),t>0&&t!==S){b(e.requestId,t);return}if(await J(u,e.diffPath,e.expressionDiffPath??null,e.threshold??p),t>0&&t!==S){b(e.requestId,t);return}const n=t>0?-t:N;await B(n),b(e.requestId,t)}let u=null,y=null,w=null,x=null,F="sil";const k=new Map;function g(){if(!x)throw new Error("Worker assets not configured");return x}function W(){const e=g();return e.encrypted?`${e.binBase}/sil.bin`:`${e.twinBase}/sil.png`}function ft(){const e=g();return e.encrypted?`${e.binBase}/atlas.bin`:`${e.twinBase}/atlas.json`}function ut(){const e=g();return e.encrypted?`${e.binBase}/expression_atlas.bin`:`${e.twinBase}/expression_atlas.json`}function dt(){const e=g();return e.encrypted?e.binBase:e.twinBase}function R(){return g().encrypted}async function $(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 z(e,t,n){const r=R(),s=r?await tt(e,n):await fetch(e).then(l=>{if(!l.ok)throw new Error(`Failed to load ${e}`);return l.json()}),a=s.sheets?.[0];if(!a?.path)throw new Error(`${e} has no sheets[0].path`);const o=r?await et(await M(it(t,a.path,r),n??"")):await $(`${t}/${a.path}`),_=new Map;for(const l of s.cells??[])l.path&&_.set(l.path,l);const i=new Map;for(const l of s.sheets??[])i.set(l.index,l);return{atlas:o,atlasMeta:s,cellByPath:_,sheetByIndex:i,diffBase:t}}async function m(e){if(R()&&!e)throw new Error("Encrypted assets enabled but no key provided");if(y&&(!R()||w===(e??null)))return y;w=e??null,k.clear();const t=dt();return y=(async()=>{const n=R()?await nt(await M(W(),e??"")):await $(W()),r=await z(ft(),t,e);let s=null;try{s=await z(ut(),t,e)}catch{s=null}return{sil:n,viseme:r,expression:s}})(),y}function A(e){return j(e.viseme.atlasMeta,e.sil.width,e.sil.height)}function wt(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 ht(e){return createImageBitmap(e)}async function K(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 _=Q(t.atlasMeta,o),i=A(e);if(X(o)){const D=wt(t,o),d=new OffscreenCanvas(i.width,i.height),c=d.getContext("2d");return c.fillStyle="#000",c.fillRect(0,0,i.width,i.height),c.drawImage(D,_.x,_.y),ht(d)}const l=`${t.diffBase}/${n}`,f=await $(l);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 pt(e,t){const n={type:"error",requestId:e,message:t};self.postMessage(n)}async function B(e){if(!u)return;const t=_t(e);if(!t&&e!==h)return;const n=await createImageBitmap(u);if(!t&&e!==h){n.close();return}const r={type:"frame",bitmap:n,generation:e};self.postMessage(r,{transfer:[n]})}function yt(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=p){const s=await m(w??void 0),{width:a,height:o}=A(s);(e.width!==a||e.height!==o)&&(e.width=a,e.height=o);const _=[];if(t){if(!s.viseme.cellByPath.get(t))throw new Error(`No viseme atlas cell for path: ${t}`);_.push(await K(s,s.viseme,t))}if(n&&s.expression){if(!s.expression.cellByPath.get(n))throw new Error(`No expression atlas cell for path: ${n}`);_.push(await K(s,s.expression,n))}const i=e.getContext("2d");if(i.clearRect(0,0,a,o),_.length===0){i.drawImage(s.sil,0,0,a,o);return}const l=ct(s.sil,_,a,o,r);i.drawImage(l,0,0)}async function T(e,t,n,r,s=p){await J(e,t,n,s),await B(r)}async function gt(e,t,n){F="sil";const r=await m(w??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 B(t)}async function mt(e,t,n,r,s={}){const a=s.from??F,o=s.transitionDurationMs??400,_=s.gapMs??o,i=s.threshold??p;if(await m(w??void 0),a===t){if(P(t,n),await T(e,q(t),null,n,i),n!==r)throw new DOMException("Aborted","AbortError");F=t;return}const l=st(a,t),f=ot(l,_),D=q(t),d=f.length>0?o/f.length:0;f.length===0&&P(`${a} → ${t} (no pair; final only)`,n);for(let c=0;c<f.length;c++){if(n!==r)throw new DOMException("Aborted","AbortError");const E=f[c],O=E.split("/").pop()?.replace("_minus_sil.png","")??`frame ${c}`;if(P(`${a} → ${t} (${O})`,n),await T(e,E,null,n,i),n!==r)throw new DOMException("Aborted","AbortError");d>0&&await yt(d,n,r)}if(n!==r)throw new DOMException("Aborted","AbortError");if(P(t,n),await T(e,D,null,n,i),n!==r)throw new DOMException("Aborted","AbortError");F=t}let h=0;self.onmessage=e=>{const t=e.data;(async()=>{try{switch(t.type){case"init":{u=new OffscreenCanvas(1,1);const n={type:"ready",requestId:t.requestId};self.postMessage(n);break}case"loadAssets":{x=t.urls,y=null,w=null,k.clear();const n=await m(t.keyHex),{width:r,height:s}=A(n),a={type:"loadAssetsDone",requestId:t.requestId,width:r,height:s};self.postMessage(a);break}case"drawFrame":{await new Promise((n,r)=>{L=L.then(()=>lt(t)).then(n).catch(r)});break}case"renderSil":{if(h=t.generation,!u)throw new Error("Worker not initialized");await gt(u,t.generation,h);const n={type:"renderDone",requestId:t.requestId,generation:t.generation};self.postMessage(n);break}case"renderViseme":{if(h=t.generation,!u)throw new Error("Worker not initialized");await mt(u,t.to,t.generation,h,{from:t.from,transitionDurationMs:t.transitionDurationMs,gapMs:t.gapMs,threshold:t.threshold});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:h};self.postMessage(r);return}pt(t.requestId,n instanceof Error?n.message:String(n))}})()}})();
|