Ë
    Pjj!k  ã                  ó*  — d Z ddlm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mZmZ ddlmZ ddlmZ ddlmZ dd	lmZ dd
lmZmZmZmZ  ej6                  e«      Z e
«       Zdd„ZejA                  d«       ed¬«      f	 	 	 	 	 dd„«       Z!y)u  
routers/websocket.py
â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
WebSocket endpoint: /ws/{session_id}?token=<jwt>

Protocol (JSON frames):
  CLIENT â†’ SERVER:
    { "type": "text",        "content": "..." }
    { "type": "audio_b64",   "audio_b64": "<base64 webm/opus>" }
    { "type": "end_session" }
    { "type": "ping" }

  SERVER â†’ CLIENT:
    { "type": "text_delta",  "content": "token" }
    { "type": "text_done",   "content": "full response", "turn": N }
    { "type": "audio_chunk", "audio_b64": "<base64 PCM16 @ 24kHz>" }
    { "type": "audio_done" }        â†� signals end of audio stream for this turn
    { "type": "sentiment",   "data": {...} }
    { "type": "transcription","content": "..." }
    { "type": "error",       "content": "message" }
    { "type": "pong" }
    { "type": "session_end", "turn": N }

AUDIO FORMAT NOTE:
  audio_chunk payloads contain raw signed-16-bit little-endian PCM @ 24 kHz
  mono (no WAV header). The frontend AudioEngine decodes each chunk directly
  into an AudioBuffer â€” zero buffering, zero lag.
  Backend: openai_service.stream_tts_chunks must use response_format="pcm".
é    )ÚannotationsN)ÚdatetimeÚtimezone)Ú	APIRouterÚQueryÚ	WebSocketÚWebSocketDisconnect)ÚWebSocketState)ÚChatMessage)Ústore)Údecode_token)Úanalyse_sentimentÚstream_ai_responseÚstream_tts_chunksÚtranscribe_audioc              ƒ  óÄ   K  — | j                   t        j                  k7  ry	 | j                  t	        j
                  |«      «      ƒ d{  –—†  y7 Œ# t        $ r Y yw xY w­w)zµBest-effort send. Skips if the socket has already disconnected so we
    don't trip the "WebSocket is not connected" RuntimeError storm Starlette
    raises after the client closes.N)Úclient_stater
   Ú	CONNECTEDÚ	send_textÚjsonÚdumpsÚ	Exception)ÚwsÚobjs     úk/Users/priyanka/Documents/AI_Agent_Assessment/pepole_hub_candidate/people_hub_candidate/routes/websocket.pyÚ_sendr   :   sM   è ø€ ð 
