Ë
    Š‰Œ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	 dZ
dZdZd	Z ej                  «       Zdddd
œ	 	 	 	 	 dd„Zdd„Z	 	 	 	 	 	 dd„Zdd„Zdd„Zd d„Zdddœ	 	 	 d!d„Zd"d„Zedœd#d„Zd$d„Zddœd%d„Zd&d„Zd'd„Zy)(u  What actually went to air, written down while it is still knowable.

The as-run â€” which race was on screen, from when to when â€” is the one fact about a broadcast that
exists **nowhere else**. `OnAirQueue` decides it, in memory, and forgets it; the race plane records
that a race was sailed, not that anybody showed it; and the picture itself is not machine-readable.
So a recipient asking "cut me race 1009 out of the day's recording" cannot be answered afterwards
unless the producer says so at the time.

Sibling of `recording_anchor.py`, and deliberately the same shape: an append-only JSONL file written
beside the show. That is the cheapest possible thing to add to a live loop â€” one line, no network, no
database, no schema migration â€” and if it fails it fails alone.

**Two race-boundary rows, and the second one is optional.** `on_air` when a race takes the air;
`off_air` when the producer drops back to the waiting picture. A race-to-race hand-over emits only
`on_air`, because that is what the producer actually does â€” `take_next` swaps the race in place
without releasing â€” so `segments()` below closes the previous race at the next one's start rather
than expecting a matching close. Getting that wrong is how an as-run acquires gaps that were never
off air.

The same append-only evidence stream also carries `commentary_selected` when one wording variant
crosses the producer seam and `commentary_started` when the programme renderer reports that its
audio actually began. The separation is intentional: the microphone scheduler can discard a stale
selected line. `segments()` ignores these audit rows when building the race partition.

