o
    rjA                     @   s  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ddlZ	ddl
Z
ejejeZejejedZejedddZejdejedd ejdejedd	 ddlmZ dd
lmZ ddlmZmZmZmZ dZdZejedddeZ g dZ!e!e"e Z#e$e#Z%e$eZ&de'ddfde'ddfde'ddfe&d e'de%fdZ(dZ)dd Z*dd Z+dd Z,e#fd d!Z-d"d# Z.d$d% Z/e0d&kre/  dS dS )'u  Publish one solo-dataset clip after a codec round-trip, in the E2E turn viewer.

Diagnostic companion to `render_solo_gt_turn.py`. That script renders the
preprocessed GT rotations with NO codec and NO generator; this one takes the
SAME clip through `encoder -> continuous latent -> decoder` — the exact decode
contract the solo generator is trained against — and renders the result.

Because the two turns differ ONLY by the codec round-trip, comparing them in
the viewer separates two causes of bad arms:

    GT already wrong          -> preprocessing / rest-pose retargeting
    GT fine, round-trip wrong -> arms/fingers codec reconstruction

`face.json` and `audio.wav` are copied byte-for-byte from the GT turn rather
than re-rendered, so the body is provably the only difference. That also keeps
this process free of the face package, whose `scripts` module collides with
GestureVRM's (the same reason `infer_turn.py` runs the body in a subprocess).

WRITER (2026-08-05, ceiling-gate A/B) — `vrma_writer.write_vrma` is NOT used.
Its built-in generic skeleton has no upperChest, so that node is orphaned and
shoulders/neck rewire onto chest; the published pose breaks (elbow 72°->149°,
see render_solo_gt_turn.py docstring). The GT turn sidesteps this by shipping
the baked vrma verbatim — which a round-trip cannot do, since its rotations are
new. So we take that same baked vrma as the TEMPLATE and overwrite only the
rotation floats, leaving hierarchy, humanoid map and node rests byte-identical:

    npz basis = inv(rest_node) o channel     (vrma_to_npz.convert_humanoid)
    => channel = rest_node o basis           (this writer)

`--self-verify` (default on) re-writes the GT basis through the same path and
asserts it reproduces the baked channels, so the writer is proven neutral
before any codec output is judged. Measured 9.7e-08 max component error, i.e.
float32 noise. Bones outside the 50-bone contract (eyes, toes) keep their GT
channels; upperChest is absent from GG_11's humanoid map entirely.

    python render_solo_codec_roundtrip_turn.py         --npz /data/mocap-convertor/datasets/solo_20260722_body_v1/processed/npz/test/solo_20260722_174736_seg000126.npz         --gt-turn GT_15_restref_seg126         --id CODEC_15_roundtrip_seg126
    N..zmotion-blenderexperimentsz
wave-handsmotion
GestureVRMlib)quaternion_to_rotation_6d)FINGER_BONES	_accessor_qmul	_read_glb   #solo_body_v1__50b_vel__scratch__r02outputs	gen_trainruns)hipsspinechest
upperChestneckheadleftShoulderleftUpperArmleftLowerArmleftHandrightShoulderrightUpperArmrightLowerArm	rightHandleftUpperLegleftLowerLegleftFootrightUpperLegrightLowerLeg	rightFoot$      0      '      r   armslegsfingers   c                 C   st   | t t jj| ddddd  } |t t jj|ddddd  }t t j| | dddd}t dt | S )	NTaxiskeepdims:0yE>r2                 ?g       @)npcliplinalgnormabssumdegreesarccos)abdots rC   l/home/gglabs/Desktop/service/kemix-engine/package/avatar-E2E-inference/./render_solo_codec_roundtrip_turn.pyangular_error_deg[   s      rE   c           
   
   C   s*  t ttjtd}|d tkrtd|d  tjtd}i i }}t	
 D ]d\}\}}tj|d | }|tjttj sNtd| t|}	|	tj|ddd	d
  |	 | ||< ttj|d| dtjttj|d| dtjf||< q+|||fS )zKLoad the selected scratch codecs, refusing any checkpoint outside this run.zcodec_selection.jsonrun_idzforeign codec selection: mean_stdcheckpointsz%checkpoint escaped the isolated run: cpuF)map_locationweights_onlynetvrm_z	_mean.npyz_std.npy)jsonloadopenospathjoinRUN_ROOTRUN_IDRuntimeErrorPARTSitemsabspath
startswithsepivcreate_vqvaeload_state_dicttorchevaltor8   astypefloat32)
device	selectionmeanstdmodelsstatspartdim_ckptmodelrC   rC   rD   load_codecsb   s$   

""

