Skip to content

mostlyright.weather.earnings.capture

Earnings webcast capture fleet (Phase 27, 27-03).

Provider-aware capture adapters that drive the PUBLIC guest path and cold-fetch the open media asset (no auth/DRM/captcha circumvention — D-27.9). Q4 (widest large-cap coverage) ships first; Notified / GlobalMeet / ChorusCall / self-hosted adapters follow in later plans, and the live-during-call IVS live() seam lands in 27-09.

CaptureNotAvailable is re-exported from mostlyright.core.exceptions so capture call sites import the ABC, the artifact, and the control-signal error from one place.

class mostlyright.weather.earnings.capture.AudioArtifact(audio_path, ticker, call_id, source_media_url, is_transient=True)

Section titled “class mostlyright.weather.earnings.capture.AudioArtifact(audio_path, ticker, call_id, source_media_url, is_transient=True)”

Bases: object

A captured-audio handle whose backing file is TRANSIENT (D-27.9).

audio_path points at a temp file extracted from the captured webcast media. is_transient is True by design — the file MUST be deleted after transcription (never persisted or served). cleanup() unlinks it; the artifact is also a context manager, so with adapter.capture(...) as a guarantees the audio is gone on exit even if transcription raises.

source_media_url / call_id / ticker carry the capture lineage for audit (which asset produced these bytes) without retaining the bytes.

  • Parameters:
    • audio_path (str)
    • ticker (str)
    • call_id (str)
    • source_media_url (str)
    • is_transient (bool)

Delete the transient audio file (idempotent — safe to call twice).

FileNotFoundError (the idempotent double-call, or an already-cleaned artifact) is suppressed SILENTLY. Any OTHER OSError (e.g. PermissionError on a read-only mount) is suppressed but LOGGED at error level: it must NOT propagate out of __exit__ (that would MASK the original exception the with block was raising — the transcription error the caller must see), but it means copyrighted earnings audio has SURVIVED on disk (a D-27.9 violation), so it cannot pass silently — the log line names the path an operator must reap manually.

class mostlyright.weather.earnings.capture.CaptureAdapter

Section titled “class mostlyright.weather.earnings.capture.CaptureAdapter”

Bases: ABC

Provider-aware webcast capture contract.

Concrete adapters (Q4CaptureAdapter, later Notified / GlobalMeet / ChorusCall / self-hosted) implement detect() (does this adapter handle the URL?) and capture() (event → transient AudioArtifact). A live() override is added to the fleet in 27-09 for the live-during-call IVS path; this VOD/replay contract is the seam it extends.

Capture event to a transient AudioArtifact.

MUST stay on the public path (no auth/DRM/captcha circumvention — D-27.9) and MUST return an artifact whose backing file is transient. Raise CaptureNotAvailable when the provider template exposes no cold-fetchable HTTP media (e.g. a dial-in bridge) rather than emitting empty audio.

Return True iff this adapter handles url’s webcast provider.

  • Return type: bool
  • Parameters: url (str)

Follow the provider’s LIVE edge, yielding (pcm_frame, spoken_at) (27-09 W5).

The live-during-call seam. Concrete adapters with a fetchable live HLS edge (Q4LiveCapture = Amazon IVS playback.live-video.net, ChorusCallLiveCapture) OVERRIDE this to open a LiveHlsCapturer against manifest_url and stream PCM frames into the streaming STT (27-10). Audio is a TRANSIENT frame stream — the generator yields decoded PCM into the consumer; it NEVER writes audio to disk or serves it (D-27.9).

The ABC DEFAULT raises LiveCaptureUnsupported: a provider with no live edge (dial-in bridge, SABR/YouTube, Lumi/VOD-only) has no live path, and the runner routes it to the authoritative post-call batch ledger instead (D-27.12). session carries any header-gated context (Origin/ Referer/UA/token) a provider needs; Q4/IVS needs none.

exception mostlyright.weather.earnings.capture.CaptureNotAvailable(message=”, , error_code=None, source=None, request_id=None)