‡�œ.×2Ñ2Ò2ØðØ�l‰lœ4Ÿ:™: c›?Ó+×+Ò+ùÜò Ùðüs:   ‚A ¢'A Á	AÁ
A ÁA ÁA Á	AÁA ÁAÁA z/ws/{session_id}Ú )Údefaultc              ƒ  ót  ‡ ‡‡‡‡‡‡‡‡‡‡‡‡K  — ‰ j                  «       ƒ d {  –—†  t        j                  d‰«       d}|r9	 |j                  d«      rt	        |«      dkD  rd}nt        |«      }t        |«      }|sYddl	m
} |j                  s2t        ‰ d	d
dœ«      ƒ d {  –—†  ‰ j                  d¬«      ƒ d {  –—†  y t        j                  d«       t        j                  ‰«      ƒ d {  –—† Š‰s0t        ‰ d	ddœ«      ƒ d {  –—†  ‰ j                  «       ƒ d {  –—†  y t        j                   ‰j"                  «      ƒ d {  –—† Š‰s0t        ‰ d	ddœ«      ƒ d {  –—†  ‰ j                  «       ƒ d {  –—†  y dd d dœŠd.ˆfd„Šd/ˆfd„Šd0ˆˆˆˆ fd„Šd1ˆˆˆˆˆ fd„Šh d£Šd2ˆfd„Šd3ˆˆˆ fd„}d4ˆˆˆˆˆ fd„Šd5ˆˆˆˆ fd„Šd6d7ˆ fd„} |«       ƒ d {  –—† }	|	rm|	j%                  d«      dk(  rY|	j%                  d«      xs dj'                  «       }
|
dv r0|
‰d<   t        j                  d |
‰«       |
d!k(  r ‰«       ƒ d {  –—†  d }		 ‰j(                  s |«       ƒ d {  –—†  d8ˆˆˆˆˆˆˆ fd#„}|	r ||	«      ƒ d {  –—† }|s
t+        «       ‚	 ‰ j-                  «       ƒ d {  –—† }	 t/        j0                  |«      } ||«      ƒ d {  –—† }|snŒB	 	 t        j<                  ‰«      ƒ d {  –—†  ‰ j>                  t@        jB                  k(  r	 ‰ j                  «       ƒ d {  –—†  y y 7 �Œ
# t        $ r"}t        j                  d‰|«       Y d }~�ŒÛd }~ww xY w7 �Œ½7 �Œ¦7 �Œv7 �Œ`7 �ŒK7 �Œ&7 �Œ7 �Œû7 �Œ�7 �Œ3# t        $ r!}t        j                  d"|«       Y d }~�ŒVd }~ww xY w7 �ŒE7 �Œ*7 �Œ# t.        j2                  $ r t        j                  d$‰«       Y �ŒKw xY w7 �Œ# t*        $ r t        j                  d%‰«       Y �Œ0t4        $ r‰}d&t7        |«      j9                  «       v sd't7        |«      j9                  «       v rt        j                  d(‰«       n1t        j;                  d)‰|d¬*«       t        ‰ d	d+dœ«      ƒ d {  –—†7   Y d }~�Œ¼d }~wt        $ r<}t        j;                  d,‰|d¬*«       t        ‰ d	d+dœ«      ƒ d {  –—†7   Y d }~�Œÿd }~ww xY w7 �Œî# t        $ r!}t        j                  d-|«       Y d }~�Œd }~ww xY w7 �Œè# t        $ r Y y w xY w# 	 t        j<                  ‰«      ƒ d {  –—†7   n,# t        $ r }t        j                  d-|«       Y d }~nd }~ww xY w‰ j>                  t@        jB                  k(  r*	 ‰ j                  «       ƒ d {  –—†7   w # t        $ r Y w w xY ww xY w­w)9Nz[WS] connected  session=%sFzak:é   Tz$[WS] auth error session=%s reason=%sr   )ÚsettingsÚerrorzAuthentication failed©ÚtypeÚcontenti1  )Úcodeu4   [WS] no/invalid token â€” allowing in APP_DEBUG modezSession not foundzScenario not foundÚvoice)ÚmodeÚvideo_avatarÚvideo_voicec                 ó,   •— ‰ j                  d«      dk(  S )Nr(   Úvideo©Úget©Ú
conn_states   €r   Ú_video_modez roleplay_ws.<locals>._video_mode‘   s   ø€ Ø�~‰~˜fÓ%¨Ñ0Ð0ó    c                 ó&   •— ‰ j                  d«      S )z¸Voice to use for any TTS call on this connection. Returns
        the avatar-paired voice once it's been picked, else None so
        stream_tts_chunks() falls back to the env default.r*   r-   r/   s   €r   Ú
_tts_voicezroleplay_ws.<locals>._tts_voice”   s   ø€ ð �~‰~˜mÓ,Ð,r2   c               “  ó¸  •K  — ‰d   r‰d   S ddl m}  d}t        ‰dd«      xs dj                  «       }|r4|t	        | j                  «       «      v r|}t        j                  d‰|«       |s)| j                  ‰¬«      }t        j                  d	‰|«       |‰d<   | j                  |«      ‰d
<   t        ‰d|| j                  |«      dœ«      ƒ d{  –—†  |S 7 Œ­w)uô  Pick an avatar lazily, on first use of video mode. Stored on
        the connection so subsequent turns reuse the same face â€” AND
        the same matched voice.

        Selection precedence:
          1. session.selected_avatar â€” set by the avatar-picker modal
             when IS_VIDEO_ROLEPLAY_SELECTION=true.
          2. seeded random pick keyed off session_id â€” the existing
             behaviour when the picker is disabled or the candidate
             didn't make a choice.
        r)   r   ©Úvideo_enginer   Úselected_avatarNz,[WS] avatar from picker session=%s avatar=%s)Úseedz'[WS] avatar random session=%s avatar=%sr*   )r$   ÚavatarÚ
avatar_url)Úservicesr7   ÚgetattrÚstripÚsetÚlist_available_avatarsÚlogÚinfoÚpick_avatarÚvoice_for_avatarr   r;   )r7   r:   Úpickedr0   ÚsessionÚ
session_idr   s      €€€€r   Ú_ensure_avatarz#roleplay_ws.<locals>._ensure_avatarš   sõ   øè ø€ ð �nÒ%Ø˜nÑ-Ð-Ý)àˆô ˜'Ð#4°dÓ;ÒA¸r×HÑHÓJˆÙ�f¤ L×$GÑ$GÓ$IÓ JÑJØˆFÜ�H‰HÐCÀZÐQWÔXáØ!×-Ñ-°:Ð-Ó>ˆFÜ�H‰HÐ>À
ÈFÔSà%+ˆ
�>Ñ"ð &2×%BÑ%BÀ6Ó%Jˆ
�=Ñ!ô �BØ(Ø Ø&×1Ñ1°&Ó9ñ
ó ÷ 	ð 	ð
 ˆð	ús   ƒCCÃCÃCc              “  óâ  •K  — ‰j                   st        ‰ddi«      ƒ d{  –—†  y ‰«       s3t        |  ‰«       ¬«      2 3 d{  –—† }t        ‰d|dœ«      ƒ d{  –—†  Œ!t        ‰ddi«      ƒ d{  –—†   ‰«       ƒ d{  –—†  t        «       }t        |  ‰«       ¬«      2 3 d{  –—† }	 |j	                  t        j                  |«      «       Œ/7 Œ¯7 ŒŽ7 Œy6 t        ‰ddi«      ƒ d{  –—†7   y7 Œ}7 Œp7 ŒM# t        $ r }t        j                  d|«       Y d}~Œxd}~ww xY w6 	 d	d
l
m}  ‰«       ƒ d{  –—†7  }|j                  t        |«      |«      ƒ d{  –—†7  }t        ‰d|d   |d   |d   |j                  dd«      dœ«      ƒ d{  –—†7   nÄ# t        $ r¸}t        j                  d|d¬«       t        ‰dt!        |«      dœ«      ƒ d{  –—†7   |rsd}t#        |«      }	t%        d	t'        |	«      |«      D ]M  }
t        |	|
|
|z    «      }t        ‰dt        j(                  |«      j+                  d«      dœ«      ƒ d{  –—†7   ŒO Y d}~nd}~ww xY wt        ‰ddi«      ƒ d{  –—†7   y­w)uw  Emit TTS for the AI's reply according to the active mode.

        chat:  no TTS, no video â€” just the audio_done sentinel so the
               frontend state machine keeps cycling.
        voice: stream PCM chunks live as before; frontend plays them
               through AudioEngine.
        video: collect the full PCM into a buffer, run Wav2Lip, and
               send a video_ready frame with the cached/generated MP4
               URL. Audio chunks are NOT forwarded to the client in
               this mode â€” the video has its own audio track baked in,
               so playing both would double the voice.
        r$   Ú
audio_doneN)r'   Úaudio_chunk)r$   Ú	audio_b64Úvideo_renderingz[WS][video] bad TTS chunk: %sr   r6   Úvideo_readyÚurlr:   ÚcachedÚfallbackF)r$   rO   r:   rP   rQ   z([WS][video] both render paths failed: %sT©Úexc_infoÚvideo_errorr#   i €  Úascii)Úvoice_onr   r   Ú	bytearrayÚextendÚbase64Ú	b64decoder   rA   Úwarningr<   r7   Úget_or_generateÚbytesr.   r"   ÚstrÚ
memoryviewÚrangeÚlenÚ	b64encodeÚdecode)Úfull_responserK   Úpcm_bufÚaudio_chunk_b64Úexcr7   r:   ÚresultÚCHUNKÚmvÚiÚsegrH   r4   r1   Úscenarior   s               €€€€€r   Ú_stream_tts_responsez)roleplay_ws.<locals>._stream_tts_responseÅ   sc  øè ø€ ð × Ò Ü˜˜V \Ð2Ó3×3Ð3ØáŒ}ô &7°}ÉJËLÕ%Y÷ S�kÜ˜B¨À[Ñ QÓR×RÑRô �B˜Ð!2Ð3Ó4×4Ð4ñ Ó×Ðä“+ˆÜ%6°}ÉJËLÕ%Y÷ 	B�/ðBØ—‘œv×/Ñ/°Ó@ÕAð= 4øðSøØRøð &Zä˜˜V \Ð2Ó3×3Ñ3Øð 	5øð 	øð	Bùô ò BÜ—‘Ð;¸S×AÑAûðBúð &Zð	Ý-Ù)Ó+×+Ð+ˆFØ'×7Ñ7¼¸g»ÈÓO×OÐOˆFÜ˜Ø)Ø" 5™MØ" 8Ñ,Ø" 8Ñ,Ø"ŸJ™J z°5Ó9ñó ÷ ò øô ò 	Ü�I‰IÐ@À#ÐPTˆIÔUô ˜ ]¼sÀ3»xÑHÓI×IÑIÙØ!�Ü Ó(�Ü˜q¤# b£'¨5Ö1�AÜ  1 Q¨¡Y Ó0�CÜ Ø$1Ü%+×%5Ñ%5°cÓ%:×%AÑ%AÀ'Ó%Jñ%ó ÷ ò ð 2ÿøð	úô  �B˜ Ð.Ó/×/Ò/ùs*  ƒI/¡C¢I/Á CÁCÁCÁI/ÁCÁI/Á2C0Á3I/ÂC2Â I/Â"D"Â&C4Â'D"Â*I/Â,$C6ÃI/ÃCÃI/ÃI/Ã)C,Ã*I/Ã2I/Ã4D"Ã6	DÃ?DÄI/ÄDÄI/Ä$F Ä4D7Ä5#F ÅEÅ3F ÆFÆF ÆI/Æ	IÆ3IÇGÇA2IÉIÉIÉ
I/ÉIÉI/É(I+É)I/>   õ	   à¤ à¥€à¤•õ	   à¤¹à¤¾à¤�õ	   à¤¹à¤¾à¤‚õ   à¤¤à¥ˆà¤¯à¤¾à¤°úlet'sú	all rightú	of courseúsounds goodÚgoÚokÚyepÚyesÚyupÚletsÚokayÚsureÚyeahÚbeginÚreadyÚstartÚalrightÚ
absolutelyc                óx   •‡— | xs dj                  «       j                  «       Š‰syt        ˆfd„‰D «       «      S )Nr   Fc              3  ó&   •K  — | ]  }|‰v –— Œ
 y ­w)N© )Ú.0ÚwÚts     €r   Ú	<genexpr>z:roleplay_ws.<locals>._is_ready_response.<locals>.<genexpr>,  s   øè ø€ Ð0¡<˜a�1˜”6¡<ùs   ƒ)r>   ÚlowerÚany)ÚtextrŠ   Ú_READY_WORDSs    @€r   Ú_is_ready_responsez'roleplay_ws.<locals>._is_ready_response$  s7   ù€ ØŠZ�R×ÑÓ ×&Ñ&Ó(ˆÙØô
 Ó0¡<Ó0Ó0Ð0r2   c            	   “  óD  •K  — ‰j                   r/‰j                   xs dj                  «       j                  «       d   nd} | rd| › d�}nd}‰j                  j	                  t        d|t        j                  t        j                  «      j                  «       ¬«      «       t        j                  ‰«      ƒ d{  –—†  |j                  d	«      D ]  }t        ‰d
