Source code for hearing.interfaces
"""Facade Protocols for the four pipeline concerns.
Every engine/component is dependency-injected as one of these; the pipeline
depends only on these interfaces, never on a concrete engine. Implementers
satisfy them *structurally* (``typing.Protocol``) — they do not subclass.
This is the contract the ``hearing-architecture`` skill defines and that every
other ``hearing-*`` concern implements against.
"""
from __future__ import annotations
from typing import (
Any,
AsyncIterator,
Iterable,
Iterator,
Optional,
Protocol,
Sequence,
runtime_checkable,
)
from hearing.types import Channel, TranscriptSegment
# ── Concern 1: capture ───────────────────────────────────────────────────────
[docs]
@runtime_checkable
class CaptureSource(Protocol):
"""Audio source — the ONLY thing that differs between batch and live.
Yields ``(channel, frames)`` so mic and system audio stay on separate
channels all the way down. A batch implementation reads a file; a live
implementation reads a device/tap.
"""
sample_rate: int
[docs]
def frames(self) -> Iterator[tuple[Channel, "Any"]]:
"""Batch / sync: yield ``(channel, frames[n_samples])`` blocks to EOF."""
...
[docs]
def astream(self) -> AsyncIterator[tuple[Channel, "Any"]]:
"""Live / async: yield ``(channel, frames)`` blocks until stopped."""
...
# ── Concern 2: STT (the pluggable facade) ────────────────────────────────────
[docs]
@runtime_checkable
class STTEngine(Protocol):
"""Speech-to-text.
``transcribe`` is the batch one-liner; ``stream_transcribe`` is the live
variant. Both yield :class:`~hearing.types.TranscriptSegment` — same shape,
same spine. The rest of the architecture never names a concrete engine.
"""
[docs]
def transcribe(
self, audio: "Any", *, sample_rate: int, language: Optional[str] = None
) -> Sequence[TranscriptSegment]:
"""Batch: whole clip in (float32 mono ndarray), segments out."""
...
[docs]
async def stream_transcribe(
self, frames: AsyncIterator["Any"], *, sample_rate: int
) -> AsyncIterator[TranscriptSegment]:
"""Live: yield interim then finalized segments as audio arrives.
Mark interim vs final via ``meta`` (e.g. ``meta['final']``). See the
``hearing-live-pipeline`` skill — this is the milestone-2 entry point.
"""
...
# ── Concern 3: diarization / speaker-id (separate concern) ────────────────────
[docs]
@runtime_checkable
class Diarizer(Protocol):
"""Labels who spoke — enriches segments with ``.speaker``.
May use the audio (pyannote) or just the channel (the channel trick).
Identity resolution (``spk_0`` -> ``"Alice"``) is an optional further step
behind the same interface.
"""
[docs]
def assign_speakers(
self,
segments: Iterable[TranscriptSegment],
*,
audio: Optional["Any"] = None,
sample_rate: Optional[int] = None,
) -> Iterable[TranscriptSegment]:
"""Yield segments with ``.speaker`` populated (enrich-by-copy)."""
...
# ── Concern 4: agent layer ───────────────────────────────────────────────────
[docs]
@runtime_checkable
class AgentConsumer(Protocol):
"""Consumes the (labeled) transcript stream and does things.
Running notes, suggested questions, RAG over related docs, post-meeting
summaries. Defaults to Claude but stays pluggable (see the ``claude-api``
skill for model ids / pricing / tool use).
"""
[docs]
async def on_segment(self, segment: TranscriptSegment) -> Optional[str]:
"""Called per finalized segment (live). Return an optional insight."""
...
[docs]
async def on_window(self, window: Sequence[TranscriptSegment]) -> Optional[str]:
"""Called on a window/turn or the whole transcript (batch).
Return an optional insight (summary, action items, a suggested
question, a surfaced document, ...).
"""
...