Ë
    Š‰Œj$  ã                  óØ   — d Z ddlm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
  eej                  j                  dd«      «      Zd	d
dddœiZ G d„ d«      Z G d„ d«      Zdd„Zedœd„Zy)u  Where the spoken words come from â€” one interface, two engines.

WHY THIS EXISTS. Commentary audio was synthesised by exactly one vendor, reached by exactly one code
path, and the coupling cost more than it looked. Three separate problems, one shape:

  * **Money.** Google's tier auto-selection lands on the most expensive voice that exists, and on
    13 August 2026 â€” one championship day â€” Text-to-Speech billed **kr 7,480 in Studio voices alone**,
    against an expected kr 277 for the whole estate.
  * **Availability.** `TextToSpeechClient()` needs Application Default Credentials at *construction*,
    so a box without them lost the whole synthesis thread rather than degrading. The office
    deployment has no ADC and is not getting any.
  * **Sovereignty.** Every spoken line left the building to be spoken. That is a vendor dependency in
    the one part of the broadcast that is pure computation.

A local engine answers all three at once, so the choice belongs behind an interface rather than an
`if` in the middle of the cache loop.

WHAT AN ENGINE OWES. Two things and no more: which voice each commentary role speaks in, and how a
line becomes audio bytes. Everything else â€” the queue, the circuit breaker, the digest key, the
index â€” stays in `live_cache.py`, which is engine-agnostic and should remain so.

THE ONE-VOICE RULE APPLIES TO BOTH. The broadcast standard is exactly **one male and one female
voice per language** â€” Male Caller for facts, Female Analyst for the story â€” held stable across a
whole programme, because a voice that drifts between renders reads as a different person arriving
mid-race. Both engines pin their set deliberately and both let the same `TTS_VOICE_<LANG>_<ROLE>`
environment variables override it, so an operator learns one convention rather than two.
é    )ÚannotationsN)ÚPathé   )Ú	LANG_CODEÚpick_voicesÚVR_PIPER_VOICESz/srv/vr/voicesÚenzen_GB-alan-mediumzen_GB-jenny_dioco-mediumz"en_GB-northern_english_male-medium)ÚleadÚtacticalÚrulesc                  ó<   — e Zd ZdZdZdZdd„Zd	d„Z	 	 	 	 	 	 d
d„Zy)ÚGoogleEngineuU   Cloud synthesis, unchanged â€” the SSML path and its plain-text retry both live here.z.mp3Úgooglec                óH   — ddl m} || _        |j                  «       | _        y )Nr   )Útexttospeech)Úgoogle.cloudr   Ú_ttsÚTextToSpeechClientÚ_client)Úselfr   s     ú>/srv/vr/releases/25f455cc4e75/v3/broadcast/live/tts/engines.pyÚ__init__zGoogleEngine.__init__<   s   € Ý-à ˆŒ	Ø#×6Ñ6Ó8ˆ�ó    c                ó.   — t        | j                  |«      S )N)r   r   )r   Úlangs     r   ÚvoiceszGoogleEngine.voicesB   s   € Ü˜4Ÿ<™<¨Ó.Ð.r   c               óÎ  — | j                   }|j                  t        |   |¬«      }|j                  |j                  j
                  |¬«      }	|r|j                  |¬«      n|j                  |¬«      }
	 | j                  j                  |
