
    PmjQ              	         U d Z ddlmZ ddlZddlZddlZddlZddlZddlm	Z	 ddl
mZmZmZmZ  ej        e          ZdZdZdZd	Zd
Zd
Z G d de          ZddddddeddZded<   dZ eh d          ZdndodZdpdZ dqdZ!da"drd!Z#dpd"Z$dsd#Z%dtd&Z&drd'Z'dud)Z(dvd+Z)dwd-Z*dxdyd/Z+dxdzd1Z,dqd2Z-d{d4Z.d5 Z/dpd6Z0d|d8Z1d}d:Z2d}d;Z3 G d< d=          Z4d~d?Z5 G d@ dAe4          Z6dBZ7dCZ8ddEZ9dxddHZ: G dI dJe4          Z; G dK dLe4          Z<ddMZ=dpdNZ>dpdOZ?dxddPZ@ G dQ dR          ZAdaBdSedT<   daCdUedV<   daD ejE                    ZFddWZGdxddYZHdd[ZIdd]ZJdd^dddZKddeZLddfZMddgZNddhZOdpdiZPdpdjZQdxddkZRddmZSdS )u  Wake-word ("Hey Hermes") detection — hands-free session trigger.

A lightweight, always-on hotword listener that fires a callback when a wake
phrase is spoken — the "Hey Siri" / "Alexa" pattern. Shared by the CLI, TUI, and
desktop GUI (one of them owns it, gated by ``wake_surface_enabled``): say the
wake word, Hermes opens a fresh session and captures voice via the existing
pipeline, then answers.

Three engines, all fully on-device (no audio leaves the machine for detection):

* **openwakeword** (default, free, no API key) — loads an ONNX model. Defaults
  to the bundled "hey hermes" model (``tools/wakewords/``) so the wake word
  works out of the box; or point ``wake_word.openwakeword.model`` at a built-in
  name (``hey_jarvis``, ``alexa``, …) or a custom ``.onnx`` for another phrase.
* **sherpa** (free, no API key, open vocabulary) — sherpa-onnx keyword
  spotting. Detects ANY typed phrase with no training: set
  ``wake_word.phrase`` and the phrase is tokenized at runtime against a small
  streaming zipformer model (~13 MB English model, one-time download).
* **porcupine** (premium) — Picovoice's engine. Needs ``PORCUPINE_ACCESS_KEY``;
  supports built-in keywords and custom ``.ppn`` files from the Picovoice
  Console.

Audio capture reuses the same 16 kHz mono int16 ``sounddevice`` path as voice
mode. The detector runs on its own daemon thread; callers ``pause()`` it while a
voice turn holds the microphone and ``resume()`` it once the system is idle
again (two input streams on one device is unreliable cross-platform).

Nothing here mutates agent context or the prompt cache — on wake we hand a plain
string to the caller, exactly like a voice transcript.
    )annotationsN)Path)AnyCallableDictOptionali>         @g      @   
   c                      e Zd ZdZdS )WakeWordInUsezCRaised when another surface or process owns the wake-word listener.N)__name__
__module____qualname____doc__     5/home/thesage/.hermes/hermes-agent/tools/wake_word.pyr   r   C   s        MMMMr   r   Fautoopenwakeword
hey hermesg333333?T)enabledsurfaceinput_deviceproviderphrasesensitivityconfirmation_framesstart_new_sessionDict[str, Any]	_DEFAULTS
hey_hermes>    hermesr"   r   onnx	frameworkstrreturnc                   t          |                                                                           dk    rdnd}t          j                            t          j                            t                    dt           d|           S )zGPath to the shipped hey_hermes model (.onnx/.tflite) for ``framework``.tfliter%   	wakewords.)	r'   striplowerospathjoindirname__file___BUNDLED_MODEL_NAME)r&   exts     r   _bundled_wakeword_pathr6   \   si    )nn**,,2244@@((fC7<<11;CV@^@^Y\@^@^___r   boolc                 Z    dd l } t          j         dk    o|                                 dk    S )Nr   darwinarm64)platformsysmachine)r;   s    r   _is_macos_arm64r>   b   s.    OOO<8#E(8(8(:(:g(EEr   c                 &    t                      rdndS )u  The openWakeWord backend to use on this platform.

    openWakeWord's ONNX backend produces near-zero scores on macOS ARM64 — its
    shared *embedding* model is the broken stage (the melspectrogram front-end
    and the wake classifier both match tflite exactly). The detector arms, the
    microphone works, and no phrase can ever cross the threshold. Prefer the
    tflite backend there; ONNX stays the default everywhere else.

    Upstream: https://github.com/dscripka/openWakeWord/issues/336
    r*   r%   )r>   r   r   r   default_inference_frameworkr@   h   s     '((488f4r   cfgc                   t          |                     d          t                    r|                     d          ni }t          |                    d          pd                                                                          }|st                      S |dk    r3t                      r%t          sdat          
                    d           dS |S )u  Resolve the effective openWakeWord backend from config.

    Honors an explicit ``openwakeword.inference_framework`` — EXCEPT the one
    combination that is provably dead: an explicit ``onnx`` on macOS ARM64,
    where ONNX's embedding model never lets a phrase cross threshold (upstream
    #336). Existing macOS users who pinned ``onnx`` before the tflite fix landed
    would otherwise keep a wake word that arms but never fires. Coerce that one
    case to tflite (with a one-time warning) instead of silently shipping a dead
    ear. Every other explicit value is respected as-is; empty falls back to the
    platform default.
    r   inference_frameworkr#   r%   Tu   wake: openwakeword.inference_framework='onnx' is set but ONNX's embedding model never fires on macOS ARM64 (openWakeWord #336) — using tflite instead. Set inference_framework to '' (auto) or 'tflite' in config.yaml to silence this.r*   )
