
    `gjă                    j   d Z ddlmZ ddlZddlZddlZddlZddlZddlZddl	Z	ddl
mZ ddlmZ  ej                  d      ZdZdZdd	Zdd
Z G d d      ZdZddZ	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 ddZddZddZddZd dZd dZd!dZd"dZd#dZe dk(  r ejB                   e              yy)$us  Headless Google Meet bot — Playwright + live-caption scraping.

Runs as a standalone subprocess spawned by ``process_manager.py``. Reads config
from env vars, writes status + transcript to files under
``$HERMES_HOME/workspace/meetings/<meeting-id>/``. The main hermes process
reads those files via the ``meet_*`` tools — no IPC beyond filesystem.

The scraping strategy mirrors OpenUtter (sumansid/openutter): we don't parse
WebRTC audio, we enable Google Meet's built-in live captions and observe the
captions container in the DOM via a MutationObserver. This is lossy and
English-biased but it is:

* deterministic (no API keys, no STT billing),
* works behind Meet's normal login / admission,
* survives Meet UI rewrites fairly well because the caption container has a
  stable ARIA role.

Run standalone for debugging::

    HERMES_MEET_URL=https://meet.google.com/abc-defg-hij \
    HERMES_MEET_OUT_DIR=/tmp/meet-debug \
    HERMES_MEET_HEADED=1 \
    python -m plugins.google_meet.meet_bot

No meet.google.com URL → exits non-zero. Any URL that doesn't start with
``https://meet.google.com/`` is rejected (explicit-by-design).
    )annotationsN)Path)Optionalzc^https://meet\.google\.com/([a-z0-9]{3,}-[a-z0-9]{3,}-[a-z0-9]{3,}|lookup/[^/?#]+|new)(?:[/?#].*)?$zsay_queue.jsonlzspeaker.pcmc                |    t        | t              syt        t        j	                  | j                                     S )zGReturn True if *url* is a Google Meet URL we're willing to navigate to.F)
isinstancestrboolMEET_URL_REmatchstrip)urls    O/root/.hermes/venv/lib/python3.12/site-packages/plugins/google_meet/meet_bot.py_is_safe_meet_urlr   9   s+    c3!!#))+.//    c                    t        j                  d| xs d      }|r|j                  d      S dt        t	        j                                S )u>  Extract the 3-segment meeting code from a Meet URL.

    For ``https://meet.google.com/abc-defg-hij`` → ``abc-defg-hij``.
    For ``.../lookup/<id>`` or ``/new`` we fall back to a timestamped id — the
    bot won't know the real code until after redirect, and callers pass this
    through to filename anyway.
    z:meet\.google\.com/([a-z0-9]{3,}-[a-z0-9]{3,}-[a-z0-9]{3,})    zmeet-)researchgroupinttime)r   ms     r   _meeting_id_from_urlr   @   sH     			E	r	A 	wwqz3tyy{#$%%r   c                  0    e Zd ZdZddZddZd	dZd	dZy)
	_BotStatezHSingle-process mutable state, flushed to ``status.json`` on each change.c                   || _         || _        || _        d| _        d| _        d| _        d| _        d | _        d | _        d | _	        d| _
        d | _        d| _        d| _        d| _        d | _        d| _        d | _        d | _        d | _        t)               | _        |j-                  dd       |dz  | _        |dz  | _        | j3                          y )NFr   T)parentsexist_okztranscript.txtzstatus.json)out_dir
meeting_idr   in_call
captioningcaptions_enabled_attemptedlobby_waitingjoin_attempted_at	joined_atlast_caption_attranscript_lineserrorexitedrealtimerealtime_readyrealtime_deviceaudio_bytes_outlast_audio_out_atlast_barge_in_atleave_reasonset_seenmkdirtranscript_pathstatus_path_flush)selfr    r!   r   s       r   __init__z_BotState.__init__X   s    $*/'"26*.04 !$(
#.2$%2615+/ %
dT2&)99"]2r   c                D   |xs dj                         xs d}|xs dj                         }|sy| d| }|| j                  v ry| j                  j                  |       | xj                  dz  c_        t	        j                         | _        t	        j                  dt	        j                  | j
                              }d| d| d	| d
}| j                  j                  dd      5 }|j                  |       ddd       | j                          y# 1 sw Y   xY w)zDAppend a caption line if we haven't seen this exact (speaker, text).r   UnknownN|r   z%H:%M:%S[z] z: 
autf-8encoding)r   r4   addr)   r   r(   strftime	localtimer6   openwriter8   )r9   speakertextkeytslinefs          r   record_captionz_BotState.record_captionx   s    =b'')6Y
!!#	4&!$**

s"#yy{]]:t~~d6J6J'KL2$b	D6,!!&&sW&= 	GGDM		 	s   +DDc                   i d| j                   d| j                  d| j                  d| j                  d| j                  d| j
                  d| j                  d| j                  d	| j                  d
| j                  dt        | j                        d| j                  d| j                  dt        j                         d| j                   d| j"                  d| j$                  | j&                  | j(                  | j*                  | j,                  d}| j.                  j1                  d      }|j3                  t5        j6                  |d      d       |j9                  | j.                         y )N	meetingIdr   inCallr#   captionsEnabledAttemptedlobbyWaitingjoinAttemptedAtjoinedAtlastCaptionAttranscriptLinestranscriptPathr*   r+   pidr,   realtimeReadyrealtimeDevice)audioBytesOutlastAudioOutAtlastBargeInAtleaveReasonz	.json.tmp   )indentrA   rB   )r!   r   r"   r#   r$   r%   r&   r'   r(   r)   r   r6   r*   r+   osgetpidr,   r-   r.   r/   r0   r1   r2   r7   with_suffix
write_textjsondumpsreplace)r9   datatmps      r   r8   z_BotState._flush   s   

488
 dll
 $//	

 '(G(G
 D..
 t55
 
 T11
 t44
 c$"6"67
 TZZ
 dkk
 299;
  !
" T00#
$ d22%
& "11"44!22,,-
0 **;7tzz$q1GDD$$%r   c                n    |j                         D ]  \  }}t        | ||        | j                          y )N)itemssetattrr8   )r9   kwargskvs       r   r3   z_BotState.set   s0    LLN 	 DAqD!Q	 r   N)r    r   r!   r   r   r   )rI   r   rJ   r   returnNone)rr   rs   )__name__
__module____qualname____doc__r:   rO   r8   r3    r   r   r   r   U   s    R@*&:r   r   u1  
(() => {
  if (window.__hermesMeetInstalled) return;
  window.__hermesMeetInstalled = true;
  window.__hermesMeetQueue = [];

  const captionSelector = '[role="region"][aria-label*="aption" i], ' +
                          'div[jsname="YSxPC"], ' +  // legacy
                          'div[jsname="tgaKEf"]';    // current (Apr 2026)

  function pushEntry(speaker, text) {
    if (!text || !text.trim()) return;
    window.__hermesMeetQueue.push({
      ts: Date.now(),
      speaker: (speaker || '').trim(),
      text: text.trim(),
    });
  }

  function scan(root) {
    // Meet captions render as a list of rows; each row contains a speaker
    // label and a text block. Selectors vary across Meet rewrites; we try
    // a few shapes and fall back to raw text.
    const rows = root.querySelectorAll('div[jsname="dsyhDe"], div.CNusmb, div.TBMuR');
    if (rows.length) {
      rows.forEach((row) => {
        const spkEl = row.querySelector('div.KcIKyf, div.zs7s8d, span[jsname="YSxPC"]');
        const txtEl = row.querySelector('div.bh44bd, span[jsname="tgaKEf"], div.iTTPOb');
        const speaker = spkEl ? spkEl.innerText : '';
        const text = txtEl ? txtEl.innerText : row.innerText;
        pushEntry(speaker, text);
      });
      return;
    }
    // Fallback: treat the whole region's innerText as one anonymous line.
    const text = (root.innerText || '').split('\n').filter(Boolean).pop();
    pushEntry('', text);
  }

  function attach() {
    const el = document.querySelector(captionSelector);
    if (!el) return false;
    const obs = new MutationObserver(() => scan(el));
    obs.observe(el, { childList: true, subtree: true, characterData: true });
    scan(el);
    return true;
  }

  // Try now and retry on interval — the caption region only appears after
  // captions are enabled and someone speaks.
  if (!attach()) {
    const iv = setInterval(() => { if (attach()) clearInterval(iv); }, 1500);
  }

  window.__hermesMeetDrain = () => {
    const out = window.__hermesMeetQueue.slice();
    window.__hermesMeetQueue = [];
    return out;
  };
})();
c                      y)u  Return a small JS snippet that tries to click the 'Turn on captions' button.

    Best-effort — Meet's caption toggle is keyboard-accessible via ``c``. We
    dispatch that keystroke as a cheap fallback. Real click targeting is too
    brittle to rely on.
    z
    (() => {
      const ev = new KeyboardEvent('keydown', {
        key: 'c', code: 'KeyC', keyCode: 67, which: 67, bubbles: true,
      });
      document.body.dispatchEvent(ev);
      return true;
    })();
    rx   rx   r   r   _enable_captions_jsrz      s    r   c        	        D   	 ddl m}	m}
 |t
        z  }|t        z  }|dz  }|j                  d       |j                          	  |	|||||d	      }|j                          || d<   fdfd| d<    |
|||      fd}t        j                  |dd      }|j                          || d<   |xs i j                  d      }|dk(  rjddl}|xs i j                  d      xs d}	 |j                  dddddd| t!        |      g|j"                  |j"                  |j"                        }|| d <   y|d"k(  rddl}ddl}|xs i j                  d      xs d#}|j)                  d$      r_	 |j                  d$d%d&d'd(d)d*d+d,d-d.d/d0t!        |      d*d1d2t+        |      d3g|j"                  |j"                  |j"                        }|| d <   yj	                  d4       yy# t        $ r}j	                  d|        Y d}~yd}~ww xY w# t        $ r}j	                  d
|        Y d}~yd}~ww xY w# t$        $ r j	                  d!       Y yw xY w# t$        $ r j	                  d4       Y yt        $ r}j	                  d5|        Y d}~yd}~ww xY w)6a  Wire up the OpenAI Realtime session + speaker thread + PCM pump.

    The speaker thread reads text lines from ``say_queue.jsonl``, sends each
    to OpenAI Realtime, and writes PCM audio into ``speaker.pcm``. A
    separate *pump* thread forwards that PCM into the OS audio sink so
    Chrome's fake mic picks it up. On Linux we pipe to ``paplay`` against
    the null-sink; on macOS the caller is expected to have the BlackHole
    device selected as default input.
    r   )RealtimeSessionRealtimeSpeakerzrealtime import failed: r*   Nzsay_processed.jsonlr   i]  )api_keymodelvoiceinstructionsaudio_sink_pathsample_ratezrealtime connect failed: sessionc                 (     j                  dd      S NstopF)get	stop_flags   r   _stop_fnz)_start_realtime_speaker.<locals>._stop_fn@  s    }}VU++r   c                 H     j                  d j                  dd            S r   )__setitem__r   r   s   r   <lambda>z)_start_realtime_speaker.<locals>.<lambda>C  s     !6!6vy}}VUZ?[!\ r   speaker_stop)r   
queue_pathprocessed_pathc                     	 j                         y # t        $ r} j                  d|         Y d } ~ y d } ~ ww xY w)Nzrealtime speaker crashed: r~   )run_until_stopped	Exceptionr3   )er   rI   states    r   _speaker_loopz._start_realtime_speaker.<locals>._speaker_loopK  s?    	>%%h/ 	>II8<I==	>s    	=8=zmeet-speakerT)targetnamedaemonspeaker_threadplatformlinuxwrite_targethermes_meet_sinkpaplayz--rawz--rate=24000z--format=s16lez--channels=1z	--device=)stdinstdoutstderrpcm_pumpuC   paplay not found — install pulseaudio-utils for realtime on LinuxdarwinzBlackHole 2chffmpegz-nostdinz-hide_bannerz	-loglevelr*   z-re-fs16lez-ar24000z-ac1-iaudiotoolboxz-audio_device_index-uL   ffmpeg not found — install via `brew install ffmpeg` for realtime on macOSz macOS pcm pump failed to start: )*plugins.google_meet.realtime.openai_clientr|   r}   r   r3   SAY_PCM_FILENAMESAY_QUEUE_FILENAMEwrite_bytestouchconnect	threadingThreadstartr   
subprocessPopenr   DEVNULLFileNotFoundErrorshutilwhich_mac_audio_device_index)rtr    bridge_infor   r   r   r   r   r   r|   r}   r   pcm_pathr   r   r   r   	t_speakerplatform_tag_spsinkproc_shutildevice_namer   rI   s          ``               @@r   _start_realtime_speakerr   	  s   *	
 ))H--J44N !%$
 	
 ByM, ]B~%G>   NSWXIOO$B  %2**:6Lw !r&&~6L:L	c99"$"v&M kk{{{{  D "BzN 
	! 	! "(b--n=P=="H yy "NKgugucc(mn-/F{/S	 ++;;;; !  "&: IIjIkW 
"[  		21#6	7.  		3A37	8h ! 	cIIaIb	cP % p		 n	o H		"B1# F	GGHsa   G$ H .AH: 2AI $	H-HH	H7H22H7:IIJ8J JJc                   ddl }	 |j                  g dddd      }ddl}| j	                         j                         }|j                  xs dj                         D ]Z  }|j                  d	|      }|s|j                  d
      j	                         j                         |k(  sI|j                  d      c S  y# t        $ r Y yw xY w)uz  Return the ffmpeg ``-audio_device_index`` for *device_name*, as a string.

    Probes ``ffmpeg -f avfoundation -list_devices true -i ''`` (which prints
    the device table on stderr) and matches *device_name* case-insensitively.
    Defaults to ``"0"`` if the device can't be found — caller will get a
    misrouted stream but not a crash, and the error will be obvious.
    r   N)r   r   avfoundationz-list_devicestruer   r   T
   )capture_outputrJ   timeout0r   z\[(\d+)\]\s+(.+)$ra   r   )
r   runr   r   r   lowerr   
splitlinesr   r   )r   r   out_reneedlerM   r   s          r   r   r     s     ggO	  
  &&(F!r--/ JJ+T2771:##%/771:   s   B; ;	CCc                   ' t         j                  j                  dd      j                         } t         j                  j                  dd      j                         }t         j                  j                  dd      j	                         dv }t         j                  j                  dd      j                         }t         j                  j                  dd      }t        t         j                  j                  d	d            }t         j                  j                  d
d      j                         j	                         }t         j                  j                  dd      }t         j                  j                  dd      }t         j                  j                  dd      }	t         j                  j                  d      xs  t         j                  j                  dd      }
| rt        |       s#t        j                  j                  d| z         y|s t        j                  j                  d       yt        |      }t        |       }t        |||       }ddi''fd}t        j                  t        j                  |       t        j                  t        j                  |       |dk(  d d d d d d}|d   re|
s|j!                  d       d|d<   nK	 dd lm}  |       }|j'                         |d!<   ||d"<   |j!                  d#|d!   j                  d$      %       	 dd(lm} t         j                  j3                         }d-d.g}|d   s|j5                  d/d0       n4|d!   r/|d!   j                  d1      d2k(  r|d!   j                  d$d      |d3<   	  |       5 }|j7                         D ]  \  }}|t         j                  |<    |j8                  j;                  | |4      }d5d6d7d8d9d:gd;}|rt        |      j=                         r||d<<    |j>                  dgi |}|jA                         }	 |jC                  | d=d>?       tE        ||       tG        ||       	 |jI                  tK                      |j!                  d#B       	 |jI                  tL               |j!                  d#tO        jN                         D       |d   r.tQ        |||d!   |
|||	'|E	       |dF   |j!                  d#G       |rtO        jN                         |z   nd }tO        jN                         tS        t         j                  j                  dHdI            z   }dJ}'d   stO        jN                         } |r| |kD  r|j!                  dKL       n|jT                  s| |z
  dMkD  r| }tW        |      }!|!r|j!                  d#d| N       nb| |kD  r=|j!                  dO|jX                  rt[        ||jX                  z
        nd dPdQR       n4t]        |      r|j!                  dSdTR       n	 |jI                  dU      }"t_        |"t`              r|"D ]  }#t_        |#tb              ste        |#j                  dVd            }$te        |#j                  dWd            }%|jg                  |$|%X       |d   sc|dF   iti        |$|      sv	 |dF   jk                         }&|&r|j!                  | Y        |dF   /|j!                  to        |dF   d[d      to        |dF   d\d       ]       tO        jp                  d^       'd   s	 |jI                  d_       |js                          |js                          |j                  d`      r)	 |d`   ju                          |d`   jw                  d,a       |db   r	  |db           |dc   	 |dc   jy                  dda       |dF   r	 |dF   js                          |d"   r	 |d"   j1                          |j!                  ddd#e       	 d d d        y# t(        $ r&}|j!                  d&| d'       d|d<   Y d }~*d }~ww xY w# t.        $ rW}|j!                  d)| d#*       t        j                  j                  d+       |d"   r|d"   j1                          Y d }~y,d }~ww xY w# t(        $ r(}|j!                  d@| d#*       Y d }~d d d        yAd }~ww xY w# t(        $ r Y hw xY w# t(        $ r }|j!                  dC|        Y d }~zd }~ww xY w# t(        $ r Y w xY w# t(        $ r) |jm                         r|j!                  dZL       Y *Y }w xY w# t(        $ r Y +w xY w# t(        $ r Y w xY w# t(        $ r Y w xY w# t(        $ r Y w xY w# t(        $ r Y w xY w# t(        $ r Y w xY w# 1 sw Y   y xY w# t(        $ r }|j!                  df| d#*       Y d }~y/d }~ww xY w)hNHERMES_MEET_URLr   HERMES_MEET_OUT_DIRHERMES_MEET_HEADED>   r   yesr   HERMES_MEET_AUTH_STATEHERMES_MEET_GUEST_NAMEzHermes AgentHERMES_MEET_DURATIONHERMES_MEET_MODE
