+
    6ESj=7                       R t ^ RIHt ^ RIt^ RIt^ RIt^ RIHtHt ^ RI	t
RtRtRtRt^KRutt^t^tR	tR
tRt]P,                  ! R4      t] ! R R4      4       tRRuttR.R/uttRR.uttRtRt Rt!RRRRRRRRRRRR0RR1RR2/t"RRRRRR0/t#]! RR7       ! R  R!4      4       t$]PJ                  ! 4       t&]$! 4       s'R" R# lt(R$ R% lt)R& R' lt*R( R) lt+R* R+ lt, ! R, R-4      t-R# )3uK  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?iJ  g333333?g?g{Gz?z3(?:ha){2,}|(?:he){2,}|\bl(?:ol|mao|mfao)\b|\bhaha\bc                  b    ] tR t^9t$ RtRtR]R&   ]! ]R7      t	R]R&   ]! ]
R7      tR]R	&   R
tR# )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____firstlineno____doc__r   __annotations__r   listr	   r
   r   __static_attributes__r       voice/prosody.pyr   r   9   s0    OD*D1D)1-E4-r   r   g?g?g?g      ?皙?animated      ?loudgffffff?urgentfastlaughingflatquietslowT)frozenc                      ] tR t^Yt$ RtRtR]R&   RtR]R&   RtR]R&   ^ t	R]R&   ]
R	 R
 l4       t]
R R l4       t]
R R l4       t]
R R l4       t]
R R l4       t]
R R l4       t]
R R l4       tRtR# )	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laughintnc                   V ^8  d   QhRR/#    returnboolr   )formats   "r   __annotate__ToneState.__annotate__e   s    88D8r   c                	(    V P                   \        8  # N)r&   _TONE_UPselfs   &r   upToneState.upd   s    !%!88r   c                   V ^8  d   QhRR/# r,   r   )r0   s   "r   r1   r2   g       <<d<r   c                	(    V P                   \        8  # r4   )r&   
_TONE_HIGHr6   s   &r   highToneState.highf       #';;*#<<r   c                   V ^8  d   QhRR/# r,   r   )r0   s   "r   r1   r2   i   r;   r   c                	(    V P                   \        8*  # r4   )r&   
_TONE_DOWNr6   s   &r   downToneState.downh   r@   r   c                   V ^8  d   QhRR/# r,   r   )r0   s   "r   r1   r2   k   s    ::T:r   c                	(    V P                   \        8*  # r4   )r&   	_TONE_LOWr6   s   &r   lowToneState.lowj   s    "&++"::r   c                   V ^8  d   QhRR/# r,   r   )r0   s   "r   r1   r2   m       ::d:r   c                	(    V P                   \        8  # r4   )r'   
_TONE_FASTr6   s   &r   r   ToneState.fastl       #'99
#::r   c                   V ^8  d   QhRR/# r,   r   )r0   s   "r   r1   r2   o   rL   r   c                	(    V P                   \        8*  # r4   )r'   
_TONE_SLOWr6   s   &r   r    ToneState.slown   rP   r   c                   V ^8  d   QhRR/# r,   r   )r0   s   "r   r1   r2   q   s    ??$?r   c                	(    V P                   \        8  # r4   )r(   
_LAUGH_HOTr6   s   &r   r   ToneState.laughingp   s    '+zzZ'? ?r   r   N)r   r   r   r   r   r&   r   r'   r(   r*   propertyr8   r>   rD   rI   r   r    r   r   r   r   r   r#   r#   Y   s    D FED%E5AsJ8 8< << <: :: :: :? ?r   r#   c                   V ^8  d   QhRR/# )r-   r.   r#   r   )r0   s   "r   r1   r1   x   s      I r   c                     \         # )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_stater]   x   s
     r   c                    V ^8  d   QhRRRR/# )r-   prz	'Prosody'r.   Noner   )r0   s   "r   r1   r1   ~   s      i D r   c                   V P                   ;'       g    . pRV9   ;'       g%    \        V P                  P                  R4      4      p\	        R
\        R\        R V 4       4      4      4      p\	        R\        R\        R V 4       4      4      4      p\        ;_uu_ 4        \        p\        VP                  \        W5P                  ,
          ,          ,           VP                  \        WEP                  ,
          ,          ,           \	        V'       d   RMRVP                  \        ,          4      VP                  ^,           R7      sR	R	R	4       R	#   + '       g   i     R	# ; i)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(   g333333?c              3  N   "   T F  p\         P                  VR 4      x  K  	  R# 5ir$   N)_TONE_ENERGY_Wget.0ts   & r   	<genexpr> _publish_tone.<locals>.<genexpr>   s      #M1N$6$6q#$>$>   #%r   c              3  N   "   T F  p\         P                  VR 4      x  K  	  R# 5irc   )_TONE_PACE_Wre   rf   s   & r   ri   rj      s      #KdL$4$4Q$<$<drk   r$   )r&   r'   r(   r*   Ng333333      )r	   r/   r   re   maxminsum
