+
    jHàj*€  ã                   ó\  a € RV tG0 t R t^ RIt^ RIt^ RIt^ RIt^ RIt^ RIt^ RIt^ RI	t	^ RI
t
^ RIt^ RIt^ RIt^ RIHt ^ RIHt ^ RIHt ^ RIHtHt ^ RIHtHt ^ RIHtHtHt ^ RIHtH t H!t!H"t"H#t# ^ R	I$H%t%H&t&H't' ]PP                  ! R
4      t)]! 4       t*]! RR4      t+]! R4      t,]! RR4      t-RRRRRRRRRRRRRRRRRRR R!R"RR#RR$R!/t.] ^ k RWt/RRRRR%RRRRR/t0R& R' lt1R( R) lt2R* R+ lt3]*Pi                  R,4      RXR- R. ll4       t5]*Pm                  R/4      RXR0 R1 ll4       t7]*Pm                  R24      RXR3 R4 ll4       t8]*Pi                  R54      RXR6 R7 ll4       t9R8 R9 lt:]*Pm                  R:4      RXR; R< ll4       t;]*Pi                  R=4      RXR> R? ll4       t<]*Pi                  R@4      RXRA RB ll4       t= ! RC RD4      t>RE RF lt?RG RH lt@RI RJ ltARK RL ltB]*Pi                  RM4      RXRN RO ll4       tC]*P‰                  RP4      RQ RR l4       tE]*P‰                  RS4      RT RU l4       tFR# )Ya&  Audio dashboard routes: transcription upload + live transcribe-stream, voice config, ElevenLabs voices, TTS speak/lease and the speak-stream WebSocket.

Extracted from ``hermes_cli.web_server``; helpers/state that tests monkeypatch on
``web_server`` stay there and are late-bound (cycle-safe).
N)Ú	APIRouter)Úhttp_failure)Úlate)Ú_ws_auth_okÚ_ws_request_is_allowed)Ú_read_dashboard_json_responseÚ_split_text_for_speak_stream)ÚHTTPExceptionÚ	WebSocketÚWebSocketDisconnect)ÚAudioTranscriptionRequestÚSTTLeaseRequestÚTTSSpeakRequestÚTTSLeaseRequestÚVoiceLiveSessionRequest)ÚAnyÚDictÚOptionalzhermes_cli.web_serverÚ_config_profile_scopezhermes_cli.web_server_profilesÚ_voice_list_error_logged_onceÚload_envzhermes_cli.configz	audio/aacz.aacz
audio/flacz.flacz	audio/m4az.m4az	audio/mp3ú.mp3z	audio/mp4z.mp4ú
audio/mpegz	audio/oggz.oggz	audio/wavz.wavz
audio/waveú
audio/webmú.webmzaudio/x-m4azaudio/x-wavú
video/webmz.opusc                ó(   € V ^8„  d   QhR\         RR/# )é   ÚpathÚreturnN©Ústr)Úformats   "ÚA/home/ubuntu/.hermes/hermes-agent/hermes_cli/web_routers/audio.pyÚ__annotate__r$   9   s   € ÷ ñ œ#ð  $ñ ó    c                 óX   €  \         P                  ! V 4       R #   \         d     R # i ; i©N)ÚosÚunlinkÚOSError)r   s   &r#   Ú_unlink_quietlyr+   9   s$   € ðÜ
�	Š	�$ŽøÜô Úðús   ‚ š)¨)c                ó:   € V ^8„  d   QhR\         \        ,          /# ©r   Úprofile©r   r!   )r"   s   "r#   r$   r$   @   s   € ÷ ,ñ ,¤h¬s¥mñ ,r%   c              ƒ  ó\   a a"  € VV 3R lp\         P                  ! V4      G Rj  x€L
 #  L5i)a  Run ``fn()`` on a worker thread under the config-only profile scope.

Home-only contextvar scope, NOT ``_profile_scope``: these calls block for a
provider round-trip and only need config/.env resolution, while
``_profile_scope`` holds a process-global skills lock for its entire body.
c                  óv   <€ \        S4      ;_uu_ 4        S ! 4       uuR R R 4       #   + '       g   i     R # ; ir'   ©r   )Úfnr.   s   €€r#   Ú_scopedÚ#_run_config_scoped.<locals>._scopedG   s"   ø€ Ü" 7×+Õ+Ù“4÷ ,×+×+Ó+ús   •'§8	N)ÚasyncioÚ	to_thread)r.   r3   r4   s   ff r#   Ú_run_config_scopedr8   @   s$   ùé € öô ×"Ò" 7Ó+×+Ð+Ñ+ùs   „!,¥*¦,c                ó0   € V ^8„  d   QhR\         R\         /# )r   Ú	mime_typer   r    )r"   s   "r#   r$   r$   Q   s   € ÷ ;ñ ;¬ð ;´ñ ;r%   c                 ó¬   € T ;'       g    R P                  R^4      ^ ,          P                  4       P                  4       p\        P	                  VR4      # )Ú Ú;r   )ÚsplitÚstripÚlowerÚ_AUDIO_MIME_EXTENSIONSÚget)r:   Ú
normalizeds   & r#   Ú_audio_extension_for_mimerD   Q   sD   € Ø—/�/˜r×(Ñ(¨¨aÓ0°Õ3×9Ñ9Ó;×AÑAÓC€JÜ!×%Ñ% j°'Ó:Ð:r%   z/api/audio/transcribec                óF   € V ^8„  d   QhR\         R\        \        ,          /# ©r   Úpayloadr.   )r   r   r!   )r"   s   "r#   r$   r$   W   s    € ÷ ;ñ ;Ü&ð;Ü19¼#µñ;r%   c           	   ƒ   ó`  aa"  € V P                   ;'       g    R P                  4       pVP                  R4      '       d   RV9  d   \        RRR7      hVP	                  R^4      w  r4RV9  d   \        RRR7      hV P
                  ;'       g+    VR,          P	                  R	^4      ^ ,          ;'       g    R
P                  4       pVP	                  R	^4      ^ ,          P                  4       pVP                  R4      '       g   VR8X  g   \        RRR7      h \        P                  ! VRR7      pT'       g   \        RRR7      h\        T4      \        8”  d   \        RRR7      hR o \        RRR4      ;_uu_ 4        \        P                   ! R\#        T4      RR7      ;_uu_ 4       pTP%                  T4       TP&                  oRRR4       ^ RIHo \-        TTT3R l4      G Rj  x€L
 p	RRR4       S'       d   \/        S4       X	P1                  R4      '       gU   T	P1                  R4      ;'       g    Rp
