
    Pmjv                    F   U 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 ddlZddlZddlZddlZddlmZmZmZmZmZ  ej        e          Zd Zd ZdefdZd	eddfd
ZdefdZdefdZ ddl!m"Z# de$fdZ%dee$         fdZ&dZ'defdZ(defdZ)defdZ*de+fdZ,dZ-dZ.dZ/dZ0dZ1dZ2ej3        4                     ej5                    d          Z6dZ7de8fdZ9de8defdZ:dpd"ed#e8d$eddfd%Z;da< ej=                    Z>d&eddfd'Z?defd(Z@ ej=                    ZAdaBeejC                 eDd)<   defd*ZEd"e8dd+fd,ZFd-ejC        ddfd.ZGdqdefd/ZHdrd0ZI G d1 d2          ZJ G d3 d4          ZKdeKeJz  fd5ZLh d6ZM ejN        d7ejO        8          ZPd9e$defd:ZQd;ZRdeSfd<ZTdqd9e$d=eeS         defd>ZUde$fd?ZVdqd@e$dAee$         dee$ef         fdBZWdCe$dDedefdEZXd@e$dAee$         dDedee$ef         fdFZYd@e$dDedee$         fdGZZda[eej\                 eDdH<    ej=                    Z]drdIZ^defdJZ_defdKZ`defdLZadCe$defdMZbdCe$defdNZc	 	 	 	 	 	 	 	 dsdUeg ef         dVee         dWedXedYedZeeg df                  d[ed\ed]efd^Zdd_Zed`ZfdZgdefdaZhdbe$ddfdcZi	 	 	 	 	 	 	 	 	 dtdUeg ef         dfeeg ef                  dZeee$gdf                  dgee8         dWedXedhed[ed\ed]edee$         fdiZjdje$defdkZkdee$ef         fdlZldudnedefdoZmdS )vaL  Voice Mode -- Push-to-talk audio recording and playback for the CLI.

Provides audio capture via sounddevice, WAV encoding via stdlib wave,
STT dispatch via tools.transcription_tools, and TTS playback via
sounddevice or system audio players.

Dependencies (optional):
    pip install sounddevice numpy
    or: uv sync --extra voice
    N)Path)AnyCallableDictListOptionalc                      ddl } ddl}| |fS )zLazy-import sounddevice and numpy.  Returns (sd, np).

    Raises ImportError or OSError if the libraries are not available
    (e.g. PortAudio missing on headless servers).
    r   N)sounddevicenumpy)sdnps     6/home/thesage/.hermes/hermes-agent/tools/voice_mode.py_import_audior   #   s%     r6M    c                      ddl } | S )u   Lazy-import numpy only (no sounddevice).  Returns the module.

    Used where we need to synthesize/convert audio samples but must NOT
    import sounddevice — see _sounddevice_output_allowed.
    r   N)r   )r   s    r   _import_numpyr   .   s     Ir   returnc                  0    t          j                    dk    S )a  Whether sounddevice may be used for audio OUTPUT.

    Returns False on macOS: importing/initializing sounddevice
    (PortAudio/CoreAudio) for output triggers a kTCCServiceMediaLibrary
    permission prompt, even though playback needs no media-library access.
    On macOS all output is routed through ``afplay`` instead. This does NOT
    affect audio *input* (recording), which legitimately needs microphone
    permission. See PR #62601 / #13291.
    Darwin)platformsystem r   r   _sounddevice_output_allowedr   8   s     ?((r   sample_ratec                    d}	 t          j        dd          }|j        }t          j        |d          5 }|                    d           |                    d           |                    |           |                    | 	                                           ddd           n# 1 swxY w Y   t          |           n2# t          $ r%}t                              d|           Y d}~nd}~ww xY w|r(	 t          j        |           dS # t           $ r Y dS w xY wdS # |r&	 t          j        |           w # t           $ r Y w w xY ww xY w)	zWrite int16 mono PCM to a temp WAV and play it via play_audio_file.

    Used on macOS so tone/beep output goes through ``afplay`` instead of
    sounddevice (avoids the TCC media-library prompt).
    N.wavF)suffixdeletewb      z!Tone tempfile playback failed: %s)tempfileNamedTemporaryFilenamewaveopensetnchannelssetsampwidthsetframeratewriteframestobytesplay_audio_file	ExceptionloggerdebugosunlinkOSError)audior   tmp_pathtmpwfes         r   _play_int16_via_tempfiler8   E   s    H)FFF8YsD!! 	,ROOAOOAOOK(((NN5==??+++		, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	,
 	!!!! = = =8!<<<<<<<<=  		(#####   	 	8 		(####   	s   2C A'B)C )B--C 0B-1C D" 
C3C.)D" .C33D" 9D 
DD"E&D;:E;
EEEEc                  T    	 t                       dS # t          t          f$ r Y dS w xY w)z/Return True if audio libraries can be imported.TF)r   ImportErrorr2   r   r   r   _audio_availabler;   _   s<    t!   uus    ''c                 R   	 |                      dd          }t          |t                    r|                    d          nt	          |dd          }t          |t
          t          f          r"|dk    rt          t          |                    S n# t          $ r Y nw xY wt          S )zReturn the preferred capture rate for the default input device.

    Falls back to the Whisper-friendly 16 kHz constant when the backend does
    not expose a numeric default rate.
    Ninputdefault_samplerater   )
query_devices
isinstancedictgetgetattrintfloatroundr-   SAMPLE_RATE)r   inforates      r   _default_input_sampleraterJ   h   s    g..1;D$1G1Gvtxx,---WUY[oquMvMvdS%L)) 	$dQhhuT{{###   s   BB 
BB)	is_termuxc                     t                      rdS 	 t          j        t          t          dt          j                  k    r8t	          t          j                  dz  dz  } |                                 r|  dS n# t          $ r Y nw xY wdS )NzGpkg install python-numpy portaudio && python -m pip install sounddevicebase_prefixbinpipz install sounddevice numpyzpip install sounddevice numpy)_is_termux_environmentsysprefixrC   r   existsr-   )pip_in_venvs    r   _voice_capture_install_hintrU   {   s     YXX:mSZ@@@@sz**U2U:K!!## B%AAAA   **s   A%A9 9
BBc                  J    t                      sd S t          j        d          S )Ntermux-microphone-record)rP   shutilwhichr   r   r   _termux_microphone_commandrZ      s&    !## t<2333r   ))pmlistpackagescom.termux.api)cmdpackager\   r]   r^   c                     t                      sdS d} t          D ]}	 t          j        t	          |          ddddddt          j                  }n1# t          t          t          f$ r d} Y Rt          j	        $ r d} Y dw xY w|j
        dk    rd} vd|j        pd	                                v r dS | r0t          j        d
          t                              d           dS dS )u  Return True iff the Termux:API Android app is installed.

    Strategy (issue #31015):

    1. Try each probe in ``_TERMUX_API_PACKAGE_PROBES`` and look for
       ``package:com.termux.api`` in stdout.  Any positive hit is
       authoritative — return True.
    2. If every probe is *inconclusive* (binary missing, permission
       denied, timeout, non-zero exit) we cannot honestly say the app
       is missing; fall back to trusting the ``termux-microphone-record``
       binary on PATH.  The binary ships with the ``termux-api`` package
       and is only useful when the Android app is installed; users who
       installed the package deliberately almost always have the app
       too.  A false negative on this gate blocks ``/voice on``
       outright (the symptom reported in #31015), while a false
       positive only surfaces a precise runtime error from the binary
       itself — strictly more actionable.
    3. If at least one probe ran cleanly and definitively did not
       mention the package, treat the app as missing and return False
       — that's the genuine "Termux:API CLI installed without the app"
       case the existing warning was written for.
    FTutf-8replace   capture_outputtextencodingerrorstimeoutcheckstdinr   zpackage:com.termux.api rW   NzlTermux package-manager probes inconclusive; trusting termux-microphone-record binary on PATH (issue #31015).)rP   _TERMUX_API_PACKAGE_PROBES
subprocessrunr\   DEVNULLFileNotFoundErrorPermissionErrorr2   TimeoutExpired
returncodestdoutlowerrX   rY   r.   r/   )inconclusiver_   results      r   _termux_api_app_installedrz      s9   . "## uL)  	^S		#GI (  FF "?G< 	 	 	LH( 	 	 	LH	 !!L#(;'B'B'D'DDD44 E  %?@@LF	
 	
 	
 t5s   3AA?,A?>A?c                  >    t                      d uot                      S N)rZ   rz   r   r   r   _termux_voice_capture_availabler}      s    %''t3S8Q8S8SSr   c                     ddl } ddl}g }t          j                            dd          }|                    d          D ]U}|                                }|                    d          r*|                    |t          d          d                    Vt          j                            d          }|r3|                    t          j
                            |d                     t          j                            d	          }|rg|                    t          j
                            |d
d                     |                    t          j
                            |d                     |D ]}|s	 |                    t          j        |          j                  s3n# t          $ r Y @w xY w|                      | j        | j                  }	 |                    d           |                    |           	 |                                  dS # t          $ r Y |                                 w xY w# |                                 w xY wdS )a  Return True if a PulseAudio/PipeWire socket is reachable on disk.

    Covers the common case where a sound server runs locally (e.g. on a
    remote SSH host) without ``PULSE_SERVER``/``PIPEWIRE_REMOTE`` being set --
    the client just connects to the default socket under the runtime dir.
    We look at ``PULSE_SERVER`` unix paths, ``PULSE_RUNTIME_PATH``, and
    ``XDG_RUNTIME_DIR`` for a ``pulse/native`` or ``pipewire-0`` socket
    (issue #35622).
    r   NPULSE_SERVERrm   ;zunix:PULSE_RUNTIME_PATHnativeXDG_RUNTIME_DIRpulsez
pipewire-0      ?TF)socketstatr0   environrB   splitstrip
startswithappendlenpathjoinS_ISSOCKst_moder2   AF_UNIXSOCK_STREAM
settimeoutconnectclose)	r   r   
candidatespulse_serverpartpulse_runtimexdg_runtimer   socks	            r   _pulse_socket_reachabler      sD    MMMKKKJ:>>."55L""3'' 3 3zz||??7## 	3d3w<<==1222JNN#788M A"',,}h??@@@*..!233K C"',,{GXFFGGG"',,{LAABBB   		==!677  	 	 	H	 }}V^V-?@@	OOC   LL JJLLLLLL  	 	 	JJLLLL	 JJLLLL5s6   :,F((
F54F5*H
H<%H?;H<<H??Ic                     g } g }t                      }t                      }t          |o|          }t          t          j                            d          p,t          j                            d          pt                                }t          d dD                       r-|r|                    d           n|                     d           ddl	m
}  |            r-|r|                    d	           n|                     d
           	 t          ddd          5 }d|                                                                v rQ|r|                    d           n9t                      r|                    d           n|                     d           ddd           n# 1 swxY w Y   n# t          t           t"          f$ r Y nw xY w	 t%                      \  }}		 |                                }
