
    (HJj                    2   U d Z ddlmZ ddlmZ ddlmZ ddlmZm	Z	 ddl
ZdZded	<   d
Zded<   dez  ez  Zded<    G d de      Z ed       G d d             ZddZ G d dee      Z ed       G d d             Z G d d      Z G d d      Zy)u  Server-side turn detection for the OpenAI-compatible ``/v1/realtime`` endpoint.

The realtime WebSocket endpoint transcribes a continuous audio stream, but on
its own it has no notion of *when a speaker's turn ends* — the client has to
send ``input_audio_buffer.commit`` by hand. This module adds OpenAI-compatible
``turn_detection`` so the server itself decides turn boundaries: it emits
``input_audio_buffer.speech_started`` / ``input_audio_buffer.speech_stopped``
and auto-commits, exactly like OpenAI's ``server_vad`` mode.

Only ``server_vad`` is implemented. It runs a streaming VAD model (Silero by
default) frame by frame and applies the same ``threshold`` /
``prefix_padding_ms`` / ``silence_duration_ms`` logic the OpenAI Realtime API
exposes. The endpointing logic (:class:`TurnDetector`) is a pure state machine,
deliberately free of any model dependency so it can be unit-tested with
synthetic probabilities; :class:`StreamingVad` adds the model and the framing.
    )annotations)	dataclass)Enum)ListOptionalNi>  intVAD_SAMPLE_RATEi   VAD_FRAME_SIZEg     @@floatVAD_FRAME_MSc                      e Zd ZdZy)TurnDetectionErrorzGRaised when a client requests an unsupported ``turn_detection`` config.N)__name__