transcribeHERMES_MEET_REALTIME_MODELzgpt-realtimeHERMES_MEET_REALTIME_VOICEalloy!HERMES_MEET_REALTIME_INSTRUCTIONSHERMES_MEET_REALTIME_KEYOPENAI_API_KEYu_   google_meet bot: refusing to launch — HERMES_MEET_URL must be a meet.google.com URL. got: %r
ra   z1google_meet bot: HERMES_MEET_OUT_DIR is required
)r    r!   r   r   Fc                    dd<   y )NTr   rx   )_sig_framer   s     r   
_on_signalzrun_bot.<locals>._on_signal  s     	&r   r,   )enabledbridger   r   r   r   r   up   realtime mode requested but no API key in HERMES_MEET_REALTIME_KEY/OPENAI_API_KEY — falling back to transcriber~   r   )AudioBridger   r   Tr   )r,   r.   zaudio bridge setup failed: u    — falling back to transcribe)sync_playwrightzplaywright not installed: )r*   r+   ztgoogle_meet bot: playwright is not installed. Run `pip install playwright && python -m playwright install chromium`
   z--use-fake-ui-for-media-streamz---disable-blink-features=AutomationControlledr   z"--use-fake-device-for-media-streamr   r   PULSE_SOURCE)headlessargsi   i   )widthheightzeMozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36
