
    `gjP              
       &   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
Z
ddlZddlmZmZmZmZ  ej"                  e      Zd ZdefdZddlmZ defdZdee   f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Z%dZ&dZ'ejP                  jS                   ejT                         d      Z+d1de,de-de,ddfdZ. G d d      Z/ G d d      Z0de0e/z  fdZ1h dZ2 ejf                  dejh                         Z5d!edefd"Z6d2d#ed$ee   deeef   fd%Z7d&ed'e,defd(Z8d#ed$ee   d'e,deeef   fd)Z9d#ed'e,dee   fd*Z:da;eejx                     e=d+<    e	j|                         Z?d3d,Z@d&edefd-ZAdeeef   fd.ZBd4d/e,de,fd0ZCy)5aV  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: pip install hermes-agent[voice]
    N)Any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     C/root/.hermes/venv/lib/python3.12/site-packages/tools/voice_mode.py_import_audior       s     r6M    returnc                  D    	 t                y# t        t        f$ r Y yw xY w)z/Return True if audio libraries can be imported.TF)r   ImportErrorOSError r   r   _audio_availabler   +   s&    ! s   
 )	is_termuxc                      t               ryy)NzGpkg install python-numpy portaudio && python -m pip install sounddevicezpip install sounddevice numpy)_is_termux_environmentr   r   r   _voice_capture_install_hintr   7   s    X*r   c                  B    t               sy t        j                  d      S )Nztermux-microphone-record)r   shutilwhichr   r   r   _termux_microphone_commandr   =   s    !#<<233r   c                      t               sy	 t        j                  g dddddt        j                        } d| j                  xs dv S # t
        $ r Y yw xY w)NF)pmlistpackageszcom.termux.apiT   capture_outputtexttimeoutcheckstdinzpackage:com.termux.api )r   
subprocessrunDEVNULLstdout	Exception)results    r   _termux_api_app_installedr/   D   s^    !#8$$
 (FMM,?R@@ s   <A
 
	AAc                  2    t               d uxr
 t               S N)r   r/   r   r   r   _termux_voice_capture_availabler2   U   s    %'t3S8Q8SSr   c                  T   ddl } ddl}g }t        j                  j	                  dd      }|j                  d      D ]A  }|j                         }|j                  d      s%|j                  |t        d      d        C t        j                  j	                  d      }|r/|j                  t        j                  j                  |d             t        j                  j	                  d	      }|r_|j                  t        j                  j                  |d
d             |j                  t        j                  j                  |d             |D ]  }|s	 |j                  t        j                  |      j                        s6	 | j                  | j                  | j                         }	 |j#                  d       |j%                  |       	 |j'                           y y# t        $ r Y w xY w# t        $ r Y |j'                          w xY w# |j'                          w xY w)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_SERVERr(   ;zunix:PULSE_RUNTIME_PATHnativeXDG_RUNTIME_DIRpulsez
pipewire-0g      ?TF)socketstatosenvirongetsplitstrip
startswithappendlenpathjoinS_ISSOCKst_moder   AF_UNIXSOCK_STREAM
settimeoutconnectclose)	r:   r;   
candidatespulse_serverpartpulse_runtimexdg_runtimerD   socks	            r   _pulse_socket_reachablerS   Y   s    J::>>."5L""3' 3zz|??7#d3w<=123
 JJNN#78M"'',,}h?@**..!23K"'',,{GXFG"'',,{LAB 	==!6!67 8 }}V^^V-?-?@	OOC LL JJL%&   		  	JJL	 JJLs6   .G'0"G6'	G32G36	H?HHHH'c                     g } g }t               }t               }t        |xr |      }t        t        j                  j                  d      xs+ t        j                  j                  d      xs
 t                     }t        d dD              r%|r|j                  d       n| j                  d       ddl	m
}  |       r%|r|j                  d	       n| j                  d
       	 t        ddd      5 }d|j                         j                         v rBt        j                  j                  d      r|j                  d       n| j                  d       ddd       	 t#               \  }}		 |j%                         }
|
s9|r|j                  d       n%|r|j                  d       n| j                  d       |  | |d S # 1 sw Y   jxY w# t        t        t         f$ r Y w xY w# t&        $ r< |r|j                  d       n%|r|j                  d       n| j                  d       Y qw xY w# t(        $ rJ |r|j                  d       n3|r|s| j                  d       n| j                  dt+                d       Y t         $ r[ |r|j                  d       nC|r|s| j                  d       n-t-               r| j                  d       n| j                  d       Y )w xY w)!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).
    r4   PIPEWIRE_REMOTEc              3   Z   K   | ]#  }t         j                  j                  |       % y wr1   )r<   r=   r>   ).0vs     r   	<genexpr>z+detect_audio_environment.<locals>.<genexpr>   s     
R2::>>!
Rs   )+)
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-0z/proc/versionrzutf-8)encoding	microsoftz%Running in WSL with PulseAudio bridgezRunning in WSL -- audio requires 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.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)r   r/   boolr<   r=   r>   rS   anyrB   hermes_constantsr]   openreadlowerFileNotFoundErrorPermissionErrorr   r   query_devicesr-   r   r   r   )rc   rd   termux_mic_cmdtermux_app_installedtermux_capturehas_forwarded_audior]   fr
   _devicess              r   detect_audio_environmentru      s    HG/1N46.A-ABN


~& 	%::>>+,	%"$ 
R&Q
RRNN_`OOS .~NNdeOOR/39 
	Qaffhnn..::>>.1NN#JKOO^
	5A	Z&&(G&NNm $NN#rsOO$LMX "\ M
	 
	 8 "  	Z #e  no XY	Z  `NNab$8OO} OO=>Y>[=\\]^_ NN_`$8OO} $%OO( OO(se   'G 5A#GG !H/ /AG' G
G G$#G$'AH,)H/ +H,,H/ /AK$AK$#K$i>     int16            @hermes_voice	frequencydurationcountc           
         	 t               \  }}	 d}t        t        |z        }t        t        |z        }g }t        |      D ]  }	|j                  d||d      }