|d	z   dœ«      ƒ d{  –—†  Œ t        ‰d|ddœ«      ƒ d{  –—†   ‰|«      ƒ d{  –—†  y7 Œ]7 Œ17 Œ7 Œ­w)uw  First AI turn: friendly greeting that doubles as a mic test.

        We deliberately don't run this through the LLM â€” keeping the
        wording deterministic means the cache key for the Wav2Lip /
        ffmpeg-dub render is stable across sessions, so every candidate
        after the first one gets an instant cached video instead of a
        30-90 s render.
        r   r   zHi z2! How are you today? Shall we start the interview?z:Hi there! How are you today? Shall we start the interview?Ú	assistant©Úroler%   Ú	timestampNÚ Ú
text_deltar#   Ú	text_done©r$   r%   Úturn)Úcandidate_namer>   ÚsplitÚmessagesÚappendr   r   Únowr   ÚutcÚ	isoformatr   Úsave_sessionr   )ÚnameÚgreetingÚtokrn   rF   r   s      €€€r   Ú_send_greetingz#roleplay_ws.<locals>._send_greeting.  s  øè ø€ ð EL×DZÒDZ�×&Ñ&Ò,¨"×3Ñ3Ó5×;Ñ;Ó=¸aÒ@Ð`bˆÙØ˜T˜FÐ"TÐU‰HàSˆHð
 	×Ñ×Ñ¤ØØÜ—l‘l¤8§<¡<Ó0×:Ñ:Ó<ô!
ô 	ô
 × Ñ  Ó)×)Ð)ð —>‘> #Ö&ˆCÜ˜ \¸cÀC¹iÑHÓI×IÑIð 'ä�B ¸È1ÑMÓN×NÐNÙ" 8Ó,×,Ñ,ð 	*øð
 JøØNøØ,úsH   ƒB7D Â:DÂ;-D Ã(DÃ)D ÄDÄD ÄDÄD ÄD ÄD ÄD c           	   “  ó
  •K  — | j                  «       } | sy‰j                  �sô‰j                  j                  t	        d| t        j                  t        j                  «      j                  «       ¬«      «        ‰
