Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,10 @@ jobs:
working-directory: sdk-python
shell: bash
run: |
if rg -n -i '\b(boundary|path|surface|authority|projection|lowering|flow|layer|contracts?)\b' \
if rg -n -i '\b(boundary|path|surface|authority|projection|lowering|flow|layer|bounded|contracts?)\b' \
README.md RELEASE_NOTES.md docs examples \
--glob '*.md' --glob '*.mdx'; then
echo "Public documentation must name the API, queue, process, service, or request directly."
echo "Public documentation must name the API, queue, limit, process, service, or request directly."
exit 1
fi

Expand Down
61 changes: 31 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,22 +75,21 @@ python -m pip install 'pocketstation[transcription]'
python examples/transcribe_voice_app.py
```

The program asks which desktop voice application to inspect. It declares one
faster-whisper Operator, connects both stems to its audio input, and prints each
transcript with its original source identity. It does not start Relay or write a
The program asks which desktop voice application to inspect. It sends the
application and microphone through one faster-whisper model, then labels each
transcript with the source that produced it. It does not start Relay or write a
recording.

The Session runs these jobs concurrently:
The Session preserves each source while the transcriber processes both:

```text
voice application ─┐
├─ one bounded faster-whisper Operator ─ transcripts
physical microphone┘
voice application ── faster-whisper ── transcript labeled "application"
physical microphone ─ faster-whisper ─ transcript labeled "microphone"
```

The complete composition is visible in
[`examples/transcribe_voice_app.py`](examples/transcribe_voice_app.py). The
example adapter imports `faster_whisper.WhisperModel` when the Operator starts;
example adapter imports `faster_whisper.WhisperModel` when transcription starts;
the provider is not part of the `pocketstation` namespace.

This example does not debug turn handling, interruption, agent latency, or
Expand Down Expand Up @@ -131,9 +130,9 @@ with pocketstation.capture(
print(frame.source_id, frame.stem_id)
```

The iterator reads a bounded native endpoint. A slow Python consumer produces
observable pressure and discontinuities; it does not create an unbounded Python
audio queue.
The iterator receives audio through a native queue that holds 32 frames by
default. If Python stops reading and the queue fills, PocketStation drops new
frames and reports the queue depth, dropped-frame count, and discontinuity.

## Send application-owned audio into a Session

Expand All @@ -154,31 +153,28 @@ cancelled, and invalid-buffer outcomes explicitly.

## Create an integration

PocketStation provides four integration APIs:

| API | Use it when |
|---|---|
| `Source` | Media or signals enter the Session. |
| `Operator` | Work transforms media or emits typed signals. |
| `Connector` | Media or signals leave for an external system. |
| `Endpoint` | You need direct control of an outbound worker. |
Create a `Connector` when Session audio needs to reach an API, socket, file, or
provider. Most Python integrations need only a send function or a small class;
PocketStation supplies the worker, queue, delivery observations, and shutdown.

Pass one function when the destination is already open:

```python
import pocketstation as pks
import pocketstation.aio as pks_aio


async def send_audio(frame: pks.AudioFrame) -> None:
await socket.send(frame.samples)


destination = pks_aio.Connector(send=send_audio)
application.send_to(destination)
```

Subclass the synchronous or asyncio Connector when the provider opens and
closes resources. The provider class owns its connection; the Session owns
bounded delivery, lineage, observations, drain, abort, and joined shutdown:
route delivery, lineage, observations, drain, abort, and joined shutdown:

