Skip to content

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, with no auth, DRM, or captcha circumvention. Q4 (widest large-cap coverage) is the shipped 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 and is 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 (an idempotent double-call, or an already-cleaned artifact) is suppressed silently. Any other OSError, such as PermissionError on a read-only mount, is suppressed but logged at error level: it must not propagate out of __exit__, which would mask the transcription error the with block was raising and that the caller must see. It still means copyrighted earnings audio survived on disk, so the log line names the path an operator must delete manually.

class mostlyright.finance.transcripts.capture.CaptureAdapter

Section titled “class mostlyright.finance.transcripts.capture.CaptureAdapter”

Bases: ABC

Provider-aware webcast capture contract.

Concrete adapters implement detect() (does this adapter handle the URL?) and capture() (event → transient AudioArtifact). Providers with a fetchable live stream can also override live().

Capture event to a transient AudioArtifact.

An implementation must stay on the public path (no auth, DRM, or captcha circumvention) and must return an artifact whose backing file is transient. Raise CaptureNotAvailableError when the provider template exposes no cold-fetchable HTTP media (for example 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).

Reserved live-during-call entry point. A concrete adapter with a fetchable live HLS endpoint overrides this to open a rolling-window live capturer against manifest_url and stream PCM frames into the streaming STT. Audio stays a transient frame stream: the generator yields decoded PCM to the consumer and never writes audio to disk or serves it.

The ABC default raises LiveCaptureUnsupported. A provider with no live endpoint (dial-in bridge, SABR/YouTube, replay-only such as Lumi) 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/User-Agent/token) a provider needs; Q4 over 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 — for example Q4’s analyst template, which is a telephone dial-in (meeting ID plus passcode, with no media or CDN host ever requested). The caller should route to dial-in capture or skip the webcast, rather than emit empty audio. This is a normal control signal, not a corruption, and subclasses EarningsError so callers can filter it cleanly.

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

Subclass override — the stable string enum surfaced via error_code.

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 supported live-during-call endpoint.

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 capture path is engine-only and must never touch the parity-critical modules (research(), merge/*, _live/_sources.py). Raised by CaptureAdapter.live() (the ABC default) for providers whose template has no fetchable live HLS endpoint: a telephone dial-in bridge, an unverified SABR/YouTube stream, or a replay-only provider such as Lumi. The runner routes such a provider to the post-call batch path and never opens a live session.

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

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 replay capture over the public guest path.

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 User-Agent-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, or Authorization header can leak onto the asset GET — the guest form gates the page, not the asset. When only client is given with no explicit transport, the media fetch reuses that client’s transport via httpx.Client._transport so a mock host still serves the request, but starts from a clean header set and empty cookie jar.

media_url is validated (https plus a Q4 media host) before any fetch; an unvalidated cold GET would be an SSRF sink. The media fetch uses generous timeouts rather than 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, because closing a borrowed shared transport would break the caller’s injected client.

A 2xx/206 carrying a zero-byte body (an expired or edited replay that answers the range but serves no media) raises CaptureNotAvailableError instead of writing an empty MP4 that flows 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. media_url is external event data and the SSRF allowlist bounds only the host, not the object size, so an asset over _MAX_MEDIA_BYTES 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-operation read timeout is aborted at the _MEDIA_TOTAL_TIMEOUT wall-clock deadline. All abort paths unlink the partial MP4 via the outer finally, so an oversized or 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)
ModuleDescription
baseCapture-adapter ABC + the transient audio artifact.
q4Q4 browser-webcast capture adapter.