isinstancegetdictr'   r-   r.   r@   r>   _warned_onnx_coercedloggerwarning)rA   subr&   s      r   resolve_inference_frameworkrK   y   s     &00G0G%N%N
V#''.
!
!
!TVCCGG1228b99??AAGGIII -*,,,F00# 	#' NN;   xr   c                 8   	 ddl } dS # t          $ r Y nw xY w	 ddlm} n# t          $ r Y dS w xY wddl}|                    d          }g |_        t          j        	                    d|           |t          j        d<   t                              d           dS )	us  Make ``import tflite_runtime.interpreter`` resolve, returning success.

    openWakeWord hardcodes that import but only declares ``tflite-runtime`` for
    ``platform_system == "Linux"``; on macOS the equivalent wheel is
    ``ai-edge-litert``. Alias the module so the upstream import succeeds. The
    alias is process-local — nothing is written to site-packages.
    r   NT)interpreterFtflite_runtimeztflite_runtime.interpreterz3wake word: bridged tflite_runtime -> ai_edge_litert)tflite_runtime.interpreterImportErrorai_edge_litertrM   types
ModuleType__path__r<   modules
setdefaultrH   debug)rN   _litertrR   pkgs       r   ensure_tflite_runtimerZ      s    ))))t   9999999   uu LLL


+
,
,CCLK+S11107CK,-
LLFGGG4s    
  