_TONE_LOCKr\   r#   r&   _TONE_ALPHAr'   r(   _LAUGH_DECAYr*   )r_   r	   r   e_instp_instcurs   &     r   _publish_tonerx   ~   s    
 77==bDd"BBtBHHLL,A'BHs3#M#M MNOFs3#Kd#K KLMF	::v

/B CCK6HH+<==Xc3		L0HIeeai	 
s   "B(EE%	c                    V ^8  d   QhRRRR/# )r-   textstrr.   r/   r   )r0   s   "r   r1   r1      s     % %# %$ %r   c                    \         P                  ! R RT ;'       g    RP                  4       4      p\        \        P                  V4      4      # )z\s+ )resublowerr/   	_LAUGH_REsearch)rz   rh   s   & r   _laughyr      s9    
vrDJJB--/0A	  #$$r   c                    V ^8  d   QhRRRR/# r-   audio
np.ndarrayr.   r   )r0   s   "r   r1   r1      s     2 2j 2Z 2r   c                8   \        V 4      \        ,          \        ,          pV^ 8X  d'   \        P                  ! ^ \        P                  R7      # V RV P                  R\        4      p\        P                  ! V^,          P                  ^R7      R,           4      # )    dtypeN)axis&.>)lenFRAMEnpzerosfloat32reshapesqrtmean)r   r*   frs   &  r   
_frame_rmsr      sl    E
ee#AAvxx,,	r		2u	%B77B!G>>q>)D011r   c                    V ^8  d   QhRRRR/# r   r   )r0   s   "r   r1   r1      s     - -
 -z -r   c                r   V R^\         ,           p \        V 4      \        ,          \        ,          pV^ 8X  d'   \        P                  ! ^ \        P
                  R7      # V RV P                  R\        4      p\         \        ,          \         \        ,          rC. pV F  p\        P                  ! V^,          P                  4       4      \        8  d   K9  WfP                  4       ,
          p\        P                  ! WfR4      \        ^,
          R pV^ ,          pVR8:  d   K  WsV p	V	P                  ^ 8X  d   K  \        V	P                  4       4      p
W,          V,          \         8  d   K  VP#                  \         W:,           ,          4       K  	  \        P$                  ! V\        P
                  R7      # )u3  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.Nr   fullgư>r   )SAMPLE_RATEr   r   r   r   r   r   _F0_HI_F0_LOr   r   _VOICED_GATE	correlatesizer)   argmax_PERIODICITYappendasarray)r   r*   frameslohioutfaczsegks   &          r   _pitch_trackr      s7   
 "1{?#EE
ee#AAvxx,,2AYr5)FF"K6$9C77AF==?#l2L\\!'	
3qE9Bi88q=

6A:$

;"&)*  ::c,,r   c                  N    ] tR t^tRtR R ltR R ltRR R lltR	 R
 ltRt	R# )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                   V ^8  d   QhRR/# r-   r.   r`   r   )r0   s   "r   r1   ProsodyReader.__annotate__   s     = =$ =r   c                	    ^ V n         RV n        RV n        RV n        RV n        \
        P                  P                  RR4      R8g  V n        R# )r   NPROSODY10)	_n	_base_wpm_base_prange
_base_loud
_base_evarosenvironre   enabledr6   s   &r   __init__ProsodyReader.__init__   sA    '+*.(,(,zz~~i5<r   c                   V ^8  d   QhRR/# r   r   )r0   s   "r   r1   r      s     
 
4 
r   c                	V   V P                   R J pR pV'       d   TMV! V P                   V4      V n         V! V P                  V4      V n        V'       d   TMV! V P                  V4      V n        V'       d   TMV! V P                  V4      V n        V ;P                  ^,          un        R # )Nc                N    Vf   V # V f   V# V \         W,
          ,          ,           # r4   )_EMA)rw   news   &&r   step,ProsodyReader._update_baseline.<locals>.step   s'    {
+3C31C+CCr   )r   r   r   r   r   )r7   wpmpranger   evarfirstr   s   &&&&&  r   _update_baselineProsodyReader._update_baseline   s|    $&	D !&4+D !2!2F;"'$T$//4-H"'$T$//4-H1r   Nc          
     ,    V ^8  d   QhRRRRRRRRR	R
/# )r-   r   r   speech_sr%   rz   r{   r   zfloat | Noner.   r   r   )r0   s   "r   r1   r      s1      *  S +2r   c                    V P                   '       g   \        4       #  V P                  WW44      p \	        T4       T#   \         d    \        4       u # i ; i  \         d     T# i ; i)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	Exceptionrx   )r7   r   r   rz   r   r_   s   &&&&& r   readProsodyReader.read   so    
 |||9	ET8B	" 	  	9	  			s    > A AAA('A(c                   V ^8  d   QhRR/# )r-   r.   r   r   )r0   s   "r   r1   r      s     W: W:G W:r   c                	
  a \        V4      p\        T;'       g    R P                  4       4      pVf_   \        \        P
                  ! \        P                  ! VP                  \        P                  4      ^,          4      R,           4      4      pV\        8  g   V^ 8X  d)   V'       d   \        RR.RR/4      # \        RRRV/R7      # R	V,          \        VR
