o
    aj#                     @   sl  d Z ddlZddlZddlZddlZddlZddlZddlZej	
ej	eZejdp:ej	ej	edddZej	de ddlmZ ddlmZmZmZ dd	lmZmZmZmZ ej	ed
dddZej	edddZdZ dZ!e"dddddddddd	Z#d*ddZ$edfddZ%dded fd!d"Z&dded fd#d$Z'G d%d& d&Z(d'd( Z)e*d)kre)  dS dS )+u  Standalone V3 face inference for the avatar E2E pipeline.

Given an assistant utterance (16k mono wav) + an emotion tag, produce the 52
ARKit blendshape timeline the avatar viewer consumes — WITHOUT V3's TTS or
KlueTeacher steps. In the E2E, OpenAI Realtime supplies the audio and the LLM
supplies the emotion tag, so we feed mel + emotion/VAD straight into the V3
face model. This is the audio-driven subset of
`face/animasync-face-v3/models/v3_face/serve_live.py`.

Pipeline:
    wav ─► mel_features                         (T, 80)   [raw log-mel, no norm — matches V3 npz "audio"]
    emotion ─► emotion_vad_anchors[emo][lvl].vad
            ─► build_conditioning               (T, 19)   [16 one-hot + 3 VAD]
    V3FaceModel(mel, cond)                       (T, 52)
    ─► crisp_mouth ─► smooth_brows ─► inject_blinks ─► clip[0,1]

Runs in the repo `.venv` (same interpreter as the body model — imports verified).

    python face_v3_infer.py --wav <path> --emotion joy --out out.json
    NFACE_V3_DIRz..facezanimasync-face-v3)ARKIT_52_NAMES)EMOTION_LABELSmel_featuresbuild_conditioning)
load_modelcrisp_mouthsmooth_browsinject_blinksmodelsv3_facecheckpointszbest_expression_v14.ptdataemotionzemotion_vad_anchors.json   i>  g333333?      ?g?g       @g{Gz?g      @g      ?)	crisp_thresholdcrisp_scalecrisp_sigmacrisp_mouthclose_sigmabrow_min_cutoff	brow_betabrow_d_cutoffblink_intervalblink_expressive_cap   c                    sH   t ttd }|| |d }t| fddd}t|d tjS )zLook up the affective VAD anchor (valence/arousal/dominance) for an emotion.

    Same selection gen_v3_face.py uses: the anchor entry whose intensity `level`
    is closest to the requested one.
    anchorsneutralc                    s   t | d   S )Nlevel)abs)ar    face_v3_infer.py<lambda>?   s    z emotion_to_vad.<locals>.<lambda>)keyvad)	jsonloadopenANCHORSgetminnpasarrayfloat32)r   r   r   lvpkr#   r"   r$   emotion_to_vad7   s   r3   c                 C   s2   |pt t j rdnd}t| |\}}||fS )z?Load once, reuse across turns (the E2E server keeps this warm).zcuda:0cpu)torchdevicecudais_availabler   )ckptr6   modelcfgr#   r#   r$   load_face_modelC   s   r<   e2ec                 C   s(   t j| tdd\}}t||||||dS )zGwav + emotion -> (T, 52) blendshapes in ARKit_52 order, post-processed.T)srmono)r   	model_devr9   scenario_id)librosar)   SRrender_face_samples)wav_pathr   r   r@   r9   rA   wav_r#   r#   r$   render_faceJ   s   
rH   c                 C   sv  |du rt |}|\}}tj| tjdd} | jdk r't| dd| j f} t| tt	d}|j
d }	t||}
t||
 |	}t . t|d|}t|d|}|||d  tj}W d   n1 suw   Y  t|dd}t|td	 td
 td td d}t|td td td t	d}t||td td t	d}t|ddtj|	fS )zCIn-memory mono 16k PCM + emotion -> post-processed ARKit-52 frames.Ndtypei   r   )r>   fps        r   r   r   r   r   )	thresholdscalepre_smooth_sigmamouth_close_sigmar   r   r   )
min_cutoffbetad_cutoffrL   r   r   )rA   mean_interval_sexpressive_caprL   )r<   r.   r/   r0   reshapesizepadr   rC   FPSshaper3   r   tolistr5   no_grad
from_numpy	unsqueezetosqueezer4   numpyastypeclipr	   POSTr
   r   )rF   r   r   r@   r9   rA   r:   r6   melTr'   condaudiocbsr#   r#   r$   rD   R   s6   