||	¬«      j                  S # t        $ r> |s‚ | j                  j                  |j                  |¬«      ||	¬«      j                  cY S w xY w)N)Úlanguage_codeÚname)Úaudio_encodingÚspeaking_rate)Ússml)Útext)ÚinputÚvoiceÚaudio_config)r   ÚVoiceSelectionParamsr   ÚAudioConfigÚAudioEncodingÚMP3ÚSynthesisInputr   Úsynthesize_speechÚaudio_contentÚ	Exception)r   r   ÚpayloadÚis_ssmlÚspokenÚ
voice_nameÚrateÚttsr%   ÚconfigÚinps              r   Ú
synthesizezGoogleEngine.synthesizeE   så   € à�i‰iˆØ×(Ñ(´yÀ±ÈZÐ(ÓXˆØ—‘°×0AÑ0A×0EÑ0EÐUY�ÓZˆÙ29ˆc× Ñ  gÐ Ô.¸s×?QÑ?QÐW^Ð?QÓ?_ˆð	gØ—<‘<×1Ñ1¸À5ÐW]Ð1Ó^×lÑlÐløÜò 	gñ ØØ—<‘<×1Ñ1Ø×(Ñ(¨fÐ(Ó5¸UÐQWð 2ó YßYfÑYfògð	gús   Á5'B ÂAC$Ã#C$N)ÚreturnÚNone©r   Ústrr8   zdict[str, str]©r   r;   r/   r;   r0   Úboolr1   r;   r2   r;   r3   Úfloatr8   Úbytes)	Ú__name__Ú
__module__Ú__qualname__Ú__doc__Úsuffixr   r   r   r7   © r   r   r   r   6   s:   „ Ù_à€FØ€Dó9ó/ðgØ"ðgØ*/ðgØ49ôgr   r   c                  óT   — e Zd ZdZdZdZdedœdd„Zdd„Zdd„Z	dd	„Z
	 	 	 	 	 	 dd
„Zy)ÚPiperEngineu¯  Local neural synthesis on the CPU, no network and no credential.

    Measured on the producer VM (2 vCPU) with real commentary lines: **~16x realtime**, so a race
    day's three to four hours of speech is about thirteen minutes of compute. The same models ran at
    33.7x on the render host's 24 threads â€” quote whichever machine will actually do the work.

    WHAT IS GIVEN UP, stated rather than glossed: Piper takes **plain text**, so the SSML
    pronunciation layer does not reach it. That layer carries per-language fixes â€” Swedish `banan`
    (the course, not the fruit) and the hard `g` in `gippa` â€” and for English exactly one entry, the
    broadcast sense of *live* as /laÉªv/. So the English loss today is one word, and the Swedish
    dictionary matters only when Swedish output is switched back on. This is the same degradation the
    cloud path already performs whenever SSML is rejected, not a new failure mode.
    z.wavÚpiperN)Ú	voice_dirÚon_logc               ó¤   — |xs t         | _        || _        i | _        | j                  j	                  «       st        d| j                  › d�«      ‚y )Nzno Piper voice directory at z (set $VR_PIPER_VOICES))Ú	VOICE_DIRÚ_dirÚ_logÚ_loadedÚis_dirÚRuntimeError)r   rI   rJ   s      r   r   zPiperEngine.__init__h   sK   € ØÒ*¤ˆŒ	ØˆŒ	Ø*,ˆŒØ�y‰y×ÑÔ!ÜÐ!=¸d¿i¹i¸[ÐH_Ð`ÓaÐað "r   c                ó&   — | j                   |› d�z  S )Nz.onnx)rM   )r   r2   s     r   Ú_modelzPiperEngine._modelo   s   € Ø�y‰y˜j˜\¨Ð/Ñ/Ð/r   c                óÐ  — t         j                  |«      }|st        d|›�«      ‚|j                  «       }i }|j	                  «       D ]Š  \  }}t
        j                  j                  d|› d|j                  «       › �«      }|r%| j                  |«      j                  «       r|||<   Œ`|r$| j                  dt        ||«      › d|›d|›�«       |||<   ŒŒ |j                  «       D �cg c]$  }| j                  |«      j                  «       rŒ#|‘Œ& }	}|	r<t        d| j                  › dd	j                  t        t        |	«      «      «      › �«      ‚i |¥|d
   |d   |d   dœ¥S c c}w )a  The role set, with the same environment overrides the cloud engine honours.

        An override naming a model this box does not have is **ignored with a log line** rather than
        raising: the commonest way to arrive here is an operator moving a deployment and carrying the
        cloud's `TTS_VOICE_EN_LEAD=en-GB-Studio-B` with them, and a broadcast that refuses to speak
        because a voice name is spelled for the other engine is a worse outcome than one that speaks
        in its curated voice and says so.
        z no local voice set for language Ú
TTS_VOICE_Ú_z[live-tts] ú=z names no local model; using zmissing Piper models in z: z, r
   r   r   )ÚmÚfÚr)ÚLOCAL_ROLE_VOICESÚgetrQ   ÚupperÚitemsÚosÚenvironrS   ÚexistsrN   Úvoice_env_nameÚvaluesrM   ÚjoinÚsortedÚset)
r   r   ÚcuratedÚupÚchosenÚroleÚdefaultÚoverrider   Úmissings
             r   r   zPiperEngine.voicesr   sb  € ô $×'Ñ'¨Ó-ˆÙÜÐ!AÀ$ÀÐJÓKÐKØ�Z‰Z‹\ˆØ!#ˆØ$Ÿ]™]›_ò 	'‰MˆD�'Ü—z‘z—~‘~¨