|j                  d|j                  z  | z  |
z        }t        t        t        dz        |dz        }|d|xxx |j                  dd	|      z  ccc || dxxx |j                  d	d|      z  ccc |j                  |d
z  dz  j                  |j                               |	|d	z
  k  s|j                  |j                  ||j                                |j                  |      }|j                  |t               t!        j"                         dz   }|j%                         r|j%                         j&                  rnt!        j"                         |k  rWt!        j(                  d       |j%                         r2|j%                         j&                  rt!        j"                         |k  rW|j+                          y# t        t        f$ r Y yw xY w# t,        $ r }t.        j1                  d|       Y d}~y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)endpointrx   {Gz?   rv   333333?i  dtype
samplerate       @zBeep playback failed: %s)r   r   r   intSAMPLE_RATErangelinspacesinpiminrB   astyperw   zerosconcatenateplaytime	monotonic
get_streamactivesleepstopr-   loggerdebug)r|   r}   r~   r
   r   gapsamples_per_beepsamples_per_gappartsittonefade_lenaudiodeadlinees                   r   	play_beepr   #  s   B4{X56kC/0u 		HAAx)9EJA66!bee)i/!34D3{T124D4IJH(Or{{1a::O(Aq( ;;LL$*u,44RXX>?519}RXXoRXXXFG		H u%
+. >>#c)mmo"--/"8"8T^^=MPX=XJJt mmo"--/"8"8T^^=MPX=X
	5 ! 6  4/334s6   H- DI DI I -H?>H?	I+I&&I+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y)TermuxAudioRecorderzBRecorder backend that uses Termux:API microphone capture commands.Fr   Nc                 n    t        j                         | _        d| _        d| _        d | _        d| _        y )NF        r   )	threadingLock_lock