..c                     	 ddl m}   |                                 d          }n# t          $ r d}Y nw xY wt	          |t
                    r|ni S )zAReturn the ``wake_word`` config section, shape-guarded to a dict.r   )load_config	wake_wordN)hermes_cli.configr\   rE   	ExceptionrD   rF   )r\   rA   s     r   load_wake_word_configr`      sq    111111kmm,,   S$''/33R/s   #& 55keyr   c                    |                      |t                               |                    }|t                               |          n|S N)rE   r!   )rA   ra   vals      r   _getre      s<    
''#y}}S))
*
*C!$9==#5r   c                    t          t          | d                                                                                    pdS )Nr   r   )r'   re   r-   r.   rA   s    r   	_providerrh      s7    tC$$%%++--3355GGr   int | str | Nonec                    t          | d          }|t          |t                    rdS t          |t                    r|S t	          |                                          }|pdS )zBConfigured PortAudio input selector, preserving indices and names.r   N)re   rD   r7   intr'   r-   )rA   rawvalues      r   _input_devicern      s_    
sN
#
#C
{jd++{t#s 
HHNNE=Dr   floatc                    t          | d          }	 t          |          }n1# t          t          f$ r t          t          d                   }Y nw xY wt          t          |d          d          S )Nr                 ?)re   ro   	TypeError
ValueErrorr!   minmax)rA   rl   ss      r   _sensitivityrx      sq    
sM
"
"C,#JJz" , , ,)M*++,s1c{{C   s   " +AArk   c                    t          | d          }	 t          |          }n# t          t          f$ r
 t          }Y nw xY wt          t          |d          d          S )zHow many consecutive over-threshold frames are required to fire.

    ``1`` restores the old single-frame behaviour; higher values reject
    ambient-speech blips at the cost of a few tens of ms of extra latency.
    Clamped to a sane 1..10.
    r      r   )re   rk   rs   rt   _DEFAULT_CONFIRMATION_FRAMESru   rv   )rA   rl   ns      r   _confirmation_framesr}      sg     s)
*
*C)HHz" ) ) )()s1ayy"s   " ==Optional[Dict[str, Any]]c                d    | | nt                      } t          t          | d                    pdS )zHHuman-facing wake phrase label (purely cosmetic; engine keys detection).Nr   r   )r`   r'   re   rg   s    r   wake_phraser      s3    ##&;&=&=CtC""##3|3r   r   c                :   ||nt                      }|                    d          sdS t          t          |d                                                                                    pd}|dk    p)||                                                                 k    S )a/  Should ``surface`` (``cli`` / ``tui`` / ``gui``) host the listener?

    True when the wake word is enabled and the configured ``surface`` is either
    ``auto`` or this exact surface.  ``auto`` makes a surface eligible; the
    process/machine ownership lock still permits only the first claimant.
    Nr   Fr   r   )r`   rE   r'   re   r-   r.   )r   rA   wants      r   wake_surface_enabledr      s     ##&;&=&=C779 utC##$$**,,2244>D6><TW]]__%:%:%<%<<<r   c                 J    	 ddl m}   |             pdS # t          $ r Y dS w xY w)Nr   get_active_profile_namedefault)hermes_cli.profilesr   r_   r   s    r   _active_profile_namer   	  sO    ??????&&((5I5   yys    
""Dict[str, str]c                    i } 	 ddl m} ddlm}m}  |            D ]}t          |dd          pt          |          }	 t           ||                    dz  } ||          }|                    d          pi }t          |t                    r|                    d          st          |                    d	          pd
|                                           }	|	r|	| |<   # t          $ r Y w xY wn# t          $ r Y nw xY w| S )a  Map ``profile name -> wake phrase`` for every wake-enabled profile.

    Reads each profile's own ``config.yaml`` raw (cheap, no full config merge).
    A profile is enrolled when its ``wake_word.enabled`` is truthy; its phrase
    defaults to ``"hey <profile>"`` when unset. Used by the sherpa engine to
    listen for every enrolled profile's phrase at once and route the wake to
    the matching profile. Best-effort: unreadable profiles are skipped.
    r   )read_user_config_raw)get_profile_dirlist_profilesnameNzconfig.yamlr]   r   r   zhey )r^   r   r   r   r   getattrr'   r   rE   rD   rF   r-   r_   )
phrasesr   r   r   infor   cfg_pathrl   wcr   s
             r   enrolled_profile_phrasesr     sh    !G::::::FFFFFFFF!MOO 	 	D4..;#d))D   5 566F**844WW[))/R!"d++ 266)3D3D RVVH-->??EEGG +$*GDM   	    NsB   :C9 A'C(&C9 'A C('C9 (
C52C9 4C55C9 9
DDc                     dd l } dd l}|| fS Nr   )numpysounddevice)npsds     r   _import_audior   8  s#    r6Mr   c                 T    	 t                       dS # t          t          f$ r Y dS w xY w)NTF)r   rP   OSErrorr   r   r   _audio_availabler   ?  s<    t!   uus    ''selectorc                   d|i}	 |                      |d          }n+# t          $ r}t          |          |d<   |cY d}~S d}~ww xY wt          |t                    rf|                    d          }|rt          |          |d<   |                    d          }t          |t          t          f          rt          |          |d<   |                    d          }t          |t          t          f          rt          |          |d<   |                    d          }t          |t          t          f          rt          |          |d	<   	 |                     t          |                    }	t          |	t                    r|	                    d          nd}
|
rt          |
          |d<   n# t          $ r Y nw xY w|S )
zResolve a PortAudio selector into JSON-safe diagnostics.

    Device discovery is diagnostic only. ``InputStream`` remains the authority
    on whether the selected device can actually open at the requested format.
    r   inputerrorNr   max_input_channelsdefault_sampleratehostapihostapi_index)	query_devicesr_   r'   rD   rF   rE   rk   ro   query_hostapis)r   r   detailsr   er   channelsrater   r   hostapi_names              r   _describe_input_devicer   G  s     *84G'22   q66 $ xx 	(!$iiGFO88011he-- 	:,/MMG()xx,--dS%L)) 	8,1$KKG()++mc5\22 	'*='9'9GO$++C,>,>??6@$6O6OYw{{6222UY ;),\):):GI&    Ns,    
AA A AA"F4 4
G Gr   c                F   t          |                     d          pd                                          }|                     d          }|p|dnt          |          }t          |                     d          pd                                          }|r| d| dn|S )Nr   r#   r   system defaultr   z ())r'   rE   r-   )r   r   r   labelr   s        r   _device_labelr   l  s    w{{6""(b))//11D{{:&&HM)9%%s8}}E'++i((.B//5577G%,7e!!w!!!!%7r   c                    t           j        dk    r	 dS t           j        dk    rdt          |            dS dt          |            dS )zEPlatform-specific remediation for an armed stream delivering silence.r9   zMicrophone delivers only silence. Grant the Hermes backend microphone access in System Settings > Privacy & Security > Microphone, then toggle the wake word.win32z&Microphone delivers only silence from z^. Set wake_word.input_device to a different PortAudio input device, then toggle the wake word.z=. Check the selected input device, then toggle the wake word.)r<   r;   r   )r   s    r   silent_audio_hintr   t  sw    
|x5	
 	

 |w)]75K5K ) ) )	
	Fw1G1G 	F 	F 	Fr   c                  H    e Zd ZU dZdZded<   dZded<   dd
ZddZddZ	dS )_Enginez?Minimal hotword-engine contract: feed int16 frames, get a bool.   rk   frame_lengthNOptional[tuple[str, str]]
last_matchr(   r7   c                    t           rc   )NotImplementedErrorselfframes     r   processz_Engine.process  s    !!r   Nonec                    dS )zDClear any internal audio/feature buffer (called on every (re)start).Nr   r   s    r   resetz_Engine.reset  s    r   c                    d S rc   r   r   s    r   closez_Engine.close  s    r   r(   r7   r(   r   )
r   r   r   r   r   __annotations__r   r   r   r   r   r   r   r   r     s|         IIL -1J0000" " " "        r   r   rm   c                    t           j        | v p3|                     d          pt           j                            |           S )N)z.onnxz.tflitez.ppn)r/   sependswithr0   exists)rm   s    r   _looks_like_pathr     s;    
% 	!>>677	!7>>%  r   c                  6    e Zd ZdZdZddZddZdd
ZddZdS )_OpenWakeWordEngineu4   openWakeWord — free, local ONNX hotword detection.r   rA   r    c                   ddl m} |                    dd           dd l}ddlm} t          |                    d          t                    r|                    d          ni }t          |                    d          pt                                                    }t          |          }t          |          | _        t          |          | _        d| _        |d	k    rt%                      s	 |                    d
d           n2# t&          $ r%}t(                              d|           Y d }~nd }~ww xY wt%                      s9t-                      rt/          d          t(                              d           d}|                                t4          v rt7          |          }	 |j                            |g           n2# t&          $ r%}t(                              d|           Y d }~nd }~ww xY w|g}	 ||	|          | _        t?          | j        j         !                                          | _"        d S )Nr   	lazy_depswake.openwakewordFprompt)Modelr   modelr*   wake.openwakeword.tflitez,wake word: tflite runtime install failed: %sz{The wake word needs the tflite backend on this Mac, but its runtime is missing. Install it with: pip install ai-edge-litertu?   wake word: no tflite runtime available — falling back to onnxr%   z'openwakeword model download skipped: %s)wakeword_modelsrC   )#toolsr   ensurer   openwakeword.modelr   rD   rE   rF   r'   r4   r-   rK   rx   
_thresholdr}   _confirm_needed_confirm_streakrZ   r_   rH   rW   r>   RuntimeErrorrI   r.   _BUNDLED_MODEL_ALIASESr6   utilsdownload_models_modellistmodelskeys_labels)
r   rA   r   r   r   rJ   	model_refr&   r   r   s
             r   __init__z_OpenWakeWordEngine.__init__  s   ######,U;;;,,,,,,)3CGGN4K4KT)R)RZcggn%%%XZ((?,?@@FFHH	/44	
 's++3C88    )>)@)@ P  !;E JJJJ P P PKQOOOOOOOOP(** #"$$ &Z   `aaa"	 ?? 666.y99I	G..	{;;;; 	G 	G 	GLLBAFFFFFFFF	GeF	RRRDK.335566s0   5D 