°2°$°a¸¿
¹
»°~Ð&FÓGˆHÙ˜DŸK™K¨Ó1×8Ñ8Ô:Ø'��t’áØ—I‘I ¬N¸2¸tÓ,DÐ+EÀQÀxÀlð S'Ø'. kð3ô 4à&��t’ð	'ð %+§M¡M£OÖV˜D¸4¿;¹;ÀtÓ;L×;SÑ;SÕ;U’4ÐVˆÐVÙÜÐ!9¸$¿)¹)¸ÀBÀtÇyÁyÔQWÔX[Ð\cÓXdÓQeÓGfÐFgÐhÓiÐiØ]�&Ð]˜v f™~°F¸:Ñ4FÈVÐT[É_Ò]Ð]ùò Ws   Ã'$E#ÄE#c                ó¼   — | j                   j                  |«      }|€>ddlm} |j	                  t        | j                  |«      «      «      }|| j                   |<   |S )Nr   )Ú
PiperVoice)rO   r\   rH   ro   Úloadr;   rS   )r   r2   r%   ro   s       r   Ú_voicezPiperEngine._voiceŽ   sO   € Ø—‘× Ñ  Ó,ˆØˆ=Ý(ð —O‘O¤C¨¯©°JÓ(?Ó$@ÓAˆEØ',ˆD�L‰L˜Ñ$Øˆr   c               ó0  — ddl m}  ||rd|z  nd¬«      }t        j                  «       }	t	        j
                  |	d«      5 }
| j                  |«      j                  ||
|¬«       d d d «       |	j                  «       S # 1 sw Y   |	j                  «       S xY w)Nr   )ÚSynthesisConfigg      ð?)Úlength_scaleÚwb)Ú
syn_config)	rH   rs   ÚioÚBytesIOÚwaveÚopenrq   Úsynthesize_wavÚgetvalue)r   r   r/   r0   r1   r2   r3   rs   r5   ÚbufferÚouts              r   r7   zPiperEngine.synthesize™   s   € å)ñ
 !¹D¨c°DªjÀcÔJˆÜ—‘“ˆÜ�Y‰Y�v˜tÓ$ð 	S¨Ø�K‰K˜
Ó#×2Ñ2°6¸3È6Ð2ÔR÷	Sà�‰Ó Ð ÷	Sà�‰Ó Ð ús   Á$A=Á=B)rI   zPath | Noner8   r9   )r2   r;   r8   r   r:   )r2   r;   r<   )r@   rA   rB   rC   rD   r   Úprintr   rS   r   rq   r7   rE   r   r   rG   rG   V   sK   „ ñð €FØ€Dà37Àõ bó0ó^ó8	ð!Ø"ð!Ø*/ð!Ø49ô!r   rG   c                ó.   — d| › d|j                  «       › �S )NrU   rV   )r]   )Ú
lang_upperrj   s     r   rb   rb   §   s   € Ø˜
�| 1 T§Z¡Z£\ NÐ3Ð3r   ©rJ   c                óÞ   — t         j                  j                  d«      xs dj                  «       j	                  «       }|dk(  rt        | ¬«      S |dk(  r
t        «       S t        d|›d�«      ‚)u  The deployment's engine, from `$VR_TTS_ENGINE`.

    Defaults to **google**, so no existing deployment changes behaviour by upgrading. Set
    `VR_TTS_ENGINE=piper` on a box that has the models and no credential â€” which is the office
    deployment exactly.
    ÚVR_TTS_ENGINEr   rH   r‚   zunknown $VR_TTS_ENGINE z (expected 'google' or 'piper'))r_   r`   r\   ÚstripÚlowerrG   r   rQ   )rJ   Úchoices     r   Úmake_enginerˆ   «   sf   € ô �j‰j�n‰n˜_Ó-Ò9°×@Ñ@ÓB×HÑHÓJ€FØ�ÒÜ &Ô)Ð)Ø�ÒÜ‹~ÐÜ
Ð0°°
Ð:YÐZÓ
[Ð[r   )r�   r;   rj   r;   r8   r;   )rC   Ú
__future__r   rw   r_   ry   Úpathlibr   Úsynthr   r   r`   r\   rL   r[   r   rG   rb   r   rˆ   rE   r   r   ú<module>rŒ      s€   ðñõ6 #ã 	Û 	Û Ý ç )ñ �—‘—‘Ð 1Ð3CÓDÓE€	ð 	Ø#Ø.Ø5ñðÐ ÷gñ g÷@N!ñ N!ób4ð  õ \r   