Section titled “exception mostlyright.weather.earnings.capture.CaptureNotAvailable(message=”, , error_code=None, source=None, request_id=None)”

Bases: EarningsError

A webcast has no cold-fetchable HTTP media asset to capture.

Raised by a capture adapter (Phase 27, 27-03) when the provider template is not an HTTP-media webcast — e.g. Q4’s analyst template is a telephone dial-in bridge (Meeting ID + passcode; zero media/CDN hosts ever fire — RESEARCH §2 CHWY). The correct disposition is to route to dial-in / demand capture or skip, NOT to silently emit empty audio. This is a normal control signal (the conference-call subclass), not a corruption — a subclass of EarningsError so callers filter it cleanly.

  • Parameters:
    • message (str)
    • error_code (str)
    • source (str | None)
    • request_id (str | None)
  • Return type: None

default_error_code : str = ‘EARNINGS_CAPTURE_NOT_AVAILABLE’

Section titled “default_error_code : str = ‘EARNINGS_CAPTURE_NOT_AVAILABLE’”

Subclass override — the stable string enum surfaced via error_code.

class mostlyright.weather.earnings.capture.ChorusCallLiveCapture

Section titled “class mostlyright.weather.earnings.capture.ChorusCallLiveCapture”

Bases: CaptureAdapter

ChorusCall live-HLS capture — a HEADER-GATED provider.

Unlike Q4/IVS, ChorusCall’s live edge is header-gated (Origin/Referer/UA), so live passes the guest-path session headers through to the LiveHlsCapturer. The live-edge GO/NO-GO is the operator-gated 27-09 Task 1 spike (Q4/IVS is already banked GO; ChorusCall is DEFERRED to a real scheduled call).

Capture event to a transient AudioArtifact.

MUST stay on the public path (no auth/DRM/captcha circumvention — D-27.9) and MUST return an artifact whose backing file is transient. Raise CaptureNotAvailable when the provider template exposes no cold-fetchable HTTP media (e.g. a dial-in bridge) rather than emitting empty audio.

Return True iff this adapter handles url’s webcast provider.

  • Return type: bool
  • Parameters: url (str)

Open a LiveHlsCapturer replaying the header-gated session.

exception mostlyright.weather.earnings.capture.LiveCaptureUnsupported(message=”, , error_code=None, source=None, request_id=None)

Section titled “exception mostlyright.weather.earnings.capture.LiveCaptureUnsupported(message=”, , error_code=None, source=None, request_id=None)”

Bases: EarningsError

A capture adapter has NO live-during-call edge (27-09 W5).