D<D77D<1G 
G<G77G<r(   r7   c                      j                             |          }t           fd|                                D                       }|r+ xj        dz  c_         j         j        k    r	d _        dS dS d _        dS )Nc              3  .   K   | ]}|j         k    V  d S rc   )r   ).0scorer   s     r   	<genexpr>z._OpenWakeWordEngine.process.<locals>.<genexpr>  s*      II5DO+IIIIIIr   rz   r   TF)r   predictanyvaluesr   r   )r   r   scoresovers   `   r   r   z_OpenWakeWordEngine.process  s    $$U++IIIIIIIII  	  A%  #t';;;'($t5 ur   r   c                j    d| _         	 | j                                         d S # t          $ r Y d S w xY wr   )r   r   r   r_   r   s    r   r   z_OpenWakeWordEngine.reset  sM      !	K 	 	 	DD	s   $ 
22c                .    |                                   d S rc   )r   r   s    r   r   z_OpenWakeWordEngine.close      

r   NrA   r    r   r   	r   r   r   r   r   r   r   r   r   r   r   r   r   r     sr        >> L97 97 97 97v           r   r   zhttps://github.com/k2-fsa/sherpa-onnx/releases/download/kws-models/sherpa-onnx-kws-zipformer-gigaspeech-3.3M-2024-01-01.tar.bz2z4sherpa-onnx-kws-zipformer-gigaspeech-3.3M-2024-01-01r   c                 .    ddl m}   |             dz  dz  S )Nr   get_hermes_homecacher+   )hermes_constantsr   r   s    r   _sherpa_model_rootr    s,    000000?w&44r   rootOptional[Path]c                @   | pt                      } | t          z  }|dz                                  r|S ddl}ddl}|                     dd           | t           dz  }t                              d           |j        	                    t          |           |                    |d          5 }|                    | d	
           ddd           n# 1 swxY w Y   |                    d           |dz                                  st          d|           |S )zBDownload + unpack the sherpa KWS model once; return its directory.
tokens.txtr   NTparentsexist_okz.tar.bz2z:wake word: downloading sherpa KWS model (one-time, ~13 MB)zr:bz2data)filter)
missing_okz sherpa KWS model unpack failed: )r  _SHERPA_KWS_MODEL_DIRr   tarfileurllib.requestmkdirrH   r   requesturlretrieve_SHERPA_KWS_MODEL_URLopen
extractallunlinkr   )r  targetr  urllibarchivetfs         r   _ensure_sherpa_modelr    sm   '%''D))F%%'' NNNJJtdJ+++-7777G
KKLMMM
N4g>>>	gw	'	' +2
d6***+ + + + + + + + + + + + + + +NNdN###\!))++ HFfFFGGGMs   0CCCc                  6    e Zd ZdZdZddZddZdd
ZddZdS )_SherpaKwsEngineuC  sherpa-onnx open-vocabulary keyword spotting — any typed phrase, zero training.

    The configured ``wake_word.phrase`` is BPE-tokenized at runtime against the
    model's vocabulary, so "hey hermes", "hey coder", or any other phrase works
    immediately. Here ``phrase`` is DETECTION config, not a cosmetic label.
    r   rA   r    c           	     n   ddl m} |                    dd           dd l}ddlm} t          |                    d          t                    r|                    d          ni }t          |                    d          pd	          	                                }|rt          |          nt                      d
z                                  st          d           t          t          |d          pd          	                                }t                      }||i}	t!          |                    dd                    rNt#                                                      D ]-\  }
}|	                    |	                                |
           .t)          |	          } |d |D             t          d
z            dt          dz                      }dd l}i | _        |                    ddddd          }t1          ||          D ]l\  }}|                                                    dd          }|	|         | j        |<   |                    d                    |          d| dz              m|                                 |j        | _        d | _         ddtC          |          z  z   }d(fd"}|"                    t          d
z             |d#           |d$           |d%          | j        |d&'          | _#        | j#        $                                | _%        d S ))Nr   r   wake.sherpaFr   )
text2tokensherpa	model_dirr#   r  zsherpa KWS model not found at r   r   profile_routingTc                6    g | ]}|                                 S r   )upper)r   ps     r   
<listcomp>z-_SherpaKwsEngine.__init__.<locals>.<listcomp>T  s     (((1QWWYY(((r   bpez	bpe.model)tokenstokens_type	bpe_modelwz.txtzhermes-kws-zutf-8)modesuffixprefixdeleteencoding _z @
g?g?patternr'   r(   c                    t                              |                     }|st          d d|            t          |d                   S )Nzsherpa KWS model file missing: /r   )sortedglobr   r'   )r6  hitsds     r   _model_filez._SherpaKwsEngine.__init__.<locals>._model_filep  sR    !&&//**D T"#RQ#R#R#R#RSSStAw<<r   zencoder-*[!8].onnxzdecoder-*[!8].onnxzjoiner-*[!8].onnxrz   )r*  encoderdecoderjoinerkeywords_filekeywords_thresholdnum_threads)r6  r'   r(   r'   )&r   r   r   sherpa_onnxr!  rD   rE   rF   r'   r-   r   r  r   r   re   r   r7   r   itemsrV   r   tempfile_display_to_profileNamedTemporaryFilezipr&  replacewriter1   r   r   _keywords_filer   rx   KeywordSpotter_spottercreate_stream_stream)r   rA   r   rD  r!  rJ   r#  r   own_profile
phrase_mapprofr'  r   r*  rF  kwtoksdisplay	thresholdr=  r<  s                       @r   r   z_SherpaKwsEngine.__init__8  sy   ######u555******#-cggh.?.?#F#FNcgghB,,23399;;	(DDOOO.B.D.DL ((** 	ECCCDDD T#x((8L99??AA*,,&,k%:
)40011 	7355;;== 7 7a%%aggii6666z""(((((q<'((!k/**	
 
 
 	 46 ((VM%RY ) 
 
 7F++ 	8 	8GAtggii''S11G0:1D$W-HHSXXd^^&67&6&6&667777



 g59 3c!2!222		  	  	  	  	  	  $22q<'((K 455K 455;233-( 3 
 
 }2244r   r(   r7   c                |   dd l }|                    ||j                  dz  }| j                            t
          |           d}| j                            | j                  r| j                            | j                   | j        	                    | j                  }|rxd}t          |          }|                    dd                                          | j                            |d          f| _        | j                            | j                   | j                            | j                  |S )	Nr   )dtypeg      @FTr4  r3  r#   )r   asarrayfloat32rP  accept_waveformSAMPLE_RATErN  is_readydecode_stream
get_resultr'   rJ  r.   rG  rE   r   reset_stream)r   r   r   samplesfiredresultrV  s          r   r   z_SherpaKwsEngine.process  s!   **U"**55?$$[':::m$$T\22 	9M''555]--dl;;F 9f++OOC--3355,00"==#
 **4<888 m$$T\22 	9 r   r   c                f    	 | j                                         | _        d S # t          $ r Y d S w xY wrc   )rN  rO  rP  r_   r   s    r   r   z_SherpaKwsEngine.reset  sB    	=6688DLLL 	 	 	DD	s   " 
00c                \    	 t          j        | j                   d S # t          $ r Y d S w xY wrc   )r/   r  rL  r   r   s    r   r   z_SherpaKwsEngine.close  sB    	Id)***** 	 	 	DD	    
++Nr   r   r   r   r   r   r   r  r  ,  sz          LG5 G5 G5 G5R   (        r   r  c                  *    e Zd ZdZddZddZdd	Zd
S )_PorcupineEngineu@   Picovoice Porcupine — premium, on-device, needs an access key.rA   r    c                R   ddl m} |                    dd           dd l}t	          j        d          pd                                }|st          d          t          |	                    d	          t                    r|	                    d	          ni }t          |	                    d
          pd                                          }dt          |          z
  }||gd}t          |          r|g|d<   n|g|d<    |j        di || _        | j        j        | _        d S )Nr   r   wake.porcupineFr   PORCUPINE_ACCESS_KEYr#   zcPorcupine wake word requires PORCUPINE_ACCESS_KEY (get a free key at https://console.picovoice.ai).	porcupinekeywordjarvisrr   )
access_keysensitivitieskeyword_pathskeywordsr   )r   r   r   pvporcupiner/   getenvr-   r   rD   rE   rF   r'   rx   r   create
_porcupiner   )	r   rA   r   rt  rp  rJ   rn  porcupine_sensitivitykwargss	            r   r   z_PorcupineEngine.__init__  sW   ######)%888i 677=2DDFF
 	D  
 '11E1Et&L&LTcggk"""RTcggi((4H55;;==
 !$l3&7&7 70:NcMd!e!eG$$ 	+'.iF?##")F:,+,66v66 O8r   r(   r7   c                >    | j                             |          dk    S r   )rw  r   r   s     r   r   z_PorcupineEngine.process  s    &&u--22r   r   c                \    	 | j                                          d S # t          $ r Y d S w xY wrc   )rw  r1  r_   r   s    r   r   z_PorcupineEngine.close  sC    	O""$$$$$ 	 	 	DD	rg  Nr   r   r   )r   r   r   r   r   r   r   r   r   r   ri  ri    sV        JJ9 9 9 9>3 3 3 3     r   ri  c                    t          |           }|dk    rt          |           S |dv rt          |           S |dv rt          |           S t	          d|          )Nrm  r"  sherpa-onnxkwsr  )r   owwlocalzUnknown wake_word provider: )rh   ri  r  r   rt   )rA   r   s     r   _build_enginer    sq    ~~H;$$$;;;$$$333"3'''
@H@@
A
AAr   c                     	 ddl m} m}m}  |            } ||          o | |          dk    S # t          $ r Y dS w xY w)u  Is a speech-to-text provider configured and enabled?

    A wake without STT arms the mic but every captured utterance dies at
    transcription — a useless (and confusing) experience. Same standard as
    voice mode's ``check_voice_requirements``: enabled + a real provider.
    r   )_get_provider_load_stt_configis_stt_enablednoneF)tools.transcription_toolsr  r  r  r_   )r  r  r  
stt_configs       r   
_stt_readyr    sy    ]]]]]]]]]]%%''
~j))QmmJ.G.G6.QQ   uus   -0 
>>c                 z   	 ddl m} m}  |  |                      }n# t          $ r Y dS w xY wdddd}|                    |          }|B	 dd	lm} |                    |          s|                                S n# t          $ r Y dS w xY w	 dd
l m	} t           |                      S # t          $ r Y dS w xY w)u  Can the configured text-to-speech provider run (or install at first use)?

    The wake flow is fully hands-free (wake → speak → hear the reply); without
    TTS the reply is silent and the loop is pointless.

    PROBE, not an installer: ``check_tts_requirements`` lazily pip-installs the
    provider SDK via ``_import_*`` → ``lazy_deps.ensure`` — running that inside
    a status poll froze wake.status for the length of a pip install (and a
    failed install marked the wake word unavailable, unmounting the desktop
    ear). When the provider's deps aren't installed yet, "installable at first
    use" counts as ready and we never touch pip from here.
    r   )r  _load_tts_configFztts.edgeztts.elevenlabsztts.mistral)edge
elevenlabsmistralNr   )check_tts_requirements)tools.tts_toolr  r  r_   rE   r   r   is_available_allow_lazy_installsr  r7   )r  r  r   _LAZY_TTS_FEATURESfeaturer   r  s          r   
_tts_readyr    sH   BBBBBBBB =!1!1!3!344   uu &  
 !$$X..G	''''''))'22 8 557778  	 	 	55	999999**,,---   uus0    
,,.A= =
B
BB, ,
B:9B:c           
        | | nt                      } t          |           }ddlm} |dk    rd}n	|dv rd}nd}|                    |          }|                                }|rt                      nd	}d
}t                      }t                      }	d}
d
}|dvr:t          |           }|dk    r%t                      p|                    d          p|}|dk    r-t          j        d          pd                                sd	}d}
nW|s|s|                    |          pd}
n;|sd}
n6|r|sd}
n/|r|	s+d                    d d|fd|	ffD                       }d| d}
|o|o|	o
|o|r|p| o|||||||	t!          |           |
d	S )zDReport whether wake-word detection can run, with a remediation hint.Nr   r   rm  rk  r}  r   r   FTr#   )rm  r"  r~  r  r  r*   r   rl  zDSet PORCUPINE_ACCESS_KEY (free key at https://console.picovoice.ai).zNThe wake word needs the tflite runtime on this Mac: pip install ai-edge-litertzHMicrophone capture needs sounddevice + numpy and a working audio device.z and c              3  $   K   | ]\  }}||V  d S rc   r   )r   r   oks      r   r   z/check_wake_word_requirements.<locals>.<genexpr>L  s@       
 
T2ac

 
 
 
 
 
r   zspeech-to-textztext-to-speechzWake word needs uN    configured — run `hermes tools` (Voice section) or see the voice-mode docs.)		availabler   deps_availableaudio_availableaccess_key_setstt_availabletts_availabler   hint)r`   rh   r   r   r  r  r   r  r  rK   rZ   r/   ru  r-   feature_install_commandr1   r   )rA   r   r   r  deps_oklazy_okaudio_okkey_okstt_oktts_okr  	tflite_okr&   missings                 r   check_wake_word_requirementsr    sa   ##&;&=&=C~~H;"	=	=	=%$$W--G,,..G &-7!!!%HF \\F\\FD ILLL/44	  -//p93I3IJd3e3epipI;	2H(I(I(OR'V'V'X'XU @W @0099?R 	@_	 @ @Y @6 @,, 
 
