152430	                const frameIndex = Math.floor(calcDelta / frameInfoInternal);
152431	                this.viewer.frame = frameIndex;
152432	                if (this.getChatState) {
152433	                    this.chatState = this.getChatState();
152434	                    (_a = this.animManager) === null || _a === void 0 ? void 0 : _a.update(this.chatState);
152435	                }
152436	                if (this.getExpressionData) {
152437	                    this.expressionData = this.updateBS(this.getExpressionData());
152438	                }
152439	                if (this.viewer.useFlame == false) {
152440	                    if (!this.mixer || !this.animManager)
152441	                        return;
152442	                    const mixerUpdateDelta = this.clock.getDelta();
152443	                    this.mixer.update(mixerUpdateDelta);
152444	                    // apply motion config
152445	                    if (this.motioncfg) {
152446	                        for (const morphTarget in this.expressionData) {
152447	                            const offset = this.motioncfg.offset[morphTarget];
152448	                            const scale = this.motioncfg.scale[morphTarget];
152449	                            if (offset !== void 0 && scale !== void 0) {
152450	                                this.expressionData[morphTarget] =
152451	                                    this.expressionData[morphTarget] * scale + offset;
152452	                            }
152453	                        }
152454	                    }
152455	                    //set expression
152456	                    this.setExpression();
152457	                }
152458	                // update
152459	                this.viewer.update(this.viewer.renderer, this.viewer.camera);
152460	                if (this.viewer.shouldRender()) {
152461	                    // render
152462	                    this.viewer.render();
152463	                    this.viewer.consecutiveRenderFrames++;
152464	                }
152465	                else {
152466	                    this.viewer.consecutiveRenderFrames = 0;
152467	                }
152468	                this.viewer.renderNextFrame = false;
152469	            }
152470	            this.viewer.selfDrivenModeRunning = true;
152471	        }
152472	        else {
152473	            throw new Error('Cannot start viewer unless it is in self driven mode.');
152474	        }
152475	    }
152476	    isHexColorStrict(value) {
152477	        if (typeof value !== 'string')
152478	            return false;
152479	        const hexColorRegex = /^(#|0x)[0-9A-Fa-f]{6}$/i;
152480	        return hexColorRegex.test(value);
152481	    }
152482	    setExpression() {
152483	        // const sortedKeys = Object.keys(this.expressionData).sort()
152484	        // const entries = sortedKeys.map((key) => this.expressionData[key])
152485	        // if (this.viewer && this.viewer.splatMesh && entries.length > 0) {
152486	        //   this.viewer.splatMesh.bsWeight = entries
152487	        // }
152488	        if (this.viewer && this.viewer.splatMesh) {
152489	            this.viewer.splatMesh.bsWeight = this.expressionData;
152490	        }
152491	        if (this.model) {
152492	            this.model.traverse((object) => {
152493	                if (object.isMesh || object.isSkinnedMesh) {
152494	                    const morphAttributes = object.geometry.morphAttributes;
152495	                    const hasMorphTargets = Object.keys(morphAttributes).length > 0;
152496	                    if (hasMorphTargets === true) {
152497	                        const morphTargetDictionary = object.morphTargetDictionary;
152498	                        for (const morphTarget in morphTargetDictionary) {
152499	                            const target = morphTargetDictionary[morphTarget];
152500	                            const data = this.expressionData[morphTarget];
152501	                            if (data !== void 0) {
152502	                                object.morphTargetInfluences[target] = Math.max(0.0, Math.min(1.0, data));
152503	                            }
152504	                        }
152505	                    }
152506	                }
152507	            });
152508	        }
152509	    }
152510	    loadFlameModel(pathName, motionConfig) {
152511	        return __awaiter(this, void 0, void 0, function* () {
152512	            const [skinModel, lbs_weight_80k, flame_params, indexes, bone_tree] = yield Promise.all([
152513	                this.unpackAndLoadGlb(pathName + '/skin.glb'),
152514	                this.unpackAndLoadJson(pathName + '/lbs_weight_20k.json'),
152515	                this.unpackAndLoadJson(pathName + '/flame_params.json'),
152516	                this.unpackAndLoadJson(pathName + '/vertex_order.json'),
152517	                this.unpackAndLoadJson(pathName + '/bone_tree.json') //  bone tree
152518	            ]);
152519	            if (!this.viewer) {

                const frameIndex = Math.floor(calcDelta / frameInfoInternal);
                this.viewer.frame = frameIndex;
                if (this.getChatState) {
                    this.chatState = this.getChatState();
                    (_a = this.animManager) === null || _a === void 0 ? void 0 : _a.update(this.chatState);
                }
                if (this.getExpressionData) {
                    this.expressionData = this.updateBS(this.getExpressionData());
                }
                if (this.viewer.useFlame == false) {
                    if (!this.mixer || !this.animManager)
                        return;
                    const mixerUpdateDelta = this.clock.getDelta();
                    this.mixer.update(mixerUpdateDelta);
                    // apply motion config
                    if (this.motioncfg) {
                        for (const morphTarget in this.expressionData) {
                            const offset = this.motioncfg.offset[morphTarget];
                            const scale = this.motioncfg.scale[morphTarget];
                            if (offset !== void 0 && scale !== void 0) {
                                this.expressionData[morphTarget] =
                                    this.expressionData[morphTarget] * scale + offset;
                            }
                        }
                    }
                    //set expression
                    this.setExpression();
                }
                // update
                this.viewer.update(this.viewer.renderer, this.viewer.camera);
                if (this.viewer.shouldRender()) {
                    // render
                    this.viewer.render();
                    this.viewer.consecutiveRenderFrames++;
                }
                else {
                    this.viewer.consecutiveRenderFrames = 0;
                }
                this.viewer.renderNextFrame = false;
            }
            this.viewer.selfDrivenModeRunning = true;
        }
        else {
            throw new Error('Cannot start viewer unless it is in self driven mode.');
        }
    }
    isHexColorStrict(value) {
        if (typeof value !== 'string')
            return false;
        const hexColorRegex = /^(#|0x)[0-9A-Fa-f]{6}$/i;
        return hexColorRegex.test(value);
    }
    setExpression() {
        // const sortedKeys = Object.keys(this.expressionData).sort()
        // const entries = sortedKeys.map((key) => this.expressionData[key])
        // if (this.viewer && this.viewer.splatMesh && entries.length > 0) {
        //   this.viewer.splatMesh.bsWeight = entries
        // }
        if (this.viewer && this.viewer.splatMesh) {
            this.viewer.splatMesh.bsWeight = this.expressionData;
        }
        if (this.model) {
            this.model.traverse((object) => {
                if (object.isMesh || object.isSkinnedMesh) {
                    const morphAttributes = object.geometry.morphAttributes;
                    const hasMorphTargets = Object.keys(morphAttributes).length > 0;
                    if (hasMorphTargets === true) {
                        const morphTargetDictionary = object.morphTargetDictionary;
                        for (const morphTarget in morphTargetDictionary) {
                            const target = morphTargetDictionary[morphTarget];
                            const data = this.expressionData[morphTarget];
                            if (data !== void 0) {
                                object.morphTargetInfluences[target] = Math.max(0.0, Math.min(1.0, data));
                            }
                        }
                    }
                }
            });
        }
    }
    loadFlameModel(pathName, motionConfig) {
        return __awaiter(this, void 0, void 0, function* () {
            const [skinModel, lbs_weight_80k, flame_params, indexes, bone_tree] = yield Promise.all([
                this.unpackAndLoadGlb(pathName + '/skin.glb'),
                this.unpackAndLoadJson(pathName + '/lbs_weight_20k.json'),
                this.unpackAndLoadJson(pathName + '/flame_params.json'),
                this.unpackAndLoadJson(pathName + '/vertex_order.json'),
                this.unpackAndLoadJson(pathName + '/bone_tree.json') //  bone tree
            ]);
            if (!this.viewer) {


Bash

grep -n "updateBS\|bsWeight\|ARKit\|blendshape\|bsName\|bs_name" static/lib_lam_renderer.js | head -30

Find updateBS and blendshape mapping


61755:                vec4 bsWeight = uintBitsToFloat(texture(boneTexture, samplerUV));
61756:                float weight = bsWeight.r;
61758:                    weight = bsWeight.g;
61761:                    weight = bsWeight.b;
61764:                    weight = bsWeight.a;
63788:        this.bsWeight = [];
64602:        if (!this.bsWeight)
64620:        for (const key in this.bsWeight) {
64621:            if (this.bsWeight.hasOwnProperty(key)) {
64622:                const value = this.bsWeight[key];
64628:        // for (let c = 0; c < this.bsWeight.length; c++) {
64630:        //     boneMatrixTextureDataInt[c + this.bonesNum * 16] = uintEncodedFloat(this.bsWeight[c]);
64651:        if (!this.bsWeight || !this.morphTargetDictionary)
64661:        for (const key in this.bsWeight) {
64662:            if (this.bsWeight.hasOwnProperty(key)) {
64663:                const value = this.bsWeight[key];
64669:        // for (let c = 0; c < this.bsWeight.length; c++) {
64670:        //     this.splatDataTextures.baseData['boneMatrix'][c + this.bonesNum * 16] = uintEncodedFloat(this.bsWeight[c]);
139527:    this.splatMesh.bsWeight = this.flame_params['expr'][this.frame];
145662:    updateBS(actionData) {
152359:    updateBS(actionData) {
152437:                    this.expressionData = this.updateBS(this.getExpressionData());
152486:        //   this.viewer.splatMesh.bsWeight = entries
152489:            this.viewer.splatMesh.bsWeight = this.expressionData;


152359	    updateBS(actionData) {
152360	        let influence = {
152361	            browDownLeft: 0.0,
152362	            browDownRight: 0.0,
152363	            browInnerUp: 0.0,
152364	            browOuterUpLeft: 0.0,
152365	            browOuterUpRight: 0.0,
152366	            mouthCheekPuff: 0.0,
152367	            cheekSquintLeft: 0.0,
152368	            cheekSquintRight: 0.0,
152369	            eyeBlinkLeft: 0.0,
152370	            eyeBlinkRight: 0.0,
152371	            eyeLookDownLeft: 0.0,
152372	            eyeLookDownRight: 0.0,
152373	            eyeLookInLeft: 0.0,
152374	            eyeLookInRight: 0.0,
152375	            eyeLookOutLeft: 0.0,
152376	            eyeLookOutRight: 0.0,
152377	            eyeLookUpLeft: 0.0,
152378	            eyeLookUpRight: 0.0,
152379	            eyeSquintLeft: 0.0,
152380	            eyeSquintRight: 0.0,
152381	            eyeWideLeft: 0.0,
152382	            eyeWideRight: 0.0,
152383	            jawForward: 0.0,
152384	            jawLeft: 0.0,
152385	            jawOpen: 0.0,
152386	            jawRight: 0.0,
152387	            mouthClose: 0.0,
152388	            mouthDimpleLeft: 0.0,
152389	            mouthDimpleRight: 0.0,
152390	            mouthFrownLeft: 0.0,
152391	            mouthFrownRight: 0.0,
152392	            mouthFunnel: 0.0,
152393	            mouthLeft: 0.0,
152394	            mouthLowerDownLeft: 0.0,
152395	            mouthLowerDownRight: 0.0,
152396	            mouthPressLeft: 0.0,
152397	            mouthPressRight: 0.0,
152398	            mouthPucker: 0.0,
152399	            mouthRight: 0.0,
152400	            mouthRollLower: 0.0,
152401	            mouthRollUpper: 0.0,
152402	            mouthShrugLower: 0.0,
152403	            mouthShrugUpper: 0.0,
152404	            mouthSmileLeft: 0.0,
152405	            mouthSmileRight: 0.0,
152406	            mouthStretchLeft: 0.0,
152407	            mouthStretchRight: 0.0,
152408	            mouthUpperUpLeft: 0.0,
152409	            mouthUpperUpRight: 0.0,
152410	            noseSneerLeft: 0.0,
152411	            noseSneerRight: 0.0,
152412	            tongueOut: 0.0
152413	        };
152414	        if (actionData != null) {
152415	            influence = actionData;
152416	        }
152417	        return influence;
152418	    }
152419	    render() {
152420	        var _a;
152421	        if (this.viewer && this.viewer.selfDrivenMode) {
152422	            if (this.viewer.webXRMode) {
152423	                this.viewer.renderer.setAnimationLoop(this.viewer.selfDrivenUpdateFunc);
152424	            }
152425	            else {
152426	                this.viewer.requestFrameId = requestAnimationFrame(() => this.render());
152427	                const frameInfoInternal = 1.0 / 30.0;
152428	                const currentTime = performance.now() / 1000;
152429	                const calcDelta = (currentTime - this.startTime) % (this.viewer.totalFrames * frameInfoInternal);

    updateBS(actionData) {
        let influence = {
            browDownLeft: 0.0,
            browDownRight: 0.0,
            browInnerUp: 0.0,
            browOuterUpLeft: 0.0,
            browOuterUpRight: 0.0,
            mouthCheekPuff: 0.0,
            cheekSquintLeft: 0.0,
            cheekSquintRight: 0.0,
            eyeBlinkLeft: 0.0,
            eyeBlinkRight: 0.0,
            eyeLookDownLeft: 0.0,
            eyeLookDownRight: 0.0,
            eyeLookInLeft: 0.0,
            eyeLookInRight: 0.0,
            eyeLookOutLeft: 0.0,
            eyeLookOutRight: 0.0,
            eyeLookUpLeft: 0.0,
            eyeLookUpRight: 0.0,
            eyeSquintLeft: 0.0,
            eyeSquintRight: 0.0,
            eyeWideLeft: 0.0,
            eyeWideRight: 0.0,
            jawForward: 0.0,
            jawLeft: 0.0,
            jawOpen: 0.0,
            jawRight: 0.0,
            mouthClose: 0.0,
            mouthDimpleLeft: 0.0,
            mouthDimpleRight: 0.0,
            mouthFrownLeft: 0.0,
            mouthFrownRight: 0.0,
            mouthFunnel: 0.0,
            mouthLeft: 0.0,
            mouthLowerDownLeft: 0.0,
            mouthLowerDownRight: 0.0,
            mouthPressLeft: 0.0,
            mouthPressRight: 0.0,
            mouthPucker: 0.0,
            mouthRight: 0.0,
            mouthRollLower: 0.0,
            mouthRollUpper: 0.0,
            mouthShrugLower: 0.0,
            mouthShrugUpper: 0.0,
            mouthSmileLeft: 0.0,
            mouthSmileRight: 0.0,
            mouthStretchLeft: 0.0,
            mouthStretchRight: 0.0,
            mouthUpperUpLeft: 0.0,
            mouthUpperUpRight: 0.0,
            noseSneerLeft: 0.0,
            noseSneerRight: 0.0,
            tongueOut: 0.0
        };
        if (actionData != null) {
            influence = actionData;
        }
        return influence;
    }
    render() {
        var _a;
        if (this.viewer && this.viewer.selfDrivenMode) {
            if (this.viewer.webXRMode) {
                this.viewer.renderer.setAnimationLoop(this.viewer.selfDrivenUpdateFunc);
            }
            else {
                this.viewer.requestFrameId = requestAnimationFrame(() => this.render());
                const frameInfoInternal = 1.0 / 30.0;
                const currentTime = performance.now() / 1000;
                const calcDelta = (currentTime - this.startTime) % (this.viewer.totalFrames * frameInfoInternal);


J'ai maintenant une image claire de l'architecture. Un point décisif: dans `app.js`, l'audio n'est joué **que si** les blendshapes LAM réussissent (`status === 'ready'`). Si l'étape LAM échoue, l'avatar reste **totalement muet** alors même que le `.wav` est généré (ce qui est le cas — fichier à 08:12). Je teste l'étape LAM pour confirmer.


timeout 290 python3 -c "from pathlib import Path; from agent_gui.avatar_lam import audio_to_blendshapes; bs=audio_to_blendshapes(Path('data/tts/c2ba25240ba9.wav')); print('OK', type(bs), 'names=', len(bs.get('names',[])), 'frames=', len(bs.get('frames',[])))"

Run LAM blendshapes on existing wav


This command requires approval


Je ne peux pas exécuter les commandes de test (le mode permission les refuse — chaque `python3`/`curl`/`conda` est bloqué). Mais la lecture du code suffit à poser un diagnostic solide.

## Ce que j'ai trouvé

Ce matin (08:00–08:12) l'avatar a été migré vers le **renderer 3D LAM** (`avatar.js` réécrit à 08:04, `lib_lam_renderer.js` ajouté à 08:02). La chaîne de parole est maintenant :

```
chat → edge-tts (.wav) → LAM audio_to_blendshapes → /api/avatar/anim
                                                          ↓
                          frontend joue l'audio SEULEMENT si status === 'ready'
```

**Le point critique** est dans `static/app.js:362` :

```js
if (data.status === 'ready' && window.avatarController) {
  await window.avatarController.speakWithAnimation(sessionId); // ← le SON est ici
  window.avatarController.startAnimation(data.bsData);
}
if (data.status === 'error') { ... setStatus('Erreur TTS/A2E'); } // ← aucun son
```

L'audio et l'animation sont **couplés** : si l'étape LAM (`avatar_lam.py` → `conda run -n lam_a2e python run_inference.py`) échoue ou dépasse les 300 s de timeout, le statut passe à `error` et **rien n'est joué**, même si le `.wav` existe.

Et justement : il y a bien un `data/tts/c2ba25240ba9.wav` (282 Ko, généré à 08:12). Donc **edge-tts + ffmpeg fonctionnent**, mais l'avatar est resté muet → c'est l'étape **LAM blendshapes** qui bloque (ou alors le navigateur bloque l'`AudioContext` faute de geste utilisateur direct au moment du poll).

## Deux causes probables, par ordre de vraisemblance

1. **LAM échoue** (env `lam_a2e`, modèle, GPU, ou format de sortie de `run_inference.py` qui doit produire les 52 noms ARKit attendus par `updateBS`). → couplage = silence total.
2. **Autoplay navigateur** : `playAudio()` crée l'`AudioContext` dans le callback de polling (`setInterval`), hors du geste utilisateur → Chrome/Firefox peut suspendre le contexte. → animation OK mais aucun son.

## Pour trancher, il me faut une de ces infos

- **soit** tu m'autorises à lancer le test LAM (`python3 -c "...audio_to_blendshapes..."` sur le wav existant), ou un `curl /api/avatar/anim` ;
- **soit** tu me dis ce que tu vois : la console navigateur (F12) affiche-t-elle « Erreur TTS/A2E », ou les logs du serveur montrent-ils une exception dans le thread `_run` ?

Veux-tu plutôt que j'applique tout de suite le correctif robuste — **découpler l'audio des blendshapes** (jouer le son dès que le `.wav` est prêt, même si LAM échoue, et débloquer l'`AudioContext` sur le clic « Envoyer ») ? Ça résoudrait le « ne parle plus » dans les deux scénarios.