
    6ESj=7              	         d Z ddlmZ ddlZddlZddlZddlmZmZ ddl	Z
dZdZdZdZd	\  ZZd
ZdZdZdZdZ ej,                  d      Ze G d d             Zd\  ZZd\  ZZd\  ZZdZdZ dZ!dddddddddZ"ddddZ# ed       G d  d!             Z$ ejJ                         Z& e$       a'd)d"Z(d*d#Z)d+d$Z*d,d%Z+d,d&Z, G d' d(      Z-y)-uK  Vocal-tone reader: HOW Ahmed said it, not WHAT he said.

The STT pipeline reduces every utterance to bare text and throws the voice
away — pace, pitch, loudness, laughter, all gone before any brain sees it.
This module recovers a few cheap acoustic facts from the SAME utterance buffer
the speaker-lock already runs on (main.py) and turns them into a one-line
descriptor like "fast, urgent" or "slow, flat" that rides along with the
transcript into both brains.

Design notes:
- **Pure numpy, ~2 ms per utterance.** No model, no torch, no GPU, no numba JIT
  (librosa.yin would add a 6.5 s first-call compile — not worth it). About 7x
  cheaper than the TitaNet speaker embedding that already runs every turn.
- **We do NOT classify emotion.** Sarcasm/joking aren't decidable from sound
  alone — the descriptor reports the delivery (e.g. "slow, flat") and Claude
  infers intent from delivery + words + context, the way a person does.
- **Relative to the speaker.** Everyone sits at a different pitch/pace/volume,
  so we track a rolling EMA baseline of Ahmed's own NEUTRAL delivery and describe
  deviations from it. The baseline only learns from neutral, clean, loud-enough
  turns so one shouty or one whisper-quiet utterance can't drag it.
- **Pitch uses a periodicity gate.** A frame only contributes an F0 if its
  autocorrelation peak is a strong fraction of zero-lag (real voicing); noise —
  including a quiet clip that main.py amplified ~10x — has a weak peak and is
  rejected, so amplified hiss can't fake a huge pitch range. Pitch is only
  trusted when enough voiced frames survive; otherwise pitch tags are skipped.
- **Rate & pitch dynamics are the reliable dimensions** (the AEC mic's AGC
  flattens absolute volume, so loudness is a soft, baseline-relative hint).
- **Never throws into the run loop.** Any failure returns a neutral read.
    )annotationsN)	dataclassfieldi>  i   g{Gz?gffffff?)K   iJ        g333333?g?g{Gz?z3(?:ha){2,}|(?:he){2,}|\bl(?:ol|mao|mfao)\b|\bhaha\bc                  X    e Zd ZU dZdZded<    ee      Zded<    ee	      Z
ded	<   y)
Prosodyu   One utterance's read. `note` is None when delivery is unremarkable —
    the caller injects nothing on neutral turns so the brain isn't spammed.Nz
str | Nonenote)default_factoryz	list[str]tagsdictfeats)__name__
__module____qualname____doc__r   __annotations__r   listr   r   r        ;/Users/ahmed/devFolder/Ultron/claude-voice/voice/prosody.pyr
   r
   9   s0    OD*D1D)1-E4-r   r
   )g?g?)皙ٿg)g?r   g      ?皙?      ?gffffff?      g333333g      )animatedloudurgentfastlaughingflatquietslow)r    r   r$   T)frozenc                      e Zd ZU dZdZded<   dZded<   dZded<   dZded	<   e	dd
       Z
e	dd       Ze	dd       Ze	dd       Ze	dd       Ze	dd       Ze	dd       Zy)	ToneStateu$  A smoothed read of Ahmed's recent vocal energy — the snapshot the TTS
    colours delivery with. `energy` < 0 = flat/tired, > 0 = animated; `pace`
    < 0 = slow; `laugh` decays from 1.0 the turn he laughs. Frozen so a worker
    thread can read it while the run loop swaps in the next one.        floatenergypacelaughr   intnc                (    | j                   t        k\  S N)r*   _TONE_UPselfs    r   upzToneState.upd   s    !%!88r   c                (    | j                   t        k\  S r0   )r*   
_TONE_HIGHr2   s    r   highzToneState.highf       #';;*#<<r   c                (    | j                   t        k  S r0   )r*   
_TONE_DOWNr2   s    r   downzToneState.downh   r8   r   c                (    | j                   t        k  S r0   )r*   	_TONE_LOWr2   s    r   lowzToneState.lowj   s    "&++"::r   c                (    | j                   t        k\  S r0   )r+   
_TONE_FASTr2   s    r   r    zToneState.fastl       #'99
#::r   c                (    | j                   t        k  S r0   )r+   
_TONE_SLOWr2   s    r   r$   zToneState.slown   rA   r   c                (    | j                   t        k\  S r0   )r,   
_LAUGH_HOTr2   s    r   r!   zToneState.laughingp   s    '+zzZ'? ?r   N)returnbool)r   r   r   r   r*   r   r+   r,   r.   propertyr4   r7   r;   r>   r    r$   r!   r   r   r   r'   r'   Y   s    D FED%E5AsJ8 8< << <: :: :: :? ?r   r'   c                     t         S )u   Latest smoothed vocal-tone snapshot (never None; neutral until warmed).
    O(1), lock-free read — safe from the TTS worker thread at synth time.)_TONE_STATEr   r   r   