ENGINE-LOCAL exception: it subclasses the core EarningsError so callers filter it with the rest of the earnings-pipeline errors, but it is deliberately NOT added to packages/core — the live seam is engine-only and must never touch the parity surface (research() / merge/* / live/_sources.py). Raised by CaptureAdapter.live() (the ABC default) for providers whose template has no fetchable live HLS edge — a dial-in bridge (telephone), a SABR/YouTube stream we have not proven, or a Lumi/VOD-only provider. The runner routes such a provider to the post-call batch path only (D-27.12), never opening a live session.

  • Parameters:
    • message (str)
    • error_code (str)
    • source (str | None)
    • request_id (str | None)
  • Return type: None

default_error_code : str = ‘EARNINGS_LIVE_CAPTURE_UNSUPPORTED’

Section titled “default_error_code : str = ‘EARNINGS_LIVE_CAPTURE_UNSUPPORTED’”

Subclass override — the stable string enum surfaced via error_code.

exception mostlyright.weather.earnings.capture.LiveEdgeFellBehind(message=”, , error_code=None, source=None, request_id=None)

Section titled “exception mostlyright.weather.earnings.capture.LiveEdgeFellBehind(message=”, , error_code=None, source=None, request_id=None)”

Bases: EarningsError

The rolling live window evicted a segment before it could be fetched.

ENGINE-LOCAL (subclasses EarningsError; NOT added to packages/core — the live seam never touches the parity surface). Raised when the lowest #EXT-X-MEDIA-SEQUENCE in a re-pulled manifest is greater than watermark + 1 — i.e. a segment the capturer had NOT yet fetched aged out of the rolling window. A silently-dropped segment would create an UNDETECTED transcript gap (a wrong mention count), so keep-up loss must fail LOUD, never leave a silent hole (threat T-27-32).

  • Parameters:
    • message (str)
    • error_code (str)
    • source (str | None)
    • request_id (str | None)
  • Return type: None

default_error_code : str = ‘EARNINGS_LIVE_EDGE_FELL_BEHIND’

Section titled “default_error_code : str = ‘EARNINGS_LIVE_EDGE_FELL_BEHIND’”

Subclass override — the stable string enum surfaced via error_code.

class mostlyright.weather.earnings.capture.LiveHlsCapturer(master_url, , session_headers=(), client=None, transport=None, poll_interval_s=None, max_stall_polls=6)

Section titled “class mostlyright.weather.earnings.capture.LiveHlsCapturer(master_url, , session_headers=(), client=None, transport=None, poll_interval_s=None, max_stall_polls=6)”

Bases: object

Follows an HLS live edge and yields (pcm_frame, spoken_at) tuples.

master_url is the multivariant MASTER (openly cold-fetchable for Q4/IVS). session_headers is an OPTIONAL iterable of (name, value) pairs replayed on every fetch — default empty (Q4/IVS needs none); header-gated providers (ChorusCall / webcasts.com) pass Origin/Referer/UA. client is an injected httpx.Client (tests pass one wrapping a MockTransport); when omitted a fresh UA-only client is built and owned. poll_interval_s overrides the manifest #EXT-X-TARGETDURATION re-pull cadence (tests pass 0.0). max_stall_polls bounds a no-new-segment stall before clean termination.

  • Parameters:
    • master_url (str)
    • session_headers (tuple [tuple [str , str ] , ])
    • client (httpx.Client | None)
    • transport (httpx.BaseTransport | None)
    • poll_interval_s (float | None)
    • max_stall_polls (int)

Yield (pcm_frame, spoken_at) following the live edge exactly-once.

Resolves the audio_only media URL off the master, then loops: fetch the media playlist, diff new segments by #EXT-X-MEDIA-SEQUENCE, fetch each NEW segment exactly once, decode to PCM in-memory, and yield it with a monotonic spoken_at derived from segment ordering x duration off the session start. On a media/segment 403 the media URL is refreshed off the OPEN master. Terminates on #EXT-X-ENDLIST or a no-new-segment stall; raises LiveEdgeFellBehind on window-eviction keep-up loss. NO audio is ever written to disk.

exception mostlyright.weather.earnings.capture.LiveManifestError(message=”, , error_code=None, source=None, request_id=None)

Section titled “exception mostlyright.weather.earnings.capture.LiveManifestError(message=”, , error_code=None, source=None, request_id=None)”

Bases: EarningsError

A live master/media playlist is malformed or missing the audio rendition.

ENGINE-LOCAL. Raised by LiveHlsCapturer._select_audio_only() when the multivariant master exposes no audio_only rendition (nothing to follow without pulling video), or when a manifest cannot be parsed.

  • Parameters:
    • message (str)
    • error_code (str)
    • source (str | None)
    • request_id (str | None)
  • Return type: None

default_error_code : str = ‘EARNINGS_LIVE_MANIFEST_ERROR’

Section titled “default_error_code : str = ‘EARNINGS_LIVE_MANIFEST_ERROR’”

Subclass override — the stable string enum surfaced via error_code.

class mostlyright.weather.earnings.capture.Q4CaptureAdapter

Section titled “class mostlyright.weather.earnings.capture.Q4CaptureAdapter”

Bases: CaptureAdapter

Q4 browser-webcast VOD capture over the public guest path (RESEARCH §2).

capture(event, , client=None, transport=None, tmp_dir=None, **_)

Section titled “capture(event, , client=None, transport=None, tmp_dir=None, **_)”

Cold-fetch the Q4 static MP4 and extract transient audio.

event carries ticker / call_id / media_url (the sniffed static.events.q4inc.com/.../*.mp4 currentSrc). When media_url is absent the event is the analyst dial-in template (no HTTP media) — CaptureNotAvailable is raised.

client may be an injected httpx.Client (a guest-flow session for later gated providers). transport is the explicit transport the cold MEDIA fetch rides on — tests pass an httpx.MockTransport directly. The cold MEDIA fetch is ALWAYS issued through a FRESH UA-only client built here (from transport when given) — it deliberately does NOT inherit the injected client’s cookies or default headers, so no cookie / token / Authorization can ever leak onto the asset GET (the guest form gates the page, not the asset — RESEARCH §2). When only client is given (no explicit transport) the media fetch reuses that client’s transport via its public httpx.Client._transport seam so the mock host still serves the request, but starts from a clean header set and empty cookie jar.

media_url is validated (https + Q4 media host) BEFORE any fetch — an unvalidated cold GET would be an SSRF sink (D-27.9). The media fetch rides a GENEROUS timeout (not httpx’s flat 5s default, which would abort a full MP4 stream at the read deadline). A BORROWED transport (from an injected client or the explicit transport param) is NEVER closed here — only a transport this method created itself is; closing a borrowed shared transport would break the caller’s injected client.

A 2xx/206 that carries a ZERO-byte body (an expired / edited replay that answers the range but serves no media) raises CaptureNotAvailable instead of writing an empty MP4 and flowing to STT — the byte count is tracked during the stream loop and an empty result fails loud.

The fetch is also bounded on SIZE and TOTAL TIME (D-27.9 resource-safety): media_url is external event data and the SSRF allow-list bounds only the host, not the object size, so an over-_MAX_MEDIA_BYTES asset is rejected up front from its declared Content-Range/Content-Length and, as a backstop, aborted mid-stream once the written bytes exceed the cap; a slow-drip source that never trips the per-op read timeout is aborted at the _MEDIA_TOTAL_TIMEOUT wall-clock deadline. All abort paths unlink the partial MP4 via the outer finally — an oversized/slow asset can never fill the ingest host’s temp disk or flow to STT.

  • Return type: AudioArtifact
  • Parameters:
    • event (dict [str , Any ])
    • client (httpx.Client | None)
    • transport (httpx.BaseTransport | None)
    • tmp_dir (str | Path | None)
    • _ (Any)

Return True when url is a Q4 webcast/asset host.

  • Return type: bool
  • Parameters: url (str)

class mostlyright.weather.earnings.capture.Q4LiveCapture

Section titled “class mostlyright.weather.earnings.capture.Q4LiveCapture”

Bases: CaptureAdapter

Q4/IVS live-during-call capture — Amazon IVS playback.live-video.net.

The LIVE edge is the IVS multivariant master (discovered from the live player page via the 27-03 guest flow), NOT the VOD static.events.q4inc.com/*.mp4 (which does not exist mid-call). Needs NO session headers — master/media/ segments are 200 cold, the token is in-URL (NKE-validated). capture / detect delegate to the VOD adapter’s contract shape; live opens a LiveHlsCapturer.

Capture event to a transient AudioArtifact.

MUST stay on the public path (no auth/DRM/captcha circumvention — D-27.9) and MUST return an artifact whose backing file is transient. Raise CaptureNotAvailable when the provider template exposes no cold-fetchable HTTP media (e.g. a dial-in bridge) rather than emitting empty audio.

Return True iff this adapter handles url’s webcast provider.

  • Return type: bool
  • Parameters: url (str)

Open a LiveHlsCapturer on the IVS master (NO session headers).

baseCapture-adapter ABC + the transient audio artifact (Phase 27, 27-03).
live_hlsLive-HLS rolling-window capturer (Phase 27, 27-09 Wave 5).
q4Q4 browser-webcast capture adapter (Phase 27, 27-03; RESEARCH §2).