| «      rÈd‰_        t        j                  ‰«      ƒ d{  –—†  d‰j                  › d�}‰j                  j                  t	        d|t        j                  t        j                  «      j                  «       ¬«      «       d}t        ‰j                  ‰«      2 3 d{  –—† }||z  }t        ‰d|d	œ«      ƒ d{  –—†  Œ&d}‰j                  j                  t	        d|t        j                  t        j                  «      j                  «       ¬«      «       t        j                  ‰«      ƒ d{  –—†  |j!                  d«      D ]  }t        ‰d|dz   d	œ«      ƒ d{  –—†  Œ t        ‰d|ddœ«      ƒ d{  –—†   ‰|«      ƒ d{  –—†  y‰j                  j                  t	        d| t        j                  t        j                  «      j                  «       ¬«      «       d}t        ‰j                  ‰«      2 3 d{  –—† }||z  }t        ‰d|d	œ«      ƒ d{  –—†  Œ&7 �Œ7 �Œ‡7 �Œn6 t	        dd
¬«      ‰j                  d<   ‰j                  j                  t	        d|t        j                  t        j                  «      j                  «       ¬«      «       d‰_        t        j                  ‰«      ƒ d{  –—†7   t        ‰d|‰j                  dœ«      ƒ d{  –—†7    ‰|«      ƒ d{  –—†7   y7 �ŒÆ7 �Œ›7 �Œ„7 �Œw7 �Œ7 Œè6 ‰xj                  dz  c_        ‰j                  j                  t	        d|t        j                  t        j                  «      j                  «       ¬«      «       t        j                  ‰«      ƒ d{  –—†7   t        ‰d|‰j                  dœ«      ƒ d{  –—†7   t#        j$                  t'        | «      «      } ‰|«      ƒ d{  –—†7   	 |ƒ d{  –—†7  }t)        |t*        «      rt        ‰d|dœ«      ƒ d{  –—†7   n,# t,        $ r }	t.        j1                  d|	«       Y d}	~	nd}	~	ww xY w‰j                  ‰j2                  k\  r#t        ‰d‰j                  dœ«      ƒ d{  –—†7   yy­w)z8Handle one learner turn: stream AI reply + optional TTS.NÚuserr“   TzYou are u‡   . The candidate has just confirmed they're ready to begin. Start the interview now â€” open with your first scripted line in character.r   r—   r#   z[ready])r”   r%   éÿÿÿÿr’   é   r˜   r™   uX   No problem â€” just say 'yes' or 'I'm ready' whenever you'd like to start the interview.r–   r   Ú	sentiment)r$   Údataz[WS] sentiment skipped: %sÚsession_end©r$   rš   )r>   Úgreeting_doner�   rž   r   r   rŸ   r   r    r¡   r   r¢   Úai_characterr   r   rš   rœ   ÚasyncioÚcreate_taskr   Ú
isinstanceÚdictr   rA   r[   Úturns)rŽ   Úopening_promptÚfull_openingÚdeltaÚnudger¥   rd   Úsentiment_taskÚsentiment_resultrg   r�   rn   rm   rF   r   s             €€€€€r   Ú_process_learner_textz*roleplay_ws.<locals>._process_learner_textN  sJ  øè ø€ à�z‰z‹|ˆÙØð ×$Ó$Ø×Ñ×#Ñ#¤KØ TÜ"Ÿ,™,¤x§|¡|Ó4×>Ñ>Ó@ô%ô ñ " $Ô'Ø(,�Ô%Ü×(Ñ(¨Ó1×1Ð1ð ˜x×4Ñ4Ð5ð 6$ð $ð ð
 × Ñ ×'Ñ'¬Ø¨Ü&Ÿl™l¬8¯<©<Ó8×BÑBÓDô)ô ð  "�Ü#5°g×6FÑ6FÈÔ#Q÷ N˜%Ø  EÑ)�LÜ ¨\ÀeÑ$LÓM×MÑMðCˆEà×Ñ×#Ñ#¤KØ ¨%Ü"Ÿ,™,¤x§|¡|Ó4×>Ñ>Ó@ô%ô ô ×$Ñ$ WÓ-×-Ð-Ø—{‘{ 3Ö'�Ü˜B¨À#ÈÁ)Ñ LÓM×MÑMð (ä˜ [¸UÈAÑNÓO×OÐOÙ& uÓ-×-Ð-Øð 	×Ñ×Ñ¤ØØÜ—l‘l¤8§<¡<Ó0×:Ñ:Ó<ô!
ô 	ð ˆÜ-¨g×.>Ñ.>ÀÔI÷ 	F�%Ø˜UÑ"ˆMÜ˜ \¸eÑDÓE×EÑEðm 2ùðNùàMùð $Rô
 (3¸È	Ô'R�× Ñ  Ñ$Ø× Ñ ×'Ñ'¬Ø$¨lÜ&Ÿl™l¬8¯<©<Ó8×BÑBÓDô)ô ð  !�”Ü×(Ñ(¨Ó1×1Ñ1Ü˜B¨ÀÐW^×WcÑWcÑ dÓe×eÑeÙ*¨<Ó8×8Ñ8Øð .ùàMùØOùØ-ùð	FùàEøð Jð 	�Š˜Ñ�Ø×Ñ×Ñ¤ØØ!Ü—l‘l¤8§<¡<Ó0×:Ñ:Ó<ô!
ô 	ô
 × Ñ  Ó)×)Ñ)ä�BØØ$Ø—L‘Lñ
ó ÷ 	ñ 	ô !×,Ñ,Ô->¸tÓ-DÓEˆñ # =Ó1×1Ñ1ð	;Ø%3×3Ð3ÐÜÐ*¬DÔ1Ü˜B¨Ð>NÑ OÓP×PÑPùÜò 	;Ü�K‰KÐ4°c×:Ñ:ûð	;úð �<‰<˜8Ÿ>™>Ò)Ü˜ ]¸G¿L¹LÑIÓJ×JÒJð *ùs[  ƒBTÂ!J,Â"BTÄ$J5Ä(J/Ä)J5Ä,TÅJ2ÅA5TÆ:N Æ;-TÇ(NÇ)TÈNÈTÈN	ÈA3TÊNÊ
NÊNÊTÊ&NÊ'TÊ/J5Ê2TÊ5BTÍMÍ"TÍ(M+Í)TÍ9M<Í:TÎTÎTÎ	TÎNÎTÎBTÐPÐ"TÐ8P;Ð9.TÑ'Q*Ñ(TÑ.R" Ñ3Q6Ñ4'R" ÒRÒR" Ò!TÒ"	SÒ+SÓTÓSÓ8TÔTÔTc              “  ó˜  •K  — 	 t        j                  | «      }t        |‰j                  «      ƒ d{  –—† }|r(t	        ‰d|dœ«      ƒ d{  –—†   ‰|«      ƒ d{  –—†  yt	        ‰dddœ«      ƒ d{  –—†  y7 ŒF7 Œ/7 Œ!7 Œ# t
        $ r<}t        j                  d‰|«       t	        ‰dd|› �dœ«      ƒ d{  –—†7   Y d}~yd}~ww xY w­w)z+Transcribe audio blob then process as text.NÚtranscriptionr#   r   z [WS] audio error  session=%s: %sr"   zAudio processing failed: )rY   rZ   r   Úlanguage_coder   r   rA   r"   )rL   ÚrawrŽ   rg   r¼   rF   rG   r   s       €€€€r   Ú_process_audioz#roleplay_ws.<locals>._process_audioº  sÅ   øè ø€ ð	]Ü×#Ñ# IÓ.ˆCÜ)¨#¨w×/DÑ/DÓE×EˆDÙÜ˜B¨ÀTÑ JÓK×KÐKÙ+¨DÓ1×1Ñ1ô ˜B¨ÀRÑ HÓI×IÑIð FøàKøØ1øð JùÜò 	]Ü�I‰IÐ8¸*ÀcÔJÜ˜ WÐ;TÐUXÐTYÐ9ZÑ[Ó\×\Ö\ûð	]üsŒ   ƒC
….B ³A:´B ÁA<ÁB ÁA>ÁB Á!C
Á"B Á4B Á5B Á9C
Á:B Á<B Á>B Â B Â	CÂ,CÂ7B:Â8CÂ=C
ÃCÃC
c              “  ó  •K  — 	 t        j                  ‰j                  «       | ¬«      ƒ d {  –—† }	 t        j                  |«      S 7 Œ# t         j                  t        f$ r Y y w xY w# t
        j                  $ r Y y w xY w­w)N)Útimeout)r±   Úwait_forÚreceive_textÚTimeoutErrorr   r   ÚloadsÚJSONDecodeError)Ú	timeout_srÀ   r   s     €r   Ú_drain_first_frame_or_timeoutz2roleplay_ws.<locals>._drain_first_frame_or_timeoutØ  sv   øè ø€ ð	Ü×(Ñ(¨¯©Ó):ÀIÔN×NˆCð	Ü—:‘:˜c“?Ð"ð	 OùÜ×$Ñ$¤iÐ0ò 	Ùð	ûô ×#Ñ#ò 	Ùð	üsU   ƒB…(A
 ­A®A
 ³A) ÁBÁA
 Á
A&Á#BÁ%A&Á&BÁ)A?Á<BÁ>A?Á?Br$   r(   ©Úchatr'   r,   z"[WS] mode (pre-open)=%s session=%sr,   ú[WS] avatar pick failed: %sc              “  ó¦  •K  — | j                  dd«      }|dk(  rt        ‰
ddi«      ƒ d {  –—†  y|dk(  r! ‰| j                  dd«      «      ƒ d {  –—†  y|dk(  r! ‰| j                  dd«      «      ƒ d {  –—†  y|dk(  rY| j                  d«      xs d	j                  «       }|d
v r1|‰d<   t        j	                  d|‰	«       |dk(  r ‰«       ƒ d {  –—†  yy|dk(  r"t        ‰
d‰j                  dœ«      ƒ d {  –—†  yt        j                  d|‰	«       y7 Œî7 ŒÊ7 Œ¦7 ŒK# t
        $ r }t        j                  d|«       Y d }~yd }~ww xY w7 ŒQ­w)Nr$   r   ÚpingÚpongrŽ   r%   rL   r(   r'   rË   z[WS] mode=%s session=%sr,   rÍ   Úend_sessionr­   r®   Fz([WS] unknown message type=%s  session=%sT)	r.   r   r>   rA   rB   r   r[   rš   Údebug)ÚmsgÚmtypeÚnew_moderg   rH   rÁ   r¼   r0   rF   rG   r   s       €€€€€€€r   Ú	_dispatchzroleplay_ws.<locals>._dispatchû  sd  øè ø€ Ø—G‘G˜F BÓ'ˆEØ˜ŠÜ˜B ¨Ð 0Ó1×1Ð1ð& ð% ˜&’Ù+¨C¯G©G°I¸rÓ,BÓC×CÐCð" ð! ˜+Ò%Ù$ S§W¡W¨[¸"Ó%=Ó>×>Ð>ð ð ˜&’ØŸG™G F›OÒ6¨w×=Ñ=Ó?�ØÐ9Ñ9Ø)1�J˜vÑ&Ü—H‘HÐ6¸À*ÔMØ 7Ò*Ù#1Ó#3×3Ð3ñ ð ˜-Ò'Ü˜B¨ÀÇÁÑ MÓN×NÐNØä—	‘	ÐDÀeÈZÔXØð' 2øàCøà>øð 4ùÜ(ò LÜŸK™KÐ(EÀs×KÐKð ûðLúð Oús|   ƒ)E¬D­%EÁDÁ%EÁ8DÁ9AEÃ
D# ÃD!ÃD# Ã#EÃ=EÃ>EÄEÄEÄ!D# Ä#	EÄ,EÅEÅEÅEz[WS] invalid JSON  session=%sz[WS] disconnected  session=%sznot connectedÚacceptz)[WS] client closed mid-stream  session=%sz"[WS] runtime error  session=%s: %srR   zSession ended unexpectedly.z[WS] error  session=%s: %sz%[WS] save_session at close failed: %s)ÚreturnÚbool)rØ   z
str | None)rØ   r^   )rd   r^   rØ   ÚNone)rŽ   r^   rØ   rÙ   )rØ   rÚ   )rŽ   r^   rØ   rÚ   )rL   r^   rØ   rÚ   )g      ø?)rÉ   Úfloat)rÓ   r´   rØ   rÙ   )"r×   rA   rB   Ú
startswithra   r   rÙ   r   r[   Úconfigr!   Ú	APP_DEBUGr   Úcloser   Úget_sessionÚget_scenarioÚscenario_idr.   r>   r�   r	   rÅ   r   rÇ   rÈ   ÚRuntimeErrorr^   rŒ   r"   r¢   r   r
   r   )r   rG   ÚtokenÚauth_okÚdecodedrg   r!   r¦   rÊ   Úpending_first_msgrÕ   rÖ   Ú