4      ,          p\        V4      pVP                  '       d6   \        VP                  4       VP                  4       R,           ,          4      MRp	\        V4      p
V
P                  \         8  dM   \        P"                  ! V
^^K.4      w  r\        W,
          4      p\        \        P$                  ! V
4      4      pMR;rR\'        V4      R\'        V^4      RV'       d   \'        V4      M^ RVe   \'        V4      MRR\'        V	^4      R\)        V
P                  4      RVRV P*                  /pV\,        8  ;'       d    V
P                  \         8  pV P*                  \.        8  g   V P0                  fD   V'       d   V P3                  W}WI4       V'       d   RMRp\        TV'       d	   R.V4      # . V4      # . oV'       d   SP5                  R4       \7        V P8                  ;'       d    W@P8                  R,          8  4      pV^8  d\   WpP0                  R,          8  d   SP5                  V'       d   RMR4       M(WpP0                  R,          8  d   SP5                  R4       Ve   V P:                  '       d   V P<                  e   WP:                  R,          8  ;'       g    WP<                  R,          8  pWP:                  R,          8  ;'       d    WP<                  R,          8  pV'       d   SP5                  R4       MV'       d   SP5                  R4       V'       dS   \>        ;QJ d    V3R lR' 4       F  '       g   K   RM	  R M! V3R lR' 4       4      '       g   SP5                  R4       M:V P8                  '       d)   W@P8                  R!,          8  d   SP5                  R"4       \>        ;QJ d    V3R# lR( 4       F  '       g   K   RM	  R M! V3R# lR( 4       4      pV'       g   V'       d   V P3                  W}WI4       . pS F  pVV9  g   K  VP5                  V4       K  	  VR$,          pV'       d   R%PA                  V4      MRp\        VVVR&7      # ))r}   Nr   r   laughterTshortwords)r   g      N@g333333?r$   r   r   pitchr   r   voicedr(   r*   g      ?g?r   r   g
ףp=
?r    g?g333333?r   r   r   c              3  ,   <"   T F	  qS9   x  K  	  R # 5ir4   r   rg   rh   r	   s   & r   ri   &ProsodyReader._read.<locals>.<genexpr>(  s     F3EaT	3E   Fr   r   c              3  ,   <"   T F	  qS9   x  K  	  R # 5ir4   r   r   s   & r   ri   r   0  s      *) &'Dy)r   :N   Nz, )r   r	   r   )r   r   )r   r    r   r   r   r   r   )!r   r   splitr%   r   r   r   astyper   _MIN_SPEECH_Sr   ro   r   r   stdr   _MIN_VOICED
percentilemedianroundr)   r   _SEED_MIN_LOUD_SEED_Nr   r   r   r/   r   r   r   anyjoin)r7   r   r   rz   r   r   n_wordsr   frmsr   r   p25p75r   	pitch_medr   seedabler   loud_hitr   r   deviatedseenrh   r	   s   &&&&&                   @r   r   ProsodyReader._read   s   4=tzzr((*+<bjj)AQ)F!G$!NOPDm#w!|zJ<*d9KLL'4'!BCCWns8S11% ;?999uTXXZ499;#567#U#::$}}URH5HC39%Fbii./I!%%F 5:vuT1~U9%v'9eFmtE$NHc%**oXsDGG
 >)GGejjK.G77W 6%%c4>!):tD4*5IIr5IIKK
# HHD??S3H,HIa<^^d**Hf=~~,,F#
 $"3"3"38S!2!2S!88 8 8//C"77 ..44 5 544 J'F# CCF3EFCCCF3EFFFKK___$(>!>KK 
 3 *)*333 *)* * H!!#t:A}A  Bx"&tyyDDt599r   )r   r   r   r   r   r   r4   )
r   r   r   r   r   r   r   r   r   r   r   r   r   r   r      s#    M=
"W: W:r   r   gٿgrn   g333333g      ).r   
__future__r   r   r~   	threadingdataclassesr   r   numpyr   r   r   r   r   r   r   r   r   r   r   r   compiler   r   r5   r=   rC   rH   rN   rS   rW   rs   rt   rd   rm   r#   Lockrr   r\   r]   rx   r   r   r   r   r   r   r   <module>r      sL  < # 	 	  ( S JJMN	 . . .$ T *u 
Iu 
J
c63#vsc64$NXsFD9 $@ @ @4 ^^
k$%
2-<A: A:r   