|
sE|r|                    d           n-|r|                    d           n|                     d           nU# t(          $ rH |r|                    d           n-|r|                    d           n|                     d           Y nw xY wn# t*          $ rZ |r|                    d           n?|r|s|                     d           n%|                     dt-                       d           Y nzt"          $ rn |r|                    d           nS|r|s|                     d           n9t/                      r|                     d           n|                     d            Y nw xY w|  | |d!S )"zDetect if the current environment supports audio I/O.

    Returns dict with 'available' (bool), 'warnings' (list of hard-fail
    reasons that block voice mode), and 'notices' (list of informational
    messages that do NOT block voice mode).
    r   PIPEWIRE_REMOTEc              3   T   K   | ]#}t           j                            |          V  $d S r|   )r0   r   rB   ).0vs     r   	<genexpr>z+detect_audio_environment.<locals>.<genexpr>)  s0      
R
R2:>>!
R
R
R
R
R
Rr   )
SSH_CLIENTSSH_TTYSSH_CONNECTIONzBRunning over SSH with a reachable PulseAudio/PipeWire sound serverzRunning over SSH -- no audio devices available.
  If a sound server (PulseAudio/PipeWire) is running on this host,
  point Hermes at it, e.g.:
    export XDG_RUNTIME_DIR=/run/user/$(id -u)
    # or: export PULSE_SERVER=unix:$XDG_RUNTIME_DIR/pulse/nativer   )is_containerzGRunning inside container (Docker/Podman/LXC) with host audio forwardinga  Running inside container (Docker/Podman/LXC) -- no audio devices.
  Forward host audio with one of (substitute $XDG_RUNTIME_DIR for your runtime dir,
  typically /run/user/$UID):
    PulseAudio:  -v $XDG_RUNTIME_DIR/pulse/native:$XDG_RUNTIME_DIR/pulse/native \
                 -e PULSE_SERVER=unix:$XDG_RUNTIME_DIR/pulse/native
    PipeWire:    -e PIPEWIRE_REMOTE=$XDG_RUNTIME_DIR/pipewire-0/proc/versionrrb   )rh   	microsoftz@Running in WSL with a reachable PulseAudio/PipeWire sound serveraS  Running in WSL without a PulseAudio bridge -- TTS playback will use the PowerShell/Media.SoundPlayer fallback. Voice INPUT (recording) still requires a PulseAudio bridge:
  1. Set PULSE_SERVER=unix:/mnt/wslg/PulseServer
  2. Create ~/.asoundrc pointing ALSA at PulseAudio
  3. Verify with: arecord -d 3 /tmp/test.wav && aplay /tmp/test.wavzRunning in WSL -- audio requires a forwarded sound server.
  PulseAudio: export PULSE_SERVER=unix:/mnt/wslg/PulseServer
  PipeWire:   export PIPEWIRE_REMOTE=$XDG_RUNTIME_DIR/pipewire-0
  Then verify: arecord -d 3 /tmp/test.wav && aplay /tmp/test.wavNzSNo PortAudio devices detected but host audio forwarding is configured -- continuingzMNo PortAudio devices detected, but Termux:API microphone capture is availablez&No audio input/output devices detectedzOAudio device query failed but host audio forwarding is configured -- continuingzMPortAudio device query failed, but Termux:API microphone capture is availablez6Audio subsystem error (PortAudio cannot query devices)zDTermux:API microphone recording available (sounddevice not required)zkTermux:API Android app is not installed. Install/update the Termux:API app to use termux-microphone-record.zAudio libraries not installed ()zBTermux:API microphone recording available (PortAudio not required)zmPortAudio system library not found -- install it first:
  Termux: pkg install portaudio
Then retry /voice on.zPortAudio system library not found -- install it first:
  Linux:  sudo apt-get install libportaudio2
  macOS:  brew install portaudio
Then retry /voice on.)	availablewarningsnotices)rZ   rz   boolr0   r   rB   r   anyr   hermes_constantsr   r&   readrw   _wsl_powershell_tts_availablerr   rs   r2   r   r?   r-   r:   rU   rP   )r   r   termux_mic_cmdtermux_app_installedtermux_capturehas_forwarded_audior   fr   _devicess              r   detect_audio_environmentr     s    HG/11N466.A-ABBN

~&& 	%:>>+,,	%"$$  
R
R&Q
R
R
RRR 
 		NN_````OOS   .-----|~~  
	NNdeeeeOOR  &/3999 	Qaffhhnn....& NN#effff244 NN^    OO[  	 	 	 	 	 	 	 	 	 	 	 	 	 	 	( 8   5A	Z&&((G N& NNNm    $ NNN#rssssOO$LMMM 	Z 	Z 	Z # Ze      Znoooo XYYY	Z  ` ` ` 	`NNabbbb 	`$8 	`OO}    OO^>Y>[>[^^^___    	NN_```` 	$8 	OO}    $%% 	OO(    OO(  , "\  sw   F3 !A:F'F3 'F++F3 .F+/F3 3GGJ #AH? >J ?AJJ JJ A!M/8A4M/.M/i>  r    int16r!            @hermes_voice333333?c                     	 ddl m}   |                                 di           }t          |t                    st
          S |                    dt
                    }n# t          $ r
 t
          cY S w xY w	 t          |          }n# t          t          f$ r
 t
          cY S w xY wt          |t                    s|dk     s|dk    st          |          rt
          S |S )aR  Read ``voice.beep_volume`` from config.yaml; clamps to 0.0-1.0.

    Defaults to 0.3 when the key is missing, invalid, or when the config
    system can't be imported (e.g. broken ~/.hermes/config.yaml during a
    partial install). Failures fall back silently so the audio cue never
    breaks the voice loop on a degenerate config.
    r   load_configvoicebeep_volume              ?)hermes_cli.configr   rB   r@   rA   _DEFAULT_BEEP_VOLUMEr-   rE   	TypeError
ValueErrorr   _is_nan)r   	voice_cfgrawvolumes       r   _get_beep_volumer     s    $111111KMM%%gr22	)T** 	(''mmM+?@@ $ $ $####$$sz" $ $ $####$#t $##Ms)   ?A A A21A26B B! B!valuec                 N    	 t          j        |           S # t          $ r Y dS w xY w)NF)mathisnanr-   )r   s    r   r   r     s9    z%      uus    
$$p  Q?	frequencydurationcountc           	      "   	 t                      }n# t          $ r Y dS w xY w	 d}t          t          |z            }t          t          |z            }t	                      }g }t          |          D ]!}	|                    d||d          }
|                    d|j        z  | z  |
z            }t          t          t          dz            |dz            }|d|xx         |                    dd	|          z  cc<   || dxx         |                    d	d|          z  cc<   |
                    ||z  d
z                      |j                             |	|d	z
  k     r/|
                    |                    ||j                             #|                    |          }t                      st!          |t                     dS 	 t#                      \  }}n# t          t$          f$ r Y dS w xY w|                    |t                     t)          j                    dz   }|                                r|                                j        rot)          j                    |k     rXt)          j        d           |                                r0|                                j        rt)          j                    |k     X|                                 dS # t4          $ r&}t6                              d|           Y d}~dS d}~ww xY w)zPlay a short beep tone using numpy + sounddevice.

    Args:
        frequency: Tone frequency in Hz (default 880 = A5).
        duration: Duration of each beep in seconds.
        count: Number of beeps to play (with short gap between).
    NgQ?r   Fendpointr!   {Gz?   r      dtype
samplerate       @zBeep playback failed: %s)r   r:   rD   rG   r   rangelinspacesinpiminr   astyper   zerosconcatenater   r8   r   r2   playtime	monotonic
get_streamactivesleepstopr-   r.   r/   )r   r   r   r   gapsamples_per_beepsamples_per_gapr   partsittonefade_lenr3   r   r   deadliner7   s                     r   	play_beepr     s   __   %4{X566kC/00&((u 		H 		HAAx)9EJJA66!be)i/!344D3{T1224D4IJJH(OOOr{{1a:::OOO(Aq( ; ;;LL$,u4<<RXFFGGG519}}RXXoRXXFFGGGu%% +,, 	$UK888F	!OOEBW% 	 	 	FF	
+... >##c)mmoo 	"--//"8 	T^=M=MPX=X=XJt mmoo 	"--//"8 	T^=M=MPX=X=X
					 4 4 4/3333333334sI    
F*K G" !K "G73K 6G77C%K 
L(L		Lr   c                 ~    t           5  t          dt          | rdndz             addd           dS # 1 swxY w Y   dS )u  Reference-count real audio output (TTS/file playback).

    Playback paths bracket their work with ``mark_audio_output_active(True)``
    / ``(False)`` so ``is_audio_output_active()`` reflects whether speech
    audio is leaving the speakers RIGHT NOW — unlike the per-turn TTS-done
    events, which stay 'busy' for a whole turn even while the pipeline is
    silently waiting for text.
    r   r    N)_audio_output_lockmax_audio_output_active_count)r   s    r   mark_audio_output_activer  &  s     
 
 
%()&-@QQbA&
 &
"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
s   266c                  Z    t           5  t          dk    cddd           S # 1 swxY w Y   dS )z>True while TTS/file audio is actually playing on the speakers.r   N)r   r  r   r   r   is_audio_output_activer  6  st    	 . .)A-. . . . . . . . . . . . . . . . . .s    $$_thinking_stopc                      	 ddl m}  ddlm}  |                                 di           }t          |t                    r! ||                    dd          d          S n# t          $ r Y nw xY wdS )z5Config gate: ``voice.thinking_sound`` (default True).r   r   )is_truthy_valuer   thinking_soundT)default)r   r   utilsr  rB   r@   rA   r-   )r   r  r   s      r   thinking_sound_enabledr  @  s    
111111))))))KMM%%gr22	i&& 	"?.55t   	    4s   AA# #
A0/A0r   c                    d}t          t          |z            }|                     d||d          }|                     dd|          }d| j        z  |                     ||z            z  t          z  }|                     |          }d|z  d	|                     |d
z            z  z   }t          dt          z            }|                     |          }	|                     dd|          |	d|<   |	|                     | dz            z  }	t                      dz  }
||	z  |
z  dz  	                    | j
                  S )z~One soft 'blub': short sine with a gentle downward pitch glide and a
    smooth attack/decay envelope (no clicks), low-volume.g{Gz?r   Fr   r   g
ףp=
?r!   皙?g?r   {Gz?r   Ng      ,@r   r   )rD   rG   r   r   cumsumr   onesexpr   r   r   )r   r   r   nr   glidephaser   attackenvr   s              r   _synth_thinking_blipr  P  s/    HK("##A
AxU33AKKT1%%EI		)e"3444{BE66%==D:bffUS[1111D#$$F
''!**C;;sC00CL2661"t)C#%F3J%'//999r   r   c                   	 t                      sdS 	 t                      \  }	n# t          t          f$ r Y dS w xY wddl}d}	fd|D             }d}|                                 s	 |
 |            rz||t          |          z           }|                    |t                     | 	                    t          |          t          z  dz              |
                                 |dz  }n3# t          $ r&}t                              d|           Y d}~dS d}~ww xY w| 	                    d	|                                d