microphonecamera)viewport
user_agentpermissionsstorage_statedomcontentloadedi0u  )
wait_untilr   znavigate failed:    )r$   z!caption observer install failed: )r#   r&   )	r   r    r   r   r   r   r   r   r   r   )r-   HERMES_MEET_LOBBY_TIMEOUT300g        duration_expired)r2   g      @)r"   r%   r'   u5   lobby timeout — host never admitted the bot within slobby_timeout)r*   r2   zhost denied admissiondeniedz6window.__hermesMeetDrain && window.__hermesMeetDrain()rI   rJ   )rI   rJ   )r1   page_closedr/   r0   )r/   r0   g      ?z`() => { const b = document.querySelector('button[aria-label*="eave call"]'); if (b) b.click(); }r   r   r   r   g      @)r"   r#   r+   zunhandled: rx   )=rc   environr   r   r   _parse_durationr   sysr   rH   r   r   r   signalSIGTERMSIGINTr3    plugins.google_meet.audio_bridger   setupr   playwright.sync_apir   ImportErrorteardowncopyinsertrm   chromiumlaunchis_filenew_contextnew_pagegoto_try_guest_name_click_joinevaluaterz   _CAPTION_OBSERVER_JSr   r   floatr"   _detect_admissionr&   r   _detect_deniedr   listdictr   rO   _looks_like_human_speakercancel_response	is_closedgetattrsleepclose	terminatewaitjoin)(r   out_dir_envheaded
auth_state
guest_name
duration_smoderealtime_modelrealtime_voicerealtime_instructionsrealtime_api_keyr    r!   r   r   r   r   r   r   r   
chrome_envchrome_argspwrp   rq   browsercontext_argscontextpagedeadlinelobby_deadlinelast_admission_checknowadmittedqueuedentryrI   rJ   	cancelledr   s(                                          @r   run_botrD    s	   
**..*B
/
5
5
7C**..!6;AACKZZ^^0"5;;=AUUF 8"=CCEJ 8.IJ 0F!KLJ::>>,l;AACIIKDZZ^^$@.QNZZ^^$@'JNJJNN+NPRSzz~~&@AiRZZ^^TdfhEi',

-/23	
 

MN;G%c*Jg*#FE
 I! MM&..*-
MM&--, :%
B 
)}II  OI  P!ByM&H$$*LLN=!%8		4M9J9N9N}9]	^