```python
class WebSocketConnector(pks_aio.Connector):
Expand Down Expand Up @@ -208,10 +204,15 @@ PocketStation calls `start()` once, interleaves both source-aware stems through
destination. See [Create an integration](docs/guides/integrations.md) for
deadlines, failures, and the advanced SPI.

Python provider callbacks execute on bounded off-realtime workers. They cannot
be used as native capture callbacks. Use compiled native extensions for native
provider code, or a process sidecar when crash
isolation is required.
Python provider callbacks execute on off-realtime workers. They cannot be used
as native capture callbacks. Use a compiled native extension for native
provider code, or a managed process when crash isolation is required.

Use a `Source` when media enters the Session, an `Operator` when work transforms
media or emits typed signals, and an `Endpoint` when an integration needs direct
control of outbound delivery. The [integration guide](docs/guides/integrations.md)
starts with the normal Connector API and introduces those advanced APIs only
when the task requires them.

## Use Relay from Python

Expand All @@ -220,8 +221,8 @@ The shared Rust `pocketstation-relay` connector publishes media. The Go Relay
service forwards WebRTC audio. Python does not encode Opus, write RTP, or own a
second media plane.

The control client uses finite request deadlines, bounded response bodies,
redacted secrets, and matching synchronous and asyncio APIs.
The control client limits request duration and response size, redacts secrets,
and provides matching synchronous and asyncio APIs.

## Sync and asyncio

Expand All @@ -243,8 +244,8 @@ does not have the same execution cost as Rust.
| Windows 11 ARM64 | Core application selection and 10 ms capture tested in a VM; installed Python distribution and physical-device qualification in progress |
| WAN and TURN | Not yet qualified |

The native binding uses PocketStation Core `1.1.4` and the shared Relay
Connector `0.1.2`.
The native binding uses PocketStation Core `1.1.7` and the shared Relay
Connector `0.1.5`.

The Rust-to-Python audio read currently copies native samples into Python-owned
bytes before exposing a `memoryview`. The view avoids another Python-side copy;
Expand All @@ -266,8 +267,8 @@ uv run mypy python tests/qualification/typing_contract.py examples
guidance.
- [`docs/README.md`](docs/README.md) — task guides, concepts, operations, and
API ownership.
- [Write application-owned audio](docs/guides/application-audio.md) — bounded
PCM input and selective output cancellation.
- [Write application-owned audio](docs/guides/application-audio.md) — PCM input
with explicit queue capacity and selective output cancellation.
- [Process audio and typed signals](docs/guides/process-audio-and-signals.md) —
Operators, named ports, generated audio, and finite model work.
- [Record and observe a Session](docs/guides/record-and-observe.md) — multistem
Expand Down
29 changes: 23 additions & 6 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## Unreleased

## 0.1.3 — 2026-09-03

Configure route delivery with one consistent set of names across Python and
the native runtime.

### Added

Advanced integrations can now configure `RouteSettings` as accepted media plus
Expand All @@ -10,9 +15,20 @@ accept the clearer `route_settings=` keyword. Runtime metrics expose
`RouteObservability`, `RouteLatencyMeasurement`, `RouteDeliveryMetrics`, and
`SignalQueueMetrics`.

Existing `EdgeContract`, `EdgeObservabilityLevel`, `RouteLatencyBoundary`,
`EdgeMetrics`, `TypedEdgeMetrics`, and `edge=` uses remain compatible throughout
the 0.1.x series.
Session operator metrics expose aggregate input delivery through
`input_delivery`, with per-port detail in `input_ports`.

This release uses PocketStation Core 1.1.7, which keeps concurrent queue-depth
observations within the configured route capacity, and Relay Connector 0.1.5.

### Changed

Connector, Endpoint, Operator, subscription, and observation APIs now use the
same route-settings vocabulary.

```console
python -m pip install --upgrade pocketstation==0.1.3
```

## 0.1.2 — 2026-09-01

Expand All @@ -29,13 +45,14 @@ Use `Connector(send=...)` when the destination is already open:
async def send_audio(frame):
await socket.send(frame.samples)


destination = pocketstation.aio.Connector(send=send_audio)
application.send_to(destination)
```

Subclass `pocketstation.Connector` for a synchronous destination or
`pocketstation.aio.Connector` for an asynchronous provider. The class owns its
provider connection. PocketStation owns the bounded routes, source and stem
provider connection. PocketStation owns route delivery, source and stem
identity, delivery observations, drain, abort, and joined shutdown.