_recording_start_time_recording_path_current_rmsselfs    r   __init__zTermuxAudioRecorder.__init__T  s/    ^^%
.2r   c                     | j                   S r1   r   r   s    r   is_recordingz TermuxAudioRecorder.is_recording[  s    r   c                 ^    | j                   syt        j                         | j                  z
  S Nr   r   r   r   r   r   s    r   elapsed_secondsz#TermuxAudioRecorder.elapsed_seconds_  $    ~~$"2"222r   c                     | j                   S r1   r   r   s    r   current_rmszTermuxAudioRecorder.current_rmse  s       r   c                    ~t               }|st        d      t               st        d      | j                  5  | j                  r
	 d d d        y t        j                  t        d       t        j                  d      }t
        j                  j                  t        d| d      | _        d d d        |d| j                  d	d
dddt        t              dt        t              g}	 t!        j"                  |ddddt         j$                         | j                  5  t        j0                         | _        d| _        d| _        d d d        t6        j9                  d       y # 1 sw Y   xY w# t         j&                  $ rI}|j(                  xs |j*                  xs t        |      j-                         }t        d|       |d }~wt.        $ r}t        d|       |d }~ww xY w# 1 sw Y   xY w)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.aacz-fz-l0z-eaacz-rz-c   r"   z Termux microphone start failed: r   zTermux voice recording started)r   RuntimeErrorr/   r   r   r<   makedirs	_TEMP_DIRr   strftimerD   rE   r   strr   CHANNELSr)   r*   r+   CalledProcessErrorstderrr,   r@   r-   r   r   r   r   info)r   on_silence_stopmic_cmd	timestampcommandr   detailss          r   startzTermuxAudioRecorder.starti  s   ,.B 
 )*K 
 ZZ 	Y	Y 	Y KK	D1o6I#%77<<	Z	{RV;W#XD 	Y $&&#%#k"#h-
	NNN74dBVZblbtbtu ZZ 	"#~~/D"DO !D	" 	457	Y 	Y" ,, 	Txx51885s1v<<>G!A'KLRSS 	N!A!EFAM	N	" 	"sC   E#AE#()E/ (G)#E,/G&AGG&G!!G&)G2c                 t    t               }|sy t        j                  |dgddddt        j                         y )N-qTr   Fr"   )r   r)   r*   r+   )r   r   s     r   _stop_termux_recordingz*TermuxAudioRecorder._stop_termux_recording  s3    ,.t$PRZ_gqgygyzr   c                 n   | j                   5  | j                  s
	 d d d        y d| _        | j                  }d | _        | j                  }d| _        d d d        | j                          rt        j                  j                  |      sy t        j                         z
  dk  r	 t        j                  |       y t        j                  j                  |      dk  r	 t        j                  |       y t        j                  d|       |S # 1 sw Y   xY w# t        $ r Y y w xY w# t        $ r Y y w xY w)NFr   r   z"Termux voice recording stopped: %s)r   r   r   r   r   r   r<   rD   isfiler   r   unlinkr   getsizer   r   )r   rD   
started_ats      r   r   zTermuxAudioRecorder.stop  s   ZZ 	"??	" 	" $DO''D#'D ))J !D	" 	##%277>>$/>>j(3.		$ 77??4 A%		$ 8$?3	" 	"    s4   D-D&D D( D	D%$D%(	D43D4c                 ~   | j                   5  | j                  }d| _        d | _        d| _        d d d        	 | j	                          r5t        j                  j                  |      r	 t        j                  |       t        j                  d       y # 1 sw Y   gxY w# t
        $ r Y dw xY w# t        $ r Y <w xY w)NFr   z Termux voice recording cancelled)r   r   r   r   r   r-   r<   rD   r   r   r   r   r   )r   rD   s     r   cancelzTermuxAudioRecorder.cancel  s    ZZ 	"''D#DO#'D  !D		"
	'') BGGNN4(		$ 	67	" 	"  		
  s.   "BB! *B0 B!	B-,B-0	B<;B<c                 $    | j                          y r1   )r   r   s    r   shutdownzTermuxAudioRecorder.shutdown  s    r   r   Nr1   )__name__
__module____qualname____doc__supports_silence_autostopr   propertyre   r   floatr   r   r   r   r   r   r   r   r   r   r   r   r   r   r   O  s    L % d   3 3 3
 !S ! !*6X{hsm 88"r   r   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	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defd       Zy)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                 B   t        j                         | _        d | _        g | _        d| _        d| _        d| _        d| _        d| _	        d| _
        d| _        d| _        d| _        d| _        d | _        t         | _        t$        | _        d| _        d| _        d| _        y )NFr   r   g      .@r   )r   r   r   _stream_framesr   r   _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	_peak_rmsr   r   s    r   r   zAudioRecorder.__init__  s    ^^%
 "$"% $'!$+.!),%($'(+ $'<(@ $!"r   c                 ^    | j                   syt        j                         | j                  z
  S r   r   r   s    r   r   zAudioRecorder.elapsed_seconds  r   r   c                     | j                   S )zBCurrent audio input RMS level (0-32767). Updated each audio chunk.r   r   s    r   r   zAudioRecorder.current_rms  s        r   c                     | j                   S )z,Whether audio recording is currently active.r   r   s    r   r   zAudioRecorder.is_recording  s     r   c                 F     j                   yt               \  } fd}d}	 |j                  t        t        t
        |      }|j                          | _         y# t        $ r8}|!	 |j                          n# t        $ r Y nw xY wt        d| d      |d}~ww xY w)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           	      V  	 |rt         j                  d|       j                  sy j                  j	                  | j                                t        