tone_staterK   x   s
     r   c           
     L   | j                   xs g }d|v xs$ t        | j                  j                  d            }t	        dt        dt        d |D                          }t	        dt        dt        d |D                          }t        5  t        }t        |j                  t        ||j                  z
  z  z   |j                  t        ||j                  z
  z  z   t	        |rdnd	|j                  t        z        |j                  d
z         addd       y# 1 sw Y   yxY w)zFold one read into the rolling tone EMA and publish the new snapshot.
    Side-effect only; reuses the tags read() already produced, so there's no
    extra DSP and no new call site anywhere.r!   r,   g333333g333333?c              3  H   K   | ]  }t         j                  |d         ywr(   N)_TONE_ENERGY_Wget.0ts     r   	<genexpr>z _publish_tone.<locals>.<genexpr>   s     #M1N$6$6q#$>    "r   r   c              3  H   K   | ]  }t         j                  |d         ywrN   )_TONE_PACE_WrP   rQ   s     r   rT   z _publish_tone.<locals>.<genexpr>   s     #KdL$4$4Q$<drU   r(      )r*   r+   r,   r.   N)r   rG   r   rP   maxminsum
_TONE_LOCKrJ   r'   r*   _TONE_ALPHAr+   r,   _LAUGH_DECAYr.   )prr   r!   e_instp_instcurs         r   _publish_tonerc   ~   s    
 77=bDd"BtBHHLL,A'BHs3#M#M MNOFs3#Kd#K KLMF	::v

/B CCK6CHH+<==Xc3		L0HIeeai	 
s   BDD#c                    t        j                  dd| xs dj                               }t        t        j                  |            S )Nz\s+ )resublowerrG   	_LAUGH_REsearch)textrS   s     r   _laughyrl      s7    
vrDJB--/0A	  #$$r   c                   t        |       t        z  t        z  }|dk(  r%t        j                  dt        j                        S | d | j                  dt              }t        j                  |dz  j                  d      dz         S )Nr   dtype   rX   )axis&.>)lenFRAMEnpzerosfloat32reshapesqrtmean)audior.   frs      r   
_frame_rmsr~      sl    E
ee#AAvxx,,	r		2u	%B77B!G>>q>)D011r   c                   | ddt         z   } t        |       t        z  t        z  }|dk(  r%t        j                  dt        j
                        S | d| j                  dt              }t         t        z  t         t        z  }}g }|D ]  }t        j                  |dz  j                               t        k  r1||j                         z
  }t        j                  ||d      t        dz
  d }|d   }|d	k  rp||| }	|	j                  dk(  rt        |	j                               }
|	|
   |z  t         k  r|j#                  t         ||
z   z          t        j$                  |t        j
                        S )
u?  Per-voiced-frame F0 via autocorrelation with a PERIODICITY gate: a frame
    only counts if its best in-band autocorr peak is >= _PERIODICITY of zero-lag.
    That rejects noise/unvoiced frames (weak peak) — including amplified silence —
    so hiss can't fake pitch. Analyses up to the first 5 s for a stable read.N   r   rn   rp   rq   fullrX   gư>)SAMPLE_RATErt   ru   rv   rw   rx   ry   _F0_HI_F0_LOrz   r{   _VOICED_GATE	correlatesizer-   argmax_PERIODICITYappendasarray)r|   r.   frameslohioutfaczsegks              r   _pitch_trackr      sC   
 "1{?#EE
ee#AAvxx,,2AYr5)FF"K6$9BC77AF==?#l2L\\!Q'	
3qE9Bi88q=

q6A:$

;"q&)*  ::c,,r   c                  :    e Zd ZdZddZddZ	 d	 	 	 d	dZd
dZy)ProsodyReaderzReads vocal tone off raw utterance audio, relative to a rolling baseline
    of the owner's own neutral delivery. One instance per session; call read()
    per turn (mirrors SpeakerGate's construct-once/check-per-turn shape).c                    d| _         d | _        d | _        d | _        d | _        t
        j                  j                  dd      dk7  | _        y )Nr   PROSODY10)	_n	_base_wpm_base_prange