#3V"<?OQW>X!Y
 
 
 
 
?7 ? ? ?  B B6 Bi B"(@G(?!# c""  r   c                  r    e Zd ZdZeddfddZedd            ZddZddZ	ddZ
ddZddZddZd dZdS )!WakeWordDetectorzBackground hotword listener. Fires ``on_wake()`` when the phrase is heard.

    The engine is built once and kept alive across pause/resume; only the audio
    stream + reader thread cycle, so toggling the mic for a voice turn is cheap.
    Nenginer   on_wakeCallable[[], None]cooldownro   
on_failure.Optional[Callable[['WakeWordDetector'], None]]r   ri   c                &   || _         || _        || _        || _        || _        d|i| _        d | _        t          j                    | _	        t          j                    | _
        d| _        t          j                    | _        d| _        d| _        d S )Nr   rq   Fr   )r  r  r  r  r   input_device_details_thread	threadingEvent_stop_callback_inflight
_last_fireLock_lockaudio_silent_silent_frames)r   r  r  r  r  r   s         r   r   zWakeWordDetector.__init__k  s      $(5?4N!37_&&
"+/"3"3^%%
 "r   r(   r7   c                @    | j         }|d uo|                                S rc   )r  is_aliver   ts     r   runningzWakeWordDetector.running  s     L}--r   r   c                H   | j         5  | j        '| j                                        r	 ddd           dS | j                                         t          j                    }g }t          j        | j        ||fdd          | _        | j        	                                 ddd           n# 1 swxY w Y   |
                    t                    s#|                                  t          d          |r*|                                  t          d          |d         dS )z-Open the mic and begin listening. Idempotent.NTz	wake-word)r  argsdaemonr   z1Timed out while opening the wake-word microphone.z(Failed to open the wake-word microphone.r   )r  r  r  r  clearr  r  Thread_runstartwait_START_TIMEOUT_SECONDS_halt_threadTimeoutErrorr   )r   readystartup_errorss      r   r  zWakeWordDetector.start  s   Z 	! 	!|'DL,A,A,C,C'	! 	! 	! 	! 	! 	! 	! 	! JO%%E24N$+y^, 	  DL L   	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! zz011 	TRSSS 	bIJJP^_`Paa	b 	bs   "B.A+B..B25B2c                .    |                                   d S rc   )r  r   s    r   pausezWakeWordDetector.pause  s    r   c                .    |                                   d S rc   )r  r   s    r   resumezWakeWordDetector.resume  r   r   c                `    |                                   | j                                         d S rc   )r  r  r   r   s    r   stopzWakeWordDetector.stop  s.    r   c                    | j         5  | j                                         | j        }|+|t	          j                    ur|                    d           | j        |u rd | _        d d d            d S # 1 swxY w Y   d S )Nr	   )timeout)r  r  setr  r  current_threadr1   r  s     r   r  zWakeWordDetector._halt_thread  s    Z 	$ 	$JNNA})*B*D*D!D!Ds###|q  #	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$s   AA33A7:A7c                   	 |                                   n2# t          $ r%}t                              d|           Y d }~nd }~ww xY w| j                                         d S # | j                                         w xY w)Nzwake word callback failed: %s)r  r_   rH   rI   r  r  )r   r   s     r   _dispatch_wakezWakeWordDetector._dispatch_wake  s    	,LLNNNN 	? 	? 	?NN:A>>>>>>>>	? #))+++++D#))++++s*    A$ 
AAA$ AA$ $A?r  threading.Eventr  list[BaseException]c           
        	 t                      \  }}nc# t          t          f$ rO}t                              d|           |                    |           |                                 Y d }~d S d }~ww xY w| j        j        }t          || j
                  | _        t                              d| j                            d          pd| j
        | j                            d          pd| j                            d          pdt                     	 |                    | j
        t          dd	|
          }|                                 n\# t"          $ rO}t                              d|           |                    |           |                                 Y d }~d S d }~ww xY w	 | j                                         n# t"          $ r Y nw xY wt                              d|t                     |                                 d}t'          dt)          t*          t          z  t'          d|          z                      }		 | j                                        s	 |                    |          \  }
}nM# t"          $ r@}t                              d|           | j                                         }Y d }~nPd }~ww xY wt5          |
dd          dk    r|
d d df         n|
}	 t7          |          r.t)          t9          |                                                    nd}n# t"          $ r t:          dz   }Y nw xY w|t:          k    r\| xj        dz  c_        | j        |	k    r@d| _        t                              dt:          t*          tA          | j                             n6| j        r/| j        rt                              d           d| _        d| _        	 | j        !                    |          }n3# t"          $ r&}t          "                    d|           Y d }~d }~ww xY w|rtG          j$                    }|| j%        z
  | j&        k    r|| _%        t                              d           | j'                                        sG| j'                                         tQ          j)        | j*        dd                                           nt          "                    d           | j                                        	 |+                                 |,                                 n# t"          $ r Y nw xY wt                              d           |r| j-        | -                    |            d S d S d S # 	 |+                                 |,                                 n# t"          $ r Y nw xY wt                              d           |r| j-        | -                    |            w w w xY w)Nz*wake word: audio libraries unavailable: %sz`wake word: opening microphone device=%s selector=%r hostapi=%s default_rate=%s requested_rate=%dr   r   r   unknownr   rz   int16)device