7 "J(7K i= 	1BC	M	r-044Z@GK%'%6%:%:="%M
>"C ~	" #((* "1 !

1"kk((#  ) G
 '+c:I !-h7L d:.6680:_-)g))9L9G##%D		#*<f	M D*-e$134		T	:I23 IIIE )}'# "= 1,((!6'
 i=,IITI2 6@		j0TH!YY[5

:EB, N $' 'iikhII+=I> }}#0D*D)K+.(06H		$(*/&) " 
 ~-		!*[`[r[r#nu?V?V.V*Wxy)zz{!} *9 "  '-		"9)1 "  !]]+cdF!&$/%+ -E#-eT#: (&)%))Ir*B&CG#&uyy'<#=D!00t0L  ")}I1J#<Wj#Q%-46yM4Q4Q4S	+4,1IIsI,K-4 i=,II(/9?PRS(T*1"Y-ATVZ*[  
 

3G  'L+ MMOMMOvvj!zN,,.zN'''2 .!&B~&( "#/'(--c-: )}yM'') (|xL))+ IIedIC}~	 ~	?  &		"=aS@_ `	a %9&  		4QC8	F

R	
 h<xL!!#b  		"3A3 7	E5~	 ~	0    I		"CA3 G	HHIv ,5 %-(,%-   ~~'		}	= (.   ! 
 ! 
 ! 
 ! 
 ! w~	 ~	@  		+aS)$	7s  A