Read it back with `segments()`, which turns the rows into the gap-free partition the metadata
contract publishes (`docs/broadcast-metadata.md` Â§4.2).
é    )ÚannotationsN)ÚCallable)ÚPathÚon_airÚoff_airÚcommentary_selectedÚcommentary_started)Ú	race_nameÚ
gun_utc_msÚleaving_instance_idc                ó`   — t        | «      t        t        |«      ||€d|dœS t        |«      |dœS )un  A race has taken the air.

    `leaving_instance_id` is the race this one displaced, and only the producer knows it â€” the queue
    hands over without either race being told. It is recorded rather than inferred so a reader can
    tell a hand-over (one race replacing another) from a fresh start after a gap, which look
    identical once you have only timestamps.N)ÚtUtcMsÚeventÚ
instanceIdÚraceNameÚgunUtcMsÚleavingInstanceId)ÚintÚON_AIRÚstr)Únow_msÚinstance_idr
   r   r   s        ú</srv/vr/releases/25f455cc4e75/v3/broadcast/live/asrun_log.pyÚ
on_air_rowr   /   s@   € ô ˜&“k¬FÄ#ÀkÓBRØ!°zÐ7I¨tØ!4ñ6ð 6ÜORÐS]ËØ!4ñ6ð 6ó    c                ó:   — t        | «      t        t        |«      dœS )uR   Nothing is on air any more â€” the producer went back to the waiting presentation.)r   r   r   )r   ÚOFF_AIRr   )r   r   s     r   Úoff_air_rowr   <   s   € ä˜&“k¬GÄ3À{ÓCSÑTÐTr   c                óÀ   — t        |«      }|€yt        | «      t        t        |d«      t        |d«      t	        |«      dœ|¥dt        |j                  d«      d«      i¥S )as  A rotated line crossed the producer seam and became eligible for the programme microphone.

    This is deliberately distinct from :func:`commentary_started_row`: the renderer may discard a
    stale colour line while another sentence owns the microphone.  Keeping both rows is what makes
    delivery rate and phrase rotation independently auditable after the show.
    Néd   ©r   r   r   r   Ú	sourceSecÚtextéX  )Ú_commentary_variantr   ÚCOMMENTARY_SELECTEDÚ_textÚ_numberÚget)r   r   r
   Ú
source_secÚ
commentaryÚvariants         r   Úcommentary_selected_rowr-   A   sk   € ô " *Ó-€GØ€Øä�f“+Ô(;Ü˜K¨Ó-¼5ÀÈCÓ;PÜ˜ZÓ(ñð -4ðð 	”�j—n‘n VÓ,¨cÓ2ñ	ð r   c                óZ  — t        |t        «      syt        |«      }t        |j	                  d«      d«      }t        |j	                  d«      d«      }|�|r|dvryt        |j	                  d«      d«      }t        | «      t        t        |j	                  d«      d	«      t        |j	                  d
«      d	«      t        |j	                  d«      «      dœ|¥t        |j	                  d«      d«      t        |j	                  d«      d«      |||t        |j	                  d«      d¬«      dœ¥S )aI  Validate one renderer receipt for the instant a rotated line actually began playing.

    Browser input is untrusted.  The server supplies ``tUtcMs`` from its own clock and clamps every
    descriptive field; the client timestamp is retained only as diagnostic context and is never
    used as the authoritative as-run time.
    NÚ
rendererIdé   ÚaudioBackend)ÚneuralÚbrowserÚrendererRoleé(   r   r    r   r"   r!   r#   r$   Úlangé   ÚclientUtcMsr   )Úminimum)r#   r6   r1   r/   r4   r8   )	Ú
isinstanceÚdictr%   r'   r)   r   ÚCOMMENTARY_STARTEDr(   Ú_integer)r   Úpayloadr,   Úrenderer_idÚbackendÚroles         r   Úcommentary_started_rowrB   T   s
  € ô �gœtÔ$ØÜ! 'Ó*€GÜ˜Ÿ™ LÓ1°2Ó6€KÜ�G—K‘K Ó/°Ó4€GØ€™k¨WÐ<QÑ-QØÜ�—‘˜^Ó,¨bÓ1€Dä�f“+Ô(:Ü˜GŸK™K¨Ó5°sÓ;Ü˜'Ÿ+™+ jÓ1°3Ó7Ü˜WŸ[™[¨Ó5Ó6ñ	
ð ;Bð	
ô
 �g—k‘k &Ó)¨3Ó/Ü�g—k‘k &Ó)¨2Ó.Ø¨{ØÜ §¡¨MÓ :ÀAÔFò
ð 
r   c                ó  — t        | j                  d«      d«      }t        | j                  d«      dd¬«      }t        | j                  d«      dd¬«      }t        | j                  d«      d	«      }|r|�	|�||kD  s|sy ||||d
œS )NÚcommentaryVariantCategoryéP   ÚcommentaryVariantIndexé   r    ©r9   ÚmaximumÚcommentaryVariantCountÚcommentaryVariantIdé@   )rD   rF   rJ   rK   )r'   r)   r=   )ÚvalueÚcategoryÚindexÚcountÚ
variant_ids        r   r%   r%   p   sŠ   € Ü�U—Y‘YÐ:Ó;¸RÓ@€HÜ�U—Y‘YÐ7Ó8À!ÈSÔQ€EÜ�U—Y‘YÐ7Ó8À!ÈSÔQ€EÜ�u—y‘yÐ!6Ó7¸Ó<€JÙ�u�}¨¨¸ÀºÉjØØ)1ÈUØ&+ÀJñPð Pr   c                ó‚   — t        | t        «      sy dj                  d„ | D «       «      j                  «       d | }|xs d S )NÚ c              3  óB   K  — | ]  }|j                  «       sŒ|–— Œ y ­w©N)Úisprintable)Ú.0Úchs     r   ú	<genexpr>z_text.<locals>.<genexpr>~   s   è ø€ Ò=˜R¨B¯N©NÕ,<”bÑ=ùs   ‚˜)r:   r   ÚjoinÚstrip)rM   ÚlimitÚcleaneds      r   r'   r'   {   s=   € Ü�eœSÔ!ØØ�g‰gÑ= 5Ô=Ó=×CÑCÓEÀfÀuÐM€GØŠ?�dÐr   rH   c               óŠ   — t        | t        «      ry 	 t        | «      }|�||k  ry |�||kD  ry |S # t        t        f$ r Y y w xY wrU   )r:   Úboolr   Ú	TypeErrorÚ
ValueError)rM   r9   rI   Úouts       r   r=   r=   ‚   s[   € ä�%œÔØðÜ�%‹jˆð Ð˜s Wš}ØØÐ˜s Wš}ØØ€Jøô ”zÐ"ò Ùðús   “0 °AÁAc                óô   — t        | t        «      s| €y 	 t        | «      }||k7  s|t        d«      t        d«      fv ry |j                  «       rt        |«      S t        |d«      S # t        t        f$ r Y y w xY w)NÚinfz-infé   )r:   r_   Úfloatr`   ra   Ú
is_integerr   Úround)rM   rb   s     r   r(   r(   ‘   sx   € Ü�%œÔ % -ØðÜ�E‹lˆð ˆc‚z�SœU 5›\¬5°«=Ð9Ñ9ØØ—~‘~Ô'Œ3ˆs‹8Ð:¬U°3¸«]Ð:øô	 ”zÐ"ò Ùðús   •A% Á%A7Á6A7)Úon_logc          
     ó�  — | €y	 t         5  | j                  j                  dd¬«       | j                  dd¬«      5 }|j	                  t        j                  |«      dz   «       ddd«       ddd«       y# 1 sw Y   ŒxY w# 1 sw Y   yxY w# t        $ r/} |d	| › d
t        |«      j                  › d|› d�«       Y d}~yd}~ww xY w)uf  Append one row. Returns whether it was written; **never raises**.

    This is called from the live broadcast loop, so a full disk, a read-only mount or a path that
    stopped existing must cost a log line and nothing else. `path=None` means as-run logging is not
    configured, which is a no-op rather than an error â€” the producer has to run without it.NFT)ÚparentsÚexist_okÚaúutf-8©Úencodingú