samplerater   rY  	blocksizez(wake word: failed to open microphone: %sz(wake word: listening (frame=%d, rate=%d)Fz wake word: stream read error: %sndim   r   Tz;wake word: mic delivers only silence (peak<=%d for %ds); %su0   wake word: mic audio detected — stream healthyzwake word: engine error: %su.   wake word: phrase detected — firing callbackzwake-word-callback)r  r  r   u0   wake word: detection within cooldown — ignoredzwake word: stream closed).r   rP   r   rH   r   appendr  r  r   r   r   r  r   rE   r]  InputStreamr  r_   r   rv   rk   _SILENCE_ALERT_SECONDSr  is_setreadrI   r   lenabs_SILENCE_PEAKr  r  r   r   rW   time	monotonicr  r  r  r  r  r  r  r   r  )r   r  r  r   r4  r   r   streamfailedsilent_alert_framesr  	_overflowr   peakrc  nows                   r   r  zWakeWordDetector._run  s:   	!OOEBW% 	 	 	LLEqIII!!!$$$IIKKKFFFFF		 {/$:2t?P$Q$Q!0%))&11E5E%)))44A	%))*>??L9	
 	
 	
	^^(&& $  F LLNNNN 	 	 	LLCQGGG!!!$$$IIKKKFFFFF			K 	 	 	D	 	>kZZZ		 "!S)?+)MPSTUWcPdPd)d%e%eff6	&j'')) ,Y&,kk,&?&?OD))    NN#EqIII!%!2!2!4!44FEEEE '.dFA&>&>!&C&CQQQT

-47JJE3s5zz~~//000ADD  - - -(1,DDD-=((''1,''*.AAA,0)Y)+A-d.GHH  
 ( .( X$VWWW*+D'(-D% K//66EE    LL!>BBBHHHH  Y.**CT_,==*-$TUUU#6==?? & 377999%,'+':'+%9   $eggg%WXXXY j'')) ,Y\   KK2333 &$/5%%%%%& &55   KK2333 &$/5%%%%&5s    A4AA//A48E 
F'AF""F'+G 
GGV	 #I< ;V	 <
K5K;V	 K&V	 -?L- ,V	 -MV	 MB V	 %P  ?V	  
P0
P+%V	 +P00C!V	 (T; ;
UU	W?(V43W?4
W>W? W>W?)
r  r   r  r  r  ro   r  r  r   ri   r   r   )r  r  r  r  r(   r   )r   r   r   r   _FIRE_COOLDOWN_SECONDSr   propertyr  r  r  r  r  r  r  r  r   r   r   r  r  d  s          $:NR26         * . . . X.b b b b.         $ $ $ $, , , ,h& h& h& h& h& h&r   r  zOptional[WakeWordDetector]	_detectorzobject | None_detector_ownerc                 .    ddl m}   |             dz  dz  S )Nr   get_default_hermes_rootruntimezwake-word.lock)r  r  r  s    r   
_lock_pathr  ,  s.    888888""$$y03CCCr   r0   c                   | pt                      }|j                            dd           t          |d          }	 t          j        dk    rddl}|                    dt          j                   |	                                dk    r)|
                    d           |                                 |                    d           |                    |                                |j        d           n9ddl}|                    |                                |j        |j        z             n=# t&          t(          f$ r)}|                                 t-          d	          |d}~ww xY w|S )