`  a g Bg	4b4g	!+c(c8"Fg	%Bd4+d41d4?'d$&d4(Ag	9e)
1g	<(e9$g	*
f	4g	:fg	f)(g	.f9g	g 	a(a		a	b1Ab,,b14	c%=c g	g  c%%g	(	c51g	4c55g	8	d!dg	d!!g	$	d1-d40d11d44+e&g	%e&&g	)	e62g	5e66g	9	fg	fg			fg	fg		f&"g	%f&&g	)	f62g	5f66g	9	gg	gg		gg g 	g>g99g>c                    	 | j                  d      j                  }|j                         r%|j                         r|j	                  |d       yyy# t
        $ r Y yw xY w)zAIf Meet is showing a guest-name input, type *guest_name* into it.zinput[aria-label*="name" i]i  r  N)locatorfirstcount
is_visiblefillr   )r;  r.  rF  s      r   r  r    sY    ,,<=CC==?w113LLUL3  4? s   AA 	AAc                Z    d}	 t        | j                  |            S # t        $ r Y yw xY w)u  True if we're clearly past the lobby and in the call itself.

    Uses a JS-side probe because Meet's DOM structure varies by client
    version. We check several high-signal indicators and declare admission
    on the first hit:

      1. Leave-call button is present (``aria-label`` contains "eave call").
      2. Caption region has appeared (we installed the observer and it attached).
      3. The participant list container is visible.

    Conservative by default — returns False on any error.
    a  
    (() => {
      const leave = document.querySelector('button[aria-label*="eave call" i]');
      if (leave) return true;
      if (window.__hermesMeetInstalled) {
        const caps = document.querySelector(
          '[role="region"][aria-label*="aption" i], ' +
          'div[jsname="YSxPC"], div[jsname="tgaKEf"]'
        );
        if (caps) return true;
      }
      const parts = document.querySelector('[aria-label*="articipants" i]');
      if (parts) return true;
      return false;
    })();
    Fr	   r  r   r;  probes     r   r  r    s4    E DMM%())     	**c                Z    d}	 t        | j                  |            S # t        $ r Y yw xY w)zGTrue when Meet is showing a 'you were denied' / 'no one admitted' page.u  
    (() => {
      const text = document.body ? document.body.innerText || '' : '';
      // English only — matches what shows up when the host denies or
      // removes a guest.
      if (/You can't join this video call/i.test(text)) return true;
      if (/You were removed from the meeting/i.test(text)) return true;
      if (/No one responded to your request to join/i.test(text)) return true;
      return false;
    })();
    FrL  rM  s     r   r  r    s4    
EDMM%()) rO  c                    | r| j                         sy| j                         j                         }|dd|j                         j                         hv ryy)u'  Whether a caption line's speaker is probably a human, not our bot echo.

    Meet attributes captions to the speaker's display name. When Chrome is
    reading our fake mic, Meet still attributes captions to *our* bot name
    (because the bot is the one "speaking"). We don't want those to trigger
    barge-in. Anything else — real participant names — does.

    Conservative: unknown / blank speakers (common when caption scraping
    falls back to raw text) do NOT trigger barge-in, because we can't tell
    whether it was a human or us.
    FunknownyouT)r   r   )rI   bot_guest_namespks      r   r"  r"  "  sM     '--/
--/


!C
y%!5!5!7!=!=!?@@r   c                   dD ]l  }	 | j                  d|d      j                  }|j                         r;|j                         r+|j	                  d       |dk(  r|j                  d	        y
n y
# t        $ r Y {w xY w)zClick 'Join now' or 'Ask to join' if either button is visible.

    Flags ``lobby_waiting`` when we hit the "waiting for host to admit you"
    state so the agent can surface that in status.
    )zJoin nowAsk to joinbuttonF)r   exacti  r  rW  T)r%   N)get_by_rolerG  rH  rI  clickr3   r   )r;  r   labelbtns       r   r  r  6  s     - 		""8%u"EKKCyy{s~~/		%	(M)IIDI1	  		s   A'A33	A?>A?c                @   | sy| j                         j                         } 	 | j                  d      rt        | dd       dz  S | j                  d      rt        | dd       dz  S | j                  d      rt        | dd       S t        |       S # t        $ r Y yw xY w)uE   Parse ``30m`` / ``2h`` / ``90`` (seconds) → float seconds, or None.Nhi  r   <   r  )r   r   endswithr  
ValueError)raws    r   r  r  H  s    
))+


C	<<Sb?T))<<Sb?R''<<Sb?"Sz s#   !B !B 'B 
B 	BB__main__)r   r   rr   r	   )r   r   rr   r   )rr   r   )r   r!  r    r   r   r!  r   r   r   r   r   r   r   r   r   r!  r   z'_BotState'rr   rs   )r   r   rr   r   )rr   r   )r.  r   rr   rs   )rr   r	   )rI   r   rT  r   rr   r	   )r   r   rr   rs   )rd  r   rr   zOptional[float])"rw   
__future__r   rg   rc   r   r	  r  r   r   pathlibr   typingr   compiler
   r   r   r   r   r   r  rz   r   r   rD  r  r  r  r"  r  r  rt   exitrx   r   r   <module>rk     s=  8 #  	 	  
     bjj '   0&*X XF< ~$RlRl Rl 	Rl
 Rl Rl Rl Rl Rl Rl 
RljB_D	 F&($" zCHHWY r   