rn   c              	   C   s  t | }| t }|r1tj| tj| dd |ddgdd} tj|tj|dd |ddgdd}t | }tt| tj}t	|}	|dd |dd  |	dd< |ddddf 
|d|ddddf 
|dtj|ddddf 
|d|	gdd|dddtf 
|dd	}
i }t E tD ]:}|| \}}t|
| | | d|}|| |}|| |dd
dd   }|| | ||< qW d   n1 sw   Y  tj|tdftjd}|d 
|dd|ddddf< |d 
|dd|ddddf< |d ddddf 
|dd|ddddf< |d 
|td|dddtf< tt|}|d ddddf }t|}|d |d< |d tj|dd dd |dd< |d| |d| fS )at  (T,NB,4) GT basis quats -> continuous codec round-trip -> (T,NB,4) + (T,3).

    The codec downsamples by CODEC_STRIDE, so T is edge-padded up to a multiple
    of it and trimmed back. The ceiling gate instead truncates; padding is used
    here so every published frame is codec output (a truncated tail spliced with
    GT would read as a pop at the loop boundary).
    r0   Nr   r5      r&   r(   r*   r+      dtyper   r,      r-   r%   r.   r)   )lenCODEC_STRIDEr8   concatenaterepeatasarrayr   rb   rc   
zeros_likereshapeNBr_   no_gradrW   
from_numpy	unsqueezera   
map2latentdecoderpermuterI   numpyzerosNFr\   rotation_6d_to_quaternion
empty_likecumsum)qtransrg   rh   rd   Tpadframesr6trans_vfeaturesrecri   meanstdxlatentoutrec_r6q_recrec_v	trans_recrC   rC   rD   	roundtripy   sF   
&&
 (
"$""2"
&r   c              	      s  t | \ t  d d d }}d d d d }|d fd	d
|d D }d}	|dp6i d}
|d D ]}|d d dkrX|d d |
krX|d  d }	q>d} fdd}t|D ]b\}}||}|rx|ddu ryqf|d }||vrqftj|| dg dtjd}tj|dd|f tjd}|ttjj	|ddddd }t
t|t|df|}||| | |d7 }qf|dur|	dur||	tj|tjd tjddd }|d!t| d"  7 }t }|d#t| d"  7 }d$t| d% t| }t|d&6}|td'd(d)| |td*t|d+ || |td*t|d, || W d   |S 1 sKw   Y  |S )-a  Copy `template_vrma`, replacing only its rotation floats with `basis`.

    Returns the max |component| deviation the caller can use to self-verify.
    Bones absent from the humanoid map (upperChest on GG_11) or outside `bones`
    (eyes, toes) keep the template's own channels.
    nodes
animationsr   
extensionsVRMC_vrm_animationhumanoid
humanBonessamplersc                    s6   i | ]}|d  d dkr|d  d  |d  d qS )targetrR   rotationnodesampleroutputrC   ).0ch)r   rC   rD   
<dictcomp>   s    z"write_channels.<locals>.<dictcomp>channelsNr   r   r   rR   translationr   r   c                    s   d |  }d |d  }| dd}|d t|kr*td|d  dt| tj|tjd	}|j|d
 kr=td|  |||j < dd |jddD |d< dd |j	ddD |d< d S )N	accessorsbufferViews
bufferView
byteOffsetr   countzframe count differs: template z	 vs data rq   
byteLengthzbufferView length mismatchc                 S      g | ]}t |qS rC   floatr   vrC   rC   rD   
<listcomp>       z2write_channels.<locals>._store.<locals>.<listcomp>r5   minc                 S   r   rC   r   r   rC   rC   rD   r      r   max)
getrt   
ValueErrorr8   ascontiguousarrayrc   nbytestobytesr   r   )acc_idxvaluesaccbvstartdata)blobgltfrC   rD   _store   s   zwrite_channels.<locals>._storer   )r6   r6   r6   r7   rq   r0   Tr1   r4   ro   ),:)
separatorszutf-8    r/       r*   rs   wbz<4sIIs   glTFrp   z<I4ss   JSONs   BIN )r   	bytearrayr   	enumerater8   rx   float64r9   r:   r;   r
   tilert   rc   rN   dumpsencodebytesrP   writestructpack)template_vrmabasisr   out_pathbonesr   animhbrot_out	trans_out	hips_noder   writtenr   slotboneentryr   restr   channel
json_chunk	bin_chunktotalfrC   )r   r   r   rD   write_channels   s`   
 
 


r   c              
   C   s   t | \}}t |\}}d}||||fffD ]`\}}}	|d d }
|
d D ]P}|d d dkr0q%|
d |d	  d
 }t|||tj}t|	d |	d |tj}ttj|| ddd}d||dk< t|tt	|||   }q%q|S )zEMax |component| difference between two vrma files' rotation channels.r6   r   r   r   r   rR   r   r   r   r   ro   r0   Tr1   r7   )