j                  
j                  | j                  
j                        dz                    }|_        t        j                  |      _        j                  t        j                          }|j"                  z
  }|j$                  kD  rd_        j(                  dk(  r|_        nRj*                  sF|j(                  z
  j,                  k\  r*d_        t         j                  d|j(                  z
         j*                  s	d_        nd_        j2                  dk(  r|_        n|j2                  z
  j,                  k\  rd_        d_        nЉj*                  rQj2                  dkD  rj0                  dk(  r|_        n|j0                  z
  j4                  k\  rd_        d_        nsj(                  dkD  rdj&                  dk(  r|_        nM|j&                  z
  j4                  k\  r1t         j                  d|j&                  z
         d_        d_        d}j*                  re|j$                  k  rVj.                  dk(  r|_        n||j.                  z
  j6                  k\  r`t         j9                  d	j6                         d}n=j*                  s1|j:                  k\  r"t         j9                  d
j:                         d}|rVj<                  5  j                  	d _        d d d        	r+	fd}t?        j@                  |d      jC                          y y y y # 1 sw Y   9xY w)Nzsounddevice status: %srx   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-stoppingc                  r    	          y # t         $ r"} t        j                  d| d       Y d } ~ y d } ~ ww xY w)NzSilence callback failed: %sTexc_info)r-   r   error)r   cbs    r   _safe_cbzAAudioRecorder._ensure_stream.<locals>._callback.<locals>._safe_cbr  s7    ^ "#, ^ &-JAX\ ] ]^s    	616targetdaemon)"r   r   r   r   rB   copyr   sqrtmeanr   float64r   maxr   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   Threadr   )indataframes	time_infostatusrmsnowelapsedshould_firer
  r	  r   r   s            @r   	_callbackz/AudioRecorder._ensure_stream.<locals>._callback  s5   5v>??LL. bggbggfmmBJJ&?1&DEFGC #D 5DN $$0nn& 0 00000&)DO))S0-0*!--#8J8J2JdNgNg2g+/(%O%(4+=+=%=?
  ++.1+ 25.--414D. 4#5#559R9RR25D/14D.%% ))A-11S858D2 4#9#99T=T=TT14D.58D2''!+ #-*-t.$2I2II%N%(4??%:<-0**-
 $##t/F/F(F**c1.1+t222d6L6LL$M$($:$:<&*))g.GKK G $0"&K 5!2204-5 ^
 "((FLLN 	 K 1L5 5s   NN()r   channelsr   callbackz#Failed to open audio input stream: z6. Check that a microphone is connected and accessible.)
r   r   InputStreamr   r   DTYPEr   r-   rL   r   )r   r
   r  streamr   r   s   `    @r   _ensure_streamzAudioRecorder._ensure_stream  s     <<#B]	O@ 	^^&!"	 $ F LLN   		!LLN  5aS 9G G 		s;   1A 	B (B+A<;B<	BBBBB c                 ^   	 t                | j                  5  | j                  r
	 ddd       yg | _        t        j                         | _        d| _        d| _        d| _        d| _        d| _        d| _        d| _        d| _        || _        ddd       | j+                          | j                  5  d| _        ddd       t,        j/                  dt0        t2               y# t        t        f$ r#}t        dt        j
                   d      |d}~ww xY w# 1 sw Y   xY w# 1 sw Y   jxY w)	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.
        z9Voice mode requires sounddevice and numpy.
Install with: z! -m pip install sounddevice numpyNFr   r   Tz.Voice recording started (rate=%d, channels=%d))r   r   r   r   sys
executabler   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r"  r   r   r   r   )r   r   r   s      r   r   zAudioRecorder.start  s    	O ZZ 	4	4 	4 DL#~~/D$D!$D!DO"%D!$D%(D"DN !D$3D!	4" 	ZZ 	#"DO	#DkS[\7 W% 	!!$ 00QS 		4 	4&	# 	#s4   
C" DAD2D#"D1DDD #D,r%   c                    | j                   y| j                   d| _         fd}t        j                  |d      }|j                          t	        d      j                         |z   }|j                         r[t	        d      j                         |k  r?|j                  d       |j                         rt	        d      j                         |k  r?|j                         rt        j                  d|       yy)	zAClose the audio stream with a timeout to prevent CoreAudio hangs.Nc                  f    	  j                           j                          y # t        $ r Y y w xY wr1   )r   rL   r-   )r!  s   r   	_do_closez;AudioRecorder._close_stream_with_timeout.<locals>._do_close  s,     s    $ 	00Tr  r   g?r%   u:   Audio stream close timed out after %.1fs — forcing ahead)
r   r   r  r   
__import__r   is_aliverE   r   warning)r   r%   r(  r   r   r!  s        @r   _close_stream_with_timeoutz(AudioRecorder._close_stream_with_timeout  s    <<	 Id;		f%//1G;jjlz&1;;=HFF3F jjlz&1;;=H::<NNWY`a r   c                    | j                   5  | j                  s
	 ddd       yd| _        d| _        | j                  s
	 ddd       yt	               \  }}|j                  | j                  d      }g | _        t        j                         | j                  z
  }t        j                  d|t        |             t        t        dz        }t        |      |k  r)t        j                  dt        |             	 ddd       y| j                  t         k  r/t        j                  d| j                  t                	 ddd       y| j#                  |      cddd       S # 1 sw Y   yxY w)	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   r   r   r   r   r   r   r   r   r   r   rC   r   r   r   r   r   
_write_wav)r   rs   r   
audio_datar  min_sampless         r   r   zAudioRecorder.stop  s8    ZZ  	/?? 	/  	/ $DO !D << 	/  	/ "OEAr1=JDLnn&)9)99GKKEwPST^P_` kC/0K:,KSQ[_]/ 	/  	/6 ~~ 55P NN,AC= 	/  	/@ ??:.A 	/  	/  	/s#   EEB2E9EEE(c                     | j                   5  d| _        g | _        d| _        d| _        ddd       t
        j                  d       y# 1 sw Y   xY w)zoStop recording and discard all captured audio.

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

        Returns the file path.
        Tr   r   r   .wavwbNzWAV written: %s (%d bytes))r<   r   r   r   r   rD   rE   waverh   setnchannelsr   setsampwidthSAMPLE_WIDTHsetframerater   writeframestobytesr   r   r   )r1  r   wav_pathwf	file_sizes        r   r0  zAudioRecorder._write_wav  s     	I-MM/2	77<<	Z	{$+GHYYx& 	1"OOH%OOL)OOK(NN:--/0		1 GGOOH-	0(IF	1 	1s   /ADDr   r1   )rz   )r   r   r   r   r   r   r   r   r   r   r   re   r   r"  r   r-  r   r   r   r   r   staticmethodr0  r   r   r   r   r     s     !%#4 3 3 3
 !S ! ! d  @D,]\b% b$ b0)/hsm )/V