z  z              |                                 dS dS )ue  Daemon loop: play alternating-pitch blips every ~0.8-1.2s until *stop*.

    Skips a blip (without stopping) whenever *should_play* returns False —
    e.g. TTS audio started flowing or the mic re-armed. macOS: sounddevice
    output is TCC-gated, and per-second afplay subprocess churn is worse
    than silence, so the loop exits immediately there.
    Nr   )g     x@gt@c                 0    g | ]}t          |          S r   )r  )r   pr   s     r   
<listcomp>z(_thinking_sound_loop.<locals>.<listcomp>w  s$    :::Q!"a((:::r   r   r  r    zThinking sound blip failed: %sr  g?)r   r   r:   r2   randomis_setr   r   rG   waitr   r-   r.   r/   )
r   should_playr   r  pitchesblipsr   blipr7   r   s
            @r   _thinking_sound_loopr$  e  s    '(( BB!    MMMG::::':::E	Akkmm /		"kkmm"QU^,555		#d))k1D8999			Q 	 	 	LL91===FFFFF	 			##--... kkmm / / / / /s%   % ::(BC/ /
D9DDc                 L   t                      sdS t          5  t          't                                          s	 ddd           dS t	          j                    }|addd           n# 1 swxY w Y   t	          j        t          || fdd                                           dS )a  Start the ambient thinking sound (idempotent).

    *should_play* is polled before each blip; return False to skip while
    speech audio flows or the mic is capturing. Returns True when the loop
    was started (or already running), False when disabled/unavailable.
    FNTzvoice-thinking-sound)targetargsdaemonr$   )	r  _thinking_lockr  r  	threadingEventThreadr$  start)r   r   s     r   start_thinking_soundr.    s    "## u	  %n.C.C.E.E%          	              
 #K #	  
 eggg4s   "A(A((A,/A,c                      t           5  t          dc} addd           n# 1 swxY w Y   | |                                  dS dS )z7Stop the ambient thinking sound instantly (idempotent).N)r)  r  setr   s    r   stop_thinking_soundr2    s     
 4 4-tn4 4 4 4 4 4 4 4 4 4 4 4 4 4 4




 s   ##c                       e Zd ZdZdZddZedefd            Zede	fd            Z
edefd            Zddd	Zdd
Zdee         fdZddZddZdS )TermuxAudioRecorderzBRecorder backend that uses Termux:API microphone capture commands.Fr   Nc                 n    t          j                    | _        d| _        d| _        d | _        d| _        d S )NFr   r   )r*  Lock_lock
_recording_start_time_recording_path_current_rmsselfs    r   __init__zTermuxAudioRecorder.__init__  s6    ^%%
.2r   c                     | j         S r|   r8  r<  s    r   is_recordingz TermuxAudioRecorder.is_recording  s
    r   c                 J    | j         sdS t          j                    | j        z
  S Nr   r8  r   r   r9  r<  s    r   elapsed_secondsz#TermuxAudioRecorder.elapsed_seconds  '     	3~$"222r   c                     | j         S r|   r;  r<  s    r   current_rmszTermuxAudioRecorder.current_rms  s      r   c                    ~t                      }|st          d          t                      st          d          | j        5  | j        r	 d d d            d S t          j        t          d           t          j	        d          }t
          j
                            t          d| d          | _        d d d            n# 1 swxY w Y   |d| j        d	d
dddt          t                    dt          t                    g}	 t!          j        |ddddddt           j                   ny# t           j        $ rG}|j        p|j        pt          |                                          }t          d|           |d }~wt.          $ r}t          d|           |d }~ww xY w| j        5  t          j                    | _        d| _        d| _        d d d            n# 1 swxY w Y   t6                              d           d S )NzTermux voice capture requires the termux-api package and app.
Install with: pkg install termux-api
Then install/update the Termux:API Android app.zrTermux voice capture requires the Termux:API Android app.
Install/update the Termux:API app, then retry /voice on.Texist_ok%Y%m%d_%H%M%S
recording_z.aac-fz-l0z-eaacz-r-crb   rc      re   z Termux microphone start failed: r   zTermux voice recording started)rZ   RuntimeErrorrz   r7  r8  r0   makedirs	_TEMP_DIRr   strftimer   r   r:  strrG   CHANNELSro   rp   rq   CalledProcessErrorstderrrv   r   r-   r   r9  r;  r.   rH   )r=  on_silence_stopmic_cmd	timestampcommandr7   detailss          r   r-  zTermuxAudioRecorder.start  s   ,.. 	B  
 )** 	K  
 Z 	Y 	Y 	Y 	Y 	Y 	Y 	Y 	Y 	Y 	Y K	D1111o66I#%7<<	;W	;W;W;W#X#XD 	Y 	Y 	Y 	Y 	Y 	Y 	Y 	Y 	Y 	Y 	Y 	Y 	Y 	Y 	Y $&#%#k""#h--
	NN74dW]fprz~  GQ  GY  Z  Z  Z  Z  Z, 	T 	T 	Tx5185s1vv<<>>GK'KKLLRSS 	N 	N 	NE!EEFFAM	N Z 	" 	"#~//D"DO !D	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	455555sP   	CACCC&D- -F#<AE>>F#FF#-'G  G$'G$c           
      z    t                      }|sd S t          j        |dgddddddt          j                   d S )N-qTrb   rc   rS  Fre   )rZ   ro   rp   rq   )r=  r]  s     r   _stop_termux_recordingz*TermuxAudioRecorder._stop_termux_recording  sk    ,.. 	Ft$QXajtv  D  LV  L^  	_  	_  	_  	_  	_  	_r   c                 r   | j         5  | j        s	 d d d            d S d| _        | j        }d | _        | j        }d| _        d d d            n# 1 swxY w Y   |                                  |rt          j                            |          sd S t          j
                    |z
  dk     r(	 t          j        |           n# t          $ r Y nw xY wd S t          j                            |          dk    r(	 t          j        |           n# t          $ r Y nw xY wd S t                              d|           |S )NFr   r   z"Termux voice recording stopped: %s)r7  r8  r:  r9  r;  rc  r0   r   isfiler   r   r1   r2   getsizer.   rH   )r=  r   
started_ats      r   r   zTermuxAudioRecorder.stop  s   Z 	" 	"? 	" 	" 	" 	" 	" 	" 	" 	" $DO'D#'D )J !D	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	##%%% 	27>>$// 	4>j(3..	$   47??4  A%%	$   48$???s:   	A#AAA*B? ?
CC5D
 

DDc                    | j         5  | j        }d| _        d | _        d| _        d d d            n# 1 swxY w Y   	 |                                  n# t
          $ r Y nw xY w|rEt          j                            |          r&	 t          j	        |           n# t          $ r Y nw xY wt                              d           d S )NFr   z Termux voice recording cancelled)r7  r:  r8  r;  rc  r-   r0   r   re  r1   r2   r.   rH   )r=  r   s     r   cancelzTermuxAudioRecorder.cancel  s"   Z 	" 	"'D#DO#'D  !D		" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	"
	'')))) 	 	 	D	 	BGNN4(( 		$   677777s/   155A 
AAB 
B&%B&c                 .    |                                   d S r|   )ri  r<  s    r   shutdownzTermuxAudioRecorder.shutdown$  s    r   r   Nr|   )__name__
__module____qualname____doc__supports_silence_autostopr>  propertyr   rA  rE   rE  rD   rI  r-  rc  r   rX  r   ri  rk  r   r   r   r4  r4    s"       LL %    d    X 3 3 3 3 X3
 !S ! ! ! X!*6 *6 *6 *6 *6X_ _ _ _hsm    88 8 8 8"     r   r4  c                       e Zd ZdZdZddZdedefdZe	defd            Z