z[asrun] could not write z (z: u   ) â€” continuing)
Ú_APPEND_LOCKÚparentÚmkdirÚopenÚwriteÚjsonÚdumpsÚ	ExceptionÚtypeÚ__name__)ÚpathÚrowri   ÚfhÚes        r   Úappendr€   �   sÁ   € ð €|ØðÜñ 	1Ø�K‰K×Ñ d°TÐÔ:Ø—‘˜3¨�Ó1ð 1°RØ—‘œŸ™ C›¨4Ñ/Ô0÷1÷	1ð ÷1ð 1ú÷	1ð ûÜò ÙÐ)¨$¨¨r´$°q³'×2BÑ2BÐ1CÀ2ÀaÀSÐHXÐYÔZÜûðúsK   …B ‹1B¼(A5Á$BÁ,B Á5A>	Á:BÂB
ÂB Â
B Â	CÂ%C Ã Cc                ó  — | j                  «       sg S g }| j                  d¬«      j                  «       D ]:  }|j                  «       }|sŒ	 |j	                  t        j                  |«      «       Œ< |S # t
        j                  $ r Y ŒTw xY w)uö   Every row written so far. A truncated last line â€” the run was killed mid-write â€” is dropped
    rather than raising, exactly as `recording_anchor.read_samples` does: the file is an append log
    and its tail is the least valuable part of it.rn   ro   )ÚexistsÚ	read_textÚ
splitlinesr[   r€   rw   ÚloadsÚJSONDecodeError)r|   ÚrowsÚlines      r   Ú	read_rowsr‰   °   s†   € ð �;‰;Œ=Øˆ	Ø€DØ—‘¨�Ó0×;Ñ;Ó=ò ˆØ�z‰z‹|ˆÙØð	Ø�K‰KœŸ
™
 4Ó(Õ)ðð €Køô ×#Ñ#ò 	Ùð	ús   Á$A4Á4B