1/ #  r   r   c                  >    t               r
t               S t               S )z=Return the best recorder backend for the current environment.)r2   r   r   r   r   r   create_audio_recorderrD  -  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                     | j                         j                         }|sy|j                  d      t        v s|t        v ryt        j                  |      ryy)zBCheck if a transcript is a known Whisper hallucination on silence.Tz.!F)r@   rj   rstripWHISPER_HALLUCINATIONS_HALLUCINATION_REPEAT_REmatch)r`  cleaneds     r   is_whisper_hallucinationrg  ]  sP     &&(G~~d55DZ9Z%%g.r   r?  modelc                     ddl m}m} t        | |      rt	        | ||      }n
 || |      }|j                  d      r:t        |j                  dd            rt        j                  d|d          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)rh  max_file_sizerh  successr`  r(   z"Filtered Whisper hallucination: %rT)rn  r`  filtered)	tools.transcription_toolsrj  rk  _should_chunk_for_transcription_transcribe_wav_in_chunksr>   rg  r   r   )r?  rh  rj  rk  r.   s        r   transcribe_recordingrs  n  su     J&x?*85P]^!(%8 zz)!9&**\SU:V!W8&:NOrtDDMr   	file_pathrl  c                     | j                         j                  d      sy	 t        j                  j	                  |       |kD  S # t
        $ r Y yw xY w)z@Return whether a CLI WAV recording needs to be split before STT.r6  F)rj   endswithr<   rD   r   r   )rt  rl  s     r   rq  rq    sI    ??%%f-wwy)M99 s   !A 	AAc          	         ddl m} g }g }	 t        | |      }|sBdddd|D ]7  }	 t        j                  j                  |      rt        j                  |       9 S t        j                  dt        |      |        t        |d	
      D ]  \  }} |||      }|j                  d      sh|j                  dd      }	ddd| dt        |       d|	 dc |D ]7  }	 t        j                  j                  |      rt        j                  |       9 S |j                  dd      j                         }