e	defd	            Ze	defd
            ZddZdddZddeddfdZdee         fdZddZddZeeddedefd            ZdS )AudioRecordera  Thread-safe audio recorder using sounddevice.InputStream.

    Usage::

        recorder = AudioRecorder()
        recorder.start(on_silence_stop=my_callback)
        # ... user speaks ...
        wav_path = recorder.stop()   # returns path to WAV file
        # or
        recorder.cancel()            # discard without saving

    If ``on_silence_stop`` is provided, recording automatically stops when
    the user is silent for ``silence_duration`` seconds and calls the callback.
    Tr   Nc                 l   t          j                    | _        d | _        g | _        d| _        d| _        t          | _        d| _	        d| _
        d| _        d| _        d| _        d| _        d| _        d| _        d | _        t$          | _        t(          | _        d| _        d| _        d| _        d| _        d S )NFr   r   g      .@r   )r*  r6  r7  _stream_framesr8  r9  rG   _sample_rate_has_spoken_speech_start
_dip_start_min_speech_duration_max_dip_tolerance_silence_start_resume_start_resume_dip_start_on_silence_stopSILENCE_RMS_THRESHOLD_silence_thresholdSILENCE_DURATION_SECONDS_silence_duration	_max_wait_max_recording_seconds	_peak_rmsr;  r<  s    r   r>  zAudioRecorder.__init__=  s    ^%%
 "$"%!, $'!$+.!),%($'(+ $'<(@ $ .1#!"r   elapsedc                 F    | j         }t          |o|dk    o||k              S )a6  Whether the configured hard recording-length cap has elapsed.

        ``voice.max_recording_seconds`` is applied by the CLI before each
        recording (see ``HermesCLI._voice_start_recording``). A value <= 0
        (or unset) disables the cap, preserving the previous unbounded
        behaviour.
        r   )r  r   )r=  r  caps      r   _max_duration_reachedz#AudioRecorder._max_duration_reachedY  s,     )C6C!G63777r   c                 J    | j         sdS t          j                    | j        z
  S rC  rD  r<  s    r   rE  zAudioRecorder.elapsed_secondsf  rF  r   c                     | j         S )zBCurrent audio input RMS level (0-32767). Updated each audio chunk.rH  r<  s    r   rI  zAudioRecorder.current_rmsl  s       r   c                     | j         S )z,Whether audio recording is currently active.r@  r<  s    r   rA  zAudioRecorder.is_recordingq  s     r   c                 t     j         dS t                      \  } fd}d}	 |                     j        t          t
          |          }|                                 nN# t          $ rA}|&	 |                                 n# t          $ r Y nw xY wt          d| d          |d}~ww xY w| _         dS )a_  Create the audio InputStream once and keep it alive.

        The stream stays open for the lifetime of the recorder.  Between
        recordings the callback simply discards audio chunks (``_recording``
        is ``False``).  This avoids the CoreAudio bug where closing and
        re-opening an ``InputStream`` hangs indefinitely on macOS.
        Nc           	        	 |rt                               d|           j        sd S j                            |                                            t          
                    
                    | 	                    
j
                  dz                                }|_        t          j        |          _        j        t          j                    }|j        z
  }|j        k    rd_        j        dk    r|_        nDj        s=|j        z
  j        k    r*d_        t                               d|j        z
             j        sd_        nd_        j        dk    r|_        n|j        z
  j        k    rd_        d_        nj        r@j        dk    r4j        dk    r|_        n|j        z
  j        k    rd_        d_        nbj        dk    rWj        dk    r|_        nD|j        z
  j        k    r1t                               d|j        z
             d_        d_        d}j        rT|j        k    rIj        dk    r|_        nj|j        z
  j        k    r"t                               d	j                   d}n4j        s-|j        k    r"t                               d
j                   d}|s7                    |          r"t                               dj                   d}|r`j         5  j        	d _        d d d            n# 1 swxY w Y   	r3	fd}tC          j"        |d          #                                 d S d S d S d S )Nzsounddevice status: %sr!   r   Tz(Speech confirmed (%.2fs above threshold)r   z'Speech attempt reset (dip lasted %.2fs)Fz'Silence detected (%.1fs), auto-stoppingz%No speech within %.0fs, auto-stoppingz3Max recording length reached (%.0fs), auto-stoppingc                      	               d S # t           $ r(} t                              d| d           Y d } ~ d S d } ~ ww xY w)NzSilence callback failed: %sTexc_info)r-   r.   error)r7   cbs    r   _safe_cbzAAudioRecorder._ensure_stream.<locals>._callback.<locals>._safe_cb  sg    ^ "#, ^ ^ ^ &-JAX\ ] ] ] ] ] ] ] ] ]^s   
 
A<Ar&  r(  )$r.   r/   r8  rw  r   copyrD   sqrtmeanr   float64r;  r  r  r  r   r   r9  r  r{  rz  ry  r|  r~  r  r  r}  r  rH   r  r  r  r7  r*  r,  r-  )indataframes	time_infostatusrmsnowr  should_firer  r  r   r=  s            @r   	_callbackz/AudioRecorder._ensure_stream.<locals>._callback  sG    ?5v>>>? L... bggbggfmmBJ&?&?1&DEEFFGGC #D 55DN $0n&& 00000&)DO)S00-0**!- ?#8J2JdNg2g2g+/(%O%(4+=%=? ? ?
  + 5.1++ 25.-4414D.. 4#559RRR25D/14D.% . )A--1S8858D22 4#99T=TTT14D.58D2'!++ #--*-t.$2III%N%(4?%:< < <-0**-
 $# 't/F(F(F*c11.1++t22d6LLL$M$($:< < <&*) 'g.G.GKK G $0 0 0"&K
 # 't'A'A''J'J 'KK U $ ;= = ="&K 
O 5 5!204-5 5 5 5 5 5 5 5 5 5 5 5 5 5 5  O^ ^ ^ ^ ^
 "(FFFLLNNNNNo 10Z
O 
OO Os   L66L:=L:)r   channelsr   callbackz#Failed to open audio input stream: z6. Check that a microphone is connected and accessible.)
rv  r   InputStreamrx  rY  DTYPEr-  r-   r   rT  )r=  r   r  streamr7   r   s   `    @r   _ensure_streamzAudioRecorder._ensure_streamx  s&    <#FBe	O e	O e	O e	O e	O e	OP 	^^,!"	 $  F LLNNNN 		 		 		!LLNNNN    DGa G G G  		 s;   <A# #
B.-B)0BB)
BB)BB))B.c                     	 t                      \  }}nd# t          $ r,}t                      rd}nd}t          d| d          |d}~wt          $ r#}t          dt
          j         d          |d}~ww xY w| j        5  | j        r	 ddd           dS g | _	        t          j                    | _        d| _        d	| _        d	| _        d	| _        d	| _        d	| _        d
| _        d
| _        || _        ddd           n# 1 swxY w Y   t-          |          | _        |                                  | j        5  d| _        ddd           n# 1 swxY w Y   t2                              d| j        t6                     dS )a}  Start capturing audio from the default input device.

        The underlying InputStream is created once and kept alive across
        recordings.  Subsequent calls simply reset detection state and
        toggle frame collection via ``_recording``.

        Args:
            on_silence_stop: Optional callback invoked (in a daemon thread) when
                silence is detected after speech. The callback receives no arguments.
                Use this to auto-stop recording and trigger transcription.

        Raises ``RuntimeError`` if sounddevice/numpy are not installed
        or if a recording is already in progress.
        z  Termux: pkg install portaudiozM  Linux:  sudo apt-get install libportaudio2
  macOS:  brew install portaudioz8PortAudio system library not found -- install it first:
z
Then retry /voice on.Nz9Voice mode requires sounddevice and numpy.
Install with: z! -m pip install sounddevice numpyFr   r   Tz.Voice recording started (rate=%d, channels=%d))r   r2   rP   rT  r:   rQ   
executabler7  r8  rw  r   r   r9  ry  rz  r{  r~  r  r  r  r;  r  rJ   rx  r  r.   rH   rY  )r=  r\  r   r   r7   portaudio_hints         r   r-  zAudioRecorder.start  sn   	!OOEB 	 	 	 &'' !B7  (!( ( (  	
  	 	 	S!$S S S  	 Z 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 DL#~//D$D!$D!DO"%D!$D%(D"DN !D$3D!	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4  6b99Z 	# 	#"DO	# 	# 	# 	# 	# 	# 	# 	# 	# 	# 	# 	# 	# 	# 	#DdFWYabbbbbsK    
A5'AA5A00A5?	C?AC??DD9EEEr   rj   c                 N   | j         dS | j         d| _         fd}t          j        |d          }|                                 t	          d                                          |z   }|                                rtt	          d                                          |k     rO|                    d           |                                r%t	          d                                          |k     O|                                rt          	                    d|           dS dS )	zAClose the audio stream with a timeout to prevent CoreAudio hangs.Nc                  |    	                                                                      d S # t          $ r Y d S w xY wr|   )r   r   r-   )r  s   r   	_do_closez;AudioRecorder._close_stream_with_timeout.<locals>._do_closeH  sH       s   (- 
;;Tr  r   皙?rj   u:   Audio stream close timed out after %.1fs — forcing ahead)
rv  r*  r,  r-  
__import__r   is_aliver   r.   warning)r=  rj   r  r   r   r  s        @r   _close_stream_with_timeoutz(AudioRecorder._close_stream_with_timeout@  s$   <F	 	 	 	 	 Id;;;				f%%//11G;jjll 	 z&11;;==HHFF3F jjll 	 z&11;;==HH::<< 	bNNWY`aaaaa	b 	br   c                 $   | j         5  | j        s	 ddd           dS d| _        d| _        | j        s	 ddd           dS t	                      \  }}|                    | j        d          }g | _        t          j                    | j        z
  }t          
                    d|t          |                     t          | j        dz            }t          |          |k     r6t                              dt          |                     	 ddd           dS | j        t           k     r4t          
                    d| j        t                      	 ddd           dS |                     || j        	          cddd           S # 1 swxY w Y   dS )