Â	B
)Úsession_end_msc          	     óô  — g }t        | d„ ¬«      D ]Ç  }t        |j                  dd«      «      |j                  d«      }}|t        k(  rW|r|d   d   €||d   d<   |j	                  |j                  d	«      |j                  d
«      |j                  d«      |ddœ«       Œ�|t
        k(  sŒ™|sŒœ|d   d   �Œ¥|d   d	   |j                  d	«      k(  sŒÀ||d   d<   ŒÉ |�|r|d   d   €t        |«      |d   d<   |S )u%  The rows as `[{instanceId, raceName, gunUtcMs, fromUtcMs, toUtcMs}]`, in order.

    `toUtcMs` is EXCLUSIVE and is whichever comes first: an explicit `off_air`, or the next race
    taking the air. `None` on the last segment means it was still on air when the log ended â€” which
    is how "what is playing right now" falls out without a second endpoint. Pass `session_end_ms` to
    close it at a known end of transmission instead.

    Rows are sorted by time rather than trusted in file order: the log is appended from one loop so
    they arrive ordered, but a reader that assumed it would break silently on a concatenated file.
    An `off_air` for a race that is not the one on air is ignored â€” it cannot close anything, and
    treating it as a close would truncate the race that IS on air.
    c                ó8   — t        | j                  dd«      «      S )Nr   r   )r   r)   )Úxs    r   ú<lambda>zsegments.<locals>.<lambda>Ð   s   € ¬¨A¯E©E°(¸AÓ,>Ó(?€ r   )Úkeyr   r   r   éÿÿÿÿÚtoUtcMsNr   r   r   )r   r   r   Ú	fromUtcMsr‘   )Úsortedr   r)   r   r€   r   )r‡   rŠ   rb   ÚrÚtr   s         r   Úsegmentsr–   Â   s  € ð €CÜ�DÑ?Ô@ò 	'ˆÜ�q—u‘u˜X qÓ)Ó*¨A¯E©E°'«Nˆ5ˆØ”FŠ?Ù�s˜2‘w˜yÑ)Ð1Ø%&��B‘˜	Ñ"Ø�J‰J a§e¡e¨LÓ&9ÀqÇuÁuÈZÓGXØ$%§E¡E¨*Ó$5ÀAÐRVñXõ Yà”gÓÚ�s˜2‘w˜yÑ)Ñ1°c¸"±g¸lÑ6KÈqÏuÉuÐUaÓObÓ6bØ%&��B‘˜	Ò"ð	'ð Ð!¡c¨c°"©g°iÑ.@Ð.HÜ  Ó0ˆˆB‰�	ÑØ€Jr   c                ó<   — | rt        | «      j                  «       S dS )zEThe configured log path, or None when as-run logging is switched off.N)r   Ú
expanduser)Úargs    r   Údefault_pathrš   ß   s   € á%(Œ4�‹9×ÑÓ!Ð2¨dÐ2r   c                 óB   — t        t        j                  «       dz  «      S )Niè  )r   Útime© r   r   Ú_now_msrž   ä   s   € ÜŒt�y‰y‹{˜TÑ!Ó"Ð"r   )r   r   r   r   r
   ú
str | Noner   ú
int | Noner   rŸ   Úreturnr;   )r   r   r   r   r¡   r;   )r   r   r   rŸ   r
   rŸ   r*   úfloat | int | Noner+   r;   r¡   údict | None)r   r   r>   Úobjectr¡   r£   )rM   r;   r¡   r£   )rM   r¤   r\   r   r¡   rŸ   )rM   r¤   r9   r    rI   r    r¡   r    )rM   r¤   r¡   r¢   )r|   úPath | Noner}   r;   ri   zCallable[[str], None]r¡   r_   )r|   r   r¡   ú
list[dict])r‡   r¦   rŠ   r    r¡   r¦   )r™   rŸ   r¡   r¥   )r¡   r   )Ú__doc__Ú
__future__r   rw   Ú	threadingrœ   Úcollections.abcr   Úpathlibr   r   r   r&   r<   ÚLockrr   r   r   r-   rB   r%   r'   r=   r(   Úprintr€   r‰   r–   rš   rž   r�   r   r   ú<module>r®      sê   ðñõ6 #ã Û Û Ý $Ý à	€Ø
€Ø+Ð Ø)Ð ð
 ˆy�~‰~Ó€ð JNØ(,ÐPTñ
6Ø%ð
6ØCMð
6ØY]ó
6óUð
Ø(:ðØHLðØQ\óó&ó8Póð 6:Ø#'ñØ ðØ,6óó	;ð MRõ ó&ð$ @Dõ ó:3ô
#r   