@streamoji/aitwin 0.6.7 → 0.6.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.
@@ -0,0 +1 @@
1
+ (function(){"use strict";function it(t,e,n){return{width:t.crop?.source_width??e,height:t.crop?.source_height??n}}function ct(t,e){const n=t.crop;return{x:e?.source_x??n?.x??0,y:e?.source_y??n?.y??0,width:e?.source_w??n?.width??t.frame_width??0,height:e?.source_h??n?.height??t.frame_height??0}}function lt(t){return t?(t.w??0)>0&&(t.h??0)>0:!1}function M(t){return t.includes("/custom-faces/")}function K(t){const e=t.binBase.trim().replace(/\/$/,""),n=t.twinBase.trim().replace(/\/$/,"");if(!(t.encrypted||M(e)||M(n)))return{twinBase:n,binBase:e,encrypted:!1};const s=M(e)?e:M(n)?n:e;return{twinBase:s,binBase:s,encrypted:!0}}function m(t){return K(t).encrypted}function _t(t){const e=t.trim();if(e.length%2!==0)throw new Error("Invalid hex key");const n=new Uint8Array(e.length/2);for(let r=0;r<e.length;r+=2)n[r/2]=Number.parseInt(e.slice(r,r+2),16);return n}function B(t){return Uint8Array.from(t)}function ut(t,e){const n=new Uint8Array(t.length+e.length);return n.set(t,0),n.set(e,t.length),n}async function ft(t,e){if(!e)throw new Error("Asset key is required for encrypted assets");if(t.byteLength<28)throw new Error("Encrypted payload too small");const n=new Uint8Array(t),r=B(n.subarray(0,12)),s=B(n.subarray(12,28)),o=B(n.subarray(28)),a=B(ut(o,s)),c=await crypto.subtle.importKey("raw",_t(e),{name:"AES-GCM"},!1,["decrypt"]);return crypto.subtle.decrypt({name:"AES-GCM",iv:r,tagLength:128},c,a)}async function $(t,e){const n=await fetch(t);if(!n.ok)throw new Error(`Failed to load encrypted asset: ${t}`);const r=await n.arrayBuffer();return ft(r,e)}async function dt(t,e){if(!e)throw new Error("Missing asset key for encrypted JSON");const n=await $(t,e),r=new TextDecoder().decode(n);return JSON.parse(r)}async function wt(t){const e=new Blob([t],{type:"image/webp"});return createImageBitmap(e)}async function pt(t){const e=new Blob([t],{type:"image/png"});return createImageBitmap(e)}function C(t,e){return`${t}/${e}_minus_sil.png`}const Y=5;function J(t){const e=t?.composite_threshold;if(typeof e=="number"&&e>0)return e;const n=t?.crop?.content_threshold;return typeof n=="number"&&n>0?n:Y}const v=["t01_0.25","t02_0.50","t03_0.75"],ht=v.length,G=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 Q(t,e){return`${t}__${e}`}function X(t,e){return`pairs/${t}/${e}_minus_sil.png`}function q(t){return`${t}_minus_sil.png`}function yt(t,e){if(e.has(t))return t;const n=/^pairs\/([^/]+)\//.exec(t);if(!n)return null;const r=n[1].split("__")[1];if(!r||r==="sil")return null;const s=q(r);return e.has(s)?s:null}function mt(t,e){return t.filter(n=>e.has(n))}function gt(t,e){if(t===e)return[];const n=[],r=Q(t,e);if(G.has(r)){for(const o of v)n.push(X(r,o));return n}const s=Q(e,t);if(G.has(s)){for(let o=ht-1;o>=0;o--)n.push(X(s,v[o]));return n}return[]}function Et(t){return t<16?[1]:t<24?[1]:t<35?[0,1,2]:[0,1,2]}function It(t,e){return Et(e).map(r=>t[r]).filter(r=>r!=null)}function bt(t,e,n){if(!n)return`${t}/${e}`;const r=e.split("/").pop()??e,s=r.includes(".")?r.slice(0,r.lastIndexOf(".")):r;return`${t}/${s}.bin`}function Dt(t,e,n,r,s=Y){const o=new OffscreenCanvas(n,r),a=o.getContext("2d",{willReadFrequently:!0});a.drawImage(t,0,0,n,r);const c=a.getImageData(0,0,n,r),i=a.createImageData(n,r);i.data.set(c.data);const f=new OffscreenCanvas(n,r).getContext("2d",{willReadFrequently:!0});for(const d of e){f.clearRect(0,0,n,r),f.drawImage(d,0,0,n,r);const _=f.getImageData(0,0,n,r);for(let u=0;u<i.data.length;u+=4){const y=_.data[u],F=_.data[u+1],x=_.data[u+2];_.data[u+3]>0&&Math.max(y,F,x)>s&&(i.data[u]=y,i.data[u+1]=F,i.data[u+2]=x,i.data[u+3]=255)}}return a.putImageData(i,0,0),o}[.32,.24,.16,.08,0].map(t=>`eyes_${t.toFixed(2)}`);const P=2,St=-20,j=16,Ft={fear:-20,anger:-20,sad:-10,neutral:0,love:0,happy:0,disgust:10};function Z(t){return`brow_${t}`}function Mt(t){return Ft[t]}function V(t){return Math.max(St,Math.min(j,t))}function tt(t){const e=V(t);if(e%P===0)return[e];const n=Math.floor(e/P)*P,r=Math.min(j,n+P),s=V(n);return e-s<=r-e?s===r?[s]:[s,r]:s===r?[r]:[r,s]}const Bt={[-20]:"anger",[-10]:"sad",0:"neutral",10:"disgust"};function Pt(t,e){if(!t)return null;if(e(t))return t;const n=t.match(/^brow_(-?\d+)\/(eyes_[^/]+)_minus_sil\.png$/);if(n){const s=Number(n[1]),o=n[2];for(const c of tt(s)){if(c===s)continue;const i=C(Z(c),o);if(e(i))return i}const a=Bt[s];if(a){const c=C(a,o);if(e(c))return c}return null}const r=t.match(/^(fear|anger|sad|neutral|love|happy|disgust)\/(eyes_[^/]+)_minus_sil\.png$/);if(r){const s=r[1],o=r[2],a=Mt(s);for(const c of[a,...tt(a)]){const i=C(Z(c),o);if(e(i))return i}}return null}const et=-1,kt=1e5;function Ot(t){return t>=kt}function Rt(t){return t<0}let g=0,nt=Promise.resolve(),E=!1;function H(t){const e=nt.then(t);return nt=e.then(()=>{},()=>{}),e}function I(t,e){const n=e>0?-e:et,r={type:"renderDone",requestId:t,generation:n};self.postMessage(r)}async function At(t){const e=typeof t.liveSeq=="number"?t.liveSeq:0,n=Ot(e);if(E&&!n){I(t.requestId,e);return}if(e>0&&!n&&(g=e),!w)throw new Error("Worker not initialized");if(e>0&&!n&&e!==g){I(t.requestId,e);return}if(await S(h??void 0),e>0&&!n&&e!==g){I(t.requestId,e);return}if(await at(w,t.diffPath,t.expressionDiffPath??null,t.threshold),e>0&&!n&&e!==g){I(t.requestId,e);return}const r=e>0?-e:et;await L(r),I(t.requestId,e)}let w=null,b=null,h=null,U=null,k="sil";const O=new Map;function D(){if(!U)throw new Error("Worker assets not configured");return U}function rt(){const t=D();return m(t)?`${t.binBase}/sil.bin`:`${t.twinBase}/sil.png`}function Tt(){const t=D();return m(t)?`${t.binBase}/atlas.bin`:`${t.twinBase}/atlas.json`}function xt(){const t=D();return m(t)?`${t.binBase}/expression_atlas.bin`:`${t.twinBase}/expression_atlas.json`}function $t(){const t=D();return m(t)?t.binBase:t.twinBase}function R(){return m(D())}async function N(t){const e=await fetch(t);if(!e.ok)throw new Error(`Failed to load image: ${t}`);const n=await e.blob();return createImageBitmap(n)}async function Ct(t){return R()?pt(await $(rt(),t??"")):N(rt())}async function st(t,e,n){const r=R(),s=r?await dt(t,n):await fetch(t).then(l=>{if(!l.ok)throw new Error(`Failed to load ${t}`);return l.json()}),o=s.sheets?.[0];if(!o?.path)throw new Error(`${t} has no sheets[0].path`);const a=r?await wt(await $(bt(e,o.path,r),n??"")):await N(`${e}/${o.path}`),c=new Map;for(const l of s.cells??[])l.path&&c.set(l.path,l);const i=new Map;for(const l of s.sheets??[])i.set(l.index,l);return{atlas:a,atlasMeta:s,cellByPath:c,sheetByIndex:i,diffBase:e}}async function S(t){if(R()&&!t)throw new Error("Encrypted assets enabled but no key provided");if(b&&(!R()||h===(t??null)))return b;h=t??null,O.clear();const e=$t();return b=(async()=>{const n=await Ct(t),r=await st(Tt(),e,t);let s=null;try{s=await st(xt(),e,t)}catch{s=null}return{sil:n,viseme:r,expression:s}})(),b}function A(t){return it(t.viseme.atlasMeta,t.sil.width,t.sil.height)}function vt(t,e){if(!t.sheetByIndex.get(e.sheet??0))throw new Error(`Unknown sheet index: ${e.sheet}`);const r=new OffscreenCanvas(e.w,e.h);return r.getContext("2d",{willReadFrequently:!0}).drawImage(t.atlas,e.x,e.y,e.w,e.h,0,0,e.w,e.h),r}async function qt(t){return createImageBitmap(t)}async function ot(t,e,n){const r=`${e.diffBase}::${n}`,s=O.get(r);if(s)return s;const o=(async()=>{const a=e.cellByPath.get(n);if(!a)throw new Error(`No atlas cell for path: ${n}`);const c=ct(e.atlasMeta,a),i=A(t);if(lt(a)){const d=vt(e,a),_=new OffscreenCanvas(i.width,i.height),u=_.getContext("2d");return u.fillStyle="#000",u.fillRect(0,0,i.width,i.height),u.drawImage(d,c.x,c.y),qt(_)}const l=`${e.diffBase}/${n}`,f=await N(l);if(f.width===i.width&&f.height===i.height)return f;throw f.close(),new Error(`Diff PNG wrong size for ${n}`)})();return O.set(r,o),o}function T(t,e){const n={type:"status",label:t,generation:e};self.postMessage(n)}function Ht(t,e){const n={type:"error",requestId:t,message:e};self.postMessage(n)}async function L(t){if(!w)return;const e=Rt(t);if(!e&&t!==p)return;const n=await createImageBitmap(w);if(!e&&t!==p){n.close();return}const r={type:"frame",bitmap:n,generation:t};self.postMessage(r,{transfer:[n]})}function Ut(t,e,n){return new Promise((r,s)=>{setTimeout(()=>{e!==n?s(new DOMException("Aborted","AbortError")):r()},t)})}async function at(t,e,n,r){const s=await S(h??void 0),o=r??J(s.viseme.atlasMeta),{width:a,height:c}=A(s);(t.width!==a||t.height!==c)&&(t.width=a,t.height=c);const i=[];if(e){const d=new Set(s.viseme.cellByPath.keys()),_=yt(e,d)??(d.has(e)?e:null);_&&i.push(await ot(s,s.viseme,_))}if(n&&s.expression){const d=Pt(n,_=>s.expression.cellByPath.has(_));d&&i.push(await ot(s,s.expression,d))}const l=t.getContext("2d");if(l.clearRect(0,0,a,c),i.length===0){l.drawImage(s.sil,0,0,a,c);return}const f=Dt(s.sil,i,a,c,o);l.drawImage(f,0,0)}async function W(t,e,n,r,s){await at(t,e,n,s),await L(r)}async function z(t,e,n){k="sil";const r=await S(h??void 0);if(e!==n)throw new DOMException("Aborted","AbortError");const{width:s,height:o}=A(r);t.width=s,t.height=o;const a=t.getContext("2d");a.clearRect(0,0,s,o),a.drawImage(r.sil,0,0,s,o),await L(e)}async function Nt(t,e,n,r,s={}){const o=s.from??k,a=s.transitionDurationMs??400,c=s.gapMs??a,i=await S(h??void 0),l=s.threshold??J(i.viseme.atlasMeta);if(o===e){if(T(e,n),e==="sil"?await z(t,n,r):await W(t,q(e),null,n,l),n!==r)throw new DOMException("Aborted","AbortError");k=e;return}const f=gt(o,e),d=new Set(i.viseme.cellByPath.keys());let _=It(f,c);_=mt(_,d);const u=_.length>0?a/_.length:0;_.length===0&&T(`${o} → ${e} (no pair; final only)`,n);for(let y=0;y<_.length;y++){if(n!==r)throw new DOMException("Aborted","AbortError");const F=_[y],x=F.split("/").pop()?.replace("_minus_sil.png","")??`frame ${y}`;if(T(`${o} → ${e} (${x})`,n),await W(t,F,null,n,l),n!==r)throw new DOMException("Aborted","AbortError");u>0&&await Ut(u,n,r)}if(n!==r)throw new DOMException("Aborted","AbortError");if(T(e,n),e==="sil"?await z(t,n,r):await W(t,q(e),null,n,l),n!==r)throw new DOMException("Aborted","AbortError");k=e}let p=0;self.onmessage=t=>{const e=t.data;(async()=>{try{switch(e.type){case"init":{w=new OffscreenCanvas(1,1);const n={type:"ready",requestId:e.requestId};self.postMessage(n);break}case"loadAssets":{U=K(e.urls),b=null,h=null,O.clear();const n=await S(e.keyHex),{width:r,height:s}=A(n),o={type:"loadAssetsDone",requestId:e.requestId,width:r,height:s,visemePaths:[...n.viseme.cellByPath.keys()]};self.postMessage(o);break}case"cancelLiveDraws":{g+=1;const n={type:"renderDone",requestId:e.requestId,generation:p};self.postMessage(n);break}case"drawFrame":{await H(()=>At(e));break}case"renderSil":{await H(async()=>{E=!0;try{if(p=e.generation,!w)throw new Error("Worker not initialized");await z(w,e.generation,p)}finally{E=!1}});const n={type:"renderDone",requestId:e.requestId,generation:e.generation};self.postMessage(n);break}case"renderViseme":{await H(async()=>{E=!0;try{if(p=e.generation,!w)throw new Error("Worker not initialized");await Nt(w,e.to,e.generation,p,{from:e.from,transitionDurationMs:e.transitionDurationMs,gapMs:e.gapMs,threshold:e.threshold})}finally{E=!1}});const n={type:"renderDone",requestId:e.requestId,generation:e.generation};self.postMessage(n);break}default:break}}catch(n){if(n instanceof DOMException&&n.name==="AbortError"){const r={type:"renderAborted",requestId:e.requestId,generation:"generation"in e?e.generation:p};self.postMessage(r);return}Ht(e.requestId,n instanceof Error?n.message:String(n))}})()}})();
package/dist/index.d.ts CHANGED
@@ -17,6 +17,11 @@ export declare type AiTwinCaptionsOptions = {
17
17
 
18
18
  export declare type AiTwinHandle = {
19
19
  speakText: (text: string, options?: AvatarTtsSpeakOptions) => Promise<void>;
20
+ /**
21
+ * Generate an LLM reply and speak it in one `/avatar_ttsWithPoses` request
22
+ * (`generateReply: true`). Prefer this over `/avatarReply` + `speakText`.
23
+ */
24
+ speakWithReply: (message: string, options?: SpeakWithReplyOptions) => Promise<SpeakWithReplyResult>;
20
25
  stop: () => void;
21
26
  setTtsEngineId: (engineId: TtsEngineId) => void;
22
27
  renderViseme: (toViseme: string, options?: AiTwinRenderVisemeOptions) => Promise<void>;
@@ -49,11 +54,11 @@ export declare type AiTwinHandle = {
49
54
  /** Mute/unmute bot audio output (TTS + voice). */
50
55
  setMuted: (muted: boolean) => void;
51
56
  isMuted: () => boolean;
52
- /** Brain id from getAiTwin (for /avatarReply and /ws/voice). */
57
+ /** Brain id from getAiTwin (for speakWithReply and /ws/voice). */
53
58
  getBrainId: () => string | null;
54
59
  /** @deprecated Use `getBrainId`. */
55
60
  getPersonaId: () => string | null;
56
- /** Client SSE brain URL from getAiTwin (for /ws/voice). */
61
+ /** Client SSE brain URL from getAiTwin (for speakWithReply / /ws/voice). */
57
62
  getBrainUrl: () => string | null;
58
63
  };
59
64
 
@@ -138,7 +143,7 @@ export declare type AiTwinProps = {
138
143
  lipsyncPlaybackWarmupMs?: number;
139
144
  /**
140
145
  * Milliseconds to hold still sil after speech before resuming the idle video loop.
141
- * Default: 200.
146
+ * Default: 500.
142
147
  */
143
148
  idleHandoffHoldMs?: number;
144
149
  /**
@@ -186,6 +191,7 @@ export declare const AiTwinWidget: ForwardRefExoticComponent<AiTwinWidgetProps &
186
191
  /** Imperative API shared by React ref and HTML widget handle. */
187
192
  export declare type AiTwinWidgetHandle = {
188
193
  speakText: (text: string, options?: AvatarTtsSpeakOptions) => Promise<void>;
194
+ speakWithReply: (message: string, options?: SpeakWithReplyOptions) => Promise<SpeakWithReplyResult>;
189
195
  connect: (options?: VoiceConnectOptions) => Promise<void>;
190
196
  disconnect: () => Promise<void>;
191
197
  stop: () => void;
@@ -236,14 +242,14 @@ export declare type AiTwinWidgetProps = {
236
242
  */
237
243
  float?: boolean;
238
244
  /**
239
- * Brain id for /avatarReply (Text mode) and `/ws/voice`.
245
+ * Brain id for `speakWithReply` (Text mode) and `/ws/voice`.
240
246
  * When omitted, uses getAiTwin `brainId` if available.
241
247
  */
242
248
  brainId?: string;
243
249
  /** @deprecated Use `brainId`. */
244
250
  personaId?: string;
245
251
  /**
246
- * Client SSE brain URL for `/ws/voice`.
252
+ * Client SSE brain URL for `speakWithReply` / `/ws/voice`.
247
253
  * When omitted, uses getAiTwin `brainUrl` if available.
248
254
  */
249
255
  brainUrl?: string;
@@ -274,6 +280,11 @@ export declare type AvatarTtsLipsyncController = {
274
280
  setTtsEngineId: (engineId: TtsEngineId) => void;
275
281
  getTtsEngineId: () => TtsEngineId;
276
282
  speak: (userQuery: string, options?: AvatarTtsSpeakOptions) => Promise<void>;
283
+ /**
284
+ * LLM reply + TTS in one `/avatar_ttsWithPoses` request (`generateReply: true`).
285
+ * Avoids a separate `/avatarReply` round-trip.
286
+ */
287
+ speakWithReply: (message: string, options?: SpeakWithReplyOptions) => Promise<SpeakWithReplyResult>;
277
288
  stop: () => void;
278
289
  getVisemeQueue: () => VisemeQueueItem[];
279
290
  getWordQueue: () => WordQueueItem[];
@@ -574,7 +585,32 @@ export declare const SPEAKING_RATE_MAX = 1.5;
574
585
 
575
586
  export declare const SPEAKING_RATE_MIN = 0.5;
576
587
 
577
- /** Merge explicit `ttsEngineId` with a possibly packed `voiceId`. Packed engine wins only if engine is omitted. */
588
+ export declare type SpeakWithReplyMessage = {
589
+ role: "user" | "assistant";
590
+ content: string;
591
+ };
592
+
593
+ /** Options for `/avatar_ttsWithPoses` with `generateReply: true`. */
594
+ export declare type SpeakWithReplyOptions = AvatarTtsSpeakOptions & {
595
+ brainId?: string;
596
+ brainUrl?: string;
597
+ history?: SpeakWithReplyMessage[];
598
+ /** Called as soon as spokenText arrives on the SSE stream. */
599
+ onSpokenText?: (spokenText: string) => void;
600
+ /** Called as soon as displayText arrives on the SSE stream. */
601
+ onDisplayText?: (displayText: string) => void;
602
+ };
603
+
604
+ export declare type SpeakWithReplyResult = {
605
+ displayText: string;
606
+ spokenText: string;
607
+ };
608
+
609
+ /**
610
+ * Merge explicit `ttsEngineId` with a possibly packed `voiceId`.
611
+ * When `voiceId` is `engine:voice`, the packed engine wins over a separate
612
+ * `ttsEngineId` (Create Twin copy format takes priority).
613
+ */
578
614
  export declare function splitVoiceIdSpec(ttsEngineId?: TtsEngineId, voiceId?: string): {
579
615
  ttsEngineId?: TtsEngineId;
580
616
  voiceId?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamoji/aitwin",
3
- "version": "0.6.7",
3
+ "version": "0.6.9",
4
4
  "description": "Embeddable React AI twin face with TTS lipsync",
5
5
  "type": "module",
6
6
  "main": "./dist/aitwin.cjs",
@@ -1 +0,0 @@
1
- (function(){"use strict";function it(t,e,n){return{width:t.crop?.source_width??e,height:t.crop?.source_height??n}}function ct(t,e){const n=t.crop;return{x:e?.source_x??n?.x??0,y:e?.source_y??n?.y??0,width:e?.source_w??n?.width??t.frame_width??0,height:e?.source_h??n?.height??t.frame_height??0}}function lt(t){return t?(t.w??0)>0&&(t.h??0)>0:!1}function M(t){return t.includes("/custom-faces/")}function K(t){const e=t.binBase.trim().replace(/\/$/,""),n=t.twinBase.trim().replace(/\/$/,"");if(!(t.encrypted||M(e)||M(n)))return{twinBase:n,binBase:e,encrypted:!1};const s=M(e)?e:M(n)?n:e;return{twinBase:s,binBase:s,encrypted:!0}}function m(t){return K(t).encrypted}function _t(t){const e=t.trim();if(e.length%2!==0)throw new Error("Invalid hex key");const n=new Uint8Array(e.length/2);for(let r=0;r<e.length;r+=2)n[r/2]=Number.parseInt(e.slice(r,r+2),16);return n}function B(t){return Uint8Array.from(t)}function ut(t,e){const n=new Uint8Array(t.length+e.length);return n.set(t,0),n.set(e,t.length),n}async function ft(t,e){if(!e)throw new Error("Asset key is required for encrypted assets");if(t.byteLength<28)throw new Error("Encrypted payload too small");const n=new Uint8Array(t),r=B(n.subarray(0,12)),s=B(n.subarray(12,28)),o=B(n.subarray(28)),a=B(ut(o,s)),c=await crypto.subtle.importKey("raw",_t(e),{name:"AES-GCM"},!1,["decrypt"]);return crypto.subtle.decrypt({name:"AES-GCM",iv:r,tagLength:128},c,a)}async function $(t,e){const n=await fetch(t);if(!n.ok)throw new Error(`Failed to load encrypted asset: ${t}`);const r=await n.arrayBuffer();return ft(r,e)}async function dt(t,e){if(!e)throw new Error("Missing asset key for encrypted JSON");const n=await $(t,e),r=new TextDecoder().decode(n);return JSON.parse(r)}async function wt(t){const e=new Blob([t],{type:"image/webp"});return createImageBitmap(e)}async function pt(t){const e=new Blob([t],{type:"image/png"});return createImageBitmap(e)}function C(t,e){return`${t}/${e}_minus_sil.png`}const Y=5;function J(t){const e=t?.composite_threshold;if(typeof e=="number"&&e>0)return e;const n=t?.crop?.content_threshold;return typeof n=="number"&&n>0?n:Y}const v=["t01_0.25","t02_0.50","t03_0.75"],ht=v.length,G=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 Q(t,e){return`${t}__${e}`}function X(t,e){return`pairs/${t}/${e}_minus_sil.png`}function q(t){return`${t}_minus_sil.png`}function yt(t,e){if(e.has(t))return t;const n=/^pairs\/([^/]+)\//.exec(t);if(!n)return null;const r=n[1].split("__")[1];if(!r||r==="sil")return null;const s=q(r);return e.has(s)?s:null}function mt(t,e){return t.filter(n=>e.has(n))}function gt(t,e){if(t===e)return[];const n=[],r=Q(t,e);if(G.has(r)){for(const o of v)n.push(X(r,o));return n}const s=Q(e,t);if(G.has(s)){for(let o=ht-1;o>=0;o--)n.push(X(s,v[o]));return n}return[]}function Et(t){return t<16?[1]:t<24?[1]:t<35?[0,1,2]:[0,1,2]}function It(t,e){return Et(e).map(r=>t[r]).filter(r=>r!=null)}function bt(t,e,n){if(!n)return`${t}/${e}`;const r=e.split("/").pop()??e,s=r.includes(".")?r.slice(0,r.lastIndexOf(".")):r;return`${t}/${s}.bin`}function Dt(t,e,n,r,s=Y){const o=new OffscreenCanvas(n,r),a=o.getContext("2d",{willReadFrequently:!0});a.drawImage(t,0,0,n,r);const c=a.getImageData(0,0,n,r),i=a.createImageData(n,r);i.data.set(c.data);const f=new OffscreenCanvas(n,r).getContext("2d",{willReadFrequently:!0});for(const d of e){f.clearRect(0,0,n,r),f.drawImage(d,0,0,n,r);const _=f.getImageData(0,0,n,r);for(let u=0;u<i.data.length;u+=4){const y=_.data[u],F=_.data[u+1],x=_.data[u+2];Math.max(y,F,x)>s&&(i.data[u]=y,i.data[u+1]=F,i.data[u+2]=x,i.data[u+3]=255)}}return a.putImageData(i,0,0),o}[.32,.24,.16,.08,0].map(t=>`eyes_${t.toFixed(2)}`);const P=2,St=-20,j=16,Ft={fear:-20,anger:-20,sad:-10,neutral:0,love:0,happy:0,disgust:10};function Z(t){return`brow_${t}`}function Mt(t){return Ft[t]}function V(t){return Math.max(St,Math.min(j,t))}function tt(t){const e=V(t);if(e%P===0)return[e];const n=Math.floor(e/P)*P,r=Math.min(j,n+P),s=V(n);return e-s<=r-e?s===r?[s]:[s,r]:s===r?[r]:[r,s]}const Bt={[-20]:"anger",[-10]:"sad",0:"neutral",10:"disgust"};function Pt(t,e){if(!t)return null;if(e(t))return t;const n=t.match(/^brow_(-?\d+)\/(eyes_[^/]+)_minus_sil\.png$/);if(n){const s=Number(n[1]),o=n[2];for(const c of tt(s)){if(c===s)continue;const i=C(Z(c),o);if(e(i))return i}const a=Bt[s];if(a){const c=C(a,o);if(e(c))return c}return null}const r=t.match(/^(fear|anger|sad|neutral|love|happy|disgust)\/(eyes_[^/]+)_minus_sil\.png$/);if(r){const s=r[1],o=r[2],a=Mt(s);for(const c of[a,...tt(a)]){const i=C(Z(c),o);if(e(i))return i}}return null}const et=-1,kt=1e5;function Ot(t){return t>=kt}function Rt(t){return t<0}let g=0,nt=Promise.resolve(),E=!1;function H(t){const e=nt.then(t);return nt=e.then(()=>{},()=>{}),e}function I(t,e){const n=e>0?-e:et,r={type:"renderDone",requestId:t,generation:n};self.postMessage(r)}async function At(t){const e=typeof t.liveSeq=="number"?t.liveSeq:0,n=Ot(e);if(E&&!n){I(t.requestId,e);return}if(e>0&&!n&&(g=e),!w)throw new Error("Worker not initialized");if(e>0&&!n&&e!==g){I(t.requestId,e);return}if(await S(h??void 0),e>0&&!n&&e!==g){I(t.requestId,e);return}if(await at(w,t.diffPath,t.expressionDiffPath??null,t.threshold),e>0&&!n&&e!==g){I(t.requestId,e);return}const r=e>0?-e:et;await L(r),I(t.requestId,e)}let w=null,b=null,h=null,U=null,k="sil";const O=new Map;function D(){if(!U)throw new Error("Worker assets not configured");return U}function rt(){const t=D();return m(t)?`${t.binBase}/sil.bin`:`${t.twinBase}/sil.png`}function Tt(){const t=D();return m(t)?`${t.binBase}/atlas.bin`:`${t.twinBase}/atlas.json`}function xt(){const t=D();return m(t)?`${t.binBase}/expression_atlas.bin`:`${t.twinBase}/expression_atlas.json`}function $t(){const t=D();return m(t)?t.binBase:t.twinBase}function R(){return m(D())}async function N(t){const e=await fetch(t);if(!e.ok)throw new Error(`Failed to load image: ${t}`);const n=await e.blob();return createImageBitmap(n)}async function Ct(t){return R()?pt(await $(rt(),t??"")):N(rt())}async function st(t,e,n){const r=R(),s=r?await dt(t,n):await fetch(t).then(l=>{if(!l.ok)throw new Error(`Failed to load ${t}`);return l.json()}),o=s.sheets?.[0];if(!o?.path)throw new Error(`${t} has no sheets[0].path`);const a=r?await wt(await $(bt(e,o.path,r),n??"")):await N(`${e}/${o.path}`),c=new Map;for(const l of s.cells??[])l.path&&c.set(l.path,l);const i=new Map;for(const l of s.sheets??[])i.set(l.index,l);return{atlas:a,atlasMeta:s,cellByPath:c,sheetByIndex:i,diffBase:e}}async function S(t){if(R()&&!t)throw new Error("Encrypted assets enabled but no key provided");if(b&&(!R()||h===(t??null)))return b;h=t??null,O.clear();const e=$t();return b=(async()=>{const n=await Ct(t),r=await st(Tt(),e,t);let s=null;try{s=await st(xt(),e,t)}catch{s=null}return{sil:n,viseme:r,expression:s}})(),b}function A(t){return it(t.viseme.atlasMeta,t.sil.width,t.sil.height)}function vt(t,e){if(!t.sheetByIndex.get(e.sheet??0))throw new Error(`Unknown sheet index: ${e.sheet}`);const r=new OffscreenCanvas(e.w,e.h);return r.getContext("2d",{willReadFrequently:!0}).drawImage(t.atlas,e.x,e.y,e.w,e.h,0,0,e.w,e.h),r}async function qt(t){return createImageBitmap(t)}async function ot(t,e,n){const r=`${e.diffBase}::${n}`,s=O.get(r);if(s)return s;const o=(async()=>{const a=e.cellByPath.get(n);if(!a)throw new Error(`No atlas cell for path: ${n}`);const c=ct(e.atlasMeta,a),i=A(t);if(lt(a)){const d=vt(e,a),_=new OffscreenCanvas(i.width,i.height),u=_.getContext("2d");return u.fillStyle="#000",u.fillRect(0,0,i.width,i.height),u.drawImage(d,c.x,c.y),qt(_)}const l=`${e.diffBase}/${n}`,f=await N(l);if(f.width===i.width&&f.height===i.height)return f;throw f.close(),new Error(`Diff PNG wrong size for ${n}`)})();return O.set(r,o),o}function T(t,e){const n={type:"status",label:t,generation:e};self.postMessage(n)}function Ht(t,e){const n={type:"error",requestId:t,message:e};self.postMessage(n)}async function L(t){if(!w)return;const e=Rt(t);if(!e&&t!==p)return;const n=await createImageBitmap(w);if(!e&&t!==p){n.close();return}const r={type:"frame",bitmap:n,generation:t};self.postMessage(r,{transfer:[n]})}function Ut(t,e,n){return new Promise((r,s)=>{setTimeout(()=>{e!==n?s(new DOMException("Aborted","AbortError")):r()},t)})}async function at(t,e,n,r){const s=await S(h??void 0),o=r??J(s.viseme.atlasMeta),{width:a,height:c}=A(s);(t.width!==a||t.height!==c)&&(t.width=a,t.height=c);const i=[];if(e){const d=new Set(s.viseme.cellByPath.keys()),_=yt(e,d)??(d.has(e)?e:null);_&&i.push(await ot(s,s.viseme,_))}if(n&&s.expression){const d=Pt(n,_=>s.expression.cellByPath.has(_));d&&i.push(await ot(s,s.expression,d))}const l=t.getContext("2d");if(l.clearRect(0,0,a,c),i.length===0){l.drawImage(s.sil,0,0,a,c);return}const f=Dt(s.sil,i,a,c,o);l.drawImage(f,0,0)}async function W(t,e,n,r,s){await at(t,e,n,s),await L(r)}async function z(t,e,n){k="sil";const r=await S(h??void 0);if(e!==n)throw new DOMException("Aborted","AbortError");const{width:s,height:o}=A(r);t.width=s,t.height=o;const a=t.getContext("2d");a.clearRect(0,0,s,o),a.drawImage(r.sil,0,0,s,o),await L(e)}async function Nt(t,e,n,r,s={}){const o=s.from??k,a=s.transitionDurationMs??400,c=s.gapMs??a,i=await S(h??void 0),l=s.threshold??J(i.viseme.atlasMeta);if(o===e){if(T(e,n),e==="sil"?await z(t,n,r):await W(t,q(e),null,n,l),n!==r)throw new DOMException("Aborted","AbortError");k=e;return}const f=gt(o,e),d=new Set(i.viseme.cellByPath.keys());let _=It(f,c);_=mt(_,d);const u=_.length>0?a/_.length:0;_.length===0&&T(`${o} → ${e} (no pair; final only)`,n);for(let y=0;y<_.length;y++){if(n!==r)throw new DOMException("Aborted","AbortError");const F=_[y],x=F.split("/").pop()?.replace("_minus_sil.png","")??`frame ${y}`;if(T(`${o} → ${e} (${x})`,n),await W(t,F,null,n,l),n!==r)throw new DOMException("Aborted","AbortError");u>0&&await Ut(u,n,r)}if(n!==r)throw new DOMException("Aborted","AbortError");if(T(e,n),e==="sil"?await z(t,n,r):await W(t,q(e),null,n,l),n!==r)throw new DOMException("Aborted","AbortError");k=e}let p=0;self.onmessage=t=>{const e=t.data;(async()=>{try{switch(e.type){case"init":{w=new OffscreenCanvas(1,1);const n={type:"ready",requestId:e.requestId};self.postMessage(n);break}case"loadAssets":{U=K(e.urls),b=null,h=null,O.clear();const n=await S(e.keyHex),{width:r,height:s}=A(n),o={type:"loadAssetsDone",requestId:e.requestId,width:r,height:s,visemePaths:[...n.viseme.cellByPath.keys()]};self.postMessage(o);break}case"cancelLiveDraws":{g+=1;const n={type:"renderDone",requestId:e.requestId,generation:p};self.postMessage(n);break}case"drawFrame":{await H(()=>At(e));break}case"renderSil":{await H(async()=>{E=!0;try{if(p=e.generation,!w)throw new Error("Worker not initialized");await z(w,e.generation,p)}finally{E=!1}});const n={type:"renderDone",requestId:e.requestId,generation:e.generation};self.postMessage(n);break}case"renderViseme":{await H(async()=>{E=!0;try{if(p=e.generation,!w)throw new Error("Worker not initialized");await Nt(w,e.to,e.generation,p,{from:e.from,transitionDurationMs:e.transitionDurationMs,gapMs:e.gapMs,threshold:e.threshold})}finally{E=!1}});const n={type:"renderDone",requestId:e.requestId,generation:e.generation};self.postMessage(n);break}default:break}}catch(n){if(n instanceof DOMException&&n.name==="AbortError"){const r={type:"renderAborted",requestId:e.requestId,generation:"generation"in e?e.generation:p};self.postMessage(r);return}Ht(e.requestId,n instanceof Error?n.message:String(n))}})()}})();