r   r	   rb   r8   r   signr=   r   r   r<   )a_pathb_pathgabagbbbworstr   r   otherr   r   r   r   ysgnrC   rC   rD   channel_deviation   s    "	r   c                     s<  t  } | jdddd | jdddd | jddd | jd	d
d | jdtdd | jddd | jddd |  }tjtdd|j	}dD ]}tj
tj||s_t| d| dqHtjtdd|j}tj|rz|jszt| dt }tj|jdd9}|d tj |d tj}dd |d D }t|d  }	t|d! }
t|d" }t|d# }W d    n1 sw   Y   jd$d  td%fkrtd&t d' j |tkrtd(t d)tjtjtj|jd*d*d+|
tjtj|jd d, }tj
|std-| tj |dd. tj|d/}d }|j!sRtj|d0}t"| || t#||}t$| |d1krRt%d2|d3d4t&'t&j() r`d5|j* nd6}t+|\}}}t }t, ||||\}t | } fd7d8t-. D }ttj/0|d9 |d9  d: }t"||| dD ]}t12tj||tj|| qt3t4tj|d;d< }i d=|jd>|j5d?t6d@t|dAtt7dBd;dCd/dDdEdFdGdHdIdJ|dKtj8|dLt9dMdNdO|dP dQ|j	dR|dSt:|dT|	|
tj8|j||dUt:|dTt:t | dTdVdW}t4tj|dXdY}t3j;||dd$dZ W d    n	1 sIw   Y  t<|j d[t7 d\|
 d]|d^d_ |d urpt<d`|dadb |. D ]\}}t<dc|ddde|df dgdh|di djdk qtt<dl|d^dm t<| d S )nNz--npzTzpreprocessed solo GT clip)requiredhelpz	--gt-turnz4existing GT turn id to copy face.json/audio.wav fromz--id)r   z	--emotionneutral)defaultz--gpur   )typer   z--no-self-verify
store_true)actionz--forcer   turns)	face.json	audio.wav/u    — render the GT turn firstz2 already exists; pass --force or choose another idF)allow_picklevrm_rotationsr   c                 S   r   rC   )strr   rC   rC   rD   r     r   zmain.<locals>.<listcomp>
bone_names
dataset_idsplitsegment_typespeech_ratioro   r/   zexpected (T,z,4) GT rotations, got z&dataset bone order does not match the z-bone contractr   vrmaz.vrmazbaked vrma not found: )exist_okz	body.vrmaz_writer_selfcheck.vrmagh㈵>zwriter is not neutral: z.3eu    — A/B would be invalidzcuda:rI   c                    sp   i | ]4\}\}}|t t d d |f d d |f  t tt d d |f d d |f ddqS )N_   )mean_angle_degp95_angle_deg)r   rE   r   r8   
percentile)r   ri   rk   slq_gtr   rC   rD   r   G  s    
("zmain.<locals>.<dictcomp>r0   g      Y@r  
num_framesidemotionfpsface_framesbody_framesfacebodyaudior  body_source'codec_roundtrip_continuous_no_generatorbody_writerbaked_template_channel_rewrite writer_self_verify_max_componentbody_vrma_templaterF   decode_mode
continuouscodec_sha256sha256compare_withclip_angle_error_degz2codec velocity, integrated from the GT first framerp   r6   )face_sbody_stotal_s)root_translationtranslation_drift_cmr  dataset_split
source_npzr
  r  timingz	meta.jsonw)ensure_asciiindentz: z	 frames [z	], codec z.2fsu     writer 자기검증 z.2eu	    (중립)z  8sz mean r  z7.3fu   °  p95 r  z8.3f   °u     이동 종점 드리프트 cm)=argparseArgumentParseradd_argumentint
parse_argsrQ   rR   rS   HEREgt_turnisfileFileNotFoundErrorr  existsforceFileExistsErrortimer8   rO   npzrb   rc   r  r   shaper{   r   BONESnormpathdirnamesplitextbasenamemakedirsno_self_verifyr   r   removerV   r_   rd   cudais_availablegpurn   r   rW   rX   r:   r;   shutilcopyfilerN   rP   r  FPSrt   rY   rU   rounddumpprint)apargsgt_dirnameturn_dirstartedr   r   source_bonesr  r	  r
  r  baked	body_pathself_verifyproberd   rg   rh   re   t0r   codec_sper_partdrift_cmr  metar   ri   rowrC   r  rD   main  s   


" $	
(
rj  __main__)1__doc__r9  rN   rQ   rS  r   sysrE  r   r8   r_   rR   rJ  rY   __file__r>  rS   KEMIXWHinsertscripts.infer_vrma
infer_vrmar\   dataloaders.vrm_datasetr   vrma_to_npzr   r	   r
   r   rU  rU   rT   BODY20listrH  rt   r{   r   slicerW   ru   rE   rn   r   r   r   rj  __name__rC   rC   rC   rD   <module>   sR    (	3E 