u   Stop recording and write captured audio to a WAV file.

        The underlying stream is kept alive for reuse — only frame
        collection is stopped.

        Returns:
            Path to the WAV file, or ``None`` if no audio was captured.
        NFr   axisz+Voice recording stopped (%.1fs, %d samples)r   z,Recording too short (%d samples), discardingz2Recording too quiet (peak RMS=%d < %d), discardingr   )r7  r8  r;  rw  r   r   r   r   r9  r.   rH   r   rD   rx  r/   r  r  
_write_wav)r=  r   r   
audio_datar  min_sampless         r   r   zAudioRecorder.stopX  sl    Z  	N  	N?  	N  	N  	N  	N  	N  	N  	N  	N $DO !D <  	N  	N  	N  	N  	N  	N  	N  	N "OOEAr1==JDLn&&)99GKKEwPST^P_P_``` d/#566K:,,KSQ[__]]]/ 	N  	N  	N  	N  	N  	N  	N  	N6 ~ 555P N,AC C C= 	N  	N  	N  	N  	N  	N  	N  	N@ ??:4;L?MMA 	N  	N  	N  	N  	N  	N  	N  	N  	N  	N  	N  	N  	N  	N  	N  	N  	N  	Ns)   	FFCF7FFF	F	c                     | j         5  d| _        g | _        d| _        d| _        ddd           n# 1 swxY w Y   t
                              d           dS )zoStop recording and discard all captured audio.

        The underlying stream is kept alive for reuse.
        FNr   zVoice recording cancelled)r7  r8  rw  r  r;  r.   rH   r<  s    r   ri  zAudioRecorder.cancel  s    
 Z 	" 	"#DODL$(D! !D		" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	"
 	/00000s   155c                     | j         5  d| _        g | _        d| _        ddd           n# 1 swxY w Y   |                                  t
                              d           dS )z<Release the audio stream.  Call when voice mode is disabled.FNzAudioRecorder shut down)r7  r8  rw  r  r  r.   rH   r<  s    r   rk  zAudioRecorder.shutdown  s    Z 	) 	)#DODL$(D!	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	)
 	'')))-.....s   *..r  r   c                f   t          j        t          d           t          j        d          }t           j                            t          d| d          }t          j        |d          5 }|	                    t                     |                    t                     |                    |           |                    |                                            ddd           n# 1 swxY w Y   t           j                            |          }t"                              d||           |S )	zTWrite numpy int16 audio data to a WAV file.

        Returns the file path.
        TrK  rM  rN  r   r   NzWAV written: %s (%d bytes))r0   rU  rV  r   rW  r   r   r%   r&   r'   rY  r(   SAMPLE_WIDTHr)   r*   r+   rf  r.   rH   )r  r   r^  wav_pathr6   	file_sizes         r   r  zAudioRecorder._write_wav  s2    	I----M/22	7<<	+G	+G+G+GHHYx&& 	1"OOH%%%OOL)))OOK(((NN:--//000		1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 GOOH--	0(IFFFs   .A1C++C/2C/rl  r|   )r   )rm  rn  ro  rp  rq  r>  rE   r   r  rr  rE  rD   rI  rA  r  r-  r  r   rX  r   ri  rk  staticmethodrG   r  r   r   r   rt  rt  +  s         !%# # # #8	8U 	8t 	8 	8 	8 	8 3 3 3 3 X3
 !S ! ! ! X! d    XH H H HT<c <c <c <c <c|b b% b$ b b b b0)Nhsm )N )N )N )NV
1 
1 
1 
1/ / / / 5@   s S    \  r   rt  c                  V    t                      rt                      S t                      S )z=Return the best recorder backend for the current environment.)r}   r4  rt  r   r   r   create_audio_recorderr    s&    &(( %"$$$??r   >   %   продолжение следует(   продолжение следует...*   ご視聴ありがとうございました,   sottotitoli creati dalla comunità amara.org5   sous-titres réalisés par la communauté d'amara.orgbye.the endthe end.	amara.org	thank you
thank you.sous-titreswww.mooji.orgplease subscribeplease subscribe.like and subscribelike and subscribe.thanks for watchingthanks for watching.thank you for watchingsubscribe to my channelthank you for watching.subscribe to my channel.untertitel von stephanie geigesbyeyouz9^(?:thank you|thanks|bye|you|ok|okay|the end|\.|\s|,|!)+$)flags
transcriptc                     |                                                                  }|sdS |                    d          t          v s	|t          v rdS t                              |          rdS dS )zBCheck if a transcript is a known Whisper hallucination on silence.Tz.!F)r   rw   rstripWHISPER_HALLUCINATIONS_HALLUCINATION_REPEAT_REmatch)r  cleaneds     r   is_whisper_hallucinationr    st      &&((G t~~d555DZ9Z9Zt%%g.. t5r   r1  c                  z   	 ddl m}   |                                 di           }t          |t                    rj|                    dt
                    }t          |t                    r|g}t          |t          t          f          rt          d |D                       }|S n# t          $ r Y nw xY wt
          S )zReturn the configured ``voice.stop_phrases`` list (default: ("stop",)).

    Malformed config (scalar, dict, list of non-strings) falls back to the
    default rather than crashing the voice loop.
    r   r   r   stop_phrasesc              3     K   | ]z}t          |t          t          t          f          $t          |                                          Et          |                                                                          V  {d S r|   )r@   rX  rD   rE   r   rw   )r   r  s     r   r   z+_load_voice_stop_phrases.<locals>.<genexpr>  s}          /0!!c3%677 <?FFLLNN FFLLNN((**           r   )
r   r   rB   r@   rA   DEFAULT_VOICE_STOP_PHRASESrX  r\   tupler-   )r   r   r   phrasess       r   _load_voice_stop_phrasesr    s    111111KMM%%gr22	i&& 		--0JKKC#s## e#e}--     47           %%s   B"B& &
B32B3r  c                     | sdS |                                                                                       d          }|sdS |t                      }||v S )u  Return True when *transcript* is EXACTLY a configured stop phrase.

    Ends the voice conversation when the user says "stop" (or another
    configured phrase) and nothing else. Deliberately strict: the whole
    utterance — after lowercasing and stripping surrounding punctuation —
    must equal a phrase, so "stop doing that and try again" still reaches
    the agent. Configure via ``voice.stop_phrases`` in config.yaml
    (set ``[]`` to disable).
    Fz.,!?;: 	
"')r   rw   r  )r  r  r  s      r   is_voice_stop_phraser    sf      u  &&((../?@@G u/11l""r   c                  >    t                      } | sdS d| d          dS )a  One-line 'Say "stop" to end the voice chat.' hint for voice-mode start.

    Sources the phrase from ``voice.stop_phrases`` (first entry) so a custom
    phrase renders correctly; returns "" when stop phrases are disabled
    (``stop_phrases: []``) so surfaces show no hint at all. Every surface
    that announces voice-mode start (CLI /voice on, TUI, desktop) uses this
    one owner instead of hardcoding the wording.
    rm   zSay "r   z" to end the voice chat.)r  )r  s    r   voice_stop_hintr  !  s1     '((G r771:7777r   r  modelc                    ddl m}m}  || |          }|                    d          s*d|                    dd          v rt	          | ||          }|                    d          r[|                    d	d          }t          |          r6t          |          s't                              d