|
st        |
      r|j                  |
        ddj!                  |      j                         j                  d      t        |      d|D ]7  }	 t        j                  j                  |      rt        j                  |       9 S # t        $ r Y w xY w# t        $ r Y w xY w# t        $ r Y fw xY w# t"        $ rw}t        j%                  d| |d       ddd| dcY d}~|D ]F  }	 t        j                  j                  |      rt        j                  |       9# t        $ r Y Dw xY w S d}~ww xY w# |D ]F  }	 t        j                  j                  |      rt        j                  |       9# t        $ r Y Dw xY w w xY w)zGSplit an oversized WAV into provider-sized chunks and join transcripts.r   )rk  )rl  Fr(   zNo audio chunks were created)rn  r`  r  z+Transcribing oversized WAV in %d chunks: %srv   )r   rm  rn  r  zUnknown transcription errorzChunk /z	 failed: r`  T provider)rn  r`  rz  chunksz'Chunked transcription failed for %s: %sr  zChunked transcription failed: N)rp  rk  _split_wav_for_transcriptionr<   rD   r   r   r   r   r   rC   	enumerater>   r@   rg  rB   rE   r-   r  )r?  rh  rl  rk  chunk_pathstranscripts
chunk_pathindexr.   r  r`  r   s               r   rr  rr    s    ;KK#28=Y$BA_`6 & 	J77>>*-IIj)	3 	A3{CSU]^!*;a!@ 	/E:%j>F::i(

7,IJ$"$%eWAc+.>-?yP ( & 	J77>>*-IIj)	  L"5;;=J"::"F"":.	/ ((;/557

:.+&	
 & 	J77>>*-IIj)	  7 7   c>!VZ[ ?]^_]`=abb% 	J77>>*-IIj) 			c & 	J77>>*-IIj) 		s   G< 4GA9G< 4G"G< 6G< AG< 4G-	GG	G*)G*-	G98G9<	I<!I7&I<'I? 14I&&	I21I27I<<I? ?K4J;:K;	K	KK	Kc                t   t        j                  t        d       g }d}t        j                  | d      5 }|j                         }t        d|j                  |j                  z        }||z
  }||k  rt        d      t        d||z        }d}		 |j                  |      }
|
sn=|	dz  }	t        j                  t         j                  j                  t         j                  j                  |             d    d|	d	d
dt        d      }|j                   }|j#                          	 t        j                  |d      5 }|j%                  |j                         |j'                  |j                         |j)                  |j*                         |j-                  |j.                  |j0                         |j3                  |
       ddd       |j5                  |       R	 ddd       |S # 1 sw Y   'xY w# t6        $ r' 	 t        j8                  |        # t:        $ r Y  w xY ww xY w# 1 sw Y   |S xY w)zDWrite WAV chunks small enough to pass the shared STT file-size gate.Tr   i   rbrv   z/STT max_file_size is too small for WAV chunkingr   _chunk03drs   r6  F)prefixsuffixdirdeleter7  N)r<   r   r   r8  rh   	getparamsr  	nchannels	sampwidth
ValueError
readframestempfileNamedTemporaryFilerD   splitextbasenamenamerL   r9  r:  r<  	frameratesetcomptypecomptypecompnamer=  rB   r-   r   r   )r?  rl  r~  header_reservesourceparamsblock_alignmax_data_bytesframes_per_chunkr  r  tempr  chunks                 r   r|  r|    s   KK	D)KN	8T	" %f!!#!V--0@0@@A&7K'NOOq.K"?@&&'78FQJE..''**277+;+;H+EFqIJ&QVWZP[[\]	D JJJLYYz40 .E&&v'7'78&&v'7'78&&v'7'78%%foovG%%f-. "":.-  %N . .  IIj)   A%N sa   C2H-)G:?B	G.G:!H-.G7	3G::	H*HH*	H&#H*%H&&H**H--H7_active_playbackc                  2   t         5  t        } daddd        r6| j                         &	 | j                          t        j                  d       	 t               \  }}|j                          y# 1 sw Y   `xY w# t        $ r Y 6w xY w# t        $ r Y yw xY w)z/Interrupt the currently playing audio (if any).NzAudio playback interrupted)	_playback_lockr  poll	terminater   r   r-   r   r   )procr
   rs   s      r   stop_playbackr    s     
    		#	NNKK45A
	     		  s.   	A/%A; B
 /A8;	BB
	BBc                    t         j                  j                  |       st        j	                  d|        y| j                  d      r]	 t               \  }}t        j                  | d      5 }|j                  |j                               }|j                  ||j                        }|j                         }ddd       |j                         t        |      |z  }t!        j"                         |z   dz   }|j%                         r|j%                         j&                  rnt!        j"                         |k  rWt!        j(                  d	       |j%                         r2|j%                         j&                  rt!        j"                         |k  rW|j+                          y
t5        j6                         }
g }|
dk(  r|j9                  d| g       |j9                  ddddd| g       |
dk(  r|j9                  dd| g       |D ]  }t;        j<                  |d         }|s	 t?        j@                  |t>        jB                  t>        jB                  t>        jB                        }tD        5  |a#ddd       |jI                  d       tD        5  da#ddd        y
 t        j	                  d|        y# 1 sw Y   xY w# t,        t.        f$ r Y /t0        $ r!}	t        j3                  d|	       Y d}	~	Sd}	~	ww xY w# 1 sw Y   xY w# 1 sw Y    y
