mostlyright.weather.earnings.capture
mostlyright.weather.earnings.capture
Section titled “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.
audio_path : str
Section titled “audio_path : str”call_id : str
Section titled “call_id : str”cleanup()
Section titled “cleanup()”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.
- Return type:
None
is_transient : bool
Section titled “is_transient : bool”source_media_url : str
Section titled “source_media_url : str”ticker : str
Section titled “ticker : str”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.
abstractmethod capture(event, **kwargs)
Section titled “abstractmethod capture(event, **kwargs)”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 type:
AudioArtifact - Parameters:
abstractmethod detect(url)
Section titled “abstractmethod detect(url)”Return True iff this adapter handles url’s webcast provider.
live(manifest_url, session=None)
Section titled “live(manifest_url, session=None)”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:
- 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.
error_code : str
Section titled “error_code : str”message : str
Section titled “message : str”request_id : str | None
Section titled “request_id : str | None”source : str | None
Section titled “source : str | None”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, **kwargs)
Section titled “capture(event, **kwargs)”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.
detect(url)
Section titled “detect(url)”Return True iff this adapter handles url’s webcast provider.
live(manifest_url, session=None)
Section titled “live(manifest_url, session=None)”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:
- 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:
- 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:
frames()
Section titled “frames()”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:
- 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:
detect(url)
Section titled “detect(url)”Return True when url is a Q4 webcast/asset host.
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, **kwargs)
Section titled “capture(event, **kwargs)”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.
detect(url)
Section titled “detect(url)”Return True iff this adapter handles url’s webcast provider.
live(manifest_url, session=None)
Section titled “live(manifest_url, session=None)”Open a LiveHlsCapturer on the IVS master (NO session headers).
Modules
Section titled “Modules”base | Capture-adapter ABC + the transient audio artifact (Phase 27, 27-03). |
|---|---|
live_hls | Live-HLS rolling-window capturer (Phase 27, 27-09 Wave 5). |
q4 | Q4 browser-webcast capture adapter (Phase 27, 27-03; RESEARCH §2). |