RT
P                  4       9   d   R RR!R R"T	P1                  R"4      /# \        RT
R7      hR RR!\3        T	P1                  R!4      ;'       g    R 4      P                  4       R"T	P1                  R"4      /#   \        P                  \        3 d    \        RRR7      hi ; i  + '       g   i     EL); i EL  + '       g   i     EL; i  S'       d   \/        S4       i i ; i5i)#r<   údata:Ú,é�  zInvalid audio payload©Ústatus_codeÚdetailz;base64z$Audio payload must be base64 encoded:é   NNr=   r   zaudio/r   z"Payload must be an audio recordingT)Úvalidatez!Audio payload is not valid base64zAudio recording is emptyi�  zAudio recording is too largez"Desktop voice transcription failedéô  zTranscription failedzhermes-desktop-voice-F)ÚprefixÚsuffixÚdeleteN)Útranscribe_recordingc                  ó   <€ S! S 4      # r'   © )Ú	temp_pathrU   s   €€r#   Ú<lambda>Ú)transcribe_audio_upload.<locals>.<lambda>�   s   ø€ Ñ?SÐT]Ô?^r%   ÚsuccessÚerrorzempty transcriptÚokÚ
transcriptÚprovider)Údata_urlr?   Ú
startswithr	   r>   r:   r@   Úbase64Ú	b64decodeÚbinasciiÚErrorÚ
ValueErrorÚlenÚ_MAX_TRANSCRIPTION_UPLOAD_BYTESr   ÚtempfileÚNamedTemporaryFilerD   ÚwriteÚnameÚtools.voice_moderU   r8   r+   rB   r!   )rG   r.   r`   ÚheaderÚencodedr:   Únormalized_mime_typeÚaudio_bytesÚtmpÚresultÚerrrX   rU   s   &&         @@r#   Útranscribe_audio_uploadru   V   s¿  ùé € ð × Ñ ×&Ð& B×-Ñ-Ó/€HØ×Ñ˜w×'Ò'¨3°hÔ+>Ü¨Ð4KÔLÐLà—n‘n S¨!Ó,�O€FØ˜ÔÜ¨Ð4ZÔ[Ð[à×"Ñ"×QÐQ f¨R¥j×&6Ñ&6°s¸AÓ&>¸qÕ&A×QÐQÀ\×XÑXÓZ€IØ$Ÿ?™?¨3°Ó2°1Õ5×;Ñ;Ó=ÐØ ×+Ñ+¨H×5Ò5Ð9MÐQ]Ô9]Ü¨Ð4XÔYÐYðYÜ×&Ò& w¸Ô>ˆ÷ Ü¨Ð4NÔOÐOÜ
ˆ;ÓÔ9Ô9Ü¨Ð4RÔSÐSà€Ið'ÜÐ>ÀÐE[×\Õ\Ü×,Ò,Ø.Ô7PÐQZÓ7[Ø÷õ ð Ø—	‘	˜+Ô&ØŸH™H�	÷õ >ä-¨gÕ7^Ó_×_ˆF÷ ]÷  Ü˜IÔ&à�:‰:�i× Ò Ø�j‰j˜Ó!×;Ð;Ð%;ˆð  §¡£Ô,Ø˜$ ¨b°*¸f¿j¹jÈÓ>TÐUÐUÜ¨°CÔ8Ð8ð 	ˆd�L¤# f§j¡j°Ó&>×&DÐ&DÀ"Ó"E×"KÑ"KÓ"MØ�F—J‘J˜zÓ*ðð øôM �N‰NœJÐ'ô YÜ¨Ð4WÔXÐXðYú÷÷ ð úò `÷ ]×\Ð\û÷  Ü˜IÕ&ð üs¤   „;L.Á AL.Â'L.Â7AL.ÄL.ÄJ> Ä3L.Ä;0L.Å,L Æ+LÆ,K*	Ç
"LÇ,K>Ç-LÇ1L Ç9)L.È#L.È;AL.Ê'L.Ê>)K'Ë'L.Ë*K;Ë5
LÌL	Ì	L ÌL+Ì+L.z/api/audio/voice-configc                ó:   € V ^8„  d   QhR\         \        ,          /# r-   r/   )r"   s   "r#   r$   r$   –   s   € ÷ "ñ "¬8´C­=ñ "r%   c           	   ƒ  óÞ   "  € ^ RI Hp  \        W4      G Rj  x€L
 pRR	/TC#  L
  \         d    h \         d0    \
        P                  R4       RRRR/pRR	R
TR\        T4      /u # i ; i5i)uã  The active profile's STT/TTS config for CLIENT-DIRECT voice.

