mostlyright.finance.transcripts.capture
Earnings webcast capture fleet.
Provider-aware capture adapters that drive the PUBLIC guest path and cold-fetch
the open media asset (no auth/DRM/captcha circumvention). Q4 (widest large-cap
coverage) is the shipped VOD/replay adapter: it captures the static post-call
media asset into a transient AudioArtifact for transcription.
CaptureNotAvailableError 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.finance.transcripts.capture.AudioArtifact(audio_path, ticker, call_id, source_media_url, is_transient=True)
Section titled “class mostlyright.finance.transcripts.capture.AudioArtifact(audio_path, ticker, call_id, source_media_url, is_transient=True)”Bases: object
A captured-audio handle whose backing file is TRANSIENT.
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 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.finance.transcripts.capture.CaptureAdapter
Section titled “class mostlyright.finance.transcripts.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 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 —)
and MUST return an artifact whose backing file is transient. Raise
CaptureNotAvailableError 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) (W5).
Reserved live-during-call seam. A concrete adapter with a fetchable live
HLS edge would OVERRIDE this to open a rolling-window live capturer
against manifest_url and stream PCM frames into the streaming STT.
Audio is a TRANSIENT frame stream — the generator yields decoded
PCM into the consumer; it NEVER writes audio to disk or serves it.
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. session carries any header-gated context (Origin/
Referer/UA/token) a provider needs; Q4/IVS needs none.
exception mostlyright.finance.transcripts.capture.CaptureNotAvailableError(message=”, , error_code=None, source=None, request_id=None)
Section titled “exception mostlyright.finance.transcripts.capture.CaptureNotAvailableError(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 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
Section titled “default_error_code: str”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”exception mostlyright.finance.transcripts.capture.LiveCaptureUnsupported(message=”, , error_code=None, source=None, request_id=None)
Section titled “exception mostlyright.finance.transcripts.capture.LiveCaptureUnsupported(message=”, , error_code=None, source=None, request_id=None)”Bases: EarningsError
A capture adapter has NO live-during-call edge (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
never opening a live session.
- Parameters:
- Return type: None
default_error_code: str
Section titled “default_error_code: str”Subclass override — the stable string enum surfaced via error_code.
class mostlyright.finance.transcripts.capture.Q4CaptureAdapter
Section titled “class mostlyright.finance.transcripts.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) —
CaptureNotAvailableError 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. 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 CaptureNotAvailableError
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 (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.
Modules
Section titled “Modules”base | Capture-adapter ABC + the transient audio artifact. |
|---|---|
q4 | Q4 browser-webcast capture adapter. |