_base_loud
_base_evarosenvironrP   enabledr2   s    r   __init__zProsodyReader.__init__   sA    '+*.(,(,zz~~i5<r   c                (   | j                   d u }d }|r|n || j                   |      | _          || j                  |      | _        |r|n || j                  |      | _        |r|n || j                  |      | _        | xj                  dz  c_        y )Nc                0    || S | |S | t         || z
  z  z   S r0   )_EMA)rb   news     r   stepz,ProsodyReader._update_baseline.<locals>.step   s)    {
+3C3s1C+CCr   rX   )r   r   r   r   r   )r3   wpmpranger   evarfirstr   s          r   _update_baselinezProsodyReader._update_baseline   s|    $&	D !&4+D !2!2F;"'$T$//4-H"'$T$//4-H1r   Nc                    | j                   s
t               S 	 | j                  ||||      }	 t	        |       |S # t        $ r t               cY S w xY w# t        $ r Y |S w xY w)zaudio: float32 mono @ 16 kHz utterance. speech_s: net voiced seconds.
        text: transcript. loud: PRE-normalization peak/level (main.py has it);
        falls back to this clip's RMS. Never raises.)r   r
   _read	Exceptionrc   )r3   r|   speech_srk   r   r_   s         r   readzProsodyReader.read   sq    
 ||9	E8T48B	" 	  	9	  			s    : A AA	A A c           	     J   t        |      }t        |xs dj                               }|Tt        t	        j
                  t	        j                  |j                  t        j                        dz        dz               }|t        k  s|dk(  r!|rt        ddgddi      S t        d|d	      S d
|z  t        |d      z  }t        |      }|j                  r-t        |j                         |j                         dz   z        nd}	t        |      }
|
j                  t         k\  rHt	        j"                  |
ddg      \  }}t        ||z
        }t        t	        j$                  |
            }nd x}}t'        |      t'        |d      |rt'        |      nd|t'        |      nd t'        |	d      t)        |
j                        || j*                  d}|t,        k\  xr |
j                  t         k\  }| j*                  t.        k  s| j0                  3|r| j3                  ||||	       |rdnd }t        ||rdg|      S g |      S g |rj5                  d       t7        | j8                  xr || j8                  dz  kD        }|dk\  rK|| j0                  dz  kD  rj5                  |rdnd       n#|| j0                  dz  k  rj5                  d       || j:                  r| j<                  s|| j:                  dz  kD  xs |	| j<                  dz  kD  }|| j:                  dz  k  xr |	| j<                  dz  k  }|rj5                  d       n|rj5                  d       |r&t?        fddD              sj5                  d       n/| j8                  r#|| j8                  d z  k  rj5                  d!       t?        fd"d#D              }|s|r| j3                  ||||	       g }D ]  }||vs|j5                  |        |d d }|rd$jA                  |      nd }t        |||%      S )&Nre   rq   rs   r   r!   laughterT)shortwords)r   g      N@g333333?r(      r      )r   r   pitchr   r   voicedr,   r.   g      ?r   g?r   r    g
ףp=
?r$   g?g333333?r   r   r"   c              3  &   K   | ]  }|v  
 y wr0   r   rR   rS   r   s     r   rT   z&ProsodyReader._read.<locals>.<genexpr>(  s     F3EaT	3E   )r   r    r   r   r#   c              3  &   K   | ]  }|v  
 y wr0   r   r   s     r   rT   z&ProsodyReader._read.<locals>.<genexpr>0  s!      *) &'qDy)r   )r    r$   r   r   r"   r   r#   z, )r   r   r   )!rl   rt   splitr)   rv   rz   r{   astyperx   _MIN_SPEECH_Sr
   rY   r~   r   stdr   _MIN_VOICED
percentilemedianroundr-   r   _SEED_MIN_LOUD_SEED_Nr   r   r   rG   r   r   r   anyjoin)r3   r|   r   rk   r   r!   n_wordsr   frmsr   r   p25p75r   	pitch_medr   seedabler   loud_hitr   r"   deviatedseenrS   r   s                           @r   r   zProsodyReader._read   s   4=tzr((*+<bjj)AQ)F!G$!NOPDm#w!|zJ<*d9KLL4'!BCCWns8S11% ;?99uTXXZ499;#567#U#::$}}URH5HC39%Fbii./I!%%FY :uT1~)2U9%'-'9eFmt$Nc%**oDGG
 >)GejjK.G77W 6%%c64>!):tD4*5IIr5IIKK
# HD4??S3H,HIa<T^^d**Hf=t~~,,F#
 $"3"38S!2!2S!88 8$//C"77 T..44 5t44 J'F# CF3EFFKK__$(>!>KK 
  *)* * H!!#vtT:A}A  BQx"&tyyDDt599r   )rF   Noner0   )
r|   
np.ndarrayr   r)   rk   strr   zfloat | NonerF   r
   )rF   r
   )r   r   r   r   r   r   r   r   r   r   r   r   r      s2    M=
 #'+2"W:r   r   )rF   r'   )r_   z	'Prosody'rF   r   )rk   r   rF   rG   )r|   r   rF   r   ).r   
__future__r   r   rf   	threadingdataclassesr   r   numpyrv   r   ru   r   r   r   r   r   r   r   r   r   compileri   r
   r1   r6   r:   r=   r@   rC   rE   r]   r^   rO   rW   r'   Lockr\   rJ   rK   rc   rl   r~   r   r   r   r   r   <module>r      s2  < # 	 	  (  BJJMN	 . . .$ " *$ 
I$ 
J
!3#s!4$NsD9 $@ @ @4 Y^^
k$%
2-<A: A:r   