xY w# t>        jJ                  $ r[ t        j	                  d|d          jM                          |jI                          tD        5  da#ddd       n# 1 sw Y   nxY wY t0        $ rC}	t        j3                  d|d   |	       tD        5  da#ddd       n# 1 sw Y   nxY wY d}	~	d}	~	w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.
    zAudio file not found: %sFr6  r  r   Nr   r   r   Tzsounddevice playback failed: %sDarwinafplayffplayz-nodispz	-autoexitz	-loglevelquietLinuxaplayr   r   )r,   r   r'   i,  r)  z+System player %s timed out, killing processzSystem player %s failed: %sz No audio player available for %s)'r<   rD   r   r   r,  rv  r   r8  rh   r  
getnframes
frombufferrw   getframerater   rC   r   r   r   r   r   r   r   r   r-   r   platformsystemrB   r   r   r)   Popenr+   r  r  waitTimeoutExpiredkill)rt  r
   r   r@  r  r1  sample_rateduration_secsr   r   r  playerscmdexer  s                  r   play_audio_filer    s    77>>)$19= &!	?"_FB9d+ 0rr}}7]]6]B
 oo/0
 GGJ;G7  
Ok9M~~'-7#=H--/bmmo&<&<AQT\A\

4  --/bmmo&<&<AQT\A\GGI __FG),-NNHik7IVWy12 ,ll3q6",!''J4F4FzOaOaisi{i{|# ,'+$,		#	&# ,'+$,,, NN5yAi0 0 W% 	 	?LL:A>>	?$, ,,,, ,LcRSfU				# ,'+$, , , ,:CFAF# ,'+$, , ,,s   
#K -AJ6:CK K %A	L.L1 LLL6K ;K K>K>K99K>L
	LL	LLAO,M8/	O8N=OO O/N;2	O;O OOc                     ddl m} m}m}  |       } ||      } | |      }|xr |dk7  }g }t	               }t               xs |}	|	s|j                  ddg       t               }