zCAcquire the cross-process microphone lease, or raise WakeWordInUse.Tr  za+bntr   N    rz   &Wake-word microphone is already owned.)r  parentr  r  r/   r   msvcrtseekSEEK_ENDtellrK  flushlockingfilenoLK_NBLCKfcntlflockLOCK_EXLOCK_NBr   BlockingIOErrorr   r   )r0   	lock_pathhandler  r  r   s         r   _acquire_machine_lockr  2  sK   $
I4$777)U##FM7d??MMMKK2;'''{{}}!!U###KKNNNNN6==??FOQ????LLLKK)FGGG_% M M MDEE1LM Ms   C2D1 1E+$E&&E+r   c                   | d S 	 t           j        dk    rHdd l}|                     d           |                    |                                 |j        d           n1dd l}|                    |                                 |j	                   n# t          $ r Y nw xY w|                                  d S # |                                  w xY w)Nr  r   rz   )r/   r   r  r  r
  r  LK_UNLCKr  r  LOCK_UNr   r   )r  r  r  s      r   _release_machine_lockr  K  s    ~7d??MMMKKNNNNN6==??FOQ????LLLKK777    	s*   B	B B6 
BB6 BB6 6Cdetectorc                   t           5  t          | ur	 ddd           dS t          }dadada	 | j                                         t          |           n# t          |           w xY w	 ddd           dS # 1 swxY w Y   dS )zDRelease ownership if the active microphone stream dies unexpectedly.N)_detector_lockr  _detector_file_lockr  r  r   r  )r  lock_handles     r   _detector_failedr  ^  s     
 