|d	                    ddddS |                    d          rddddS |S )a  Transcribe a WAV recording using the existing Whisper pipeline.

    Delegates to ``tools.transcription_tools.transcribe_audio()``.
    Filters out known Whisper hallucinations on silent audio.

    Args:
        wav_path: Path to the WAV file.
        model: Whisper model name (default: from config or ``whisper-1``).

    Returns:
        Dict with ``success``, ``transcript``, and optionally ``error``.
    r   )MAX_FILE_SIZEtranscribe_audior  successzFile too larger  rm   )r  max_file_sizer  z"Filtered Whisper hallucination: %rT)r  r  filtered	no_speech)r  r  r  )	tools.transcription_toolsr  r  rB   _transcribe_wav_in_chunksr  r  r.   rH   )r  r  r  r  ry   raw_transcripts         r   transcribe_recordingr  3  s     JIIIIIIIhe444F
 ::i   _%5GR9P9P%P%P*85P]^^^ zz) IL"55#N33 	I<P=
 =
 	I KK<f\>RSSS#24HHH
 zz+ FrEEEMr   	file_pathr  c                     |                                                      d          sdS 	 t          j                            |           |k    S # t
          $ r Y dS w xY w)z@Return whether a CLI WAV recording needs to be split before STT.r   F)rw   endswithr0   r   rf  r2   )r  r  s     r   _should_chunk_for_transcriptionr  `  sg    ??%%f-- uwy))M99   uus   "A 
AAc          	      
   ddl m} g }g }	 t          | |          }|sOdddd|D ]F}	 t          j                            |          rt          j        |           7# t          $ r Y Cw xY wS t          	                    dt          |          |            t          |d	
          D ]\  }} |||          }|                    d          s}|                    dd          }	ddd| dt          |           d|	 dc |D ]F}	 t          j                            |          rt          j        |           7# t          $ r Y Cw xY wS |                    dd                                          }
|
r$t          |
          s|                    |
           dd                    |                                          |                    d          t          |          d|D ]F}	 t          j                            |          rt          j        |           7# t          $ r Y Cw xY wS # t"          $ rz}t                              d| |d           ddd| dcY d}~|D ]F}	 t          j                            |          rt          j        |           7# t          $ r Y Cw xY wS d}~ww xY w# |D ]F}	 t          j                            |          rt          j        |           7# t          $ r Y Cw xY ww xY w)zGSplit an oversized WAV into provider-sized chunks and join transcripts.r   )r  )r  Frm   zNo audio chunks were created)r  r  r  z+Transcribing oversized WAV in %d chunks: %sr    )r-  r  r  r  zUnknown transcription errorzChunk /z	 failed: r  T provider)r  r  r	  chunksz'Chunked transcription failed for %s: %sr  zChunked transcription failed: N)r  r  _split_wav_for_transcriptionr0   r   re  r1   r2   r.   rH   r   	enumeraterB   r   r  r   r   r-   r  )r  r  r  r  chunk_pathstranscripts
chunk_pathindexry   r  r  r7   s               r   r  r  j  s    ;:::::KK#28=YYY 	a$BA_``6 & 	 	J7>>*-- *Ij)))   		3 	A3{CSCSU]^^^!*;a!@!@!@ 	/ 	/E:%%j>>>F::i(( 

7,IJJ$"$PePPc+.>.>PPPP   ( & 	 	J7>>*-- *Ij)))   		  L"55;;==J /"::"F"F /"":... ((;//5577

:..+&&	
 
 & 	 	J7>>*-- *Ij)))   			  c c c>!VZ[[[ =a^_=a=abbbbbbb% 	 	J7>>*-- *Ij)))   			c & 	 	J7>>*-- *Ij)))   		s   H0 3A
A*)A*.BH0 3D;;
EEBH0 +3H
H,+H,0
J4:&J/ J4!J7 *3J
J+*J+/J44J7 7L=3K10L1
K>	;L=K>	>Lc                   t          j        t          d           g }d}t          j        | d          5 }|                                }t          d|j        |j        z            }||z
  }||k     rt          d          t          d||z            }d}		 |
                    |          }
|
sn|	dz  }	t          j        t           j                            t           j                            |                     d          d|	d	d
dt          d          }|j        }|                                 	 t          j        |d          5 }|                    |j                   |                    |j                   |                    |j                   |                    |j        |j                   |                    |
           ddd           n# 1 swxY w Y   |                    |           n5# t6          $ r( 	 t          j        |           n# t:          $ r Y nw xY w w xY w	 ddd           n# 1 swxY w Y   |S )zDWrite WAV chunks small enough to pass the shared STT file-size gate.TrK  i   rbr    z/STT max_file_size is too small for WAV chunkingr   _chunk03dr   r   F)rR   r   dirr   r   N)r0   rU  rV  r%   r&   	getparamsr  	nchannels	sampwidthr   
readframesr"   r#   r   splitextbasenamer$   r   r'   r(   r)   	frameratesetcomptypecomptypecompnamer*   r   r-   r1   r2   )r  r  r  header_reservesourceparamsblock_alignmax_data_bytesframes_per_chunkr  r  tempr  chunks                 r   r  r    s   K	D))))KN	8T	"	" %f!!##!V-0@@AA&7K''NOOOq.K"?@@	&&'788F QJE.'**27+;+;H+E+EFFqI]]QV]]]]	  D JJJLLLYz400 .E&&v'7888&&v'7888&&v'7888%%fovGGG%%f---. . . . . . . . . . . . . . . "":....   Ij))))   D/	 % % % % % % % % % % % % % % %N ss   C?H?5G;
BGG;G	G;!G	"G;:H?;
H-HH-
H(%H-'H((H--H??II_active_playbackc                  p   t           5  t          } daddd           n# 1 swxY w Y   | rT|                                 @	 |                                  t                              d           n# t          $ r Y nw xY w	 t                      \  }}|                                 dS # t          $ r Y dS w xY w)z/Interrupt the currently playing audio (if any).NzAudio playback interrupted)	_playback_lockr(  poll	terminater.   rH   r-   r   r   )procr   r   s      r   stop_playbackr.    s    
                                   		#	NNKK45555 	 	 	D	A
					   s0   
"" .A/ /
A<;A< %B' '
B54B5c                      	 t          dddd          5 } d|                                                                 v cddd           S # 1 swxY w Y   dS # t          $ r Y dS w xY w)	z5True when running inside Windows Subsystem for Linux.r   r   rb   rc   rh   ri   r   NF)r&   r   rw   r-   )r   s    r   _is_wslr1    s    /3KKK 	3q!&&((.."2"22	3 	3 	3 	3 	3 	3 	3 	3 	3 	3 	3 	3 	3 	3 	3 	3 	3 	3   uus3   A (A
A 
AA AA 
A%$A%c                      	 t          ddd          5 } d|                                                                 v cddd           S # 1 swxY w Y   dS # t          $ r Y dS w xY w)aU  Return True when running inside WSL2 (Windows Subsystem for Linux 2).

    Reads /proc/version and checks for the Microsoft kernel signature.
    Returns False on any error (non-WSL Linux, Docker, SSH, etc.).
    Extracted as a module-level function so tests can patch it directly
    without fighting builtins.open patching complexity.
    r   rb   rc   r0  r   NF)r&   r   rw   r2   )_fvs    r   _is_wsl2_envr4    s    /GIFFF 	5##((**"2"2"4"44	5 	5 	5 	5 	5 	5 	5 	5 	5 	5 	5 	5 	5 	5 	5 	5 	5 	5   uus3   A (A	A 	AA AA 
A$#A$c                      t          t                      o't          j        d          ot          j        d                    S )ac  Return True when the WSL2 PowerShell TTS playback fallback can be used.

    This only covers OUTPUT (TTS playback via Media.SoundPlayer on the
    Windows host) -- it does NOT make microphone recording work. A caller
    using this to relax the audio-environment gate must still surface the
    existing PulseAudio-bridge guidance for recording/STT.
    powershell.exeffmpeg)r   r4  rX   rY   r   r   r   r   r     sB      	#L)**	#L""  r   c                     t          d           	 t          |           t          d           S # t          d           w xY w)ay  Play an audio file through the default output device.

    Strategy:
    1. WAV files via ``sounddevice.play()`` when available.
    2. System commands: ``afplay`` (macOS), ``ffplay`` (cross-platform),
       ``aplay`` (Linux ALSA).

    Playback can be interrupted by calling ``stop_playback()``.

    Returns:
        ``True`` if playback succeeded, ``False`` otherwise.
    TF)r  _play_audio_file_impl)r  s    r   r,   r,     sH     T"""($Y// '''' ''''s	   / A c                 .   t           j                            |           st                              d|            dS |                     d          rt                      r	 t                      \  }}t          j	        | d          5 }|
                    |                                          }|                    ||j                  }|                                }d d d            n# 1 swxY w Y   t                      rt!          d|z            }t!          d|z            }|                    dd||j                  }	|                    |j                  }
|
d |xx         |	z  cc<   |                    t!          d	|z            |j                  }|                    |                    ||j                  |
                    |j                  |g          }d
}nd}|                    |||           t/          |          |z  }t1          j                    |z   dz   }|                                r|                                j        rot1          j                    |k     rXt1          j        d           |                                r0|                                j        rt1          j                    |k     X|                                 dS # t<          t>          f$ r Y n1t@          $ r%}t          !                    d|           Y d }~nd }~ww xY wtE          j#                    }g }|dk    r|$                    d| g           |dk    rtK          j&        d          rtK          j&        d          rtO                      r	 dd l(}tS          j*        g dtR          j+        d          ,                    d          -                                }tS          j*        dd|gtR          j+        d          ,                    d          -                                }|r.|.                                j/        d d         }t           j        0                    |d| d          }tS          j*        dd|gtR          j+        d          ,                    d          -                                }|r|1                    d d!          }d"| d#}d$0                    te          j0        dd%| d&d'|d(d)d*g	          te          j0        dd+d,|g          g          }te          j0        d-d&|g          }d.| d/| d0}|3                    dd1d2|g           n# t@          $ r Y nw xY w|$                    d3d4d5d(d)| g           |dk    r|$                    d6d7| g           |D ]}tK          j&        |d                   }|r	 dd8l4m5} tS          j6        |tR          j+        tR          j+        tR          j+         |d9          :          } tn          5  | a8d d d            n# 1 swxY w Y   | 9                    d;<           | j:        }!tn          5  d a8d d d            n# 1 swxY w Y   |!dk    r dS t          !                    d=|d         |!           # tR          j;        $ rn t                              d>|d                    | <                                 | 9                                 tn          5  d a8d d d            n# 1 swxY w Y   Y ot@          $ rN}t          !                    d?|d         |           tn          5  d a8d d d            n# 1 swxY w Y   Y d }~d }~ww xY wt                              d@|            dS )ANzAudio file not found: %sFr   r  r   r  r   r   g?i   r   )r   	blocksizer   r   Tzsounddevice playback failed: %sr   afplayLinuxr6  r7  )zcmd.exez/czecho %TEMP%   )r[  rj   rc   )ri   wslpathz-u   zhermes-tts-z-w'z''z(New-Object Media.SoundPlayer 'z').PlaySync()z && z-irO  wavz	-loglevelquietz-yz
-NoProfilez-Commandrmz( z ); rc=$?; z
; exit $rcz/bin/shrR  ffplayz-nodispz	-autoexitaplayrb  )hermes_subprocess_env)inherit_credentials)rv   r[  rl   r  ,  r  z1System player %s exited with code %d, trying nextz+System player %s timed out, killing processzSystem player %s failed: %sz No audio player available for %s)=r0   r   re  r.   r  r  r   r   r%   r&   r  
getnframes
frombufferr   getframerater1  rD   r   r  r   r   r   r   r   r   r   r   r   r   r   r:   r2   r-   r/   r   r   r   rX   rY   r4  uuidro   check_outputrq   decoder   uuid4hexr   rc   shlexinserttools.environments.localrG  Popenr*  r(  r  ru   rt   kill)"r  r   r   r6   r  r  r   silence_samplesfade_samplesfadeaudio_floattailr;  duration_secsr   r7   r   playersrM  _win_tmp_raw_win_tmp_wsl_unique_wsl_wav_win_wav_win_wav_safe
_ps_script_ps_cmd_cleanup	_full_cmdr_   exerG  r-  rcs"                                     r   r9  r9  &  s	    7>>)$$ 19===u &!! (?&A&C&C (?'	?"__FB9d++ 0rr}}77]]6]BB
 oo//0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 yy "%cK&7"8"8"3#455{{3\{LL(//
;;M\M***d2***xxD;$6 7 7rxxHH^^HH_BHH==&&rx00-  

 !			GGJ;)GLLL  
OOk9M~''-7#=H--// !bmmoo&< !AQAQT\A\A\
4    --// !bmmoo&< !AQAQT\A\A\GGIII4W% 	 	 	D 	? 	? 	?LL:A>>>>>>>>	? _FG),--- V\*:;;X@V@V>> (	')6444%-q      &	&**5577   *6l3%-q      &	&**5577    H"jjll.rr2G!w||L:U:U:U:UVVH)6"D(3)11      fIf..uuww    H(0(8(8d(C(CZmZZZ # #)++!J$	4(0+w(N O O!J(8,
(2(4 5 5/ # # $):tT8.D#E#E %R$Q$QX$Q$Q$Q	q9dI*FGGG    NNHik7IVWWWy1222 $, $,l3q6"" "	,!, KJJJJJ!'%-%-$,--%HHH   $ , ,'+$, , , , , , , , , , , , , , ,		#	&&&_# , ,'+$, , , , , , , , , , , , , , ,7744 PRUVWRXZ\]]]], , , ,LcRSfUUU				# , ,'+$, , , , , , , , , , , , , , , , , ,:CFAFFF# , ,'+$, , , , , , , , , , , , , , ,,?"	,H NN5yAAA5s  #&K$ 	AC-!K$ -C11K$ 4C15G-K$ $L%7	L% L  L%GU$ $
U10U1AZ"X-!Z"-X1	1Z"4X1	5'Z"Y+Z"+Y/	/Z"2Y/	3	Z"?"Z""A]5\]5\]5\]5	]5')]0]]0]##]0&]#']00]5rI    F    0u  should_stop	thresholdsustained_mscalibration_mscapture
on_triggerpre_roll_msendpoint_silence_msmax_utterance_msc	                 	   	 t                      \  }	}
n# t          t          f$ r	 |rdndcY S w xY wddlm} t          t          dz            }t          d|dz            }t          d|dz            }t          d|dz            }t          d|dz            } |t          |d          	          } |t          d|dz            	          }d}d
}d}	 |	                    t          dd|          5 } |             sw|	                    |          \  }}t          |
                    |
                    |                    |
j                  dz                                }|r'|                    |                                           |dz  }t#          |          |k     r|                    |           |d
k    r\t#          |          |k    rIt          |
                    t'          |          d                    }t          |t(          dz            }n0t          |
                    t'          |          d                    }t          ||          }t          t          |p	t(          dz            |dz            }t+          |d          }||k     r|                    |           ||k    r|dz   nd}|dk    r/t,                              d|||||||t#          |          	  	         ||k     rt,                              d||||||           |r>	  |             n2# t2          $ r%}t,                              d|           Y d}~nd}~ww xY w|s	 ddd           dS t'          |          }d}t5          |          D ]}|	                    |          \  }}|                    |                                           t          |
                    |
                    |                    |
j                  dz                                }|t(          k     r|dz   nd}||k    r nt6                              |
                    |d                    cddd           S 	 ddd           n# 1 swxY w Y   n2# t2          $ r%}t,                              d|           Y d}~nd}~ww xY w|rdndS )u  Block until sustained speech is heard on the mic, or *should_stop*.

    Barge-in monitor: run in a side thread while TTS is playing. Without
    *capture* it returns ``True`` when the user started talking (cut playback).
    With ``capture=True`` it ALSO records the interruption — a rolling
    *pre_roll_ms* buffer means the utterance is kept from its first syllable,
    not from the moment detection tripped — and keeps rolling until the user
    goes quiet for *endpoint_silence_ms*, then returns the WAV path (or
    ``None`` if speech never tripped). *on_trigger* fires at the moment of
    detection so the caller can stop playback while capture continues.

    The noise floor is calibrated from the first *calibration_ms* of input —
    playback is already audible then, so speaker bleed is baked into the
    floor and only louder-than-playback speech trips the trigger. Requiring
    *sustained_ms* of consecutive above-threshold blocks filters out coughs,
    keyboard thumps, and playback transients.
    NFr   dequeQ?r       d   maxlenr   r   r   r  r   r;  r!   Z   g       @     @@zfVAD above-trigger: block=%d rms=%.0f floor=%.0f trigger=%.0f consec=%d/%d min_floor=%.0f window_len=%duh   VAD TRIPPED: block=%d rms=%.0f floor=%.0f trigger=%.0f consec=%d min_floor=%.0f — cutting TTS playbackz$Barge-in trigger callback failed: %sTr  zBarge-in listener failed: %s)r   r:   r2   collectionsry  rD   rG   r  r  r   rE   r  r  r   r  r   r  r   
percentiler\   r  r   r.   r/   rH   r-   r   rt  r  r   ) rn  ro  rp  rq  rr  rs  rt  ru  rv  r   r   ry  blockcalib_blockstrip_blocksendpoint_blocks
max_blocksfloor_windowpre_rollconsecutive	min_floor	block_idxr  datar   r  _pct90_floortriggerr7   r  rC  s                                    r   listen_for_speechr    s   8*BB! * * *)ttE)))* "!!!!!d"##Eq.B.//La+,,K!0B677OQ(B.//J $)5L#0F0F#G#G#GLe3q+*;#<#<===HKIIe8^^{QgY^^__ b	Pci!kmm aP ++e,,aBGGBGGDKK
,C,Cq,H$I$IJJKK 1OODIIKK000Q	
 |$$|33 '',,,& ##L(9(9\(I(I"2==l1C1CR#H#HIIF #F,AA,E F FII"2==l1C1CR#H#HIIF
 VY// eI$J1F1JKKVVY\ZZ
 gv..
 == '',,,14kAooQ??LLD!3#YL0A0A	   ,, HsFG[)  
  PP"
$ P P P%KQOOOOOOOOP  kb	P b	P b	P b	P b	P b	P b	P b	Pr %)NNz**  A$kk%00GD!MM$))++...BJ0G0G10L(M(M N NOOC),/D)D)DEAII!E// 0$//vA0N0NOOEb	P b	P b	P b	P b	P b	P b	P b	PaPb	P b	P b	P b	P b	P b	P b	P b	P b	P b	P b	P b	P b	P b	P b	PF  8 8 83Q777777778%44%s    ..R 6H.R%
L0/R0
M:MRMR%R 2C:R,R :R R

R R
R 
SR<<Sg     p@r  c                  n    t           j                            dd                                          dk    S )NHERMES_VOICE_DEBUGrm   1)r0   r   rB   r   r   r   r   _voice_debug_enabledr    s*    :>>.3399;;sBBr   msgc                     t                               |            t                      r3	 t          d|  t          j        d           dS # t          $ r Y dS w xY wdS )u   VAD decision-point diagnostic — always logger.debug, plus stderr when
    HERMES_VOICE_DEBUG=1 so live hardware tuning doesn't need a log tail.z[voice-vad] T)fileflushN)r.   r/   r  printrQ   r[  r-   )r  s    r   _vad_logr    s{     LL 	&&&SZtDDDDDD 	 	 	DD	 s   A 
AA    
is_playing
multipliergrace_msc
                    	 t                      \  }
}n# t          t          f$ r Y dS w xY wddlm} t          t          dz            }t          d|dz            }t          d|dz            }t          dt          t          |dz                                }t          d|dz            }t          d|dz            }t          d|	dz            }|rt          |          nt          } |d	          } |t          d|dz            	          } ||	          }t          t                    }d
}d
}d
}d}d}d}	 |
                    t          dd|          5 } |             s|                    |          \  } }!t          |                    |                    |                     |j                  dz                                }"|                    |                                            |dz  }|t)           |                      nd
}#|s|#s|                    |"           t+          |          |k    s|#r|r0t          |                    t/          |          d                    nt          t                    }$t          |$t          t                              }d}t1          d|dd|$ddt+          |           d|dd	           |s|#r'|s%|r|dk    r|}t1          d| d|dz   d           d}|#}|#rdn|dz   }||z  }%|#rt          |%t2                    }%n%t          |%t          t                    dz            }%t5          |%t6                    }%|#sm|"|%k     rg|                    |"           |rPt          t          |                    t/          |          d                    t          t                              }|"|%k    }&|&r&|dk    r t1          d| d|"dd|%dd| d 	           d
}&|dk    r|dz  }|                    |&           |"|%d!z  k    r;t1          d"| d|"dd#|dd|%dd$|& d%t9          |           d&| d'|#rd(nd)            |&rt9          |          |k    s%|#rd(nd)}'t1          d*|' d+| d|"dd#|dd|%dd%t9          |           d&t+          |                      |r?	  ||'           n2# t:          $ r%}(t<                              d,|(           Y d}(~(nd}(~(ww xY wt/          |          })d}*tA          |          D ]}!|                    |          \  } }!|)                    |                                            t          |                    |                    |                     |j                  dz                                }"|"t          k     r|*dz   nd}*|*|k    r ntB          "                    |#                    |)d-                    cddd           S 	 ddd           n# 1 swxY w Y   n2# t:          $ r%}(t<                              d.|(           Y d}(~(nd}(~(ww xY wdS )/uJ  Listen across an ENTIRE agent turn; return the captured interruption.

    Runs from utterance-submit to turn-complete. Two phases, decided per
    30ms block by *is_playing* (usually ``is_audio_output_active``):

    * ``generation`` — no TTS audio flowing. The room is quiet; the first
      *calibration_ms* establish the noise floor (pre-playback calibration).
      Trigger = quiet_floor x *multiplier* (clamped to a sane minimum), i.e.
      ordinary speech detection.
    * ``playback`` — TTS audio flowing. The quiet baseline is HELD (never
      recalibrated against speaker bleed); the trigger is additionally
      clamped up to ``PLAYBACK_MIN_TRIGGER`` so bleed alone can't trip it,
      and a *grace_ms* window after playback first starts suppresses trips
      from the playback onset transient.

    Detection is a windowed majority — >=80% of the last *sustained_ms*
    worth of blocks above trigger (with the current block above) — so
    intra-word energy dips don't reset progress the way the old strictly-
    consecutive counter did.

    On detection ``on_trigger(phase)`` fires (cut TTS / interrupt the turn),
    then capture continues from the rolling *pre_roll_ms* buffer until
    *endpoint_silence_ms* of quiet, and the WAV path is returned. Returns
    ``None`` when *should_stop* ends the turn without speech.
    Nr   rx  rz  r    r{  r  r|  r}  Fi'  r   r  r!   r  Tzcalibrated quiet floor=z.0fz (pct90=z, z blocks, mult=gr   !   zplayback started (block=u   ) — grace mszgrace suppression: block=z rms=z	 trigger=z (z blocks left)r   zblock=z floor=z above=z window=r  z phase=playback
generationz	TRIPPED (z	): block=z'full-duplex trigger callback failed: %sr  zFull-duplex listener failed: %s)$r   r:   r2   r  ry  rD   rG   r  rF   rE   DEFAULT_BARGE_MULTIPLIERr  r  r   r  r  r   r  r   r  r   r   r  r\   r  PLAYBACK_MIN_TRIGGERr   TRIGGER_CEILINGsumr-   r.   r/   r   rt  r  r   )+rn  r  rs  r  rp  rq  r  rt  ru  rv  r   r   ry  r  r  r  trip_neededgrace_blocksr  r  multambientr  recent_abovequiet_floorfloor_lockedplaying_prevplayback_seengrace_remainingblocks_since_playbackr  r  r  r   r  playingpct90r  abover  r7   r  rC  s+                                              r   full_duplex_listenr    s   JBB!   tt "!!!!!d"##Eq.B.//La+,,KaU;#4556677Kq(b.))L!0B677OQ(B.//J *H50HD#e3///Ge3q+*;#<#<===H"'%{";";";L-..KLLMO"Is;^^"Qg  
 
 p	P!kmm mP ++e,,aBGGBGGDKK
,C,Cq,H$I$IJJKK		,,,Q	0:0F$zz||,,,E
 $ !" ,s+++7|||33w3  '>E"--Wr"B"BCCC!&'<!=!= 
 '*%7L1M1M&N&N'+ .kH . .&+J. .36w<<. .$(-. . .  
 ( !   )< )( ,AB,F,F*6 ;y ; ;%1B%6; ; ;   %)M&-4(S:ORS:S% &, M!'+?@@GG!'51F+G+G!+KLLGg77
  3==NN3''' &)!"--Wr"B"BCC!"788' '
 w "_q00QI Q QCN Q Q#*PQ Q2AQ Q Q   "E"Q&&#q(O##E***'C-''K K KS K KS K K#*?K K7<K K"%l"3"3K K6AK K 07!HLK K    #l"3"3{"B"B&-?

<F F F	 F FJ F F(EF F7>EF F!,//F F25l2C2CF F  
  SS"
5))))$ S S S%NPQRRRRRRRRS
 %)NNz**  A$kk%00GD!MM$))++...BJ0G0G10L(M(M N NOOC),/D)D)DEAII!E// 0$//vA0N0NOOap	P p	P p	P p	P p	P p	P p	P p	PmPp	P p	P p	P p	P p	P p	P p	P p	P p	P p	P p	P p	P p	P p	P p	Pb  ; ; ;6::::::::;4sz    ))X/ !M/X#SX#
T'TX#TC=X#	X/ X/ #X''X/ *X'+X/ /
Y9YYr	  c                    | sdS |                                                                  }|dk    rdS 	 ddlm} ddlm}  |              ||          }| |d            ||          }n4# t          $ r'}t                              d	||           Y d}~dS d}~ww xY w|dS 	 t          |
                                          S # t          $ r)}t                              d
||d           Y d}~dS d}~ww xY w)z@Return True when *provider* resolves to an available STT plugin.Fnoner   )get_provider)_ensure_plugins_discoveredNT)forcez2STT plugin requirements check skipped for '%s': %szfSTT plugin provider '%s' is_available() raised during requirements check: %s - treating as unavailabler  )rw   r   agent.transcription_registryr  hermes_cli.pluginsr  r-   r.   r/   r   is_availabler  )r	  keyr  r  plugin_providerexcs         r   _check_plugin_stt_providerr  X  sy    u
..


 
 
"
"C
f}}u======AAAAAA""$$$&,s++" '&T2222*l3//O   @#s	
 	
 	
 uuuuu	 u
O0022333   2 	 	
 	
 	
 uuuuus/   :A/ /
B 9BB ( C	 	
C<C77C<c                  H   ddl m} m}m}m}  |            } ||          } | |          }|dv }d}d}	|r|s |||          }|t          |          }	|o|p|dup|	}
g }t                      }t                      p|}|s|                    ddg           t                      }|o	|
o|d         }g }|r|
                    d	           n=|r|
                    d
           n%|
                    dt                       d           |s|
                    d           n|dk    r|
                    d           n|dk    r|
                    d           n|dk    r|
                    d           n|dk    r|
                    d           n|dk    r|
                    d           n|dk    r|
                    d           ni|dk    r|
                    d           nM||
                    d| d           n1|	r|
                    d| d           n|
                    d           |d         D ]}|
                    d |            |                    d!g           D ]}|
                    d |            |||
|d"                    |          |d#S )$zCheck if all voice mode requirements are met.

    Returns:
        Dict with ``available``, ``audio_available``, ``stt_available``,
        ``missing_packages``, and ``details``.
    r   )_get_provider_load_stt_config$_resolve_command_stt_provider_configis_stt_enabled>   xaigroqlocalopenaimistral
elevenlabslocal_commandNFr
   r   r   z)Audio capture: OK (Termux:API microphone)zAudio capture: OKzAudio capture: MISSING (r   z5STT provider: DISABLED in config (stt.enabled: false)r  z'STT provider: OK (local faster-whisper)r  z STT provider: OK (local command)r  zSTT provider: OK (Groq)r  zSTT provider: OK (OpenAI)r  z"STT provider: OK (Mistral Voxtral)r  zSTT provider: OK (xAI Grok STT)r  z$STT provider: OK (ElevenLabs Scribe)zSTT provider: OK (command: zSTT provider: OK (plugin: u   STT provider: MISSING (uv pip install faster-whisper — `pip install faster-whisper` also works if pip is on PATH, or set GROQ_API_KEY / VOICE_TOOLS_OPENAI_KEY)r   zEnvironment: r   
)r   audio_availablestt_availablemissing_packagesr`  environment)r  r  r  r  r  r  r}   r;   extendr   r   rU   rB   r   )r  r  r  r  
stt_configstt_enabledstt_providernative_stt_availablecommand_stt_configplugin_stt_availabler  missingr   	has_audio	env_checkr   details_partsr  notices                      r   check_voice_requirementsr    s               "!##J .,,K =,,L' ,    L/ LAA*
 
 %#=l#K#K   	 T)	   G466N ""4nI 1w/000 )**IFmF	+0FIM ZHIIII	 Z01111X8S8U8UXXXYYY 