__module____qualname____doc__     `/Users/ahmed/devFolder/claude-voice/.venv/lib/python3.12/site-packages/mlx_audio/realtime_vad.pyr   r   !   s    Qr   r   T)frozenc                  D    e Zd ZU dZdZded<   dZded<   dZded	<   dd
Zy)ServerVadConfigz@Resolved ``server_vad`` parameters, mirroring the OpenAI schema.g      ?r   	thresholdi,  r   prefix_padding_msi  silence_duration_msc                L    d| j                   | j                  | j                  dS )N
server_vad)typer   r   r   r   r   r   selfs    r   to_dictzServerVadConfig.to_dict-   s(     !%!7!7#'#;#;	
 	
r   N)returndict)	r   r   r   r   r   __annotations__r   r   r"   r   r   r   r   r   %   s*    JIu s ""
r   r   c           
     r   | sy| j                  d      }|dk(  rt               }t        t        | j                  d|j                              t	        | j                  d|j
                              t	        | j                  d|j                                    S |dk(  rt        d	      t        d
|      )zMap an OpenAI ``turn_detection`` object onto a :class:`ServerVadConfig`.

    Returns ``None`` for ``null`` (manual-commit mode). Raises
    :class:`TurnDetectionError` for ``semantic_vad`` (not implemented yet) or an
    unknown ``type``.
    Nr   r   r   r   r   r   semantic_vadz@semantic_vad is not supported by this server yet; use server_vadzunknown turn_detection type: )getr   r   r   r   r   r   r   )turn_detectiontd_typedefaultss      r   parse_turn_detectionr,   6   s     +//7G,$3$5N..{H<N<NOP!""#68R8RS !$""#8(:V:VW!
 	
 .  N
 	
 <WKH
IIr   c                      e Zd ZdZdZy)TurnEventKindspeech_startedspeech_stoppedN)r   r   r   SPEECH_STARTEDSPEECH_STOPPEDr   r   r   r.   r.   R   s    %N%Nr   r.   c                  &    e Zd ZU dZded<   ded<   y)	TurnEventzHA detected turn boundary. ``audio_ms`` is the offset from session start.r.   kindr   audio_msN)r   r   r   r   r%   r   r   r   r4   r4   W   s    R
Mr   r4   c                  :    e Zd ZdZddZddZed	d       Zd
dZy)TurnDetectoru-  Pure endpointing state machine over per-frame speech probabilities.

    Feed it one VAD probability per frame via :meth:`push`. It emits a
    ``SPEECH_STARTED`` when the probability first crosses ``threshold`` and a
    ``SPEECH_STOPPED`` once ``silence_duration_ms`` of sub-threshold audio has
    elapsed after speech. ``prefix_padding_ms`` only shifts the reported
    ``audio_start_ms`` earlier — it does not gate transcription.

    The running clock is kept across turns so reported offsets stay monotonic
    for the lifetime of the session.
    c                <    || _         d| _        d| _        d| _        y )N        F)_config_elapsed_ms
_in_speech_silence_ms)r!   configs     r   __init__zTurnDetector.__init__l   s     (."% %"%r   c                   | xj                   |z  c_         g }|| j                  j                  k\  }| j                  st|rpd| _        d| _        | j                   |z
  | j                  j
                  z
  }|j                  t        t        j                  t        dt        |                         |S |r	d| _        |S | xj                  |z  c_        | j                  | j                  j                  k\  rJd| _        d| _        |j                  t        t        j                  t        | j                                      |S )NTr:   r   F)r<   r;   r   r=   r>   r   appendr4   r.   r1   maxr   r   r2   )r!   probabilityframe_msevents	is_speechstarts         r   pushzTurnDetector.pushr   s   H$"$%)?)??	"&#& $$x/$,,2P2PP  m::C3u:<NO  #&     H, ##t||'G'GG&+DO'*D$MM!-">">DDTDT@UV r   c                    | j                   S N)r=   r    s    r   	in_speechzTurnDetector.in_speech   s    r   c                     d| _         d| _        y)z=Clear speech state after a turn is committed; keep the clock.Fr:   N)r=   r>   r    s    r   
reset_turnzTurnDetector.reset_turn   s    r   Nr?   r   )rD   r   rE   r   r#   List[TurnEvent]r#   boolr#   None)	r   r   r   r   r@   rI   propertyrL   rN   r   r   r   r8   r8   _   s*    
&6  r   r8   c                  :    e Zd ZdZddZddZed	d       Zd
dZy)StreamingVada  Drive a :class:`TurnDetector` from a streaming VAD model.

    ``vad_model`` must expose the Silero streaming protocol:
    ``initial_state(sample_rate=...)`` and
    ``feed(chunk, state, sample_rate=...) -> (probability, state)``, consuming
    fixed :data:`VAD_FRAME_SIZE`-sample windows at :data:`VAD_SAMPLE_RATE`.
    Audio that doesn't fill a whole frame is buffered until the next call.
    c                    || _         || _        |j                  t              | _        t        |      | _        t        j                  dt        j                        | _
        y )Nsample_rater   )dtype)_vadr;   initial_stater	   _stater8   	_detectornpzerosfloat32_buffer)r!   	vad_modelr?   s      r   r@   zStreamingVad.__init__   sG    	(.--/-J'3F';#%88ARZZ#@r   c                   ddl m} |j                  rCt        j                  | j
                  |j                  t        j                        g      | _        g }| j
                  j                  d   t        k\  r| j
                  dt         }| j
                  t        d | _        | j                  j                  || j                  t              \  }| _        |j                  |       t        t        j                   |      j#                  d      d         }|j%                  | j&                  j)                  |t*                     | j
                  j                  d   t        k\  r|S )zFeed 16 kHz float32 ``samples``; return any turn events detected.

        Runs MLX work, so call it from a worker thread rather than the event
        loop.
        r   NrY   )mlx.corecoresizer`   concatenaterc   astyperb   shaper
   r\   feedr^   r	   evalr   arrayreshapeextendr_   rI   r   )r!   samplesmxrF   framerD   probs          r   processzStreamingVad.process   s	    	<<>>4<<

9S*TUDL"$ll  #~5 $_n =E<<8DL'+yy~~t{{ (6 ($K GGK  5 = =b A! DEDMM$..--dLAB ll  #~5 r   c                .    | j                   j                  S rK   )r_   rL   r    s    r   rL   zStreamingVad.in_speech   s    ~~'''r   c                8    | j                   j                          y rK   )r_   rN   r    s    r   rN   zStreamingVad.reset_turn   s    !!#r   NrO   )rr   z
np.ndarrayr#   rP   rQ   rS   )	r   r   r   r   r@   rv   rU   rL   rN   r   r   r   rW   rW      s+    A, ( ($r   rW   )r)   zOptional[dict]r#   zOptional[ServerVadConfig])r   
__future__r   dataclassesr   enumr   typingr   r   numpyr`   r	   r%   r
   r   
ValueErrorr   r   r,   strr.   r4   r8   rW   r   r   r   <module>r      s   " # !  !    ~-?e ?R R $
 
 
 J8&C &
 $  5 5p,$ ,$r   