/ 
/H$$
/ 
/ 
/ 
/ 
/ 
/ 
/ 
/ *	"	/O!!###!+....!+.....
/ 
/ 
/ 
/ 
/ 
/ 
/ 
/ 
/ 
/ 
/ 
/ 
/ 
/ 
/ 
/ 
/ 
/s-   A9A9AA9A((A99A= A=)configr  r  ownerobjectr  c          	        |t          d          t          5  t          Pt          |urt	          d          | t          _        t                                           t          cddd           S t                      }	 ||nt                      }t          |          }t          || t          t          |                    }|a|a|a|                                 |cddd           S # t          $ rI t          +	 t                                           n# t          $ r Y nw xY wdadadat#          |            w xY w# 1 swxY w Y   dS )a/  Claim, build, and start the detector. Idempotent for the same owner.

    Raises if engine construction fails (missing deps / access key / model);
    callers should probe :func:`check_wake_word_requirements` first. A different
    owner, including another process, receives :class:`WakeWordInUse`.
    Nz wake-word owner must not be Noner  )r  r   )rt   r  r  r  r   r  r  r  r`   r  r  r  rn   r  r  r_   r  r  )r  r   r  r  rA   r  r  s          r   start_listeningr#  n  s    };<<< 
     e++#$LMMM 'I                ,--	".&&4I4K4KC"3''F'+*3//	  H !I#O"-NN+               ,  
	 
	 
	$NN$$$$    DI"O"&!+...
	-                   sO   AE1E A!C..E DE
D'$E&D''EEEEc                h    t           5  t          d uot          | u cd d d            S # 1 swxY w Y   d S rc   )r  r  r  r   s    r   owns_listenerr&    s    	 B B$AE)AB B B B B B B B B B B B B B B B B Bs   '++c                    t           5  t          	t          | ur	 ddd           dS t                                           	 ddd           dS # 1 swxY w Y   dS )z;Release the microphone only when ``owner`` holds the lease.NFT)r  r  r  r  r%  s    r   pause_listeningr(    s    	  u < <        		                    AAAAc                    t           5  t          	t          | ur	 ddd           dS t                                           	 ddd           dS # 1 swxY w Y   dS )z;Re-open the microphone only when ``owner`` holds the lease.NFT)r  r  r  r  r%  s    r   resume_listeningr+    s    	  u < <        		                 r)  c                   t           5  t          	t          | ur	 ddd           dS t          }t          }dadada	 |                                 t          |           n# t          |           w xY w	 ddd           dS # 1 swxY w Y   dS )z<Fully stop the detector only when ``owner`` holds the lease.NFT)r  r  r  r  r  r  )r   detr  s      r   stop_listeningr.    s     
  u < <        )	"	/HHJJJ!+....!+....                 s-   BBA B A11BB	Bc                 d    t           5  t          } d d d            n# 1 swxY w Y   | d uo| j        S rc   )r  r  r  r-  s    r   is_listeningr1    ss    	                d?*s{*     c                 d    t           5  t          } ddd           n# 1 swxY w Y   | duo| j        S )a  True when the armed stream has delivered only silence (dead mic).

    The stream opens fine but every frame is zeros, so detection can never
    fire. Lets status surfaces show "listening but the microphone appears
    silent" instead of a healthy state.
    N)r  r  r  r0  s    r   audio_is_silentr4    sv     
                d?/s//r2  c                b   t           5  t          }ddd           n# 1 swxY w Y   |t          |j                  S | | nt	                      } t          |           }	 t                      \  }}n0# t          t          f$ r}|t          |          dcY d}~S d}~ww xY wt          ||          S )zDReturn configured/active PortAudio input diagnostics for status UIs.N)r   r   )r  r  rF   r  r`   rn   r   rP   r   r'   r   )rA   r-  r   r   r4  r   s         r   get_input_device_statusr6    s   	                
C,---##&;&=&=CS!!H7AA! 7 7 7$s1vv666666667!"h///s*     A1 1BBBBr   c                     t           5  t          } ddd           n# 1 swxY w Y   | dS t          | j        dd          S )z(matched phrase, profile) of the most recent wake fire, if the engine
    reports per-phrase matches (sherpa multi-profile routing). None otherwise.Nr   )r  r  r   r  r0  s    r   get_last_matchr8    s     
                
{t3:|T222r2  )r%   )r&   r'   r(   r'   r   )r(   r'   )rA   r    r(   r'   )r(   r    )rA   r    ra   r'   r(   r   )rA   r    r(   ri   )rA   r    r(   ro   )rA   r    r(   rk   rc   )rA   r~   r(   r'   )r   r'   rA   r~   r(   r7   )r(   r   )r   ri   r(   r    )r   r    r(   r'   )rm   r'   r(   r7   )r(   r   )r  r  r(   r   )rA   r    r(   r   )rA   r~   r(   r    )r0   r  r   )r  r  r(   r   )r  r  r   r!  r  r~   r(   r  )r   r!  r(   r7   )r(   r   )Tr   
__future__r   loggingr/   r<   r  r  pathlibr   typingr   r   r   r   	getLoggerr   rH   r]  r  r  r{   r  r  r   r   r!   r   r4   	frozensetr   r6   r>   r@   rG   rK   rZ   r`   re   rh   rn   rx   r}   r   r   r   r   r   r   r   r   r   r   r   r   r  r  r  r  r  ri  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r#  r&  r(  r+  r.  r1  r4  r6  r8  r   r   r   <module>r?     s    > # " " " " "  				 



            0 0 0 0 0 0 0 0 0 0 0 0		8	$	$      !   N N N N NL N N N 7	 		 	 	 	 	 # "#M#M#MNN ` ` ` ` `F F F F5 5 5 5     F   <0 0 0 06 6 6 6
H H H H   ! ! ! !   4 4 4 4 4= = = = =$      L     " " " "J8 8 8 8   0       ,   Y Y Y Y Y' Y Y YBC  O 5 5 5 5    *u u u u uw u u up* * * * *w * * *ZB B B B    ) ) ) )XA A A A AP{& {& {& {& {& {& {& {&D )-	 , , , ,!% % % % % !!D D D D    2   &/ / / /( (,	0 0 0 0 0 0fB B B B
         $+ + + +	0 	0 	0 	00 0 0 0 0 3 3 3 3 3 3r   