```python
Expand Down Expand Up @@ -98,15 +115,15 @@ The first release includes:
- synchronous and asyncio Session APIs;
- exact application selection and default-microphone capture;
- 10 ms and 20 ms audio profiles;
- bounded audio and typed-signal streams;
- audio and typed-signal streams with explicit queue capacities;
- Python-authored Sources, Operators, Connectors, and Endpoints;
- application-owned PCM input and generated-audio output cancellation;
- provider-neutral voice composition with revisable transcripts;
- Relay publication and short-lived browser invitations; and
- source-aware multistem recording with structured outcomes.

The package uses PocketStation Core for capture, routing, timing, recording,
and lifecycle. Python provider work runs on bounded off-realtime workers and
and lifecycle. Python provider work runs on off-realtime workers and
does not execute on native capture callbacks.

### Voice interruption example
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ need the lower-level API.
- [Write application-owned audio into a Session](guides/application-audio.md)
- [Process audio and typed signals](guides/process-audio-and-signals.md)
- [Record stems and inspect Session delivery](guides/record-and-observe.md)
- [Compose a bounded voice workflow](guides/voice.md)
- [Compose a voice workflow](guides/voice.md)
- [Publish a named AudioBus through Relay](guides/relay.md)
- [Create a Source, Operator, Connector, or Endpoint](guides/integrations.md)

Expand Down
6 changes: 0 additions & 6 deletions docs/concepts/route-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,3 @@ Advanced Connector and Endpoint preparation objects expose `route_settings`.
attempted delivery, drops, and discontinuities. Typed signals use
`SignalQueueMetrics`. Check those observations before increasing capacity: a
larger finite queue can hold older audio without solving the slow destination.

`EdgeContract` remains an import-compatible name for `RouteSettings` in the
0.1.x series. Existing `edge=` keyword arguments continue to work. New code
should use `route_settings=` so the decision is clear at the call site.
`EdgeMetrics` and `TypedEdgeMetrics` remain compatibility names for the clearer
metrics types.
8 changes: 4 additions & 4 deletions docs/concepts/session-and-bounds.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ captures and routes audio through finite queues.
application ─┐
microphone ──┼─ Session ─┬─ Python or native Operator
owned PCM ───┘ ├─ Connector or Endpoint
├─ bounded frame iterator
├─ frame iterator
└─ multistem recording
```

Expand All @@ -25,14 +25,14 @@ declared capacities. When a queue is full, PocketStation returns or records
pressure according to that route's policy. It does not hide pressure in an
unbounded `asyncio.Queue`.

A slow Python consumer can still lose frames at its own bounded endpoint.
Inspect the route counters and discontinuities whenever complete delivery
A slow Python consumer can still lose frames when its route queue fills.
Inspect the queue depth, dropped-frame counters, and discontinuities whenever complete delivery
matters.

## Python does not run on capture callbacks

Python-authored Sources, Operators, Connectors, and Endpoints execute on
bounded off-realtime workers. Native capture callbacks remain allocation-free,
off-realtime workers. Their queues have configured capacities. Native capture callbacks remain allocation-free,
lock-free, blocking-free, async-free, log-free, and panic-free.