keep_goingÚraw_msgrÓ   r�   rH   r�   rÁ   r¼   rn   r4   r1   r0   rm   rF   s   ``             @@@@@@@@@@@r   Úroleplay_wsrê   F   s6  ÿüè ø€ ð �)‰)‹+×ÐÜ‡H�HÐ)¨:Ô6ð €GÙð
	QØ×Ñ Ô&ô �u“: ’>Ø"‘Gä& uÓ-�Ü˜w›-�ñ å#Ø×!Ò!Ü˜ WÐ9PÑQÓR×RÐRØ—(‘( �(Ó%×%Ð%ØÜ�‰ÐJÔKô ×%Ñ% jÓ1×1€GÙÜ�B Ð5HÑIÓJ×JÐJØ�h‰h‹j×ÐØä×'Ñ'¨×(;Ñ(;Ó<×<€HÙÜ�B Ð5IÑJÓK×KÐKØ�h‰h‹j×ÐØð*  ØØñ€Jõ1õ-÷)ð )÷VP0ñ P0òp€Lõ1÷-÷@jKñ jK÷X]ð ]ö<ñ <Ó=×=ÐÙÐ.×2Ñ2°6Ó:¸fÒDØ%×)Ñ)¨&Ó1Ò<°W×CÑCÓEˆØÐ1Ñ1Ø!)ˆJ�vÑÜ�H‰HÐ9¸8ÀZÔPØ˜7Ò"Ù)Ó+×+Ð+ð !ÐðJØ×ÒÙ Ó"×"Ð"÷
	ó 	ñ0 Ù(Ð):Ó;×;ˆJÙÜ)Ó+Ð+àØŸO™OÓ-×-ˆGðÜ—j‘j Ó)�ñ  )¨›~×-ˆJÙØð ð ð&	FÜ×$Ñ$ WÓ-×-Ð-ð
 �?‰?œn×6Ñ6Ò6ðØ—h‘h“j× Ñ ð 7ð] úô( ò 	QÜ�K‰KÐ>À
ÈC×PÒPûð	Qúð SùØ%ùð
 2ùàJùØùð =ùàKùØùðX >ùð ,úÜ ò DÜ—K‘KÐ =¸s×CÒCûðDúð #ùð< <ùð
 .úô ×'Ñ'ò Ü—‘Ð;¸ZÔHÚðúð .úô ò >Ü�‰Ð0°*×=Üò Yð œc #›hŸn™nÓ.Ñ.°(¼cÀ#»h¿n¹nÓ>NÑ2NÜ�H‰HÐ@À*ÕMä�I‰IÐ:¸JÈÐVZˆIÔ[Ü˜ WÐ9VÑWÓX×XÑXÿùÜò UÜ�	‰	Ð.°
¸CÈ$ˆ	ÔOô �B Ð5RÑSÓT×T×Tûð	Uúð .úÜò 	FÜ�K‰KÐ?À×EÒEûð	Fúð !úÜò Ùðûð	FÜ×$Ñ$ WÓ-×-Ò-øÜò 	FÜ�K‰KÐ?À×EÑEûð	Fúð �?‰?œn×6Ñ6Ò6ðØ—h‘h“j× Ò øÜò Ùðúð 7üs¾  �V8£L-¤V8Á8L0 Á;&V8Â!MÂ"V8Â;M!Â<2V8Ã.M$Ã/V8ÄM'ÄV8ÄM*Ä 'V8ÅM-ÅV8Å M0Å!V8Å8M3Å9A V8ÇM6ÇA"V8È<
M< ÉM9ÉM< ÉV8ÉO% É$N)É%O% ÊN,Ê$O% Ê'N/Ê(O% Ê-N2 ËO% ËO"ËO% ËT  ËS! Ë0SË1S! Ë5V8ÌT Ì&TÌ'T Ì+V8Ì0	MÌ9MÍV8ÍMÍV8Í!V8Í$V8Í'V8Í*V8Í-V8Í0V8Í3V8Í6V8Í9M< Í<	N&ÎN!ÎV8Î!N&Î&V8Î)O% Î,O% Î/O% Î2)OÏO% ÏOÏO% Ï%SÐT  ÐSÐA8RÒR
ÒRÒT  ÒSÒ+SÓ
SÓSÓT  ÓSÓT  ÓS! Ó!	TÓ*TÔ V8ÔTÔV8ÔT Ô	TÔV8ÔTÔV8Ô V5Ô"UÔ:T=Ô;UÕ V5Õ	U*Õ
U%Õ V5Õ%U*Õ* V5ÖV%ÖV!ÖV%Ö$V5Ö%	V1Ö.V5Ö0V1Ö1V5Ö5V8)r   r   r   r´   rØ   rÚ   )r   r   rG   r^   rä   r^   )"Ú__doc__Ú
__future__r   r±   rY   r   Úloggingr   r   Úfastapir   r   r   r	   Ústarlette.websocketsr
   Úmodels.schemasr   r<   r   Úservices.authr   Úservices.openai_servicer   r   r   r   Ú	getLoggerÚ__name__rA   Úrouterr   Ú	websocketrê   r‡   r2   r   Ú<module>r÷      s£   ðñõ: #Û Û Û Û ß 'ç DÓ DÝ /å &Ý Ý &÷ó ð €g×Ñ˜Ó!€Ù	‹€ó	ð ×ÑÐ$Ó%ñ ˜rÔ"ðwØðwàðwð òwó &ñwr2   