o
    ijI                  
   @   sp  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	ddl
mZmZ ddlmZmZ ddlZddlZejejeZejejeddddd	Zejejedddd	Zejejedddd
dZejejedddd
ZejejeddddZejejedddddZejeddZ ejeddZ!dd Z"e"ejed g dZ#ej$%ddZ&ej$%ddZ'dZ(ddddd d!e#d"id gd#d$Z)e* Z+da,i Z-e* Z.d%d& Z/d'd( Z0d)d* Z1dEd,d-Z2d.d/ Z3d0d1 Z4d2d3 Z5dFd5d6Z6dGd8d9Z7d:d; Z8d<d= Z9d>d? Z:G d@dA dAeZ;dBdC Z<e=dDkr6e<  dS dS )Hu  avatar E2E hub — realtime streaming v2.

The browser connects to OpenAI Realtime directly over WebRTC. The assistant's
remote audio is captured in the browser in parallel with delayed live playback,
then sent here as 0.8s PCM16 chunks. A persistent body worker and the warm
causal face model return time-aligned frames before that delayed audio is heard.

  GET  /                       -> conversation.html
  POST /api/session            -> mint OpenAI Realtime ephemeral token (server key stays here)
  POST /api/warmup             -> warm face + body once, before conversation starts
  POST /api/stream/start       -> create stateful face/body stream
  POST /api/stream/chunk       -> PCM16 chunk -> newest face/body frames
  POST /api/stream/end         -> flush final short chunk and release stream
  POST /api/infer_turn {audio_b64, emotion, id?}
                               -> render_turn(wav, emotion) -> {id, dir, meta}; rebuilds turns index
  GET  /outputs/turns/<id>/*   -> the produced face.json / body.vrma / audio.wav (viewer reads these)

    OPENAI_API_KEY=sk-... python server.py [--port 8318]

Needs OPENAI_API_KEY only for /api/session; /api/infer_turn works without it (offline test).
    N)SimpleHTTPRequestHandlerThreadingHTTPServer)unquoteurlsplitz..shaderzpseudo-nilotoonznpm-packagesrcvendorjsmassetszlipsync-wasmv2zidle01.vrmaoutputsturnsmicc                 C   s   zGt | 8}|D ]+}| }|r|dsd|vrq|dd\}}tj| | dd qW d   W dS 1 s@w   Y  W dS  tyQ   Y dS w )u   Minimal .env loader (no dependency): KEY=VALUE per line, # comments, optional
    quotes. Does NOT override vars already in the environment (real env wins). The
    .env holds the OpenAI secret and is git-ignored — never log its values.#=   "'N)openstrip
startswithsplitosenviron
setdefaultFileNotFoundError)pathflinekv r!   	server.py_load_dotenv6   s   
$&r#   z.env)neutraljoylaughter
excitement	agreement	gratitudesadnesscryingsulkapologystruggleangerrefusalsurpriseflustershyREALTIME_MODELzgpt-realtimeREALTIME_VOICEmarinun  당신은 친근한 한국어 아바타입니다. 답변은 짧고 대화체로. 매 사용자 발화에 대한 첫 응답에서는 음성을 생성하지 말고 set_emotion 도구를 정확히 한 번 호출해 다음 발화의 톤에 가장 맞는 감정 하나를 넘기세요. 도구 결과를 받은 뒤 이어지는 응답에서만 짧게 한 번 말하세요.functionset_emotionzNBefore speaking, report the single emotion that best matches the coming reply.objectemotionstring)typeenum)r<   
propertiesrequired)r<   namedescription
parametersc                  C   s   t d u r
dd l} | a t S )Nr   )_infer
infer_turn)rD   r!   r!   r"   inference_modulef   s   rE   c                  O   s   t  j| i |S N)rE   render_turn)akwr!   r!   r"   rG   n   s   rG   c                 C   sP   | s
t jdt jdS tj| dd}t|d rtdt j|ddt jd S )	Nr   )dtypeT)validate   z$PCM16 payload has an odd byte lengthz<i2g      @)	npzerosfloat32base64	b64decodelen
ValueError
frombufferastype)valuerawr!   r!   r"   _decode_pcm16r   s   rX   streamc                 C   s6   | p| dt  jd d  } tdd| d d S )N_   z[^\w-]+@   )uuiduuid4hexresub)rV   prefixr!   r!   r"   	_clean_id{   s    rc   c                   C   s2   t  t  W  d    S 1 sw   Y  d S rF   )_lockrE   warm_modelsr!   r!   r!   r"   re      s   $re   c                 C   sT   t  t| |u rt| d W d   dS W d   dS 1 s#w   Y  dS )zCRemove only this state instance; a reused id must never be removed.N)_streams_lock_streamsgetpop)	stream_idstater!   r!   r"   _remove_stream   s   "rl   c                 C   sN   zt  | | W d   W dS 1 sw   Y  W dS  ty&   Y dS w )zABest-effort worker cleanup after a partially completed operation.N)rd   body_stream_cancel	Exception)modrj   r!   r!   r"   _cancel_body_quietly   s   &rp         @c              	   C   sR  t  }d |t dt d}t | tv rtd| d|t| < W d    n1 s,w   Y  d}|d Q z0t |j| ||dd}d	}|j	|
 | d
}W d    n1 sZw   Y  ||d< d|d< W n ty   d|d< |rzt||  t| |  w W d    n1 sw   Y  | |d |d |d |d |ddt|jdS )Nstarting)facer:   createdstatuslockstream z already existsFrv      )r:   guidanceemit_framesT)scenario_idrs   activeru   failedfpssample_raterz   bonesrotation_spacevrm_normalized_humanoid)rj   r~   r   rz   r   r   expressions)rE   time	threadingLockrf   rg   KeyErrorrd   body_stream_start
FaceStreamget_face_modelrn   rp   rl   rh   listARKIT_52_NAMES)rj   r:   ry   ro   rk   body_started	body_metaface_streamr!   r!   r"   start_stream   sT   



r   Fc                 C   s2  t  t| }W d    n1 sw   Y  |d u r"td| t|}t }t }|d  |d dkrDtd| d|d  |rJd|d< |tv rR||d< |d }zt	B t }|ri|j
| ||d	}	n|j| ||d	}	t }
t|	d
d}t|	dd}|d j||||d}t }W d    n1 sw   Y  | |||	dg |	ddt|d |ttt | d tt|| d tt|
| d tt||
 d d}W n ty   d|d< t||  t| |  w |rd|d< t| | |W  d    S 1 sw   Y  d S )Nzunknown stream rv   ru   r|   rw   z is closingr:   )pcm_b64r:   frame_startr   framesrs   )r   bodyr   r        )rj   r   r   r   r   rs   r:   inference_msqueue_msbody_msface_msr}   closed)rf   rg   rh   r   rX   r   perf_counterrE   EMOTIONSrd   body_stream_endbody_stream_chunkintpush_pcmrM   roundtolistrn   rp   rl   )rj   r   r:   endrk   pcmt0ro   lock_acquired_atr   body_finished_atr   n_framesrs   face_finished_atpacketr!   r!   r"   stream_frames   sv   





	
&r   c              
   C   s  t  t| }W d    n1 sw   Y  |d u r | ddS t }|d P |d dv r?t| | | ddW  d    S d|d< z"t ||  W d    n1 sVw   Y  W d|d< t| | n
d|d< t| | w W d    n1 szw   Y  | ddS )	NF)rj   	cancelledrv   ru   )r   r}   r   r   T)rf   rg   rh   rE   rl   rd   rm   )rj   rk   ro   r!   r!   r"   cancel_stream  s.   



r   c                  C   s|   g } t ttjtddtjjdD ]}tt	|}d|d  |d< | 
| qtj| t	tjtddd	d
d | S )N*z	meta.json)keyoutputs/turns/iddirz
index.jsonwFr   )ensure_asciiindent)sortedglobr   r   join	TURNS_DIRgetmtimejsonloadr   appenddump)idxmetamr!   r!   r"   rebuild_index  s   
,"r   c               	   C   s   t jd} | stdddtddtiitdgtgdd	i}tj	d
d|  dd|dd}|
  | }|dpC|dp@i d}|sTtdt|d d  ||dttdS )NOPENAI_API_KEYz$OPENAI_API_KEY not set on the serversessionrealtimeoutputvoiceaudior?   )r<   modelr   instructionsoutput_modalitiestoolstool_choicez1https://api.openai.com/v1/realtime/client_secretszBearer zapplication/json)AuthorizationContent-Type   )headersr   timeoutrV   client_secretz no ephemeral token in response: ,  
expires_at)rV   r   r   emotions)r   r   rh   RuntimeErrorr4   r5   INSTRUCTIONSSET_EMOTION_TOOLrequestspostraise_for_statusr   dumpsr   )r   r   rdvalr!   r!   r"   mint_ephemeral_token"  s0   
	
r   c                       sD   e Zd Z fddZ fddZ fddZdd Zd	d
 Z  ZS )Handlerc                    s   t  j|dti| d S )N	directory)super__init__HERE)selfrH   rI   	__class__r!   r"   r   >  s   zHandler.__init__c           	   	      s   t t|j}|dkrtS dtfdtfdtfdtfdtff}|D ]F\}}|	|s*q |t
|d d}tjtj||}ztj||f|k}W n tyW   d	}Y nw |r^|  S tj|d
  S t |S )a  Expose the canonical Pseudo NiloToon ESM sources without copying them.

        SimpleHTTPRequestHandler normally serves only HERE. The conversation
        viewer imports the shader from /pseudo-nilotoon/*, which maps to the
        sibling package's browser-ready npm source directory. Its one relaxed
        idle clip is exposed as an exact file route rather than mounting the
        surrounding lipsync package.
        z/idle01.vrmaz/pseudo-nilotoon-core/z/pseudo-nilotoon-jsm/z/pseudo-nilotoon-vendor/z/pseudo-nilotoon-assets/z/pseudo-nilotoon/N/F__not_found__)r   r   r   IDLE_VRMA_PATHPSEUDO_NILOTOON_CORE_SRCPSEUDO_NILOTOON_JSMPSEUDO_NILOTOON_VENDORPSEUDO_NILOTOON_ASSETSPSEUDO_NILOTOON_SRCr   rR   lstripr   realpathr   
commonpathrS   r   translate_path)	r   r   request_pathmountsrb   rootrel	candidateinside_rootr   r!   r"   r   A  s6   	
zHandler.translate_pathc                    s*   d| j pdv rt j|g|R   d S d S )Nz/api/ )r   r   log_message)r   fmtargsr   r!   r"   r   c  s   zHandler.log_messagec                 C   sT   t j|dd }| | | dd | dtt| |   | j	| d S )NF)r   r   zapplication/json; charset=utf-8Content-Length)
r   r   encodesend_responsesend_headerstrrR   end_headerswfilewrite)r   codeobjbr!   r!   r"   _jsong  s   
zHandler._jsonc                 C   s  | j dd }zt| jdd}|rt| j|ni }W n t	y= } z| 
ddd| iW  Y d }~S d }~ww |dkrz| 
dt W S  tjyn } z| 
d	dd
|jjd d  iW  Y d }~S d }~w t	y } z| 
ddt|iW  Y d }~S d }~ww |dkrz| 
ddt dW S  t	y } z| 
ddt|iW  Y d }~S d }~ww |dkr|dd}|tvrd}t|d}z| 
dt||t|dddW S  t	y } z| 
ddt|iW  Y d }~S d }~ww |dv rw|d}|s| 
dddiS |dd}|dr(|s(| 
dddiS zt|||d|dd }| 
d|W S  tyZ } z| 
d!dt|iW  Y d }~S d }~w t	yv } z| 
ddt|iW  Y d }~S d }~ww |d"kr|d}|s| 
dddiS z	| 
dt|W S  t	y } z| 
ddt|iW  Y d }~S d }~ww |d#krf|dd}|tvrd}|d$}	|	s| 
ddd%iS tjtdd& td'd(|dpd)tt d*  d d+ }
tj  t|
 d,}t!|d-}|"t#$|	 W d    n	1 sw   Y  z*t% t&|||
}W d    n	1 s3w   Y  t'  | 
d|
d.|
 |d/W S  t	ye } z| 
ddt|iW  Y d }~S d }~ww | 
d!dd0iS )1N?r   r  i  errorzbad request body: z/api/session   i  zOpenAI token mint failed: r   i  z/api/warmupT)oktimingi  z/api/stream/startr:   r$   r   ry   rq   )ry   )z/api/stream/chunkz/api/stream/endrj   zmissing stream_idr   r   z/chunkzmissing pcm_b64z/end)r:   r   i  z/api/stream/cancelz/api/infer_turn	audio_b64zmissing audio_b64exist_okz\W+rZ   turn_r   0   z.wavwbr   )r   r   r   zunknown endpoint)(r   r   r   r   rh   r   loadsrfilereadrn   r  r   r   	HTTPErrorresponsetextr  re   r   rc   r   floatendswithr   r   r   r   makedirsMIC_DIRr`   ra   r   r   r   r	  rP   rQ   rd   rG   r   )r   r   nreqer:   rj   r   r   b64out_idwav_pathr   r   r!   r!   r"   do_POSTo  s   ".  
 

  

 


4 zHandler.do_POST)	__name__
__module____qualname__r   r   r   r  r)  __classcell__r!   r!   r   r"   r   =  s    "r   c                  C   s   t  } | jdtdd |  }tjtdd t  tj	
dr"dnd}td	t d
|j  td|j d tdt dt d|  tdtj	
dd  td td|jft  d S )Nz--porti~   )r<   defaultTr  r   setzNOT set (/api/session will 503)zserving z on http://127.0.0.1:z  open   http://127.0.0.1:z/conversation.htmlz	  model  z  voice z  OPENAI_API_KEY z	  body   AVATAR_BODY_PROFILEv4zD  mode   realtime v2 (24 frames / 0.8s per body chunk, 30fps output)z	127.0.0.1)argparseArgumentParseradd_argumentr   
parse_argsr   r!  r   r   r   rh   printr   portr4   r5   r   r   serve_forever)aprH   r   r!   r!   r"   main  s   r:  __main__)rY   )rq   )NF)>__doc__r2  rP   r   r   r   r`   r   r   r]   http.serverr   r   urllib.parser   r   numpyrM   r   r   dirnameabspath__file__r   r   r   r   r   r   r   r   r   r   r"  r#   r   r   rh   r4   r5   r   r   r   rd   rC   rg   rf   rE   rG   rX   rc   re   rl   rp   r   r   r   r   r   r   r:  r*  r!   r!   r!   r"   <module>   s    



	
	
*C	 