Use a compiled extension when code must stay native. Use a process sidecar when
Expand Down
5 changes: 3 additions & 2 deletions docs/getting-started/capture.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ discover it again. For a saved selection, use
in [Persist a source at its supported scope](../operations/platform-support.md#persist-a-source-at-its-supported-scope).

The context manager starts one native Session and joins it when the block
exits. The iterator reads a finite native Endpoint; it does not create an
unbounded Python audio queue.
exits. The iterator receives audio through a native queue that holds 32 frames
by default. If Python stops reading and the queue fills, PocketStation drops
new frames and reports the loss.

## Add a microphone or recording

Expand Down
2 changes: 1 addition & 1 deletion docs/guides/application-audio.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,4 @@ playback device. A Connector and receiver need their own clear operation and
playout acknowledgement before an application can claim audible cancellation.

Continue with [multistem recording and observations](record-and-observe.md) or
[bounded voice composition](voice.md).
[voice composition](voice.md).
9 changes: 6 additions & 3 deletions docs/guides/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ capture, buffering, routing, and shutdown in every integration.

```text
application ─┐
microphone ──┼→ independent bounded Session routes
microphone ──┼→ independent Session routes
generated ───┘ ↓
one Connector lifecycle
Expand All @@ -54,9 +54,11 @@ Use one function when the provider connection is already open:
import pocketstation as pks
import pocketstation.aio as pks_aio


async def send_audio(frame: pks.AudioFrame) -> None:
await socket.send(frame.samples)


destination = pks_aio.Connector(send=send_audio)
application.send_to(destination)
```
Expand All @@ -77,6 +79,7 @@ Use a class when the integration is reused or owns provider state:
import pocketstation as pks
import pocketstation.aio as pks_aio


class WebSocketConnector(pks_aio.Connector):
def __init__(self, url: str, token: str) -> None:
self.url = url
Expand All @@ -101,7 +104,7 @@ microphone.send_to(destination)
```

One Connector object is one provider lifecycle. PocketStation creates two
bounded routes, calls `start()` once, preserves each frame's source and stem
routes with separate delivery queues, calls `start()` once, preserves each frame's source and stem
identity, and calls `stop()` once. Two Connector objects create two independent
destinations.

Expand All @@ -124,7 +127,7 @@ Connector is slow or fails.
| Method | Provider responsibility | PocketStation responsibility |
|---|---|---|
| `start()` | Open and authenticate the configured destination. | Run on the managed worker after the Session start gate, apply a finite async deadline, retain failure in the terminal outcome, and close once. |
| `send(frame)` | Encode or publish one frame without retaining it indefinitely. | Deliver off realtime from a bounded route and preserve frame lineage. |
| `send(frame)` | Encode or publish one frame without retaining it indefinitely. | Deliver off realtime from a route with a configured queue capacity and preserve frame lineage. |
| `stop()` | Close sockets, files, tasks, and provider resources. | Call once after drain, abort, startup rollback, timeout, or delivery failure. |

`AudioFrame` includes source, stream, stem, sequence, timestamp, clock,
Expand Down
3 changes: 2 additions & 1 deletion docs/guides/process-audio-and-signals.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Use an `Operator` when work consumes Session media or signals and emits a
derived result. Transcription, classification, translation, speech synthesis,
and policy checks are Operators when their outputs remain inside the Session.

A Python Operator runs on a bounded off-realtime worker. Capture and unrelated
A Python Operator runs on an off-realtime worker. Capture and unrelated
destinations continue while the Operator works. The Operator does not create a
second Session or call Python from a native capture callback.

Expand All @@ -27,6 +27,7 @@ manifest = OperatorManifest(
outputs=(PortSpec.output("output", result),),
)


@operator(manifest)
async def uppercase(input_port, envelope):
assert input_port == "input"
Expand Down
3 changes: 2 additions & 1 deletion docs/guides/record-and-observe.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ a browser played the same sample.

## Stop or cancel deliberately

Leaving the context requests normal stop and drains accepted bounded work.
Leaving the context requests normal stop and drains work already accepted by
the route queues.
Call `cancel()` on the explicit `RunningSession` API when active provider or
sidecar work must abort. Both shutdown modes join Session workers before returning
a terminal `StopResult`.
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/voice.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Compose a bounded voice workflow
# Compose a voice workflow

`pocketstation.voice` defines provider-neutral Python protocols.
`pocketstation.aio.Session` composes those providers around one native Session.
Expand Down
4 changes: 2 additions & 2 deletions docs/reference/api-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ loss behavior.
| Run a managed process | `pocketstation.sidecar` |
| Load a trusted native extension | `pocketstation.extensions` |

Provider callbacks run on bounded off-realtime workers. Native capture
Provider callbacks run on off-realtime workers. Native capture
callbacks never call Python.

## Build a voice workflow
Expand All @@ -61,7 +61,7 @@ callbacks never call Python.
- one `DuplexVoiceModel`.

Provider implementations remain in example or separately installed provider
packages. Read [Compose a bounded voice workflow](../guides/voice.md) before
packages. Read [Compose a voice workflow](../guides/voice.md) before
depending on interruption or playout observations.

## Handle failures
Expand Down
3 changes: 0 additions & 3 deletions docs/reference/events-and-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,6 @@ Missing measurements remain `None`. Sender time, Relay receive time, browser
jitter-buffer time, and acoustic playout are different observations.
`RouteMetrics.source_latency_measurement` returns a
`RouteLatencyMeasurement`; `source_latency_unit` gives its unit.
`RouteLatencyBoundary` and `source_latency_boundary` remain 0.1.x compatibility
names.

## Terminal results

After stop or cancellation, inspect `StopResult` before reporting success:
Expand Down
Loading
Loading