TUUUU		 	 FGGGG		(	(?@@@@			67777		!	!89999		"	"ABBBB			>????		%	%CDDDD		'J<JJJKKKK	 
I,IIIJJJJ<	
 	
 	
 Z( 8 86W667777--	2.. 7 75V556666 $&#99]++   r     max_age_secondsc                 "   t           j                            t                    sdS d}t	          j                    }t          j        t                    D ]}|                                r|j                            d          rl|j        	                    d          rR	 ||
                                j        z
  }|| k    rt          j        |j                   |dz  }# t          $ r Y w xY w|rt                              d|           |S )zRemove old temporary voice recording files.

    Args:
        max_age_seconds: Delete files older than this (default: 1 hour).

    Returns:
        Number of files deleted.
    r   rN  r   r    z"Cleaned up %d old voice recordings)r0   r   isdirrV  r   scandiris_filer$   r   r  r   st_mtimer1   r2   r.   r/   )r  deletedr  entryages        r   cleanup_temp_recordingsr    s    7==## qG
)++CI&&  ==?? 	uz44\BB 	uzGZGZ[aGbGb 	EJJLL11((Iej)))qLG     D97CCCNs    A C!!
C.-C.)r   r   r    r|   rl  )NrI  rj  FNrk  rl  rm  )	NNNrI  r  r  rk  rl  rm  )r  )nrp  loggingr   r0   r   rerR  rX   ro   rQ   pathlibr   r"   r*  r   r%   typingr   r   r   r   r   	getLoggerrm  r.   r   r   r   r   rD   r8   r;   rJ   r   rK   rP   rX  rU   rZ   rn   rz   r}   r   rA   r   rG   rY  r  r  r  r  r   r   
gettempdirrV  r   rE   r   r   r   r  r6  r   r  r  r)  r  r+  __annotations__r  r  r$  r.  r2  r4  rt  r  r  compile
IGNORECASEr  r  r  r  r  r  r  r  r  r  r  r(  rU  r*  r.  r1  r4  r   r,   r9  r  r  r  r  r  r  r  r  r  r  r   r   r   <module>r     s}	  	 	 	   				  				       



              6 6 6 6 6 6 6 6 6 6 6 6 6 6		8	$	$    
)T 
) 
) 
) 
)     4$    S      A @ @ @ @ @+S + + + +&4HSM 4 4 4 4 74 7 7 7 7tT T T T T2 2 2 2 2jQ$ Q Q Q Ql     GLL,,..??	  %    25 T    34 34 34e 343 34t 34 34 34 34J  #Y^%% 
T 
d 
 
 
 
 . . . . .  !!,0) 0 0 0     : :% : : : :*/y / / / / /D d    0   z z z z z z z z@B B B B B B B BJ}/BB       > &2:@
-        $ ' &% & & & &0# #S # #SW # # # #(8 8 8 8 8$* *3 *x} *SRUX * * * *Zs 3 4    // C=/ 	/
 
#s(^/ / / /d-3 -# -$s) - - - -j 04 (:+, 3 3 3!!   (    d    t    (s (t ( ( ( (,\S \T \ \ \ \H  $/3#"[& [&"d(#[&}[& [& 	[&
 [& "d(+,[& [& [& [& [& [& [&r     Cd C C C C# $     0426"&#"t t"d(#t"d(+,t 3%+./t 	t
 t t t t t t c]t t t tt% % % % % %Pc$sCx. c c c cR S C      r   