|	xr	 |xr |
d   }g }|r|j                  d       n1|	r|j                  d       n|j                  d	t                d
       |s|j                  d       nV|dk(  r|j                  d       n?|dk(  r|j                  d       n(|dk(  r|j                  d       n|j                  d       |
d   D ]  }|j                  d|         |
j                  dg       D ]  }|j                  d|         ||	||dj                  |      |
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is_stt_enablednoner   r	   rb   z)Audio capture: OK (Termux:API microphone)zAudio capture: OKzAudio capture: MISSING (ra   z5STT provider: DISABLED in config (stt.enabled: false)localz'STT provider: OK (local faster-whisper)groqzSTT provider: OK (Groq)openaizSTT provider: OK (OpenAI)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)rc   zEnvironment: rd   
)rb   audio_availablestt_availablemissing_packagesr   environment)rp  r  r  r  r2   r   extendru   rB   r   r>   rE   )r  r  r  
stt_configstt_enabledstt_providerr  missingrp   	has_audio	env_checkrb   details_partsr,  notices                  r   check_voice_requirementsr  d  s    ZY!#J ,K ,L:LF$:MG46N "4nIw/0 )*IFmF	+0FIMHI	0178S8U7VVWXYTU		 FG		67		!89<	
 Z( 8}WI678--	2. 7}VH567 $&#99]+  r   max_age_secondsc                 $   t         j                  j                  t              syd}t	        j                         }t        j
                  t              D ]  }|j                         s|j                  j                  d      s0|j                  j                  d      sL	 ||j                         j                  z
  }|| kD  r$t        j                  |j                         |dz  } |rt        j                  d|       |S # t        $ r Y w xY w)zRemove old temporary voice recording files.

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

    Returns:
        Number of files deleted.
    r   r   r6  rv   z"Cleaned up %d old voice recordings)r<   rD   isdirr   r   scandiris_filer  rA   rv  r;   st_mtimer   r   r   r   )r  deletedr  entryages        r   cleanup_temp_recordingsr    s     77==#G
))+CI& ==?uzz44\BuzzGZGZ[aGbEJJL111(IIejj)qLG 97CN  s   !AD	DD)ip  gQ?rv   r1   r   )i  )Dr   loggingr<   r  rer   r)   r$  r  r   r   r8  typingr   r   r   r   	getLoggerr   r   r   re   r   rg   r   r   r   r   r   r/   r2   rS   dictru   r   r   r   r;  r   r   rD   rE   
gettempdirr   r   r   r   r   r   rD  rc  compile
IGNORECASErd  rg  rs  rq  rr  r|  r  r  __annotations__r   r  r  r  r  r  r   r   r   <module>r     s|  	  	  	   
     , ,			8	$$  A+S +4HSM 44 "T T2 2j@$ @J     GGLL,,,.?	&4 &4e &43 &4t &4Xz z@[ [|
}/BB  > &2::@
--   "3 x} SRUX 8s 3 4 // C=/ 	/
 
#s(^/d-3 -# -$s) -j 04 (:++, 3!(Ks Kt Kb=$sCx. =FS C r   