Lets the desktop cut the audio relay hop: mic audio goes straight to the
profile's STT provider and reply text is synthesized on the client with
the profile's TTS provider â€” the desktopâ†”gateway link carries only text.
Providers that can only run on this host (local whisper, edge-tts,
command/plugin providers) resolve to ``{"mode": "relay"}`` and the
desktop keeps using the /api/audio/* relay endpoints.

Same trust boundary as every profile-scoped route: the caller is an
authenticated client that can already drive the agent. Keys in the
response are held in client memory only, never persisted client-side.
Gate: ``voice.client_direct`` in config.yaml (default true).
)Úresolve_client_voice_configNz%Client voice-config resolution failedÚmodeÚrelayÚreasonzresolution errorr]   TÚsttÚtts)Útools.voice_client_configrx   r8   r	   Ú	ExceptionÚ_logÚ	exceptionÚdict)r.   rx   rs   Úfallbacks   &   r#   Úget_client_voice_configr„   •   s„   é € õ  FðDÜ)¨'ÓO×Oˆð �$Ð!˜&Ð!Ð!ñ PøÜô ØÜô DÜ�‰Ð>Ô?Ø˜G XÐ/AÐBˆØ�d˜E 8¨U´D¸³NÐCÒCðDüs6   ‚A-Š% ˜#™% �A-£% ¥A*º-A*Á'A-Á)A*Á*A-z/api/audio/voice-live/statusc                ó:   € V ^8„  d   QhR\         \        ,          /# r-   r/   )r"   s   "r#   r$   r$   ³   s   € ÷ "ñ "¬´#­ñ "r%   c              ƒ  ó®   "  € ^ RI Hp \        RRR4      ;_uu_ 4        \        W4      G Rj  x€L
 pRRR4       RR/XC#  L  + '       g   i     L; i5i)zµWhich voice chat mode the profile selected (``chained`` | ``gpt-live``) and whether GPT-Live
can start. Non-secret: the desktop decides which conversation engine to mount from this.)Úresolve_gpt_live_statusz!GPT-Live status resolution failedrQ   zGPT-Live status failedNr]   T)Útools.voice_liver‡   r   r8   )r.   r‡   rs   s   &  r#   Úget_voice_live_statusr‰   ²   sM   é € õ 9Ü	Ð9¸3Ð@X×	YÕ	YÜ)¨'ÓK×Kˆ÷ 
Zà�$Ð!˜&Ð!Ð!ñ L÷ 
Z×	Yüs+   ‚AžA­A ®A²AÁ AÁA	ÁAz/api/audio/voice-live/sessionc                óF   € V ^8„  d   QhR\         R\        \        ,          /# rF   )r   r   r!   )r"   s   "r#   r$   r$   ½   s    € ÷ "ñ "Ô-Dð "ÌxÔX[Í}ñ "r%   c              ƒ  ón  a aa"  € ^ RI Ho S P                  ;'       g    RoSP                  4       '       g   \	        RRR7      h \        VVV V3R l4      G Rj  x€L
 pR
R/TC#  L
  \         d   p\	        R\        T4      R7      hRp?i\         d   p\	        R	\        T4      R7      hRp?ii ; i5i)aF  Exchange the renderer's WebRTC SDP offer for a GPT-Live session answer.

The project API key stays on this host; the renderer only receives the session id and the
SDP answer. Client delegation is fixed at creation: every ``session.delegation.created`` the
renderer receives becomes a Hermes turn on the session it belongs to.
)Úcreate_webrtc_sessionr<   rK   zAn SDP offer is requiredrL   c                  ó*   <€ S ! SSP                   4      # r'   )Úhistory)rŒ   rG   Úsdps   €€€r#   rY   Ú+create_voice_live_session.<locals>.<lambda>Ë   s   ø€ Ñ;PÐQTÐV]×VeÑVeÔ;fr%   Ni÷  éö  r]   T)	rˆ   rŒ   r�   r?   r	   r8   rf   r!   ÚRuntimeError)rG   r.   rs   ÚexcrŒ   r�   s   f&  @@r#   Úcreate_voice_live_sessionr”   ¼   sž   úé € õ 7ð �+‰+×
Ð
˜€CØ�9‰9�;Š;Ü¨Ð4NÔOÐOð>Ü)¨'Ö3fÓg×gˆð
 �$Ð!˜&Ð!Ð!ñ høÜô >Ü¨´C¸³HÔ=Ð=ûÜô >Ü¨´C¸³HÔ=Ð=ûð>üsR   …2B5¸B5ÁA( ÁA&ÁA( Á B5Á&A( Á(B2Á3B	Â	B2ÂB2ÂB-Â-B2Â2B5c                óR   € V ^8„  d   QhR\         \        \        3,          R\        /# )r   Úvoicer   ©r‚   r!   r   )r"   s   "r#   r$   r$   Ó   s"   € ÷ 8ñ 8¤4¬¬S¨¥>ð 8´cñ 8r%   c                 ó   € \        V P                  R 4      ;'       g    V P                  R4      ;'       g    R4      P                  4       p\        V P                  R4      ;'       g    R4      P                  4       pV'       d	   V RV R2# T# )rl   Úvoice_idÚVoiceÚcategoryr<   z (Ú))r!   rB   r?   )r–   rl   r›   s   &  r#   Ú_elevenlabs_voice_labelr�   Ó   su   € Üˆu�y‰y˜Ó ×DÐD E§I¡I¨jÓ$9×DÐD¸WÓE×KÑKÓM€DÜ�5—9‘9˜ZÓ(×.Ð.¨BÓ/×5Ñ5Ó7€Hç%-ˆdˆV�2�h�Z˜qÐ!Ð7°4Ð7r%   z/api/audio/elevenlabs/voicesc                ó:   € V ^8„  d   QhR\         \        ,          /# r-   r/   )r"   s   "r#   r$   r$   Û   s   € ÷ G1ñ G1¬´#­ñ G1r%   c              ƒ  ób  a
"  € \        V 4      ;_uu_ 4        \        4       P                  R4      ;'       g    RP                  4       pRRR4       X'       g+    ^ RIHpHp  V! R4      ;'       g    RP                  4       pV'       g   RRR. /# \        P                  P                  RR	R
RV/R7      o
 R V
3R llp\        P                  ! V4      G Rj  x€L
 p\%        R4       . pTP                  R4      ;'       g    .  F™  p\1        T\2        4      '       g   K  \/        TP                  R4      ;'       g    R4      P                  4       p	T	'       g   KW  TP5                  RT	R\/        TP                  R4      ;'       g    T	4      R\7        T4      /4       K›  	  TP9                  R R7       RRRT/#   + '       g   i     EL|; i  T d<    \        P                  P                  R4      ;'       g    RP                  4       p ELŽi ; i  \         d     EL i ; i ELN  \        P                  P                    d™   pTP"                  R9   dB   \%        RTP"                   24      '       d   \&        P)                  RT4       RRR. RR/u Rp?# \%        RTP"                   24      '       d   \&        P+                  RT4       \-        RRR7      hRp?i\         dC   p\%        \/        T4      4      '       d   \&        P+                  RT4       \-        RRR7      hRp?ii ; i5i)zËReturn ElevenLabs voices when an API key is configured.

The desktop UI uses this for the ``tts.elevenlabs.voice_id`` dropdown.
Only non-secret voice metadata is returned; the API key stays server-side.
ÚELEVENLABS_API_KEYr<   N)ÚUnscopedSecretErrorÚ
get_secretÚ	availableFÚvoicesz#https://api.elevenlabs.io/v1/voicesÚAcceptzapplication/jsonz
xi-api-key)Úheadersc                óF   € V ^8„  d   QhR\         \        \        3,          /# ©r   r   r—   )r"   s   "r#   r$   Ú+get_elevenlabs_voices.<locals>.__annotate__ü   s   € ÷ 	?ñ 	?œœS¤#˜X�ñ 	?r%   c                  óª   <€ \         P                  P                  S^
R7      ;_uu_ 4       p \        V 4      uuRRR4       #   + '       g   i     R# ; i)é
   ©ÚtimeoutN)ÚurllibÚrequestÚurlopenr   )Úresponser¯   s    €r#   Ú_fetchÚ%get_elevenlabs_voices.<locals>._fetchü   s6   ø€ Ü—‘×'Ñ'¨¸Ð'×<Ô<ÀÜ4°XÓ>÷ =×<×<Ó<ús   «AÁA	zhttp-u>   ElevenLabs voices unavailable: %s â€” check ELEVENLABS_API_KEYr\   Úunauthorizedz ElevenLabs voice list failed: %sr‘   z Could not load ElevenLabs voicesrL   r™   rl   Úlabelc                 óf   € \        V P                  R 4      ;'       g    R4      P                  4       # )rµ   r<   )r!   rB   r@   )Úitems   &r#   rY   Ú'get_elevenlabs_voices.<locals>.<lambda>!  s#   € ¤ T§X¡X¨gÓ%6×%<Ð%<¸"Ó!=×!CÑ!CÔ!Er%   )ÚkeyT)i‘  i“  )r   r   rB   r?   Úagent.secret_scoper¡   r¢   r(   Úenvironr   r®   r¯   ÚRequestr6   r7   r\   Ú	HTTPErrorÚcoder   r€   ÚinfoÚwarningr	   r!   Ú
isinstancer‚   Úappendr�   Úsort)r.   Úapi_keyr¡   r¢   r²   rG   r“   r¤   r–   r™   r¯   s   &         @r#   Úget_elevenlabs_voicesrÅ   Ú   s¯  øé € ô 
˜w×	'Õ	'Ü“:—>‘>Ð"6Ó7×=Ð=¸2×DÑDÓFˆ÷ 
(çð
	ßJðOÙ%Ð&:Ó;×AÐA¸r×HÑHÓJ�÷
 Ø˜U H¨bÐ1Ð1ä�n‰n×$Ñ$Ø-ØÐ-¨|¸WÐEð %ó €Gð
X÷	?ð 	?ô  ×)Ò)¨&Ó1×1ˆô" " $Ô'à€FØ—‘˜XÓ&×,Ð,¨"Ò,ˆÜ˜%¤×&Ò&Ùä�u—y‘y Ó,×2Ð2°Ó3×9Ñ9Ó;ˆßÙà�‰Ø˜ &¬#¨e¯i©i¸Ó.?×.KÐ.KÀ8Ó*LØÔ,¨UÓ3ð
ö 	ñ -ð ‡K�KÑE€KÔFØ˜˜x¨Ð0Ð0÷ 
(×	'Ð	'ûð 'ô OÜŸ:™:Ÿ>™>Ð*>Ó?×EÐEÀ2×LÑLÓN“ðOûäô 	Úð	úò 2øÜ�<‰<×!Ñ!ô Xð
 �8‰8�zÔ!Ü,¨u°S·X±X°JÐ-?×@Ò@Ü—	‘	ÐZÐ\_Ô`Ø ¨°"°g¸~ÐNÕNÜ(¨5°·±°
Ð);×<Ò<Ü�L‰LÐ;¸SÔAÜ¨Ð4VÔWÐWûÜô XÜ(¬¨S«×2Ò2Ü�L‰LÐ;¸SÔAÜ¨Ð4VÔWÐWûðXüsê   ƒL/—2F<Á	L/ÁH Á#G Á2G ÂL/Â+L/Â7"H* ÃH'ÃH* Ã$L/Ä:L/Ä>L/Å,L/Æ4L/Æ<G	Ç	L/Ç,HÇ=HÈH ÈL/ÈHÈH ÈH$È L/È#H$È$L/È'H* È*L,É	AKÊL,ÊL/ÊAKËL,Ë)L,Ë*=L'Ì'L,Ì,L/z/api/audio/speakc                óF   € V ^8„  d   QhR\         R\        \        ,          /# rF   )r   r   r!   )r"   s   "r#   r$   r$   &  s   € ÷ 5ñ 5œoð 5¼Ä½ñ 5r%   c           
   ƒ  ó‚  a	a
a"  € V P                   ;'       g    RP                  4       o
S
'       g   \        RRR7      h\        RRR4      ;_uu_ 4        ^ RIHo \        VV
V3R	 l4      G R
j  x€L
 pR
R
R
4        \        X\        4      '       d   \        P                  ! V4      MTpTP                  R4      '       g'   \        RTP                  R4      ;'       g    RR7      hTP                  R4      o	S	'       d&   \        P                  P                  S	4      '       g   \        RRR7      h\         P                  \        P                  P#                  S	4      ^,          P%                  4       R4      pR T	3R llp \&        P(                  ! T4      G R
j  x€L
 p\,        P.                  ! T4      P1                  R4      pRRRRT RT 2RTRTP                  R4      /#  ELx  + '       g   i     EL; i  \         d    \        RRR7      hi ; i L{  \*         d   p\        RRT 2R7      hR
p?ii ; i5i)zõSynthesize speech and return audio as base64 data URL.

Used by the desktop voice-conversation mode to play back assistant
responses without exposing the on-disk file path; reuses the TTS provider
chain configured under ``tts.`` in config.yaml.
r<   rK   zText is requiredrL   zDesktop voice TTS failedrQ   úSpeech synthesis failed©Útext_to_speech_toolc                  ó   <€ S! S 4      # r'   rW   )ÚtextrÊ   s   €€r#   rY   Úspeak_text.<locals>.<lambda>6  s   ø€ Ñ@SÐTXÔ@Yr%   NúInvalid TTS responser[   r\   Ú	file_pathúAudio file missingr   c                ó$   € V ^8„  d   QhR\         /# r¨   )Úbytes)r"   s   "r#   r$   Ú speak_text.<locals>.__annotate__H  s   € ÷ 'ñ 'œeñ 'r%   c                  óÜ   <€  \        SR 4      ;_uu_ 4       p V P                  4       uuRRR4       \        S4       #   + '       g   i     M; i \        S4       R#   \        S4       i ; i)ÚrbN)ÚopenÚreadr+   )ÚfhrÏ   s    €r#   Ú_read_and_unlinkÚ$speak_text.<locals>._read_and_unlinkH  sQ   ø€ ð	'Ü�i ×&Ô&¨"Ø—w‘w“y÷ 'Ñ&ô ˜IÕ&÷ '×&úÐ&ô ˜IÖ&øŒO˜IÕ&ús   ƒA —=§
A ½A	ÁA ÁA+zCould not read audio: Úasciir]   Tr`   rI   z;base64,r:   r_   )rÌ   r?   r	   r   Útools.tts_toolrÊ   r8   rÁ   r!   ÚjsonÚloadsr   rB   r(   r   ÚisfileÚ_SPEAK_MIME_BY_EXTÚsplitextr@   r6   r7   r*   rb   Ú	b64encodeÚdecode)rG   r.   Úresult_jsonrs   r:   rÙ   rq   r“   ro   rÏ   rÌ   rÊ   s   &&       @@@r#   Ú
speak_textrå   %  sì  úé € ð �L‰L×Ð˜B×%Ñ%Ó'€DßÜ¨Ð4FÔGÐGô 
Ð0°#Ð7P×	QÕ	QÝ6ä.¨wÕ8YÓZ×Zˆ÷ 
Rð
LÜ,6°{ÄC×,HÒ,H”—’˜KÔ(Èkˆð �:‰:�i× Ò ÜØ F§J¡J¨wÓ$7×$TÐ$TÐ;Tô
ð 	
ð —
‘
˜;Ó'€IßœBŸG™GŸN™N¨9×5Ò5Ü¨Ð4HÔIÐIä"×&Ñ&¤r§w¡w×'7Ñ'7¸	Ó'BÀ1Õ'E×'KÑ'KÓ'MÈ|Ó\€I÷'ð 'ðTÜ#×-Ò-Ð.>Ó?×?ˆô ×Ò˜{Ó+×2Ñ2°7Ó;€Gàˆd�J %¨	 {°(¸7¸)Ð DÀkÐS\Ø�F—J‘J˜zÓ*ðð òE [÷ 
R×	QÐ	Qûô ô LÜ¨Ð4JÔKÐKðLúñ2 @øÜô TÜ¨Ð6LÈSÈEÐ4RÔSÐSûðTüsœ   …,H?²"H?ÁG*Á/G'Á0G*Á4H?Á=.G> Â+H?ÃH?Ã  H?Ä$H?Ä&AH?ÆH ÆHÆH Æ#AH?Ç'G*Ç*G;	Ç5	H?Ç>HÈH?ÈH ÈH<È'H7È7H<È<H?z/api/audio/tts-leasec                óF   € V ^8„  d   QhR\         R\        \        ,          /# rF   )r   r   r!   )r"   s   "r#   r$   r$   _  s"   € ÷ "Lñ "Lœ_ð "L´xÄµ}ñ "Lr%   c           	   ƒ  ó¨  a aa"  € S P                   ;'       g    RP                  4       oS'       g   \        RRR7      hVV V3R lp \        P                  ! V4      G Rj  x€L
 pRRRSRS P                  /TC#  L  \         d    h \
         d>   p\        P                  RSS P                  T4       RRR	R
R
\        T4      /p Rp?LdRp?ii ; i5i)uô  Desktop TTS-output toggles as warm-up / release signals.

``active: true`` registers a lease on the TTS engine and pre-loads the
configured provider (local model, lazily-installed SDK) so the first spoken
reply doesn't pay the load as dead air; ``active: false`` drops the lease
and, once no surface holds one, unloads resident local models. Blocking
work runs off the event loop. Warm-up failures are reported in the body,
never as an HTTP error â€” the toggle must succeed even when preload fails.
r<   rK   úlease is requiredrL   c                  óR  <€ ^ RI Hp Hp SP                  '       d'   \	        S4      ;_uu_ 4        V ! S4      uuRRR4       #  \	        S4      ;_uu_ 4        V! S4      uuRRR4       #   + '       g   i     L7; i  + '       g   i     R# ; i  \
         d    T! S4      u # i ; i)é    )Úacquire_tts_leaseÚrelease_tts_leaseN)Útools.tts_tool_lifecyclerë   rì   Úactiver   r	   )rë   rì   ÚleaserG   r.   s     €€€r#   Ú_applyÚtts_lease.<locals>._applym  sx   ø€ ßQØ�>�>ˆ>Ü& w×/Õ/Ù(¨Ó/÷ 0Ò/ð	,Ü& w×/Õ/Ù(¨Ó/÷ 0Ò/÷ 0×/ú÷
 0×/Ð/ûäô 	,Ù$ UÓ+Ò+ð	,ús;   ¯A)ÁB ÁA<Á
B Á)A9	Á<B	ÂB ÂB ÂB&Â%B&NzTTS lease %s (%s) failed: %sÚleasesÚactionr\   r]   Trï   rî   ©
rï   r?   r	   r6   r7   r   r€   rÀ   rî   r!   ©rG   r.   rð   rs   r“   rï   s   ff   @r#   Ú	tts_leaserö   ^  s½   úé € ð �]‰]× Ð ˜b×'Ñ'Ó)€EßÜ¨Ð4GÔHÐH÷,ðHÜ×(Ò(¨Ó0×0ˆð �$˜ ¨°'·.±.ÐKÀFÐKÐKñ 1øÜô ØÜô HÜ�‰Ð3°U¸G¿N¹NÈCÔPØ˜D (¨G°W¼cÀ#»hÐG�ûðHüóL   …,C²CÁA< Á!A:Á"A< Á&CÁ:A< Á<CÂCÂ3C
ÃCÃ
CÃCc                   ó>   a € ] tR tRt o RtRt^tV 3R lR ltRtV t	R# )Ú_SyncSentencePCMStreameri„  aª  Speak one sentence through the sync TTS tool and yield int16 mono PCM.

Not a second provider: ``text_to_speech_tool`` is the same stack the CLI
speaker uses for edge and every other non-chunked provider. The desktop
socket only plays PCM, so the written file is decoded before it is sent.
``sample_rate`` is updated before the first yield, matching the chunked
streamers whose rate is only final once synthesis has answered.
éÀ]  c                ó    <€ V ^8„  d   QhRS[ /# )r   rÌ   r    )r"   Ú__classdict__s   "€r#   r$   Ú%_SyncSentencePCMStreamer.__annotate__‘  s   ø€ ÷ ñ ™3ñ r%   c              #  ó`   "  € \        V4      w  r#V'       d   W0n        V'       d   Vx € R # R # 5ir'   )Ú_sync_sentence_to_pcmÚsample_rate)ÚselfrÌ   ÚpcmÚrates   &&  r#   ÚstreamÚ_SyncSentencePCMStreamer.stream‘  s'   é € Ü)¨$Ó/‰	ˆßØ#ÔßØŒIñ ùs   ‚#.¦.)r   N)
Ú__name__Ú
__module__Ú__qualname__Ú__firstlineno__Ú__doc__r   Úchannelsr  Ú__static_attributes__Ú__classdictcell__)rü   s   @r#   rù   rù   „  s    ø‡ € ñð €KØ€H÷ö r%   rù   c                ó0   € V ^8„  d   QhR\         R\        /# )r   rÌ   r   ©r!   Útuple)r"   s   "r#   r$   r$   ™  s   € ÷ #ñ #¤ð #¬ñ #r%   c                ó„  € ^ RI Hp \        P                  ! RR7      w  r#\        P
                  ! V4       Rp V! WR7      p \        V\        4      '       d   \        P                  ! V4      MTp\        T\        4      '       d   TP                  R4      '       gR   \        T\        4      '       d%   \        TP                  R4      ;'       g    R	4      MR	p\        T;'       g    R
4      hTP                  R4      ;'       g    Tp	\        T	\        4      '       dJ   \        P                  P!                  T	4      '       d%   \        P                  P#                  T	4      ^ 8:  d   \        R4      h\        P                  P%                  T	4      \        P                  P%                  T4      8w  d   T	p\'        T	4      w  r«T
'       g   \        R4      hY«3\)        T4       T'       d   \)        T4       # #   \        \        3 d   p\        R4      ThRp?ii ; i  \)        T4       T'       d   \)        T4       i i ; i)zCSynthesize *text* with the configured sync provider and return PCM.rÉ   r   )rS   N)rÌ   Úoutput_pathrÎ   r[   r\   r<   rÈ   rÏ   rÐ   zTTS audio decoded to silence)rÜ   rÊ   ri   Úmkstempr(   ÚcloserÁ   r!   rÝ   rÞ   Ú	TypeErrorrf   r’   r‚   rB   r   rß   ÚgetsizeÚabspathÚ_audio_file_to_pcmr+   )rÌ   rÊ   ÚfdÚtmp_pathÚextraÚrawrG   r“   rN   Úwrittenr  r  s   &           r#   rÿ   rÿ   ™  sª  € å2ä×#Ò#¨6Ô2�L€BÜ‡H‚HˆR„LØ€Eð#Ù! tÔBˆð	@Ü)3°C¼×)=Ò)=”d—j’j ”oÀ3ˆGô ˜'¤4×(Ò(°·±¸I×0FÒ0FÜ8BÀ7ÌD×8QÒ8Q”S˜Ÿ™ WÓ-×3Ð3°Ô4ÐWYˆFÜ˜v×BÐBÐ)BÓCÐCØ—+‘+˜kÓ*×6Ð6¨hˆÜ˜'¤3×'Ò'¬r¯w©w¯~©~¸g×/FÒ/FÌ"Ï'É'Ï/É/ÐZaÓJbÐfgÔJgÜÐ3Ó4Ð4Ü�7‰7�?‰?˜7Ó#¤r§w¡w§¡°xÓ'@Ô@ØˆEÜ& wÓ/‰	ˆßÜÐ=Ó>Ð>Øˆyä˜Ô!ßÜ˜EÕ"ð øô! œ:Ð&ô 	@ÜÐ5Ó6¸CÐ?ûð	@ûô 	˜Ô!ßÜ˜EÕ"ð úsN   ¹	H Á.G9 Á1,H Â2H ÃH Ã(H Ä=H ÅBH Ç9HÈ
HÈHÈH È!H?c                ó0   € V ^8„  d   QhR\         R\        /# ©r   r   r   r  )r"   s   "r#   r$   r$   ¸  s   € ÷ $ñ $œSð $¤Uñ $r%   c                ó4  € \        V R4      ;_uu_ 4       pVP                  ^4      pRRR4       \        X4      ^8¼  d>   VP                  R4      '       d'   VR,          R8X  d   \	        V 4      w  r4V'       d   W43# \        V 4      #   + '       g   i     Lh; i)zNDecode *path* to int16 mono PCM. WAV via the stdlib; anything else via ffmpeg.rÕ   Ns   RIFF:é   é   Ns   WAVE)rÖ   r×   rg   ra   Ú_wav_s16le_monoÚ_ffmpeg_s16le_mono)r   rØ   Úheadr  r  s   &    r#   r  r  ¸  sr   € ä	ˆd�D×	Ô	˜RØ�w‰w�r‹{ˆ÷ 
ä
ˆ4ƒy�B„˜4Ÿ?™?¨7×3Ò3¸¸T½
ÀgÔ8MÜ# DÓ)‰	ˆßØ�9ÐÜ˜dÓ#Ð#÷ 
×	ús   •BÂB	c                ó0   € V ^8„  d   QhR\         R\        /# r  r  )r"   s   "r#   r$   r$   Ã  s   € ÷ 	ñ 	œ#ð 	¤%ñ 	r%   c                 óÌ  € ^ RI p VP                  V R4      ;_uu_ 4       pVP                  4       ^8w  g+   VP                  4       ^8w  g   VP	                  4       ^ 8:  d   RuuRRR4       # VP                  VP                  4       4      \        VP	                  4       4      3uuRRR4       #   + '       g   i     R# ; i  TP                  \        \        3 d    Ru # i ; i)rê   NrÕ   )r%   rê   )ÚwaverÖ   ÚgetsampwidthÚgetnchannelsÚgetframerateÚ
readframesÚ
getnframesÚintre   ÚEOFErrorr*   )r   r(  Úwfs   &  r#   r#  r#  Ã  s¨   € ÛðØ�Y‰Y�t˜T×"Ô" bØ�‰Ó  AÔ%¨¯©Ó):¸aÔ)?À2Ç?Á?ÓCTÐXYÔCYØ÷ #Ò"ð —=‘= §¡£Ó1´3°r·±Ó7HÓ3IÐI÷ #×"×"Ó"ûð �J‰Jœ¤'Ð*ô ØŠðús;   †C  AB.Á!
C Á,7B.Â#
C Â.B?	Â9C Â?C ÃC#Ã"C#c                ó0   € V ^8„  d   QhR\         R\        /# r  r  )r"   s   "r#   r$   r$   Ï  s   € ÷  ñ  œSð  ¤Uñ  r%   c                 ób  € ^ RI p^ RIHp VP                  R4      pV'       g   \	        R4      hV! VRV RRRR	R
RRRR.^<R7      pVP
                  ^ 8w  g   VP                  '       g<   VP                  ;'       g    RP                  RR4      R,          p\	        RV 24      hVP                  R3# )rê   N)Ú_ffmpeg_runÚffmpegz7ffmpeg is required to decode TTS audio for speak-streamz-iz-fÚs16lez-acÚ1z-arÚ24000z	-loglevelr\   zpipe:1r¬   r%   zutf-8Úreplace:NéÈ   NzTTS audio decode failed: rú   )	ÚshutilÚtools.tts_tool_deliveryr3  Úwhichr’   Ú
returncodeÚstdoutÚstderrrã   )r   r:  r3  r4  rs   r?  s   &     r#   r$  r$  Ï  s­   € Ûå3à�\‰\˜(Ó#€FßÜÐTÓUÐUÙØØ	ˆt�T˜7 E¨3°°wÀÈWÐV^Ð_Øô€Fð
 ×Ñ˜AÔ V§]§] ]Ø—-‘-×&Ð& 3×.Ñ.¨w¸	ÓBÀ4ÕHˆÜÐ6°v°hÐ?Ó@Ð@Ø�=‰=˜%ÐÐr%   z/api/audio/stt-leasec                óF   € V ^8„  d   QhR\         R\        \        ,          /# rF   )r   r   r!   )r"   s   "r#   r$   r$   ã  s"   € ÷ Lñ Lœ_ð L´xÄµ}ñ Lr%   c           	   ƒ  ó¨  a aa"  € S P                   ;'       g    RP                  4       oS'       g   \        RRR7      hVV V3R lp \        P                  ! V4      G Rj  x€L
 pRRRSRS P                  /TC#  L  \         d    h \
         d>   p\        P                  RSS P                  T4       RRR	R
R
\        T4      /p Rp?LdRp?ii ; i5i)ui  Desktop voice-input sessions as STT warm-up / release signals.

``active: true`` registers a lease and pre-loads the configured local STT
model (first-use download + load) so the transcription request doesn't pay
the cold cost inside its timeout; ``active: false`` drops the lease. The
model stays resident after the last release â€” it is shared with the
gateway/CLI surfaces in this process, and ``stt.local.unload_after_idle_seconds``
still governs eviction. Blocking work runs off the event loop. Warm-up
failures are reported in the body, never as an HTTP error â€” recording must
start even when preload fails.
r<   rK   rè   rL   c                  óº   <€ ^ RI Hp Hp SP                  '       d'   \	        S4      ;_uu_ 4        V ! S4      uuRRR4       # V! S4      #   + '       g   i     L; i)rê   )Úacquire_stt_leaseÚrelease_stt_leaseN)Útools.stt_leaserC  rD  rî   r   )rC  rD  rï   rG   r.   s     €€€r#   rð   Ústt_lease.<locals>._applyó  s>   ø€ ßHØ�>�>ˆ>Ü& w×/Õ/Ù(¨Ó/÷ 0Ò/á  Ó'Ð'÷ 0×/ús   ¯A
Á
A	NzSTT lease %s (%s) failed: %srò   ró   r\   r]   Trï   rî   rô   rõ   s   ff   @r#   Ú	stt_leaserG  â  s½   úé € ð �]‰]× Ð ˜b×'Ñ'Ó)€EßÜ¨Ð4GÔHÐH÷(ðHÜ×(Ò(¨Ó0×0ˆð �$˜ ¨°'·.±.ÐKÀFÐKÐKñ 1øÜô ØÜô HÜ�‰Ð3°U¸G¿N¹NÈCÔPØ˜D (¨G°W¼cÀ#»hÐG�ûðHür÷   z/api/audio/transcribe-streamc                ó(   € V ^8„  d   QhR\         RR/# ©r   Úwsr   N©r
   )r"   s   "r#   r$   r$     s   € ÷ Iñ I¤9ð I°ñ Ir%   c              ƒ  óª	  a aaa"  € \        S 4      '       g   S P                  RR7      G Rj  x€L
  R# \        S 4      '       g   S P                  RR7      G Rj  x€L
  R# S P                  4       G Rj  x€L
  S P                  P                  R4      ;'       g    RP                  4       ;'       g    Ro\        P                  ! 4       o\        P                  ! 4       oVVV3R lpRp\        P                  ! \        4      ;_uu_ 4        SP                  RV4      G Rj  x€L
 pRRR4       Vfc   \        P                  ! \        4      ;_uu_ 4        S P                  RR	R
R/4      G Rj  x€L
  S P                  4       G Rj  x€L
  RRR4       R# VV 3R lp\        P                  ! V! 4       4      pRp \         P"                  ! S P%                  4       G Rj  x€L
 4      p\'        VP                  R4      ;'       g    ^ 4      pRTu;8:  d   R8:  g   M \)        RV 24      hVP+                  V4        S P-                  4       G Rj  x€L
 pVP                  R4      R8X  d   M�VP                  R4      '       d   VP/                  VR,          4       Ka  VP                  R4      '       g   Kz  \         P"                  ! VR,          4      P                  R4      '       g   K®  Rp V'       g#   VP9                  4        VP9                  4        R# SP                  RVP:                  4      G Rj  x€L
 p
\        P<                  ! ^ 4      G Rj  x€L
  VP9                  4        \        P                  ! \        4      ;_uu_ 4        V
P                  R4      '       dA   S P                  RRRV
P                  RR4      RV
P                  RR4      /4      G Rj  x€L
  M6S P                  RR	R
V
P                  R	4      ;'       g    R/4      G Rj  x€L
  S P                  4       G Rj  x€L
  RRR4       R#  EL ELâ ELË EL   + '       g   i     EL'; i ELë ELÖ  + '       g   i     R# ; i EL– EL(  \0        \(        \2        3 d"   p	\4        P7                  RT	4        Rp	?	ELÀRp	?	ii ; i EL ELd LÝ L¨ L’  + '       g   i     R# ; i5i)uU  Live STT for Desktop dictation: mic PCM in while the user speaks, partial + final text out.

Protocol:
  client â†’ ``{"sample_rate": N}`` first (8000-48000), then binary s16le mono PCM frames,
           ``{"eos": true}`` when the recording ends (disconnect = cancel)
  server â†’ ``{"type": "partial", "text": ...}`` as speech is recognized,
           ``{"type": "final", "transcript": ..., "provider": ...}``,
           ``{"type": "error", "message": ...}`` â€” also when no live STT is available, so the
           client falls back to ``POST /api/audio/transcribe`` with its recorded blob.
é1  ©r¾   Né3  r.   r<   c                  ó�   <€ ^ RI Hp  \        S4      ;_uu_ 4        V ! VV3R lR7      uuRRR4       #   + '       g   i     R# ; i)rê   )Úopen_streaming_sessionc                 ó<   <€ SP                  SP                  V 4      # r'   )Úcall_soon_threadsafeÚ
put_nowait)rÌ   ÚloopÚpartialss   &€€r#   rY   Ú5transcribe_stream_ws.<locals>._open.<locals>.<lambda>  s   ø€ À$×B[ÑB[Ð\d×\oÑ\oÐquÔBvr%   )Ú
on_partialN)Útools.transcription_streamingrQ  r   )rQ  rU  rV  r.   s    €€€r#   Ú_openÚ#transcribe_stream_ws.<locals>._open  s)   ø€ ÝHÜ" 7×+Õ+Ù)Õ5vÔw÷ ,×+×+Ó+ús	   ›4´A	Útyper\   Úmessagez'no live STT for the configured providerc               “   ó€   <"  €  SP                  RRRS P                  4       G Rj  x€L
 /4      G Rj  x€L
  K6   L L5i)Tr\  ÚpartialrÌ   N)Ú	send_jsonrB   )rV  rJ  s   €€r#   Ú_forward_partialsÚ/transcribe_stream_ws.<locals>._forward_partials*  s3   øé € ØØ—,‘, ¨	°6ÀÇÁÃ×;OÐPÓQ×QÒQÑ;OÑQùs   ƒ#>¦:§>³<´>¼>Fr   i@  i€»  zunsupported sample_rate Tzwebsocket.disconnectrÒ   rÌ   Úeosz(transcribe-stream client ended early: %sr[   Úfinalr^   r_   ztranscription failed)r   r  r   ÚacceptÚquery_paramsrB   r?   r6   Úget_running_loopÚQueueÚ
contextlibÚsuppressr   Úrun_in_executorr`  Úensure_futurerÝ   rÞ   Úreceive_textr.  rf   Úset_input_rateÚreceiveÚ
push_audior   r’   r€   ÚdebugÚcancelÚfinalizeÚsleep)rJ  rZ  Úsessionra  Ú	forwarderÚendedÚfirstr  r]  r“   rs   rU  rV  r.   s   f          @@@r#   Útranscribe_stream_wsry    s¶  ûé € ô �r�?Š?Ø�h‰h˜DˆhÓ!×!Ð!ÙÜ! "×%Ò%Ø�h‰h˜DˆhÓ!×!Ð!ÙØ
�)‰)‹+×Ðà�‰×"Ñ" 9Ó-×3Ð3°×:Ñ:Ó<×DÐDÀ€GÜ×#Ò#Ó%€DÜ%Ÿmšm›o€H÷xð
 €GÜ	×	Ò	œY×	'Õ	'Ø×,Ñ,¨T°5Ó9×9ˆ÷ 
(à‚Ü× Ò ¤×+Õ+Ø—,‘, ¨°Ð<eÐfÓg×gÐgØ—(‘(“*×Ð÷ ,ñ 	öRô ×%Ò%Ñ&7Ó&9Ó:€IØ€EðDÜ—
’
 §¡Ó!2×2Ó3ˆÜ�5—9‘9˜]Ó+×0Ð0¨qÓ1ˆØ�tÖ$˜uÖ$ÜÐ7¸°vÐ>Ó?Ð?Ø×Ñ˜tÔ$ØØŸJ™J›L×(ˆGØ�{‰{˜6Ó"Ð&<Ô<ØØ�{‰{˜7×#Ò#Ø×"Ñ" 7¨7Õ#3Ö4Ø—‘˜V×$Ô$¬¯ª°G¸FµOÓ)D×)HÑ)HÈ×)OÔ)OØ�Ø÷ Ø�‰ÔØ×ÑÔÙØ×'Ñ'¨¨g×.>Ñ.>Ó?×?€FÜ
�-Š-˜Ó
×ÐØ×ÑÔÜ	×	Ò	œY×	'Õ	'Ø�:‰:�i× Ò Ø—,‘, ¨°¸v¿z¹zÈ,ÐXZÓ?[Ø *¨F¯J©J°zÀ2Ó,Fð Hó I÷ Iñ Ið —,‘, ¨°¸F¿J¹JÀwÓ<O×<iÐ<iÐSiÐjÓk×kÐkØ�h‰h‹j×Ð÷ 
(Ñ	'òo 	"ò 	"âò :÷ 
(×	'Ð	'úò hÚ÷ ,Ö+ñ 	úò 3ò )øô  ¤¬\Ð:ô DÜ�
‰
Ð=¸s×CÒCûðDúò @ÚñIñ lÙ÷ 
(×	'Ð	'üs“  †'S­P9®,SÁP<ÁSÁ4P?Á5%SÂSÂ3ASÄQÄ"QÄ#QÄ'.SÅQÅ.QÅ/QÆQÆQÆ.SÆ:#Q9 ÇQ3Ç$Q9 ÈAQ9 ÉQ6ÉQ9 É)SÉ*Q9 Ê.Q9 Ê4/Q9 Ë(Q9 Ë*SË3ASÌ4R3Ì5SÍR6Í6SÎ	AR?ÏR9Ï*R?ÐR?ÐR;ÐR?Ð*R=Ð+R?Ð/SÐ<SÐ?SÑQÑQ	Ñ	SÑQÑQÑQ0	Ñ*	SÑ3Q9 Ñ6Q9 Ñ9R0ÒR+Ò%SÒ+R0Ò0SÒ6SÒ9R?Ò;R?Ò=R?Ò?S	Ó
	Sz/api/audio/speak-streamc                ó(   € V ^8„  d   QhR\         RR/# rI  rK  )r"   s   "r#   r$   r$   R  s   € ÷ qñ qœið q¨Dñ qr%   c           	   ƒ  óÌ  a aaa	a
aaaaaaaa"  € \        S 4      '       g   S P                  RR7      G Rj  x€L
  R# \        S 4      '       g   S P                  RR7      G Rj  x€L
  R# S P                  4       G Rj  x€L
  S P                  P                  R4      ;'       g    RP                  4       ;'       g    Ro\        P                  ! 4       oV3R lp \        P                  ! V4      G Rj  x€L
 w  ooo	Sf   \        4       oR	oVVV 3R
 lp\        P                  ! 4       oR	oR	o\         P"                  ! 4       o\        P"                  ! 4       o
VV3R lpVV	V
VVVVVV3	R lo\        P$                  ! VRR7      P'                  4        VVV 3R lp\        P(                  ! V! 4       4      p  S
P                  4       G Rj  x€L
 pVf   M*V! 4       G Rj  x€L
  S P+                  V4      G Rj  x€L
  KH  SP-                  4       '       gW   S'       d%   S'       g   S P/                  RR/4      G Rj  x€L
  M*V! 4       G Rj  x€L
  S P/                  RR/4      G Rj  x€L
  SP5                  4        SP7                  R4       VP9                  4        \:        P<                  ! \        4      ;_uu_ 4        S P                  4       G Rj  x€L
  RRR4       R#  EL« EL� ELj ELö  \         d!    \        P                  R4       R^ / uooo	 ELi ; i ELP EL? EL) Lè LÚ LÁ  \0        \2        3 d     LÒi ; i Ln  + '       g   i     R# ; i  SP5                  4        SP7                  R4       TP9                  4        \:        P<                  ! \        4      ;_uu_ 4        S P                  4       G Rj  x€L 
  RRR4       i   + '       g   i     i ; i; i5i)u  Streaming TTS for the desktop: text in, raw int16 PCM frames out.

The socket is a per-reply speech *session*: the client feeds text
incrementally as LLM deltas arrive, the server cuts sentences
(``SentenceChunker`` â€” same cutter as the CLI/TUI speaker pipeline) and
streams each one's PCM the moment it's ready, so speech overlaps generation.

Protocol:
  client â†’ ``{"text": "..."}`` frames (incremental; may combine with done),
           ``{"done": true}`` when the reply is complete,
           ``{"stop": true}`` or disconnect = barge-in
  server â†’ ``{"type": "start", "sample_rate": N, "channels": 1}`` (sent
           with the first PCM frame, once the provider's rate is final),
           binary PCM frames, then ``{"type": "end"}``
  server â†’ ``{"type": "fallback"}`` only when sentence synthesis produced
           no audio. Providers with no chunked API (edge, the default)
           still speak per sentence via ``text_to_speech_tool`` and stream
           that PCM â€” the client POST is the last resort, not the Edge path.
rM  rN  NrO  r.   r<   c                  óÈ   <€ ^ RI Hp  ^ RIHpHpHp \        S4      ;_uu_ 4        V! 4       pV ! V4      pV! V! V4      V4      pRRR4       XXX3#   + '       g   i     L; i)rê   )Úresolve_streaming_provider)Ú_get_providerÚ_load_tts_configÚ_resolve_max_text_lengthN)Útools.tts_streamingr}  rÜ   r~  r  r€  r   )r}  r~  r  r€  ÚcfgÚstreamerÚcapr.   s          €r#   Ú_resolveÚ!speak_stream_ws.<locals>._resolveu  sW   ø€ ÝBß\Ñ\Ü" 7×+Õ+Ù"Ó$ˆCÙ1°#Ó6ˆHÙ*©=¸Ó+=¸sÓCˆC÷ ,ð ˜˜cÐ!Ð!÷	 ,×+ús   ¥AÁA!	z'speak-stream provider resolution failedFc               “   ó’   <"  € S '       d   R # Ro SP                  RRRSP                  RSP                  /4      G R j  x€L
  R #  L5i)NTr\  Ústartr   r  )r`  r   r  )Ú
start_sentrƒ  rJ  s   €€€r#   Ú_send_startÚ$speak_stream_ws.<locals>._send_start�  sC   øé € çÙØˆ
Ø�l‰lØ�W˜m¨X×-AÑ-AÀ:Èx×O`ÑO`Ðaó
÷ 	
ô 	
ùs   ƒ;A¾A¿Ac                  óv   <€ \        S4      ;_uu_ 4        S ! 4        R R R 4       R #   + '       g   i     R # ; ir'   r2   )Ú_synthesizer.   s   €€r#   Ú_produceÚ!speak_stream_ws.<locals>._producež  s$   ø€ ô # 7×+Õ+ÙŒM÷ ,×+×+Ò+ús   •'§8	c                  ó€  <	aa	a
€ ^ RI Hp  ^ RIHp V P	                  S4      oRo	^o
VV	V
VV3R lp V! 4        F•  pV! V4      pV'       g   K  \        VS4       Fp  pSP                  V4       FX  pSP                  4       '       d"      SP                  SP                  R4       R# RoSP                  SP                  V4       KZ  	  Kr  	  K—  	  SP                  SP                  R4       R#   \         d#   p\        P                  RT4       Ro Rp?LFRp?ii ; i  SP                  SP                  R4       i ; i)rê   )ÚSentenceChunker)Ú_strip_markdown_for_ttsg      à?c               3   ó,  <"  € ^ p SP                  4       '       gO    SP                  SR7      p^ p Tf   SP                  4        Rj  x€L
  R# SP                  T4       Rj  x€L
  Kd  R#   \        P                   d�    T ^,          p SP                  P                  4       pT'       d#   RSP                  9   d   RSP                  9  d    KË  TP                  R4      '       g   T S8¼  d   SP                  4        Rj  x€L 
   EK  i ; i LÇ L®5i)rê   r¬   z<thinkz</think>N)Ú.Ú!Ú?u   â€¦Ú:)	Úis_setrB   ÚqueueÚEmptyÚbufr?   ÚendswithÚflushÚfeed)Ú
idle_pollsÚdeltaÚbufferedÚchunkerÚidle_poll_secondsÚidle_polls_before_force_flushÚstopÚtext_qs      €€€€€r#   Ú
_sentencesÚ8speak_stream_ws.<locals>._synthesize.<locals>._sentencesµ  sä   øé € ØˆJØ—k‘k—m’mð	Ø"ŸJ™JÐ/@˜JÓA�Eð �
Ø’=Ø&Ÿ}™}›×.Ð.ÙØ"Ÿ<™<¨Ó.×.Ò.ñ $øô —{‘{ô Ø !•O�JØ&Ÿ{™{×0Ñ0Ó2�Hß#¨°G·K±KÔ(?ÀJÐV]×VaÑVaÔDaÚ Ø×(Ñ(Ð)D×EÒEÈÐWtÔItØ#*§=¡=£?×2Ñ2Ûðúñ /á.ùsc   ƒD�A, ¯DÁDÁ	DÁ#DÁ$DÁ,?DÂ,"DÃDÃ1DÄDÄDÄDÄDÄDÄDNTz!speak-stream synthesis failed: %s)r�  r‘  Útools.tts_text_normalizer’  Úfrom_configr   r  r˜  rS  rT  r   r€   rÀ   )r‘  r’  r§  ÚsentenceÚcleanedÚpieceÚchunkr“   r¢  r£  r¤  r„  r‚  ÚchunksrU  Úproduced_audior¥  rƒ  Úsynthesis_failedr¦  s           @@@€€€€€€€€€r#   r�  Ú$speak_stream_ws.<locals>._synthesize¥  s  û€ å7ÝDà!×-Ñ-¨cÓ2ˆð  ÐØ()Ð%÷	/ñ 	/ð&	?Ù&žL�Ù1°(Ó;�ßÙÜ9¸'À3ÖG�EØ!)§¡°Ö!7˜ØŸ;™;Ÿ=š=Ú"ð ×%Ñ% f×&7Ñ&7¸Ö>ð *.˜Ø×1Ñ1°&×2CÑ2CÀUÖKó	 "8ó Hñ	 )ð ×%Ñ% f×&7Ñ&7¸Ö>øô	 ô 	$Ü�L‰LÐ<¸cÔBØ#Õûð	$ûð ×%Ñ% f×&7Ñ&7¸Õ>ús0   ±AC/ Â'*C/ Ã/DÃ:DÄD ÄDÄD ÄD=T)ÚtargetÚdaemonc               “   óÌ  <"  €   \         P                  ! SP                  4       G Rj  x€L
 4      p V P                  R4      '       d"   SP	                  \        V R,          4      4       V P                  R4      '       d   M,V P                  R4      '       g   K–  SP	                  R4       K©  SP                  4        SP	                  R4       R#  L§  \         d     L2i ; i5i)TNrÌ   r¥  Údone)rÝ   rÞ   rm  rB   Úputr!   r   Úset)Úframer¥  r¦  rJ  s    €€€r#   Ú_pump_clientÚ%speak_stream_ws.<locals>._pump_clientÛ  s¦   øé € ð
	ØÜŸ
š
¨¯©Ó):×#:Ó;�Ø—9‘9˜V×$Ò$Ø—J‘Jœs 5¨¥=Ó1Ô2Ø—9‘9˜V×$Ò$ØØ—9‘9˜V×$Ô$Ø—J‘J˜tÖ$ð 	�‰Œ
Ø�
‰
�4Öñ $;øô ô 	Ùð	üsP   ƒC$…$C ©CªC Á	7C ÂC$ÂC ÂC Â.#C$ÃC ÃC!ÃC$Ã C!Ã!C$r\  rƒ   Úend)r   r  r   re  rf  rB   r?   r6   rg  r7   r   r€   r�   rù   Ú	threadingÚEventr™  rh  ÚThreadrˆ  rl  Ú
send_bytesr˜  r`  r   r’   r¸  r·  rr  ri  rj  )rJ  r…  rŠ  rŽ  rº  Úpumpr®  r�  r„  r‚  r¯  rU  r°  r.   r‰  r¥  rƒ  r±  r¦  s   f      @@@@@@@@@@@@r#   Úspeak_stream_wsrÂ  Q  sç  ÿüé € ô* �r�?Š?Ø�h‰h˜DˆhÓ!×!Ð!ÙÜ! "×%Ò%Ø�h‰h˜DˆhÓ!×!Ð!ÙØ
�)‰)‹+×Ðð
 �‰×"Ñ" 9Ó-×3Ð3°×:Ñ:Ó<×DÐDÀ€Gä×#Ò#Ó%€Dõ"ð)Ü#*×#4Ò#4°XÓ#>×>Ñˆ�#�sð Òô ,Ó-ˆð €J÷
ô �?Š?Ó€DØ€NØÐÜŸ+š+›-€FÜ#ŸMšM›O€Fö÷2?õ 2?ôh ×Ò˜H¨TÔ2×8Ñ8Ô:÷ô" × Ò ¡£Ó0€DðØØ Ÿ*™*›,×&ˆEØŠ}ØÙ“-×ÐØ—-‘- Ó&×&Ò&Ø�{‰{�}Š}÷  ¯Ø—l‘l F¨JÐ#7Ó8×8Ñ8á!“m×#Ð#Ø—l‘l F¨E ?Ó3×3Ð3ð 	�‰Œ
Ø�
‰
�4ÔØ�‰ŒÜ× Ò ¤×+Õ+Ø—(‘(“*×Ð÷ ,Ñ+òw 	"ò 	"âò& ?øÜô )Ü�‰Ð@ÔAØ! 1 bÐˆ�#“sð)úò^ 'ò  Ú&ñ
 9á#Ù3øÜ¤Ð.ô Ùðúñ ÷ ,×+Ð+ûð 	�‰Œ
Ø�
‰
�4ÔØ�‰ŒÜ× Ò ¤×+Õ+Ø—(‘(“*×Ñ÷ ,×+×+Ð+ÿs€  �'O$¶K!·,O$Á#K$Á$O$Á=K'Á>%O$Â$O$Â<O$ÃK- Ã4K*Ã5K- Ã=B9O$Æ7L* ÇLÇL* ÇLÇ L* Ç8L!Ç9L* ÈL* ÈL* È%L* È;L$È<L* É M É
L* ÉL&ÉL* É&L(É'L* É+AO$Ê>MËMËMËO$Ë$O$Ë'O$Ë*K- Ë-'LÌO$ÌLÌO$ÌL* ÌL* Ì!L* Ì$L* Ì&L* Ì(L* Ì*L>Ì;M Ì=L>Ì>M ÍMÍM	Í	O$ÍAO!Î+O	Î?OÏ O	Ï	O!ÏOÏO!Ï!O$c                ób   € V ^8„  d   Qh/ ^ \         9   d   \        \        \        3,          ;R&   # )r   rA   )Ú__conditional_annotations__r‚   r!   )r"   s   "r#   r$   r$      s,   € ÷ ñ ÷Rò œœS¤#˜X�ñ òSr%   i  �r'   )HrÄ  r
  rb   rd   ri  Úloggingr™  ri   r½  r6   rÝ   r(   Úurllib.parser®   Úurllib.requestÚfastapir   Úhermes_cli.web_routers._commonr   Úhermes_cli.web_depsr   Úhermes_cli.web_server_chatr   r   Úhermes_cli.web_server_gatewayr   r   r	   r
   r   Úhermes_cli.web_modelsr   r   r   r   r   Útypingr   r   r   Ú	getLoggerr€   Úrouterr   r   r   rA   rh   rà   r+   r8   rD   Úpostru   rB   r„   r‰   r”   r�   rÅ   rå   rö   rù   rÿ   r  r#  r$  rG  Ú	websocketry  rÂ  r$   )rÄ  s   @r#   Ú<module>rÓ     s~  øðöó Û Û Û Û Û Û Û Û Û 	Û Û Ý Ý 7Ý $ß Jß eß AÑ A÷õ ÷ 'Ñ &à×ÒÐ0Ó1€Ù	‹€ñ Ð4Ð6VÓWÐ Ù $Ð%DÓ EÐ Ù�
Ð/Ó0€ð �˜ w°¸VÀ[ÐRXØ�˜ v¨{¸FÀKÐQWØ�&˜,¨°ÀÈÐW]Ø�'ð	*Ð ó ð #3Ð ð ˆL˜& +¨w¸ÀVÈ[Øˆ\ðÐ õõ,õ";ð
 ‡�Ð$Ó%ö;ó &ð;ð| ‡�Ð%Ó&ö"ó 'ð"ð8 ‡�Ð*Ó+ö"ó ,ð"ð ‡�Ð,Ó-ö"ó .ð"õ,8ð ‡�Ð*Ó+öG1ó ,ðG1ðT ‡�ÐÓ ö5ó !ð5ðp ‡�Ð#Ó$ö"Ló %ð"L÷Jñ õ*#õ>$õ	õ ð& ‡�Ð#Ó$öLó %ðLðB ×ÑÐ0Ó1ôIó 2ðIðX ×ÑÐ+Ó,ôqó -òqr%   