"rD   c                   @   s$   e Zd ZdZd	ddZd
ddZdS )
FaceStreamzACausal V3 wrapper that returns frames aligned to each body chunk.r   r   c                 C   s:   || _ || _|| _|| _tjdtjd| _d | _d| _	d S )Nr   rI   )
r@   rA   r   blendr.   zerosr0   pcm_lastframes_emitted)selfr@   rA   r   rm   r#   r#   r$   __init__t   s   
zFaceStream.__init__Nc                 C   s  t |}|du r| jnt |}|dk rtd|| jkr'td| j d| tj|tjdd}|jr>t| j	|g| _	|dkrNtj
dttftjdS || }t t|t t }| j	dtt| j	| }t||| j| j| jd\}	}
t|	|k rtj|	d|t|	 fd	fd
d}	|	||  }| jdur| jdkrt| jt|}t|D ]}|d |d  }d| | j |||   ||< q|d  | _|| _t|ddtjS )a  Append PCM and return the exact absolute frame range emitted by body.

        ``librosa``'s centered STFT produces one more mel frame than the
        duration-derived 30fps body timeline (12,800 samples -> 25 mel frames,
        while body emits frames 0..23). Selecting ``bs[-n_frames:]`` therefore
        led face by one frame. Track the body frame cursor explicitly and slice
        ``[frame_start:frame_start + n_frames]`` instead.

        A request may also contain more PCM than body consumed into complete
        steps. Limit inference to the sample boundary represented by the body
        frame end so that pending future audio cannot leak into this packet's
        post-processing.
        Nr   zn_frames must be non-negativez#face frame discontinuity: expected z, got rI   rK   )r   r@   rA   )r   r   edge)mode   r   rM   )intrq   
ValueErrorr.   r/   r0   rW   rX   concatenatero   rn   lenr   roundrC   rZ   r-   rD   r   r@   rA   rY   copyrp   rm   rangerd   rc   )rr   	pcm_chunkr   n_framesframe_startro   	frame_endrepresented_samplesinference_pcmrk   rG   outkir!   r#   r#   r$   push_pcm}   s>   


  zFaceStream.push_pcm)r   r   )N)__name__
__module____qualname____doc__rs   r   r#   r#   r#   r$   rl   q   s    
	rl   c                     s  t  } | jddd | jddddt d | jd	td
dd | jdtd | jddd |  }|jtvrEt	
d|jdt  tjtj|jd }t|j|j|j|j|d\ }tjtjtj|jdd ttjtj|jd tt|t fddt|D dt|jd td|jdd|dd d d df  dd  d d d!f  dd" d d d#f  dd$|j  d S )%Nz--wavT)requiredz	--emotionr   zone of: z, )defaulthelpz--levelr   zVAD intensity 1-5)typer   r   z--ckpt)r   z--outzunknown emotion u    — must be one of r   )r9   rA   )exist_okc                    s   g | ]}d d  | D qS )c                 S   s   g | ]	}t t|d qS r   )r{   float).0vr#   r#   r$   
<listcomp>   s    z#main.<locals>.<listcomp>.<listcomp>r#   )r   trk   r#   r$   r      s    zmain.<locals>.<listcomp>)rA   rL   
num_framesnamesblendshapeswzface: 10s 4dzf  jawOpenMax=   z.2fz  browInnerMax=   z  smileMax=,   z -> )argparseArgumentParseradd_argumentjoinr   rw   DEFAULT_CKPT
parse_argsr   sysexitospathsplitextbasenamerF   rH   r   r9   makedirsdirnameabspathr   r(   dumprZ   r   r}   r*   printmax)apr!   sidrg   r#   r   r$   main   s:   
 

r   __main__r   )+r   r   r(   r   r   rb   r.   rB   r5   r   r   r   __file__HEREenvironr,   r   FACEinsertscripts.compiler.constantsr   scripts.compiler.data_pipeliner   r   r   models.v3_face.inferr   r	   r
   r   r   r+   rZ   rC   dictre   r3   r<   rH   rD   rl   r   r   r#   r#   r#   r$   <module>   sJ    



?
