From ad7177433fac3b3c536b8436ac76f080310639eb Mon Sep 17 00:00:00 2001 From: Raphael Avocegamou Date: Tue, 25 Aug 2026 21:54:31 -0700 Subject: [PATCH] docs: keep engineering records local --- .github/CODEOWNERS | 3 - .github/PULL_REQUEST_TEMPLATE.md | 23 +- .gitignore | 11 + AGENTS.md | 87 - README.md | 34 +- docs/REPO_CONTRACT.md | 14 - docs/adr/PY-004-backpressure-policy.md | 29 - docs/adr/PY-005-relay-listener-slice.md | 29 - docs/adr/PY-006-clock-sync-src.md | 29 - docs/adr/PY-007-capability-negotiation.md | 29 - .../PY-008-workspace-release-sequencing.md | 29 - docs/adr/PY-009-pion-writertp-profile.md | 29 - docs/adr/PY-010-jitter-buffer.md | 29 - docs/adr/PY-011-spsc-ring.md | 29 - docs/adr/PY-012-opus-frame-duration.md | 29 - .../PY-013-internal-format-channel-layout.md | 29 - docs/architecture/PocketStation-v2.3.md | 12 - docs/architecture/pocketstation-v3.0.md | 2275 ----------------- docs/standards/FAKE_SCAFFOLD_INVENTORY.md | 78 - docs/standards/PRODUCTION_ENGINEERING_BAR.md | 236 -- docs/standards/STAFF_ENGINEERING_BAR.md | 214 -- .../STRUCTURE_NAMING_STYLE_THINKING.md | 537 ---- pocketstation/__init__.py | 2 +- pocketstation/station.py | 7 +- pocketstation/types.py | 8 +- tests/test_station.py | 2 +- tests/test_types.py | 2 +- 27 files changed, 58 insertions(+), 3777 deletions(-) delete mode 100644 AGENTS.md delete mode 100644 docs/REPO_CONTRACT.md delete mode 100644 docs/adr/PY-004-backpressure-policy.md delete mode 100644 docs/adr/PY-005-relay-listener-slice.md delete mode 100644 docs/adr/PY-006-clock-sync-src.md delete mode 100644 docs/adr/PY-007-capability-negotiation.md delete mode 100644 docs/adr/PY-008-workspace-release-sequencing.md delete mode 100644 docs/adr/PY-009-pion-writertp-profile.md delete mode 100644 docs/adr/PY-010-jitter-buffer.md delete mode 100644 docs/adr/PY-011-spsc-ring.md delete mode 100644 docs/adr/PY-012-opus-frame-duration.md delete mode 100644 docs/adr/PY-013-internal-format-channel-layout.md delete mode 100644 docs/architecture/PocketStation-v2.3.md delete mode 100644 docs/architecture/pocketstation-v3.0.md delete mode 100644 docs/standards/FAKE_SCAFFOLD_INVENTORY.md delete mode 100644 docs/standards/PRODUCTION_ENGINEERING_BAR.md delete mode 100644 docs/standards/STAFF_ENGINEERING_BAR.md delete mode 100644 docs/standards/STRUCTURE_NAMING_STYLE_THINKING.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 48732f1..3ebf74c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,6 +1,3 @@ # Replace @raph with your GitHub username/team before pushing. * @raph -/docs/architecture/ @raph -/docs/adr/ @raph /.github/ @raph -/AGENTS.md @raph diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index c96f7a8..d5c487e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,17 +1,12 @@ -## Linked issue +## What changed -Closes # +Describe the developer problem and the change that solves it. -## Summary +## User impact -## Scope control +Describe any API, behavior, compatibility, performance, or documentation change. -- [ ] I modified only this repo. -- [ ] I respected the repo phase gate. -- [ ] I did not edit architecture docs unless explicitly assigned. -- [ ] I did not add dependencies without approval. - -## Tests run +## Validation ```bash @@ -19,5 +14,11 @@ Closes # ## Risks -## Reviewer focus +List known limits, follow-up work, or deployment considerations. + +## Checklist +- [ ] Tests cover the behavior I changed. +- [ ] Public behavior is documented. +- [ ] Logs, examples, and fixtures contain no credentials or personal data. +- [ ] Breaking changes and migration steps are clearly identified. diff --git a/.gitignore b/.gitignore index 0c698b1..22b7634 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,14 @@ venv/ .mypy_cache/ .ruff_cache/ .claude/ + +# Local agent and engineering records +/AGENTS.md +/PHASE*_PROGRESS.md +/docs/REPO_CONTRACT.md +/docs/PYTHON_CAPABILITY_MATRIX.md +/docs/PYTHON_SDK_CORE_PARITY_REFERENCE.md +/docs/PYTHON_SDK_DESIGN.md +/docs/adr/ +/docs/architecture/ +/docs/standards/ diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index f2b16e6..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,87 +0,0 @@ -# AGENTS.md β€” pocketstation-io/sdk-python - -## Code writing standard β€” MANDATORY - -Before writing any code, read `docs/standards/CODE_PROTOCOL.md`. -All 14 laws apply to this repo. Python specifics: `@dataclass` field alignment, -unit suffixes (`latency_ms`, `gain_db`), `Protocol` with full method declarations (no empty Protocol), -cross-language vocab identical to Rust. No code ships until it passes the checklist. - ---- - -## 🐞 NON-TRIVIAL BUG β†’ EMPIRICAL DEBUGGING FRAMEWORK (MANDATORY) - -When a defect's cause is NOT obvious from reading, OR a first obvious fix didn't -move the number, OR it's intermittent / "works sometimes" / cross-service β€” STOP -and apply the Empirical Debugging Framework. This binds **every agent and every -sub-agent** for the whole life of the defect. If you spot such a bug you are bound -by it: localize and record it, then fix it under this method or hand back the -reproduction + ruled-out list. Never paper over it, and never ship a guess for a -hard bug. - -Core loop: **research prior art first β†’ corner the bug repoβ†’scopeβ†’fileβ†’functionβ†’lines -β†’ prove it by removing/swapping the suspect component (show it working WITHOUT the -suspect, then reintroduce one variable at a time) β†’ fix the real lines β†’ no fix -lands without a test that moves the original symptom metric on the real path β†’ -record every ruled-out cause.** - -**Proportionality β€” do NOT over-apply:** for an obvious bug you can SEE (typo, -missing await, off-by-one, wrong constant, missing import), just fix it directly + -a test. Running the full ceremony on a one-liner is itself an anti-pattern β€” token -burn and over-engineering that defeats the purpose. Escalate the instant an -"obvious" fix fails or you start guessing. Full method: -`docs/standards/EMPIRICAL_DEBUGGING_FRAMEWORK.md` in the parent factory repo. - ---- - - -## Source of truth - -Before editing, read: - -1. `docs/architecture/pocketstation-v3.0.md` -2. `docs/REPO_CONTRACT.md` -3. Relevant ADRs in `docs/adr/` -4. The assigned GitHub issue - -## Phase gate - -This repo activates in **Phase 5**. - -If the current project phase is earlier, do not implement code here unless the issue has `phase-exception-approved`. - -## Rules - -- One issue = one branch = one PR. -- Do not edit unrelated repos. -- Do not create `pocketstation-io/protocol` before Phase 2. -- Do not change v3.0 architecture unless explicitly assigned. -- Do not add dependencies without approval. -- Do not bypass CI. - -## Engineering Standards - -Before code changes, every agent must read: - -- `docs/standards/STAFF_ENGINEERING_BAR.md` -- `docs/standards/STRUCTURE_NAMING_STYLE_THINKING.md` -- `docs/standards/PRODUCTION_ENGINEERING_BAR.md` -- `docs/REPO_CONTRACT.md` -- relevant ADRs -- current phase progress file -- `FAKE_SCAFFOLD_INVENTORY.md` - -All code follows the structure, naming, documentation, test naming, -comment style, and thinking process defined there. - -Every non-trivial implementation documents: -- invariant -- ownership model -- failure behavior -- test coverage -- phase scope -- what is intentionally not implemented - -Every PR that introduces a fake/mock/scaffold adds a row to -FAKE_SCAFFOLD_INVENTORY.md. Every PR that replaces one burns -the row down. \ No newline at end of file diff --git a/README.md b/README.md index 217e1a4..827f520 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,28 @@ -# sdk-python +# PocketStation Python SDK -**Organization:** `pocketstation-io` -**Repository:** `pocketstation-io/sdk-python` -**v2.3 tier:** Tier 2 β€” Client SDKs -**Activation phase:** Phase 5 -**Language/package:** Python/PyPI -**Release strategy:** PyPI pocketstation +Use Python to connect to a PocketStation control plane and Relay, receive PCM +audio, and send application-owned PCM back over the same session. -This is an independently releasable PocketStation v2.3 repository folder. It is not meant to be merged permanently into a monorepo. +> **Status: preview.** This repository does not have a PyPI release. The API in +> `main` may change while the native PocketStation Session binding is completed. -Agents must respect the phase gate in `docs/REPO_CONTRACT.md`. +## Develop locally + +You need Python 3.11 or newer. + +```bash +python -m venv .venv +source .venv/bin/activate +python -m pip install -e '.[dev]' +python -m pytest +``` + +The current package uses HTTP for session creation and a WebSocket for binary +PCM. Configure the control-plane and Relay URLs in your application; the SDK +does not start hidden infrastructure or select a hosted service for you. + +## Related projects + +- [PocketStation Core](https://github.com/pocketstation-io/pocketstation) +- [PocketStation Relay](https://github.com/pocketstation-io/relay) +- [PocketStation Control Plane](https://github.com/pocketstation-io/control-plane) diff --git a/docs/REPO_CONTRACT.md b/docs/REPO_CONTRACT.md deleted file mode 100644 index b45ab73..0000000 --- a/docs/REPO_CONTRACT.md +++ /dev/null @@ -1,14 +0,0 @@ -# Repo Contract β€” pocketstation-io/sdk-python - -- Tier: Tier 2 β€” Client SDKs -- Activation phase: Phase 5 -- Language/package: Python/PyPI -- Release strategy: PyPI pocketstation - -## Dependency rule - -Follow PocketStation v2.3 Β§14.4. Do not add reverse dependencies that violate the dependency graph. - -## Release rule - -Follow PocketStation v2.3 Β§14.5. Release automation is required before public release. diff --git a/docs/adr/PY-004-backpressure-policy.md b/docs/adr/PY-004-backpressure-policy.md deleted file mode 100644 index e67b7dc..0000000 --- a/docs/adr/PY-004-backpressure-policy.md +++ /dev/null @@ -1,29 +0,0 @@ -# PY-004-backpressure-policy β€” Backpressure Policy on Pool / Ring Exhaustion - -## Status -Accepted for v2.3 scaffold. Reversal requires Phase 0/1 measurement data. - -## Context -PocketStation v2.3 requires this ADR before implementation lands. See `docs/architecture/pocketstation-v2.3.md`. - -## Decision -Drop newest when the pool or SPSC ring is full. Stable latency is more important than preserving every frame. Blocking the producer is forbidden. - -## Options considered - -See v2.3 Β§26 for the complete option list. - -## Consequences - -- Agents must follow this decision until a new ADR supersedes it. -- Tests/benchmarks must verify the decision in the relevant phase. - -## Test / measurement plan - -- Add unit tests for correctness. -- Add benchmark where performance matters. -- Add soak/load tests where reliability matters. - -## Reversal trigger - -Measured Phase 0/1 data shows this decision breaks latency, reliability, safety, or developer usability targets. diff --git a/docs/adr/PY-005-relay-listener-slice.md b/docs/adr/PY-005-relay-listener-slice.md deleted file mode 100644 index 227155a..0000000 --- a/docs/adr/PY-005-relay-listener-slice.md +++ /dev/null @@ -1,29 +0,0 @@ -# PY-005-relay-listener-slice β€” Relay Listener Slice Model - -## Status -Accepted for v2.3 scaffold. Reversal requires Phase 0/1 measurement data. - -## Context -PocketStation v2.3 requires this ADR before implementation lands. See `docs/architecture/pocketstation-v2.3.md`. - -## Decision -Phase 1 may use RWMutex around the listener slice. Phase 2 migrates to copy-on-write atomic pointer to avoid per-packet lock contention. - -## Options considered - -See v2.3 Β§26 for the complete option list. - -## Consequences - -- Agents must follow this decision until a new ADR supersedes it. -- Tests/benchmarks must verify the decision in the relevant phase. - -## Test / measurement plan - -- Add unit tests for correctness. -- Add benchmark where performance matters. -- Add soak/load tests where reliability matters. - -## Reversal trigger - -Measured Phase 0/1 data shows this decision breaks latency, reliability, safety, or developer usability targets. diff --git a/docs/adr/PY-006-clock-sync-src.md b/docs/adr/PY-006-clock-sync-src.md deleted file mode 100644 index 9d3fa71..0000000 --- a/docs/adr/PY-006-clock-sync-src.md +++ /dev/null @@ -1,29 +0,0 @@ -# PY-006-clock-sync-src β€” Clock Sync / Async Sample Rate Conversion - -## Status -Accepted for v2.3 scaffold. Reversal requires Phase 0/1 measurement data. - -## Context -PocketStation v2.3 requires this ADR before implementation lands. See `docs/architecture/pocketstation-v2.3.md`. - -## Decision -Use PI-controlled linear interpolation for voice default. Keep a hook for higher-quality SRC in music mode. - -## Options considered - -See v2.3 Β§26 for the complete option list. - -## Consequences - -- Agents must follow this decision until a new ADR supersedes it. -- Tests/benchmarks must verify the decision in the relevant phase. - -## Test / measurement plan - -- Add unit tests for correctness. -- Add benchmark where performance matters. -- Add soak/load tests where reliability matters. - -## Reversal trigger - -Measured Phase 0/1 data shows this decision breaks latency, reliability, safety, or developer usability targets. diff --git a/docs/adr/PY-007-capability-negotiation.md b/docs/adr/PY-007-capability-negotiation.md deleted file mode 100644 index 705092a..0000000 --- a/docs/adr/PY-007-capability-negotiation.md +++ /dev/null @@ -1,29 +0,0 @@ -# PY-007-capability-negotiation β€” Capability Negotiation - -## Status -Accepted for v2.3 scaffold. Reversal requires Phase 0/1 measurement data. - -## Context -PocketStation v2.3 requires this ADR before implementation lands. See `docs/architecture/pocketstation-v2.3.md`. - -## Decision -Auto-insert adapter nodes such as ResampleNode and MonoMixNode, but expose NegotiatedCapability to the caller. - -## Options considered - -See v2.3 Β§26 for the complete option list. - -## Consequences - -- Agents must follow this decision until a new ADR supersedes it. -- Tests/benchmarks must verify the decision in the relevant phase. - -## Test / measurement plan - -- Add unit tests for correctness. -- Add benchmark where performance matters. -- Add soak/load tests where reliability matters. - -## Reversal trigger - -Measured Phase 0/1 data shows this decision breaks latency, reliability, safety, or developer usability targets. diff --git a/docs/adr/PY-008-workspace-release-sequencing.md b/docs/adr/PY-008-workspace-release-sequencing.md deleted file mode 100644 index 3999e8e..0000000 --- a/docs/adr/PY-008-workspace-release-sequencing.md +++ /dev/null @@ -1,29 +0,0 @@ -# PY-008-workspace-release-sequencing β€” Workspace Release Sequencing - -## Status -Accepted for v2.3 scaffold. Reversal requires Phase 0/1 measurement data. - -## Context -PocketStation v2.3 requires this ADR before implementation lands. See `docs/architecture/pocketstation-v2.3.md`. - -## Decision -Publish crates in dependency order with retry/backoff. One root tag, same workspace version. - -## Options considered - -See v2.3 Β§26 for the complete option list. - -## Consequences - -- Agents must follow this decision until a new ADR supersedes it. -- Tests/benchmarks must verify the decision in the relevant phase. - -## Test / measurement plan - -- Add unit tests for correctness. -- Add benchmark where performance matters. -- Add soak/load tests where reliability matters. - -## Reversal trigger - -Measured Phase 0/1 data shows this decision breaks latency, reliability, safety, or developer usability targets. diff --git a/docs/adr/PY-009-pion-writertp-profile.md b/docs/adr/PY-009-pion-writertp-profile.md deleted file mode 100644 index 9cf235d..0000000 --- a/docs/adr/PY-009-pion-writertp-profile.md +++ /dev/null @@ -1,29 +0,0 @@ -# PY-009-pion-writertp-profile β€” Pion WriteRTP Allocation Profile - -## Status -Accepted for v2.3 scaffold. Reversal requires Phase 0/1 measurement data. - -## Context -PocketStation v2.3 requires this ADR before implementation lands. See `docs/architecture/pocketstation-v2.3.md`. - -## Decision -Benchmark whether WriteRTP mutates packets or allocates per listener. No claim of zero-allocation relay until measured. - -## Options considered - -See v2.3 Β§26 for the complete option list. - -## Consequences - -- Agents must follow this decision until a new ADR supersedes it. -- Tests/benchmarks must verify the decision in the relevant phase. - -## Test / measurement plan - -- Add unit tests for correctness. -- Add benchmark where performance matters. -- Add soak/load tests where reliability matters. - -## Reversal trigger - -Measured Phase 0/1 data shows this decision breaks latency, reliability, safety, or developer usability targets. diff --git a/docs/adr/PY-010-jitter-buffer.md b/docs/adr/PY-010-jitter-buffer.md deleted file mode 100644 index 3c934ca..0000000 --- a/docs/adr/PY-010-jitter-buffer.md +++ /dev/null @@ -1,29 +0,0 @@ -# PY-010-jitter-buffer β€” Jitter Buffer Algorithm - -## Status -Accepted for v2.3 scaffold. Reversal requires Phase 0/1 measurement data. - -## Context -PocketStation v2.3 requires this ADR before implementation lands. See `docs/architecture/pocketstation-v2.3.md`. - -## Decision -Start with adaptive jitter buffer target depth. Keep room to replace with NetEQ-class behavior after Phase 1 measurement. - -## Options considered - -See v2.3 Β§26 for the complete option list. - -## Consequences - -- Agents must follow this decision until a new ADR supersedes it. -- Tests/benchmarks must verify the decision in the relevant phase. - -## Test / measurement plan - -- Add unit tests for correctness. -- Add benchmark where performance matters. -- Add soak/load tests where reliability matters. - -## Reversal trigger - -Measured Phase 0/1 data shows this decision breaks latency, reliability, safety, or developer usability targets. diff --git a/docs/adr/PY-011-spsc-ring.md b/docs/adr/PY-011-spsc-ring.md deleted file mode 100644 index fd6cf78..0000000 --- a/docs/adr/PY-011-spsc-ring.md +++ /dev/null @@ -1,29 +0,0 @@ -# PY-011-spsc-ring β€” SPSC Ring Buffer Choice - -## Status -Accepted for v2.3 scaffold. Reversal requires Phase 0/1 measurement data. - -## Context -PocketStation v2.3 requires this ADR before implementation lands. See `docs/architecture/pocketstation-v2.3.md`. - -## Decision -Use rtrb by default: fixed capacity, allocation at construction, lock-free/wait-free reads and writes. - -## Options considered - -See v2.3 Β§26 for the complete option list. - -## Consequences - -- Agents must follow this decision until a new ADR supersedes it. -- Tests/benchmarks must verify the decision in the relevant phase. - -## Test / measurement plan - -- Add unit tests for correctness. -- Add benchmark where performance matters. -- Add soak/load tests where reliability matters. - -## Reversal trigger - -Measured Phase 0/1 data shows this decision breaks latency, reliability, safety, or developer usability targets. diff --git a/docs/adr/PY-012-opus-frame-duration.md b/docs/adr/PY-012-opus-frame-duration.md deleted file mode 100644 index eadd11e..0000000 --- a/docs/adr/PY-012-opus-frame-duration.md +++ /dev/null @@ -1,29 +0,0 @@ -# PY-012-opus-frame-duration β€” Opus Frame Duration - -## Status -Accepted for v2.3 scaffold. Reversal requires Phase 0/1 measurement data. - -## Context -PocketStation v2.3 requires this ADR before implementation lands. See `docs/architecture/pocketstation-v2.3.md`. - -## Decision -20ms default. 10ms optional for voice-agent mode after benchmarks justify CPU/overhead tradeoff. - -## Options considered - -See v2.3 Β§26 for the complete option list. - -## Consequences - -- Agents must follow this decision until a new ADR supersedes it. -- Tests/benchmarks must verify the decision in the relevant phase. - -## Test / measurement plan - -- Add unit tests for correctness. -- Add benchmark where performance matters. -- Add soak/load tests where reliability matters. - -## Reversal trigger - -Measured Phase 0/1 data shows this decision breaks latency, reliability, safety, or developer usability targets. diff --git a/docs/adr/PY-013-internal-format-channel-layout.md b/docs/adr/PY-013-internal-format-channel-layout.md deleted file mode 100644 index 98d717c..0000000 --- a/docs/adr/PY-013-internal-format-channel-layout.md +++ /dev/null @@ -1,29 +0,0 @@ -# PY-013-internal-format-channel-layout β€” Internal Sample Format and Channel Layout - -## Status -Accepted for v2.3 scaffold. Reversal requires Phase 0/1 measurement data. - -## Context -PocketStation v2.3 requires this ADR before implementation lands. See `docs/architecture/pocketstation-v2.3.md`. - -## Decision -Internal format: interleaved f32, little-endian, normalized [-1.0,1.0], 48kHz. Voice mono, music stereo, broadcast configurable. - -## Options considered - -See v2.3 Β§26 for the complete option list. - -## Consequences - -- Agents must follow this decision until a new ADR supersedes it. -- Tests/benchmarks must verify the decision in the relevant phase. - -## Test / measurement plan - -- Add unit tests for correctness. -- Add benchmark where performance matters. -- Add soak/load tests where reliability matters. - -## Reversal trigger - -Measured Phase 0/1 data shows this decision breaks latency, reliability, safety, or developer usability targets. diff --git a/docs/architecture/PocketStation-v2.3.md b/docs/architecture/PocketStation-v2.3.md deleted file mode 100644 index e3cb58a..0000000 --- a/docs/architecture/PocketStation-v2.3.md +++ /dev/null @@ -1,12 +0,0 @@ -# PocketStation Architecture - -**Canonical document:** [`pocketstation-io/docs`](https://github.com/pocketstation-io/docs/blob/main/content/architecture/pocketstation-v2.3.md) - -This repo previously contained a copy of the architecture document. The copy has been -replaced with this pointer to eliminate sync drift between the canonical source and 17 -downstream copies. - -**Last synced copy removed:** 2026-06-09 (v2.3) -**Reason:** Wave 5 β€” single source of truth enforcement per FAANG-tier repo standards -**Tracking issue:** any architecture change must update `pocketstation-io/docs` only; -sub-repos reference the canonical via this pointer. diff --git a/docs/architecture/pocketstation-v3.0.md b/docs/architecture/pocketstation-v3.0.md deleted file mode 100644 index 4672cbc..0000000 --- a/docs/architecture/pocketstation-v3.0.md +++ /dev/null @@ -1,2275 +0,0 @@ -# PocketStation -## Program Document v3.0 - -**Date:** 2026-06-26 -**Status:** Green-light version. AudioGraph is the product center. v2.3 core algorithm, platform specs, and engineering ADRs are fully preserved underneath the new graph abstraction. No further structural rewrites planned. -**Supersedes:** v2.3 (Universal Audio Fabric / mobile-first SDK + relay positioning) - ---- - -## Changelog v2.3 β†’ v3.0 - -| v2.3 | v3.0 | Why | -|---|---|---| -| "Universal Audio Fabric" as vision phrase | "Realtime Audio Graph Infrastructure" | "Fabric" is poetic; "graph" is executable, observable, and defensible | -| Source β†’ route β†’ output | SourceNode β†’ Edge β†’ Bus β†’ Policy/Model/Transport β†’ Sink | The product must support branching, model triggers, policies, and per-edge observability | -| Relay as primary infra artifact | Relay as one TransportNode implementation | A relay alone can be commoditized; a graph runtime with source identity and policies cannot | -| Mobile-first SDK framing | Capture-first, graph-first, cross-platform | Existing desktop CLI/capture work is a strong wedge; treating it as secondary was wrong | -| Creator station as first product | Developer graph API first; creator station is one graph template | Creators are a wedge, not the full market | -| Voice-agent backend as a route kind | ModelNode as a first-class graph node | AI-era workloads need model switching, fan-out, fallback, semantic triggers, and model observability | -| Room = source + listeners | Room = distributed GraphSession | Allows multi-source, multi-bus, model nodes, private enterprise sessions, and graph replay | -| ProcessorGraph as internal implementation detail | AudioGraph as the public product center | The graph is not inside PocketStation β€” PocketStation IS the graph runtime | -| `start_broadcast()` as primary API | `AudioGraph::new().connect().run()` as primary API | The graph wiring is the product; `start_broadcast()` becomes a convenience template | -| Observability = metrics | Observability = per-edge, per-session, per-model product surface | Enterprise voice systems need debuggability across the entire path | - -The non-changing core algorithm (frame pool, SPSC ring, codec, clock sync, hot-path rules) is preserved exactly. The graph abstraction sits above it. The hot path is unchanged. - ---- - -## Table of Contents - -1. [Vision and Thesis](#1-vision-and-thesis) -2. [What PocketStation Is and Is Not](#2-what-pocketstation-is-and-is-not) -3. [The AudioGraph β€” Product Center](#3-the-audiograph--product-center) -4. [The Non-Changing Core Algorithm](#4-the-non-changing-core-algorithm) -5. [Source Capability Model](#5-source-capability-model) -6. [Architecture Decision Records](#6-architecture-decision-records) -7. [Platform Adapter Specifications](#7-platform-adapter-specifications) -8. [Relay Architecture β€” GraphSession](#8-relay-architecture--graphsession) -9. [Security Model](#9-security-model) -10. [Model Nodes and ML Processing Layer](#10-model-nodes-and-ml-processing-layer) -11. [Observability](#11-observability) -12. [Full Tech Stack](#12-full-tech-stack) -13. [Developer API](#13-developer-api) -14. [Repository Structure](#14-repository-structure) -15. [Build Phase Plan](#15-build-phase-plan) -16. [Market Reality](#16-market-reality) -17. [Target Markets β€” Ranked and Honest](#17-target-markets--ranked-and-honest) -18. [Business Model](#18-business-model) -19. [Infrastructure Cost Strategy](#19-infrastructure-cost-strategy) -20. [Funding Strategy](#20-funding-strategy) -21. [Research Path](#21-research-path) -22. [Competitive Landscape](#22-competitive-landscape) -23. [Threat Analysis](#23-threat-analysis) -24. [Kill Criteria](#24-kill-criteria) -25. [Strategic Positioning](#25-strategic-positioning) -26. [Open Engineering Questions](#26-open-engineering-questions) - ---- - -## 1. Vision and Thesis - -### 1.1 The Permanent Vision - -> **Any audio β†’ any graph β†’ any human, app, device, room, model, or agent.** - -The v2.3 line "Any audio β†’ any route β†’ any output" remains valid. v3.0 makes it precise: the route is not a pipe. It is a graph with typed edges, semantic sources, policy nodes, model nodes, and per-edge observability. Every source is a node. Every connection is a contract. - -### 1.2 What "PocketStation" Means - -**Pocket:** a fast, discrete, self-contained node of any signal type β€” audio, metadata, transcript events, control signals. Not "fits in your pocket." A pocket of signal in motion. Compact by design, not by limitation. - -**Station:** a directed orchestration point. Not a room people join. A station receives, processes, and dispatches with intent β€” like a relay station, a base station, a switching station that knows the semantics of what it handles and routes accordingly. - -Together: a fast, programmable orchestration node for any signal type. Audio now. JSON events, video, telemetry later. The name survives every product evolution. - -### 1.3 The One-Sentence Thesis - -> PocketStation is audio-native realtime graph infrastructure: capture any audio source, give it semantic identity, route it through programmable local/remote/model graph nodes, and expose quality and latency telemetry across every edge. - -### 1.4 The Technical Claim - -> We are not building video-call rooms. We are building realtime audio graphs that cross devices, apps, browsers, phones, and AI models. - -### 1.5 The Strategic Position - -> LiveKit owns realtime rooms. Vapi owns voice-agent deployment. OpenAI, Deepgram, and ElevenLabs own models. PocketStation owns the programmable audio I/O graph between operating systems, apps, humans, agents, and model pipelines. - -### 1.6 The Distinction - -**Routing alone = thin.** Capture app audio and send it to a remote listener. Useful, not venture-scale. - -**Audio graph infrastructure = the full thesis:** - -``` -OS audio capture - β†’ per-source identity and capability discovery - β†’ graph wiring with typed edge contracts - β†’ local transform nodes (VAD, noise suppression, gain, mix) - β†’ policy nodes (ducking, routing decisions, model switching) - β†’ model nodes (STT, LLM, TTS, translation, diarization) - β†’ transport nodes (relay, WebRTC, RTP, QUIC, local pipe) - β†’ sink nodes (browser, mobile, recording, AI backend, virtual mic) - β†’ per-edge observability (latency, loss, jitter, drift, cost) -``` - -That is the company. - ---- - -## 2. What PocketStation Is and Is Not - -### Is - -``` -A realtime audio graph runtime for developers. -A cross-platform audio source capture and identity layer. -A distributed audio room/relay infrastructure built around semantic sources and buses. -A model-routing fabric for STT, LLM, TTS, translation, diarization, and audio enhancement. -A developer SDK for voice AI, creator broadcast, accessibility, education, game audio, and enterprise audio. -A local + cloud audio observability system. -A future virtual endpoint layer for OS-level audio integration. -``` - -### Is Not - -``` -A generic LiveKit clone. -A generic Vapi clone. -A model company competing with OpenAI, Deepgram, or ElevenLabs. -Only a podcasting or creator app. -Only a desktop audio router. -A promise to silently capture restricted OS audio that platforms do not allow. -A Spotify redistribution tool. -A video-conferencing platform. -``` - -### Positioning Statements - -**Developer:** -> Build realtime audio graphs across apps, devices, browsers, and AI models. - -**AI infrastructure:** -> PocketStation gives voice AI systems programmable audio I/O, routing, model switching, and latency observability. - -**Creator:** -> Route any app, mic, music, or device into a live station, recording, remote listener, or AI tool. - -**Enterprise:** -> Private realtime audio graph infrastructure with observability, policy, fallback, and model-provider control. - ---- - -## 3. The AudioGraph β€” Product Center - -### 3.1 The Graph Is the Product - -v2.3 had the right internal primitives: `ProcessorGraph`, `AudioProcessorNode`, `RoutePlan`, `SourceCapability`, observability. Those primitives were framed as implementation details beneath the product. The v3.0 change: they are the product. - -The relay is not the company. The relay is `TransportNode::Relay`. The ML layer is not a feature. It is `ModelNode` and `TransformNode` instances in the graph. Platform adapters are `SourceNode` adapter implementations. Creator station is a graph template. The iOS SDK is the Swift wrapper for `SourceNode` adapters on iOS. Everything composes through the graph. - -### 3.2 Core Vocabulary - -**Node** β€” a typed unit of audio or data work: - -```rust -pub trait AudioGraphNode: Send + Sync { - fn id(&self) -> NodeId; - fn kind(&self) -> NodeKind; - fn inputs(&self) -> Vec; - fn outputs(&self) -> Vec; - fn constraints(&self) -> NodeConstraints; -} -``` - -**Port** β€” a named, typed stream input or output: - -```rust -pub struct PortSpec { - pub name: String, - pub media_type: MediaType, - pub sample_rate: Option, - pub channels: Option, - pub frame_duration_ms: Option, - pub semantic_role: Option, -} -``` - -**Edge** β€” a typed streaming contract connecting one output port to one input port: - -```rust -pub struct EdgeSpec { - pub from: PortRef, - pub to: PortRef, - pub contract: EdgeContract, - pub qos: QosPolicy, - pub observability: EdgeObservabilityPolicy, -} -``` - -**Graph** β€” the compiled execution plan: - -```rust -pub struct AudioGraph { - nodes: Graph>, - edges: Vec, - policies: Vec, -} -``` - -### 3.3 Node Taxonomy - -#### SourceNode - -```rust -pub enum SourceNode { - Mic, - SystemOutput, - App(String), - Device(DeviceId), - BrowserTab(TabId), - File(PathBuf), - NetworkStream(StreamUrl), - VirtualInput(VirtualDeviceId), - ModelOutput(ModelNodeId), // TTS or agent output as a source - SyntheticSine, // testing/Phase 0 -} -``` - -Every source has semantic identity: - -```rust -pub struct SourceIdentity { - pub source_id: SourceId, - pub display_name: String, - pub kind: SourceKind, - pub platform: PlatformId, - pub app_bundle_id: Option, - pub device_id: Option, - pub human_owner: Option, - pub agent_owner: Option, - pub model_owner: Option, - pub capture_capability: SourceCapability, - pub clock_domain: ClockDomainId, - pub privacy_class: PrivacyClass, -} -``` - -Generic WebRTC has tracks. PocketStation has **meaningful sources**. This is a durable moat. - -Source nodes emit typed output ports: `audio.raw`, `audio.voice`, `audio.music`, `audio.system`, `metadata.source_state`, `metrics.capture`. - -#### TransformNode - -Deterministic signal-processing nodes. All run on the Rust processing thread, never inside platform audio callbacks. - -```rust -pub enum TransformNode { - Gain { db: f32 }, - Resample { sample_rate: u32 }, - MonoMix, - StereoUpmix, - NoiseSuppress, - EchoCancel, - VAD, - SourceSeparate, - LoudnessNormalize { target_lufs: f32 }, - Encode { codec: Codec }, - Decode { codec: Codec }, - Duck { target: NodeSelector, db: f32, attack_ms: u32, release_ms: u32 }, - Gate { threshold_dbfs: f32 }, - Limiter { ceiling_dbfs: f32 }, - Compressor { ratio: f32, threshold_dbfs: f32 }, - Watermark, -} -``` - -Built-in (Phase 0): `PassthroughNode`, `GainNode`, `ResampleNode`, `MonoMixNode`. -Optional (Phase 4+, feature flags): `VadNode`, `NoiseSuppressorNode`, `EchoCancelNode`, `SourceSeparationNode`. - -All nodes declare `accepted_channels()`. Graph auto-inserts `MonoMixNode` upstream of any mono-only node. Insertions are zero-allocation passes using the existing pool. - -#### PolicyNode - -Policy nodes are not just DSP β€” they decide what should happen. - -```rust -pub enum PolicyNode { - Duck { target: NodeSelector, db: f32, attack_ms: u32, release_ms: u32 }, - Gate { condition: TriggerExpr }, - RouteIf { condition: TriggerExpr, to: NodeSelector }, - PrivacyRedact { policy: PrivacyPolicy }, - Failover { primary: NodeSelector, fallback: NodeSelector }, - CostCap { max_usd_per_hour: f32 }, - ModelSwitch { condition: TriggerExpr, provider: ModelProviderId }, - StartRecording { stems: Vec }, - LatencyFallback { threshold_ms: u32, fallback: NodeSelector }, -} - -pub enum GraphAction { - RouteEnable(RouteId), - RouteDisable(RouteId), - SetGain { bus_id: BusId, db: f32 }, - SwitchModel { node_id: NodeId, provider: ModelProviderId }, - StartRecording { stem_ids: Vec }, - ApplyPrivacyMode(PrivacyMode), - TriggerWebhook(WebhookEvent), -} -``` - -#### ModelNode - -Model nodes make PocketStation AI-native without becoming a model company. - -```rust -pub enum ModelNode { - Transcribe(ModelProvider), - Translate(ModelProvider), - TextToSpeech(ModelProvider), - SpeechToSpeech(ModelProvider), - EmotionDetect(ModelProvider), - SpeakerDiarize(ModelProvider), - IntentDetect(ModelProvider), - KeywordSpot { keywords: Vec }, - AudioClassify(ModelProvider), - Agent(AgentProvider), -} -``` - -Model nodes expose latency, cost, and quality telemetry: - -```rust -pub struct ModelNodeMetrics { - pub provider: ModelProviderId, - pub model_name: String, - pub input_audio_ms: u64, - pub first_token_ms: Option, - pub first_audio_ms: Option, - pub total_response_ms: Option, - pub error_rate: f32, - pub cost_estimate_usd: Option, -} -``` - -Model output (TTS, agent speech) can be fed back into the graph as a `SourceNode::ModelOutput` β€” completing the full loop. - -#### TransportNode - -```rust -pub enum TransportNode { - LocalBus, - Relay(RoomId), - WebRTC(PeerConfig), - RTP(RtpConfig), - QUIC(QuicConfig), - WebSocket(WsConfig), - FileSegment(SegmentConfig), - VirtualDevice(VirtualDeviceId), - SIP(SipConfig), -} -``` - -Transport nodes move graph buses across process, device, network, or storage boundaries. Phase 1 uses `TransportNode::Relay` over Pion v4 WebRTC. The abstraction lets Phase 5+ add QUIC or SIP without changing the graph API. - -#### SinkNode - -```rust -pub enum SinkNode { - Speaker, - Browser, - MobileApp, - DesktopApp, - VirtualMic, - MultiStemRecording(RecordingId), - TranscriptLog(LogId), - MetricsExport(ExporterId), - AgentInput(AgentId), - Webhook(WebhookConfig), -} -``` - -### 3.4 Edge Contract - -An edge is not `connect(A, B)`. It is a typed streaming contract: - -```rust -pub struct EdgeContract { - pub media_type: MediaType, - pub clock_domain: ClockDomain, - pub ordering: OrderingPolicy, - pub backpressure: BackpressurePolicy, - pub latency_budget_ms: u32, - pub jitter_budget_ms: u32, - pub loss_policy: LossPolicy, - pub conversion_policy: ConversionPolicy, - pub encryption: EncryptionMode, -} -``` - -Recommended defaults: - -```rust -BackpressurePolicy::DropNewest -OrderingPolicy::MonotonicPerStream -ConversionPolicy::AutoInsertAdapters -LossPolicy::ConcealForAudio_DropForMetadata -EncryptionMode::TransportOnly -``` - -For model edges: - -```rust -BackpressurePolicy::CoalesceMetadata -LossPolicy::NeverDropFinalTranscript -``` - -For music/broadcast: - -```rust -BackpressurePolicy::IncreaseBufferUntilLimitThenDropNewest -LossPolicy::NeverResampleWithoutDeclaration -``` - -### 3.5 Compile-Time Graph Validation - -Before runtime, the graph validates: - -1. Media type compatibility across connected ports -2. Channel / sample-rate compatibility -3. Auto-insertion of required adapters (ResampleNode, MonoMixNode) -4. Cycle detection (unless explicitly permitted) -5. Clock-domain crossing verification -6. Privacy / encryption policy enforcement -7. Latency budget estimation -8. Model cost estimation - -```rust -let plan = graph.compile()?; -println!("{} nodes, {} edges", plan.node_count(), plan.edge_count()); -println!("estimated transport p95: {}ms", plan.estimated_p95_ms()); -println!("estimated model cost: ${:.4}/hr", plan.estimated_cost_usd_per_hour()); -``` - -### 3.6 The Layered Architecture - -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Product Surfaces β”‚ -β”‚ CLI / SDK / Creator App / Web Receiver / Enterprise UI β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - ↓ -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Graph Control Plane β”‚ -β”‚ Graph definitions, sessions, policies, routes, auth β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - ↓ -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Graph Runtime β”‚ -β”‚ SourceNode, TransformNode, PolicyNode, ModelNode, β”‚ -β”‚ TransportNode, SinkNode, EdgeContract, EdgeMetrics β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - ↓ -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Audio Core (Β§4 β€” unchanged from v2.3) β”‚ -β”‚ Frame pool Β· ring bus Β· clock sync Β· mixer Β· codec β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - ↓ -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Platform Source Adapters (Β§7 β€” unchanged from v2.3) β”‚ -β”‚ macOS Β· Windows Β· Linux Β· iOS Β· Android Β· browser β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - ↓ -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Transport Layer β”‚ -β”‚ WebRTC / RTP / QUIC / WebSocket / local pipe / virt dev β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` - -The Audio Core and Platform Source Adapters are not changed. The Graph Runtime is the new public product surface above them. - -### 3.7 The Holy-Shit Demo - -```rust -let graph = AudioGraph::new(); - -let mic = graph.source(SourceNode::Mic); -let discord = graph.source(SourceNode::App("Discord")); -let spotify = graph.source(SourceNode::App("Spotify")); - -let vad = graph.transform(TransformNode::VAD); -let stt = graph.model(ModelNode::Transcribe(deepgram())); -let agent = graph.model(ModelNode::SpeechToSpeech(openai_realtime())); -let emotion = graph.model(ModelNode::EmotionDetect(local_model())); -let duck = graph.policy(PolicyNode::Duck { - target: spotify.selector(), - db: -12.0, - attack_ms: 40, - release_ms: 400, -}); - -let relay = graph.transport(TransportNode::Relay("room-demo")); -let rec = graph.sink(SinkNode::MultiStemRecording("demo-session")); -let browser = graph.sink(SinkNode::Browser); - -// Mic β†’ VAD β†’ STT + agent. -graph.connect(mic.out("voice"), vad.in_("audio"))?; -graph.connect(vad.out("speech"), stt.in_("audio"))?; -graph.connect(vad.out("speech"), agent.in_("audio"))?; -graph.connect(stt.out("transcript"), relay.in_("events"))?; - -// Discord β†’ emotion detector. -graph.connect(discord.out("audio"), emotion.in_("audio"))?; -graph.connect(emotion.out("stress_signal"), relay.in_("events"))?; - -// Spotify ducks when mic or Discord is active. -graph.connect([mic.out("voice"), discord.out("audio")], duck.in_("sidechain"))?; -graph.connect(spotify.out("music"), duck.in_("program"))?; -graph.connect(duck.out("audio"), relay.in_("music"))?; - -// Everything recorded as separate stems. -graph.connect([mic.out("voice"), discord.out("audio"), spotify.out("music")], rec.in_("stems"))?; - -// Agent speech goes to browser listener. -graph.connect(agent.out("audio"), relay.in_("agent_voice"))?; -graph.connect(relay.out("mix"), browser.in_("audio"))?; - -let plan = graph.compile()?; -graph.run(plan).await?; -``` - -This is no longer "phone audio to browser." This is why PocketStation is not a relay clone. - -### 3.8 Compatibility Layer - -The old broadcast API remains as a convenience wrapper that compiles to a graph template internally. It is never removed β€” it is just sugar: - -```rust -// This: -station.start_broadcast(source).await?; - -// Compiles to: -// SourceNode β†’ TransformNode::Encode β†’ TransportNode::Relay β†’ SinkNode::Browser -``` - -Simple users get the simple API. Advanced users get the graph. Both are valid. Neither breaks the other. - ---- - -## 4. The Non-Changing Core Algorithm - -This is what executes inside every graph edge. It does not change when a new node type is added. It does not change when a new platform adapter is added. It does not change when a new transport is added. - -``` -InputNode β†’ NormalizeNode β†’ FrameBus β†’ ClockSync β†’ RingBuffer β†’ -ProcessorGraph β†’ Mixer β†’ Encoder β†’ Transport β†’ -Receiver/JitterBuffer β†’ OutputNode -``` - -Pipeline shape is invariant across platform, OS, codec, source, and node type. - -### 4.1 AudioFrame β€” Pool-Backed - -```rust -/// Pool-owned memory handle. No heap allocation after pool init. -pub struct AudioBufferHandle { - pool: Arc, - index: u32, - len: u32, -} - -impl AudioBufferHandle { - pub fn as_slice(&self) -> &[f32] { - self.pool.slot(self.index, self.len) - } -} - -/// Drop contract β€” load-bearing part of the design. Must remain forever: -/// - wait-free (one atomic op against free_mask) -/// - panic-free (no .expect, no .unwrap, no array OOB) -/// - allocation-free (no Vec, String, Box, format!, etc.) -/// - non-logging (no log::*, no tracing::*, no eprintln!) -/// Debug builds assert no double-release (free_mask bit must be 0 before set). -impl Drop for AudioBufferHandle { - fn drop(&mut self) { - #[cfg(debug_assertions)] - debug_assert!(self.pool.is_in_use(self.index), - "double-release of slot {}", self.index); - self.pool.release(self.index); // single atomic fetch_or - } -} - -pub struct AudioFrame { - pub stream_id: StreamId, - pub source_id: SourceId, // graph node identity - pub bus_id: Option, // semantic bus identity (v3.0) - pub sample_rate: u32, // 48_000 internally (DOCS-013) - pub channels: u8, // 1 voice / 2 music (DOCS-013) - pub format: SampleFormat, // F32LE interleaved (DOCS-013) - pub timestamp_ns: u64, // monotonic, per-node, never wall clock - pub sequence_number: u64, // monotonic per stream - pub buffer: AudioBufferHandle, -} -``` - -```rust -/// Phase 0 pool β€” 64-slot ceiling imposed by AtomicU64 bitset. -/// At 20ms frame duration (DOCS-012): 64 Γ— 20ms = 1.28s of headroom. -pub struct AudioBufferPool { - slots: Box<[f32]>, // contiguous block, allocated once at session start - slot_size: usize, // samples per slot β€” 960 at 48kHz/20ms (DOCS-012) - free_mask: AtomicU64, // bitset of free slots; 64-slot cap -} - -impl AudioBufferPool { - /// Wait-free. Returns None on overrun. Backpressure: see DOCS-004. - pub fn acquire(&self) -> Option { /* ... */ } -} -``` - -### 4.2 Internal Sample Format and Channel Layout (DOCS-013) - -**Internal sample format:** interleaved f32, little-endian, normalized [-1.0, 1.0]. -**Internal sample rate:** 48000 Hz. Resample at adapter boundary if platform delivers otherwise. - -``` -AudioMode::Voice β†’ 1 channel (mono), 48kHz -AudioMode::Music β†’ 2 channels (stereo), 48kHz -AudioMode::Broadcast β†’ configurable, default stereo -``` - -`AudioGraphNode::accepted_channels()` declares what each node can ingest. Graph builder checks the chain and inserts adapter nodes at the right positions. All adapter insertions are zero-allocation passes using the existing pool. - -### 4.3 Opus Frame Duration (DOCS-012) - -Frame duration cascades into pool slot size, packet rate, jitter buffer sizing, CPU per second, bitrate overhead, and perceived latency. This is explicit and must not be left implicit. - -``` -20ms (default) 50 pkt/sec Β· standard Β· balanced overhead/latency -10ms (voice-agent) 100 pkt/sec Β· lowest latency Β· higher CPU -40/60ms 16-25 pkt/sec Β· broadcast-only, VAD off -``` - -Recommended: 20ms default. 10ms enabled for voice-agent mode after Phase 1 latency benchmarks confirm CPU/overhead tradeoff is acceptable. - -Knock-on effects: -- Pool slot size: 20ms Γ— 48kHz = 960 samples; 10ms = 480; 60ms = 2880 -- Ring buffer headroom: 8 frames Γ— 20ms = 160ms; with 10ms frames, ring may need growth - -### 4.4 Hot-Path Rules β€” Enforced, Not Aspirational - -These rules apply to the execution layer beneath every graph edge. - -``` -No heap allocation on the hot path (verified by DHAT gate in CI) -No locks (SPSC ring buffer + atomic pool bitset) -No blocking (audio callback returns immediately) -No logging on the hot path (metrics are atomic counters) -No async/.await in the audio callback -No ObjC/Swift method calls on callback thread (iOS) -No JNI calls per audio frame (Android) -No Rust panic across any FFI boundary -No ML inference on the callback thread (ML nodes run on processing thread) -``` - -### 4.5 ProcessorGraph - -```rust -pub struct ProcessorGraph { - nodes: Vec>, -} - -pub trait AudioProcessorNode: Send { - /// Process one frame. Some passes downstream, None gates. - /// Must be allocation-free and wait-free. - /// Runs on the Rust processing thread, never inside platform audio callbacks. - fn process(&mut self, frame: AudioFrame) -> Option; - - /// Channel layout this node accepts. Graph auto-inserts MonoMixNode - /// or appropriate adapter when upstream layout differs (DOCS-013). - fn accepted_channels(&self) -> ChannelLayout { - ChannelLayout::Either - } -} -``` - -Built-in Phase 0: `PassthroughNode`, `GainNode`, `ResampleNode`, `MonoMixNode`. -Optional Phase 4+, feature flags: `VadNode`, `NoiseSuppressorNode`, `EchoCancelNode`, `SpeakerEmbedNode`. - ---- - -## 5. Source Capability Model - -The system asks "which source capabilities are available right now on this platform?" β€” not "can we capture everything?" This is the foundation of `SourceNode` adapter discovery. - -### 5.1 SourceCapability - -```rust -/// Each variant maps 1:1 to a real platform mechanism. -/// No ambiguous cross-platform names. -#[derive(Debug, Clone, PartialEq)] -pub enum SourceCapability { - Microphone, - OwnAppAudio, - DesktopSystemLoopback, // Windows WASAPI, macOS SCKit, Linux PipeWire - EligibleAppPlayback, // Android AudioPlaybackCapture (policy-gated) - ScreenProjectionMix, // Android MediaProjection - PluginHostAudio, // iOS AUv3 - BroadcastExtensionAudio, // iOS ReplayKit - ExternalRouteInput, // iOS AirPlay-style receiver, experimental - VirtualDeviceInput, // Windows SysVAD, macOS AudioDriverKit - NetworkStreamInput, - FileOrBuffer, - HardwareInput, -} - -pub struct AudioSourceDescriptor { - pub id: SourceId, - pub name: String, - pub platform: PlatformId, - pub capability: SourceCapability, - pub latency_class: LatencyClass, - pub reliability_class: ReliabilityClass, - pub requires_user_action: bool, - pub available_now: bool, - pub policy_notes: Option, -} -``` - -### 5.2 Platform Source Availability - -**iOS:** - -```json -[ - {"capability": "Microphone", "available_now": true, "reliability": "UserPermission"}, - {"capability": "OwnAppAudio", "available_now": true, "reliability": "AlwaysAvailable"}, - {"capability": "PluginHostAudio", "available_now": true, "reliability": "UserAction"}, - {"capability": "BroadcastExtensionAudio", "available_now": true, "reliability": "UserAction"}, - {"capability": "ExternalRouteInput", "available_now": false, "reliability": "Experimental"} -] -``` - -iOS has no `DesktopSystemLoopback`. If Apple ships a future system-level capture API it becomes a new enum variant. - -**Android:** - -```json -[ - {"capability": "Microphone", "available_now": true, "reliability": "UserPermission"}, - {"capability": "OwnAppAudio", "available_now": true, "reliability": "AlwaysAvailable"}, - {"capability": "EligibleAppPlayback", "available_now": true, "reliability": "PolicyGated", - "policy_notes": "Android 10+ AudioPlaybackCapture. Capturable audio limited by per-app capture policy. Most restrictive policy wins."}, - {"capability": "ScreenProjectionMix", "available_now": true, "reliability": "UserAction"} -] -``` - -**Desktop (macOS / Windows / Linux):** - -```json -[ - {"capability": "Microphone", "available_now": true, "reliability": "UserPermission"}, - {"capability": "OwnAppAudio", "available_now": true, "reliability": "AlwaysAvailable"}, - {"capability": "DesktopSystemLoopback", "available_now": true, "reliability": "AlwaysAvailable", - "policy_notes": "Windows WASAPI loopback / macOS screencapturekit / Linux PipeWire native"}, - {"capability": "HardwareInput", "available_now": true, "reliability": "UserAction"}, - {"capability": "VirtualDeviceInput", "available_now": false, "reliability": "FutureAPI"} -] -``` - -### 5.3 Platform Adapter Trait - -```rust -pub trait PlatformAdapter: Send + Sync { - fn platform(&self) -> PlatformId; - fn source_capabilities(&self) -> Vec; - fn output_capabilities(&self) -> Vec; - fn open_source(&self, request: SourceRequest) - -> Result, AdapterError>; - fn open_output(&self, request: OutputRequest) - -> Result, AdapterError>; -} -``` - ---- - -## 6. Architecture Decision Records - -v2.3 ADRs (DOCS-001 through DOCS-013) are fully preserved. v3.0 adds ADR-014 through ADR-022. All open questions are tracked in Β§26. - -### DOCS-001: FFI/JNI Boundary Ownership - -**iOS: Platform owns the audio callback thread.** - -``` -AVAudioEngine installTap fires (Apple realtime thread, priority 47) - ↓ -Swift writes f32 samples into AudioBufferPool slot - ↓ one memcpy of f32 data β€” unavoidable, accepted -Swift writes AudioFrame header + buffer handle into SPSC ring - ↓ -Rust reads ring on its own processing thread - ↓ -ProcessorGraph β†’ Encoder β†’ TransportNode -``` - -Rules: -- Swift callback never allocates, never blocks, never calls into Rust synchronously -- Pool slot acquisition happens before the callback (pre-allocated) -- Ring write is wait-free (SPSC, single atomic store as commit) -- Ring sized for 8 frames (160ms headroom at 20ms frames) -- Rust never dereferences `AVAudioPCMBuffer` memory - -**Android: Rust owns the audio thread for mic capture.** - -```rust -let stream = AudioStreamBuilder::new() - .input() - .callback(Box::new(PocketStationCallback { bus: bus.clone() })) - .sample_rate(48000) - .format(AudioFormat::F32) - .performance_mode(PerformanceMode::LowLatency) - .open_stream()?; -``` - -For `EligibleAppPlayback`: Kotlin writes to a pre-allocated `ByteBuffer.allocateDirect()`. Rust reads from the raw pointer. JNI is called once at session init to pass the pointer, never per frame. - -Lifetime contract: ByteBuffer lives for session duration. Rust reads only within `on_capture_ready()`. Kotlin signals teardown via `AtomicBoolean`; Rust acknowledges before Kotlin frees. - -**Desktop: CPAL. Rust owns the callback. Zero FFI.** - -```rust -device.build_input_stream(&config, move |data: &[f32], info| { - let handle = pool.acquire().expect("pool exhausted"); - handle.copy_from_slice(data); - let frame = AudioFrame::new(handle, info.timestamp_ns(), source_id, bus_id); - let _ = bus.push(frame); -}, |err| tracing::error!("{err}"), None) -``` - -### DOCS-002: Star Topology β€” No Relay Chains - -All audio flows `source β†’ cloud relay β†’ listeners`. No device-to-device chains. Relay chains stack latency, require intermediate decode/encode, and provide no capability that star topology does not. WebRTC ICE handles LAN-direct paths automatically. - -### DOCS-003: Custom Go Relay β€” Graph-Aware, Not LiveKit - -Custom relay using Pion v4. The relay control plane speaks graph language (GraphSession, source_id, bus_id, route_table) while WebRTC/Pion handles the media plane internally. LiveKit's architecture is built for video conferencing; its subscription model is not designed for semantic audio buses or per-source routing policies. - -**Pion version policy:** pin to `github.com/pion/webrtc/v4`. Track v5 release notes; upgrade only after Phase 3 SDK packaging is stable. - -### DOCS-004: Backpressure Policy on Pool/Ring Exhaustion - -When `AudioBufferPool::acquire()` returns `None` or the SPSC ring is full: - -``` -A. Drop newest β€” stable latency, source-side audio loss visible -B. Drop oldest β€” keeps fresh audio, encoder glitch -C. Block producer β€” violates no-blocking rule, non-starter -``` - -**Decision:** A. Drop newest. Documented as DOCS-004. - -### DOCS-005: Relay Forward-Loop Locking - -Phase 1 uses `sync.RWMutex` per packet. Bottlenecks above ~200 listeners/room. Phase 2 switches to copy-on-write atomic pointer: - -```go -type RelaySession struct { - buses atomic.Pointer[map[BusID]*AudioBus] -} -``` - -### DOCS-006: Clock Sync / Async Sample Rate Conversion - -``` -A. Fixed-rate + drop/duplicate β€” voice-acceptable with VAD -B. PI-controlled linear interpolation β€” ~100 lines, voice default -C. Variable-rate SRC (libsoxr / rubato) β€” music quality, ~5x CPU -``` - -**Decision:** B for voice, hook for C in music/broadcast mode. - -### DOCS-007: Capability Negotiation on Partial Match - -``` -A. Fail with CapabilityMismatch -B. Auto-insert ResampleNode + MonoMixNode -C. Return descriptor delta, caller decides -``` - -**Decision:** B with explicit `negotiated: NegotiatedCapability` on the stream. - -### DOCS-008: Workspace Release Sequencing - -``` -pocketstation-frame (no deps) -pocketstation-bus (deps: frame) -pocketstation-graph (deps: frame) -pocketstation-codec (deps: frame, bus) -pocketstation-route (deps: frame) -pocketstation-metrics (deps: frame, bus) -pocketstation-audio (re-export, deps: all above) -``` - -Tooling: `cargo-release --workspace` with sequenced publish. Git tag: single `v0.X.Y` at workspace root. First publish: after Phase 1 demo validates the API surface, not at Phase 0 exit. - -### DOCS-009: Pion WriteRTP Allocation Profile - -Before Phase 1 ships: - -1. Does `TrackLocalStaticRTP.WriteRTP` mutate `pkt` (header, SSRC, sequence)? -2. If yes, need `pkt.Clone()` per bus subscriber? -3. GC pressure at 50 pkt/sec Γ— N subscribers? -4. Use shared payload with per-subscriber header rewriting (SFU pattern)? - -### DOCS-010: JitterBuffer Algorithm - -Owns 60ms of the 170ms transport-P95 budget. - -``` -A. Fixed-delay buffer β€” simple; poor for two-way voice -B. Adaptive (NetEQ-class) β€” WebRTC standard; ~500 lines -C. RTT-variance-driven with PLC β€” wraps webrtc-audio-processing -``` - -**Decision:** B for Phase 1, optional upgrade to C in Phase 4. - -### DOCS-011: SPSC Ring Buffer Crate Choice - -**Default candidate:** `rtrb` β€” fixed-capacity allocation at construction, wait-free reads/writes, cache-line padding, maintained. Verify against criteria in Phase 0 prototype. - -### DOCS-012: Opus Frame Duration - -See Β§4.3. This ADR is resolved: 20ms default, 10ms optional for voice-agent mode post-Phase-1 benchmarks. - -### DOCS-013: Internal Sample Format and Channel Layout - -See Β§4.2. This ADR is resolved: interleaved f32, 48kHz, mode-dependent channel count. - ---- - -## 7. Platform Adapter Specifications - -### 7.1 iOS Adapter β€” SourceNode Adapter Priority Order - -``` -Priority 1: AVAudioEngine own-app (OwnAppAudio) - Any audio PocketStation plays is captured via installTap. - Zero restriction, always works, no user action. - Reliability: AlwaysAvailable - -Priority 2: Microphone - Direct mic capture via AVAudioEngine input node. - User permission required. - Reliability: UserPermission - -Priority 3: AUv3 effect plugin (PluginHostAudio) - PocketStation loads as an audio effect in any AUv3 host. - GarageBand, Logic iPad, Cubasis, AUM, 580+ compatible apps. - Reliability: UserAction - -Priority 4: ReplayKit broadcast extension (BroadcastExtensionAudio) - User-approved screen + app audio broadcast. - IPC via App Group container to main app β†’ graph source. - Reliability: UserAction - -Priority 5: AirPlay-style receiver (ExternalRouteInput) - PocketStation appears as audio output destination. - Not a normal App Store SDK path. - Reliability: Experimental -``` - -iOS does not have silent global capture of all apps' audio. PocketStation does not claim it. - -`.voiceChat` enables Apple's built-in AEC but restricts AirPlay routing. Use iOS native AEC only in `AudioMode::Voice`; use `webrtc-audio-processing` elsewhere. - -### 7.2 Android Adapter β€” SourceNode Adapter Priority Order - -``` -Priority 1: Microphone + own-app audio (AAudio/oboe, Rust-owned thread) -Priority 2: EligibleAppPlayback (Android 10+ AudioPlaybackCapture, policy-gated) -Priority 3: ScreenProjectionMix (MediaProjection screen + audio, user grants per session) -Priority 4: Own-app playback capture (always works, no permission) -``` - -`EligibleAppPlayback` is meaningfully stronger than iOS equivalents because AudioPlaybackCapture is an official API. Source apps can opt out (`ALLOW_CAPTURE_BY_NONE`), and audio with `USAGE_VOICE_COMMUNICATION` is not capturable. - -### 7.3 Desktop Adapter - -CPAL handles device I/O. Loopback capture of other apps and virtual device creation require native APIs per platform. - -``` -Windows: - Phase 1: WASAPI loopback via windows-rs (DesktopSystemLoopback) - Phase 2: CPAL for own-device I/O - Phase 6: SysVAD virtual speaker driver (VirtualDeviceInput, C++/WDK) - -macOS: - Phase 1: screencapturekit-rs (DesktopSystemLoopback, ~1.9% CPU on Apple Silicon) - Phase 2: CPAL CoreAudio for own-device I/O - Phase 6: AudioDriverKit virtual device (C++/DriverKit) - -Linux: - Phase 1: PipeWire native via pipewire-rs (DesktopSystemLoopback + graph nodes) - Phase 2: CPAL for fallback ALSA/PulseAudio on non-PipeWire systems -``` - -Linux PipeWire is the deepest desktop platform β€” it exposes the full audio graph natively. Build and validate the SourceNode adapter model on Linux first. - ---- - -## 8. Relay Architecture β€” GraphSession - -### 8.1 Two Product Modes - -**Mode A β€” Broadcast** -``` -One source β†’ relay β†’ N listeners -Star topology, one-to-many -Source publishes a named bus (e.g. monitor_mix) -Listeners subscribe to that bus by bus_id -No decode, no re-encode at relay (Phase 1) -``` - -**Mode B β€” Voice Agent** -``` -Client graph β†’ relay β†’ AI backend (bidirectional) -Multiple buses: uplink voice, downlink agent speech, events -Relay handles ICE, TURN, DTLS for all edges -GraphSession metadata: session_id, graph_id, source_ids, latency budget -Webhook events: session_started, utterance_detected, model_response, session_ended -``` - -### 8.2 GraphSession Replaces Room - -Old relay model: - -```go -type Room struct { - id string - source *webrtc.TrackRemote - listeners []*webrtc.TrackLocalStaticRTP -} -``` - -New relay model: - -```go -type RelaySession struct { - ID string - GraphID string - Sources map[SourceID]*SourceSession - Buses map[BusID]*AudioBus - Subscribers map[SubscriberID]*BusSubscription - Routes atomic.Pointer[RouteTable] - Policies []PolicyBinding - Metrics *RelaySessionMetrics -} - -type AudioBus struct { - ID string - SourceID string - Role BusRole // voice, music, monitor, mix, stem, agent_output, events - Codec Codec - Clock ClockDomain - Subscribers atomic.Pointer[[]Subscriber] - Metrics BusMetrics -} -``` - -The relay still uses Pion v4 WebRTC internally for the media plane. The control plane speaks graph semantics. Transport can still forward raw RTP without decoding. - -### 8.3 Core Relay β€” Phase 1 MVP (~1200–1500 lines total) - -```go -func (r *RelaySession) forwardBus(bus *AudioBus) { - for { - pkt, _, err := bus.Source.ReadRTP() - if err != nil { return } - bus.Metrics.PacketCount.Add(1) - bus.Metrics.ByteCount.Add(uint64(len(pkt.Payload))) - subs := bus.Subscribers.Load() - for _, s := range *subs { - _ = s.Track.WriteRTP(pkt) // see DOCS-009 for mutation/clone ADR - } - } -} -``` - -**Phase 1 relay includes:** RTP forwarding by bus_id, GraphSession lifecycle, JWT auth, QR/room codes, TURN/STUN config, SSE presence, source identity propagation, per-bus metrics. - -**Production relay grows to (~3000–5000 lines):** reconnect logic, regional routing, load balancing, per-bus SLO enforcement, webhook events, recording trigger, live route-table updates, multi-source mixing, graceful shutdown with session migration. - -### 8.4 Signaling Protocol - -``` -Client β†’ Server (WebSocket JSON): - PUBLISH: graph_id, session_id, bus_id, token, SDP offer - SUBSCRIBE: session_id, bus_id, SDP offer - ICE: candidate - ROUTE: live route-table update - LEAVE: session_id - -Server β†’ Client: - SDP_ANSWER: SDP answer - ICE: candidate - SESSION_STATE: sources[], buses[], routes[], metrics - BUS_EVENT: type, bus_id, source_id, payload - ERROR: code, message -``` - -### 8.5 Control Plane API - -``` -POST /v1/graphs Create graph session β†’ {graph_id, session_id} -GET /v1/sessions/{id} Session state β†’ {sources, buses, routes, metrics} -DELETE /v1/sessions/{id} Close session -POST /v1/sessions/{id}/sources Register source β†’ {source_id, token} -POST /v1/sessions/{id}/buses Register bus β†’ {bus_id} -POST /v1/sessions/{id}/routes Update route table -POST /v1/sessions/{id}/subscribe Get listener token + ICE config -GET /v1/sessions/{id}/events SSE event stream -GET /v1/sessions/{id}/metrics Per-bus latency, loss, jitter -GET /v1/apps/{id}/usage Graph-session minutes this billing period -``` - ---- - -## 9. Security Model - -### Phase 1 β€” Transport Security - -``` -WebRTC DTLS/SRTP: transport encrypted between client and relay -Relay can read Opus payloads in Phase 1 β€” accepted and disclosed -Session access controlled by JWT tokens (short-lived, session-scoped) -No public session listing β€” sessions are ephemeral by default -HTTPS everywhere for control plane -``` - -Say: "Encrypted transport. Session access requires a token." -Do not say: "End-to-end encrypted." Not true until Phase 3. - -### Phase 2 β€” Access Controls - -``` -Short-lived source tokens (15-minute expiry, renewable) -Subscriber tokens with max-subscriber-count enforcement -Session expiry (auto-close after N hours of inactivity) -Abuse rate limiting (max sessions per IP, max subscribers per session) -Source privacy classes: Public / UserConscented / EnterprisePrivate / PrivacyRedacted -``` - -### Phase 3 β€” True E2EE (SFrame, RFC 9605) - -SFrame defines frame-level encryption where the relay forwards media with metadata visible but payload encrypted. - -```rust -pub enum EncryptionMode { - TransportOnly, // Phase 1 - SFrameE2EE, // Phase 3: relay is routing-blind to audio - EnterpriseKeyManager, // Phase 5 -} -``` - -### Phase 5 β€” Enterprise - -``` -Private relay deployment (customer VPC) -Audit logs -Source privacy enforcement (block source from reaching cloud model) -Model allowlist/denylist per session -Data retention controls -HIPAA BAA (when operationally ready) -SOC 2 Type II (12–18 month process) -``` - ---- - -## 10. Model Nodes and ML Processing Layer - -All ML runs as graph nodes. All models run on-device by default. Raw audio is never sent to external ML APIs unless the developer explicitly routes to a model node. - -**Threading rule (load-bearing):** Model nodes run on the Rust processing thread, never inside platform audio callbacks. VAD inference is often fast enough that the distinction seems academic β€” until denoise or AEC pushes 30ms inference into the callback path and the audio system glitches. The boundary is enforced architecturally: callbacks write to the SPSC ring and return; the processing thread drains the ring and runs all `AudioProcessorNode::process()` calls including model inference. - -### 10.1 VAD - -Silero VAD β€” MIT licensed, 1.8MB ONNX, <1ms inference on CPU. - -```rust -pub struct VadNode { - model: OrtSession, - state: VadState, - threshold: f32, -} - -impl AudioProcessorNode for VadNode { - fn process(&mut self, frame: AudioFrame) -> Option { - let prob = self.model.infer(frame.buffer.as_slice()); - self.state = VadState::from_prob(prob, self.threshold); - match self.state { - VadState::Speech | VadState::Onset => Some(frame), - VadState::Silence | VadState::Offset => None, - } - } - - fn accepted_channels(&self) -> ChannelLayout { - ChannelLayout::MonoOnly - } -} -``` - -VAD gating saves 40–60% relay bandwidth in typical voice sessions. For voice agents, VAD drives end-of-utterance detection without external API calls. - -### 10.2 Noise Suppression - -DTLN-rs β€” open source Rust, WASM-compatible, processes 1s audio in 33ms on M1. Accepts mono. - -### 10.3 Echo Cancellation - -`webrtc-audio-processing` (libwebrtc AEC3) or iOS native AEC via `.voiceChat` mode. See Β§7.1 for iOS AEC routing constraint. - -### 10.4 Bandwidth-Adaptive Codec Control - -Relay measures per-bus RTCP RR. Source adjusts Opus settings: - -``` -Loss < 1%, RTT < 100ms: bitrate=96kbps, complexity=10, fec=false -Loss 1-5%, RTT < 200ms: bitrate=64kbps, complexity=5, fec=true -Loss > 5%, any RTT: bitrate=32kbps, complexity=3, fec=true, dtx=true -Loss > 15%: trigger ICE restart, fallback TURN relay -``` - -### 10.5 Future Model Nodes - -``` -SpeakerDiarizationNode TitaNet/Sortformer -EmotionCueNode distress, stress, sarcasm detection -SourceSeparationNode overlapping speaker isolation -AudioEnhancementNode bandwidth extension -RealtimeTranslationNode Whisper + MT (500ms+, broadcast only) -AudioWatermarkNode EU AI Act compliance -LocalWhisperNode on-device STT fallback -``` - ---- - -## 11. Observability - -Observability ships with Phase 0. Not Phase 5. - -### 11.1 Per-Edge Metrics - -```rust -pub struct EdgeMetrics { - pub frames_in: Counter, - pub frames_out: Counter, - pub frames_dropped: Counter, - pub queue_depth: Gauge, - pub p50_latency_ms: Histogram, - pub p95_latency_ms: Histogram, - pub p99_latency_ms: Histogram, - pub jitter_ms: Histogram, - pub drift_ppm: Gauge, - pub clipping_events: Counter, - pub rms_dbfs: Gauge, - pub loudness_lufs: Gauge, - pub packet_loss_pct: Gauge, - pub model_first_token_ms: Option, - pub model_first_audio_ms: Option, - pub model_cost_usd: Option, -} -``` - -Developer API: - -```rust -graph.observe(edge_id) - .on_latency_p95(|ms| warn!("edge {edge_id} latency p95 = {ms}")) - .on_drop(|count| warn!("edge {edge_id} dropped {count} frames")) - .on_model_cost(|usd| if usd > budget { graph.trigger(PolicyNode::CostCap) }); -``` - -### 11.2 Per-Session Bus Metrics - -```go -type BusMetrics struct { - PacketsForwarded prometheus.CounterVec - BytesForwarded prometheus.CounterVec - SubscriberCount prometheus.GaugeVec - ForwardLatencyNs prometheus.HistogramVec - SubscriberLossRate prometheus.GaugeVec - SessionDurationSec prometheus.HistogramVec - ModelLatencyMs prometheus.HistogramVec - ModelCostUsd prometheus.CounterVec -} -``` - -### 11.3 Latency Budget β€” Transport-Only Targets - -PocketStation owns the **transport segment**: capture β†’ encode β†’ relay β†’ decode β†’ playback. It does not own STT, LLM, or TTS latency. - -``` -Source capture β†’ FrameBus: ≀ 5ms -FrameBus β†’ Opus encoder: ≀ 2ms -Opus β†’ WebRTC send: ≀ 1ms -WebRTC β†’ relay (network, P95): ≀ 50ms -Relay β†’ subscriber (network, P95): ≀ 50ms -Subscriber WebRTC β†’ JitterBuffer: ≀ 2ms -JitterBuffer β†’ output (adaptive): ≀ 60ms - -Target transport P95: ≀ 170ms -Target transport P99: ≀ 250ms - -Voice agent transport-segment P95: ≀ 185ms - Leaves remaining budget for STT/LLM/TTS to land the full - conversational loop near the ≀500–700ms perceptual ceiling. - PocketStation does not promise the full loop β€” only its segment. -``` - -### 11.4 Developer-Facing Latency Breakdown - -```json -{ - "session_id": "...", - "graph_id": "...", - "edge_id": "mic:voiceβ†’relay:uplink", - "source_id": "...", - "bus_id": "clean_mic", - "capture_ms": 3.2, - "encode_ms": 1.1, - "relay_rtt_ms": 44.0, - "jitter_buffer_ms": 55.0, - "decode_ms": 0.8, - "transport_e2e_ms": 104.1, - "model_first_token_ms": 210.0, - "model_first_audio_ms": 380.0, - "packet_loss_pct": 0.3, - "clock_drift_ppm": 12 -} -``` - -### 11.5 SLI / SLO Definitions - -``` -SLI: Session completion - Sessions where both source and last subscriber disconnect cleanly, - or session duration β‰₯ 30 minutes with no fatal media-plane error. - SLO target: 99.9% - -SLI: Transport latency - capture_ms + encode_ms + relay_rtt_ms + jitter_buffer_ms + decode_ms - Measurement: per-session P95 from subscriber client - SLO target: 95% of sessions ≀ 250ms - -SLI: Source publish success - Source token validated β†’ WebRTC negotiated β†’ first RTP packet forwarded - SLO target: 99.5% within 3 seconds of token presentation -``` - -### 11.6 CLI Observability - -```bash -pks sources # list available source nodes -pks session inspect {session_id} # graph topology + node states -pks session edges {session_id} # all edges + latency per edge -pks session trace {session_id} \ - --edge mic:voiceβ†’openai:audio # per-edge packet trace -pks session record {session_id} \ - --stems mic,discord,spotify,agent # start multi-stem recording -``` - ---- - -## 12. Full Tech Stack - -| Layer | Language / Library | Rationale | -|---|---|---| -| Audio graph runtime | **Rust** | Type-safe node contracts, zero-cost graph execution | -| Audio engine core | **Rust** | Memory safety, zero-cost abstractions, lock-free | -| iOS adapter shell | **Swift** | Required for AVAudioEngine, AUv3, AVAudioSession | -| Android adapter shell | **Kotlin** | Required for AudioPlaybackCapture, MediaProjection, AAudio JNI | -| Desktop device I/O | **Rust (CPAL)** | Fallback path for standard device I/O | -| Windows system loopback | **Rust (windows-rs WASAPI)** | CPAL does not expose loopback | -| macOS system loopback | **Rust (screencapturekit-rs)** | Native CoreMedia capture, ~1.9% CPU | -| Linux audio graph | **Rust (pipewire-rs)** | Graph-level access, virtual nodes | -| Windows virtual driver | **C++/WDK** | OS forced | -| macOS AudioDriverKit | **C++** | DriverKit requires C++ | -| Cloud relay | **Go + Pion v4** | Current stable; goroutines for N subscribers | -| Control plane API | **Go** | Shared codebase with relay | -| Protocol (Phase 2+) | **Protobuf** | Graph/session/bus/source wire types | -| Web receiver | **TypeScript + WebRTC** | Browser-native; no framework for a subscriber | -| ML inference nodes | **Rust + ONNX Runtime** | WASM-compatible | -| Python SDK | **PyO3 bindings** | Voice AI developers write Python | -| Research tooling | **Python** | Latency measurement, DSP experiments | - -**Never:** Python in any production audio path or relay. Never Python for the relay. - ---- - -## 13. Developer API - -### 13.1 The AudioGraph API (Primary) - -See Β§3.7 for the full holy-shit demo. The core wiring pattern: - -```rust -let graph = AudioGraph::new(); - -// Source discovery -let sources = graph.discover_sources().await?; - -// Build graph -let mic = graph.source(SourceNode::Mic); -let relay = graph.transport(TransportNode::Relay("session-abc")); -let sink = graph.sink(SinkNode::Browser); - -graph.connect(mic.out("voice"), relay.in_("voice"))?; -graph.connect(relay.out("mix"), sink.in_("audio"))?; - -// Compile validates types, inserts adapters, estimates latency/cost -let plan = graph.compile()?; - -// Observe edges before running -graph.observe(plan.edge("mic:voiceβ†’relay:voice")) - .on_latency_p95(|ms| tracing::warn!("high latency: {ms}ms")); - -graph.run(plan).await?; -``` - -### 13.2 Convenience API (Sugar β€” Compiles to Graph) - -```rust -// Simple broadcast: compiles to SourceNode β†’ Encode β†’ TransportNode::Relay β†’ Browser -let station = PocketStation::builder() - .relay_url("wss://relay.pocketstation.io") - .session_id("abc123") - .mode(AudioMode::Voice) - .opus_frame_duration_ms(20) - .add_processor(VadNode::default()) - .add_processor(NoiseSuppressorNode::default()) - .on_subscriber_count(|n| println!("{n} listening")) - .build() - .await?; -let source = station.open_best_source(SourcePreference::Voice).await?; -station.start_broadcast(source).await?; -``` - -### 13.3 Platform SDKs - -```swift -// iOS -let station = try await PocketStation(sessionID: "abc123", role: .source) -station.onSubscriberCount = { n in label.text = "\(n) listening" } -try await station.start() -``` - -```kotlin -// Android -val station = PocketStation.builder(context) - .sessionId("abc123") - .role(Role.SOURCE) - .build() -lifecycleScope.launch { - station.subscriberCount.collect { label.text = "$it listening" } -} -station.start() -``` - -```python -# Python (voice AI developers) -import asyncio -from pocketstation import AudioGraph, SourceNode, ModelNode, SinkNode - -async def main(): - graph = AudioGraph() - mic = graph.source(SourceNode.Mic) - stt = graph.model(ModelNode.Transcribe("deepgram")) - agent = graph.model(ModelNode.SpeechToSpeech("openai-realtime")) - out = graph.sink(SinkNode.Browser) - - graph.connect(mic.out("voice"), stt.in_("audio")) - graph.connect(stt.out("transcript"), agent.in_("context")) - graph.connect(agent.out("audio"), out.in_("audio")) - - await graph.run() - -asyncio.run(main()) -``` - ---- - -## 14. Repository Structure - -### 14.1 Why Separate Repos - -One independently releasable unit = one repository. An iOS developer adding the Swift SDK should not download the Go relay, Windows WDK driver, Python bindings, and research notebooks. The iOS Swift package cannot be published to Swift Package Index from inside a Rust workspace root. - -Exception: Cargo workspace where internal crates are tightly coupled, share types, and always release together. - -### 14.2 GitHub Organization - -``` -github.com/pocketstation-io/ core infrastructure, SDKs, services, apps -github.com/pocketstation-examples/ standalone examples, one per use case -``` - -### 14.3 The Full Repo Map - -#### Tier 0 β€” Core Rust (Cargo workspace) - -``` -pocketstation-io/audio-core - crates/ - pocketstation-frame/ AudioFrame, AudioBufferPool, SampleFormat - pocketstation-bus/ FrameBus, SpscRingBuffer, ClockSync - pocketstation-graph/ AudioGraphNode trait, ProcessorGraph, EdgeMetrics - pocketstation-codec/ OpusEncoder, OpusDecoder, JitterBuffer - pocketstation-route/ SourceCapability, SourceIdentity, RoutePlan - pocketstation-metrics/ BusMetrics, OTEL integration - pocketstation-audio/ re-exports all above as single entry point - benches/ - tests/ - ffi/ cbindgen β†’ C headers for Swift/Kotlin -``` - -#### Tier 1 β€” Graph Runtime (Cargo workspace, separate from audio-core) - -``` -pocketstation-io/audio-graph - crates/ - pocketstation-graph-api/ AudioGraph, AudioGraphNode, PortSpec, EdgeSpec - pocketstation-graph-nodes/ SourceNode, TransformNode, PolicyNode, ModelNode, TransportNode, SinkNode - pocketstation-graph-runtime/ Graph compile, validation, execution planner - pocketstation-graph-observe/ EdgeMetrics, GraphMetrics, tracing integration - examples/ -``` - -#### Tier 2 β€” Protocol (created Phase 2) - -``` -pocketstation-io/protocol - Language: Protobuf + generated Go, Rust, TypeScript, Swift, Kotlin - Contains: GraphSession, SourceIdentity, BusDescriptor, EdgeDescriptor, - signaling messages, control plane types, metric schemas - Published: Generated code vendored into each SDK repo -``` - -#### Tier 3 β€” Client SDKs - -``` -pocketstation-io/sdk-ios Swift Package Index PocketStation -pocketstation-io/sdk-android Maven Central io.pocketstation:android -pocketstation-io/sdk-js npm @pocketstation/client -pocketstation-io/sdk-rust crates.io pocketstation-client -pocketstation-io/sdk-python PyPI pocketstation -``` - -#### Tier 4 β€” Server Services - -``` -pocketstation-io/relay Go + Pion v4 (GraphSession, AudioBus) -pocketstation-io/api-server Go (control plane, graph/session/metrics APIs) -``` - -#### Tier 5 β€” ML Nodes (Cargo workspace, separate from audio-core) - -``` -pocketstation-io/audio-ml - crates/ - pocketstation-vad/ Silero VAD - pocketstation-denoise/ DTLN-rs - pocketstation-aec/ webrtc-audio-processing - models/ ONNX model files, git-lfs -``` - -#### Tier 6 β€” Model Connectors - -``` -pocketstation-io/model-connectors - openai-realtime/ - deepgram-stt/ - elevenlabs-tts/ - local-whisper/ - sip-adapter/ -``` - -#### Tier 7 β€” OS Driver Extensions - -``` -pocketstation-io/driver-windows C++/WDK -pocketstation-io/driver-macos C++/DriverKit -``` - -#### Tier 8 β€” Applications - -``` -pocketstation-io/app-creator React Native (iOS + Android) -pocketstation-io/app-web-receiver Static TS, no framework -pocketstation-io/app-desktop Tauri (Rust + web frontend) -pocketstation-io/cli pks command -pocketstation-io/docs docs.pocketstation.io -``` - -#### Tier 9 β€” Examples - -``` -github.com/pocketstation-examples/ - voice-agent-openai - voice-agent-android - graph-multi-source - creator-station-ios - latency-benchmark - model-routing-fallback - desktop-loopback-linux - enterprise-private-graph -``` - -### 14.4 Release Strategy - -``` -audio-core: SemVer. cargo-release --workspace. First publish: Phase 1 exit. -audio-graph: SemVer aligned with audio-core major. -sdk-ios: SemVer aligned with audio-core major. SPM tag. -sdk-android: SemVer aligned with audio-core major. Maven Central. -sdk-python: SemVer independent. PyPI. -relay: SemVer independent. Docker image on tag. -protocol: SemVer independent. Vendored into SDKs. -``` - ---- - -## 15. Build Phase Plan - -Phases as milestones. No week numbers. The sequence is the contract; the dates are not. - -### Phase 0 β€” Core DNA - -Prove the hot path. Prove the graph vocabulary. No OS audio APIs. No UI. - -Build: -``` -AudioBufferPool (zero per-frame allocation, 64-slot cap documented) -AudioFrame with pool handles, interleaved f32 48kHz (DOCS-013) -SPSC ring buffer (proptest-verified invariants; crate: rtrb) -FrameBus -ProcessorGraph (PassthroughNode, GainNode) with accepted_channels routing -ClockSync (PI-controlled linear interpolation, DOCS-006) -Opus encoder/decoder at 20ms default (DOCS-012) -Fake sine-wave source -File output sink -Full per-frame BusMetrics -Backpressure policy decided (DOCS-004) -AudioGraph API sketch: node types, port names, edge contracts (design only) -SourceIdentity type (v3.0 addition) -BusDescriptor type (v3.0 addition) -GraphManifest JSON draft (v3.0 addition) -``` - -Exit criteria: -``` -sine_wave β†’ ProcessorGraph β†’ Opus encode β†’ decode β†’ WAV file -Zero heap allocation on hot path (DHAT in CI) -Ring buffer handles 10,000 frames/sec under 1-hour soak, zero overruns -BusMetrics P50/P95/P99 print correctly after 60-second run -Old simple route API compiles to a graph template internally -DOCS-004 through DOCS-013 written and merged -ADR-014 through ADR-016 drafted -audio-core crate is publish-ready (not yet published) -``` - -### Phase 1 β€” First Graph Route - -Turn existing relay + web receiver + CLI capture into the first real graph demo. - -Build: -``` -Desktop source adapters: macOS (screencapturekit-rs), Linux (PipeWire), Windows (WASAPI) -Go relay MVP with GraphSession, source_id, bus_id, route_table (~1200-1500 lines) -Pion v4 WriteRTP allocation profile resolved (DOCS-009) -JitterBuffer algorithm chosen and benchmarked (DOCS-010) -Cloudflare Workers control plane -QR code generation -WebRTC signaling (types live in relay repo) -Browser receiver: shows source name, bus name, latency metrics -E2E latency measurement dashboard -First crates.io publish of audio-core -CLI: pks sources, pks session create, pks route, pks run -``` - -Exit: -``` -Desktop source β†’ relay β†’ browser on different network (30-minute stable session) -P95 transport latency ≀ 250ms measured -Source identity visible in browser receiver -Latency dashboard shows per-edge breakdown (capture, encode, relay, jitter, decode) -audio-core v0.1.0 published on crates.io -CLI works on macOS and Linux -``` - -### Phase 2 β€” Graph v0: Multi-Source, Multi-Bus, Policies - -Prove PocketStation is not forwarding one track. - -Build: -``` -Multiple sources in one GraphSession -Named buses and stems -Local mixer node -Remote bus subscription by bus_id -Per-bus recording -Ducking policy node (live) -Simple trigger policy node -Graph manifest save/replay -Android mic source (AAudio/oboe, Rust-owned thread) -Android EligibleAppPlayback (Kotlin + direct ByteBuffer) -JNI bridge finalized (zero per-frame JNI calls) -Relay: copy-on-write subscriber slice (DOCS-005), reconnect, rate limiting, session expiry -SLO instrumentation (Β§11.5) -sdk-ios first SPM publish -protocol repo created with provisional proto definitions -``` - -Exit: -``` -At least 3 concurrent sources in one session -At least 2 simultaneous bus outputs -At least 1 policy action changes routing or gain live -At least 1 multi-stem recording works -Android source β†’ browser: same performance targets -Relay survives source disconnect + reconnect without losing subscribers -1-hour memory soak: zero RSS growth -``` - -### Phase 3 β€” Model Nodes and AI Pipeline Routing - -Make the AI-era wedge real. - -Build: -``` -ModelNode interface -OpenAI Realtime connector -Deepgram STT connector -ElevenLabs TTS connector -Local Whisper connector -ModelRouter policy (latency/cost/privacy decision) -Streaming transcript events in GraphSession -TTS return stream as SourceNode::ModelOutput -Cost/latency metrics per model node -sdk-android first Maven Central publish -SDK API finalized (Rust crate API stable) -Python SDK (PyO3 bindings) -Documentation: 3 quickstart guides, graph API reference -3 demo apps (voice-agent, podcast, remote-monitor) -NLnet grant application submitted -``` - -Exit: -``` -Mic β†’ OpenAI Realtime β†’ agent speech β†’ browser: working end-to-end -Model latency metrics visible per edge -Model output is treated as a SourceNode in the graph -At least one routing policy uses model/transcript event as trigger -External developer follows iOS quickstart in ≀ 2 hours -``` - -### Phase 4 β€” Creator Station + Developer SDK Polish - -Make it usable by people who are not the author. - -Build: -``` -iOS app: Choose sources β†’ Graph runs β†’ QR β†’ subscriber count β†’ health meter -Android app: same UX -Web receiver: stem selector, latency display, event stream -Desktop app (Tauri): same session pairing, WASAPI/SCKit source -Graph template system (voice-agent, podcast, meeting-transcription, remote-monitor) -CLI: pks session inspect, pks session trace, pks session record -``` - -Exit: -``` -Non-technical person starts station, friend listens in ≀ 1 minute -Station survives 30-minute session on 4G -At least 10 external users run real sessions -At least 3 design partners give feedback on graph API -``` - -### Phase 5 β€” Deep Source Expansion + E2EE - -Build: -``` -iOS AUv3 effect plugin (PluginHostAudio) -iOS ReplayKit broadcast extension (BroadcastExtensionAudio) -macOS screencapturekit-rs full integration -Linux PipeWire virtual sink + node -SFrame E2EE (Phase 3 security model) -Enterprise: private relay deployment, source privacy classes, model allowlist -Observability: per-edge cost tracking, fallback reason logs -``` - -Exit: -``` -AUv3 loads in 3 major iOS host apps without crash -SFrame E2EE: relay cannot distinguish audio content from silence -Enterprise design partner can run private relay -Graph metrics can diagnose a failed voice agent session -``` - -### Phase 6 β€” Virtual Endpoints and Scale - -Build: -``` -Windows SysVAD virtual speaker driver (C++/WDK) -macOS AudioDriverKit virtual audio device (C++/DriverKit) -Linux PipeWire virtual mic/sink -Any app can select PocketStation virtual mic or speaker -Virtual endpoint maps to a graph bus -Multi-region relay (Fly.io edge: EU/US/APAC) -Horizontal relay scaling -Live route-table updates without session drop -``` - -### Phase 7 β€” Ecosystem and Moat - -Build: -``` -Node plugin SDK (external developers build graph nodes) -Model connector marketplace -Graph template library -Enterprise policy packs -Benchmark suite + certification tests for low-latency nodes -Research publications -``` - ---- - -## 16. Market Reality - -Three signals: - -1. LiveKit raised $100M at $1B valuation building realtime voice/AI infrastructure and reportedly powers ChatGPT Voice Mode. The benchmark and the acquisition signal. - -2. Deepgram raised $130M at $1.3B in January 2026. ElevenLabs raised $500M at $11B in February 2026. Voice AI infrastructure is venture-scale and actively funded. - -3. A 2026 Salesforce AI Research tutorial on enterprise voice agents found that "realtime" performance in production still depends primarily on streaming and pipelining across cascaded STT β†’ LLM β†’ TTS components, not on any single magic model. Measured P50 time-to-first-audio: 947ms with a well-tuned cascaded pipeline. The battle is pipeline, routing, latency, capture, and control β€” not only model quality. - -Grand View Research estimates: WebRTC at **$8.71B in 2024, 45.7% CAGR through 2030**. Call-center AI at **$1.99B in 2024, projected $7.08B by 2030**. Podcasting at **$30.72B in 2024, projected $131.13B by 2030**. - -That is the entire market thesis. PocketStation sits at the intersection of all of them. - ---- - -## 17. Target Markets β€” Ranked and Honest - -### Market 1 β€” Voice AI Developer Infrastructure (Start Here) - -**Who:** Teams building voice agents, AI meeting tools, voice-enabled mobile apps, accessibility tools. - -**Pain:** No major AI voice API ships mobile audio capture or a cross-platform audio graph. Developers re-implement ~1,000 lines of AVAudioSession lifecycle per project. Model routing, fallback, and observability are always custom. - -**PocketStation removes:** the entire audio I/O and pipeline plumbing that every voice AI team builds themselves. - -**Reach:** crates.io, PyPI, npm, Show HN, direct outreach to voice AI startups. - -### Market 2 β€” Creator Broadcast - -**Who:** DJs, podcasters, mobile creators, radio-style streamers, live event hosts. - -**Pain:** Broadcasting from phone to listeners requires OBS + audio interface + port forwarding + Discord. No one-tap cross-app solution. - -**PocketStation gives:** start session β†’ share QR β†’ route any source β†’ listeners hear it β†’ stems recorded. - -### Market 3 β€” Enterprise Private Audio Infrastructure - -**Who:** Companies building contact-center AI, meeting intelligence, compliance-sensitive voice workflows. - -**Pain:** Need private relay, model provider control, source privacy enforcement, audit logs, SLO dashboards. - -**PocketStation gives:** private graph infrastructure with observability, policy, and compliance path. - -### Market 4 β€” Consumer Audio Sharing - -**Reality:** Consumer default is Bluetooth, AirPlay, Discord, FaceTime. PocketStation wins only when demonstrably simpler or solving a moment none of those reach. Do not chase. Let it come organically from creator station. - ---- - -## 18. Business Model - -### Phase 0–2 β€” Credibility Before Revenue - -``` -Open source core, adapters, relay source -Docs, benchmarks, demo videos -Free hosted relay (time-limited during development) -No paywall for first 1,000 developers -``` - -### Phase 3 β€” Developer Revenue - -``` -Free tier: 10,000 participant-minutes/month -Usage-based: $0.0004/participant-minute (matches LiveKit) -Developer: $49/month β€” 100K minutes + graph events + per-edge analytics -``` - -### Phase 4 β€” Enterprise Revenue - -``` -Scale plan: $299/month + overage β€” 1M minutes + SLA 99.9% -Enterprise: Contract pricing β€” private relay, model allowlist, - HIPAA BAA (when ready), SOC 2, custom SLA, source license -``` - -### Pricing Units (v3.0 Expansion) - -``` -graph-session minutes -participant minutes -model-routing minutes (per model node in graph) -recording stem storage -private relay deployment -observability retention days -enterprise policy packs -``` - -### North Star Metric - -**Weekly Active Sessions (WAS):** unique graph sessions with at least one source and one subscriber exchanging audio in the past 7 days. - -``` -100 WAS β†’ NLnet grant -1,000 WAS β†’ YC viable -10,000 WAS β†’ seed round -100,000 WAS β†’ Series A or acquisition -``` - ---- - -## 19. Infrastructure Cost Strategy - -### Stack - -``` -Audio relay origin Hetzner CX23 (€3.49/mo, 20TB included) -Edge relay nodes Fly.io (30+ cities, pay per second) -Control plane Cloudflare Workers (free 100K req/day) -Session state Cloudflare Durable Objects -TURN relay Cloudflare Calls ($0.05/GB) -Recording storage Cloudflare R2 (zero egress) -Metrics Grafana Cloud (free tier 10K series) -``` - -### Bandwidth Math - -``` -Opus 64kbps stereo = 8KB/s = 28.8MB/hour per subscriber stream -Hetzner CX23 included: 20TB/month β†’ 694,444 subscriber-hours included -``` - -### Cost by Phase - -``` -Phase 0–1 (0 users): ~€5/month -Phase 2–3 (≀100 users): ~€20/month -Phase 4 (100–1K users): ~€50/month -Phase 5 (1K–10K users): ~€130/month -Phase 6 (10K–100K): ~€430/month -Scale (100K+): Revenue required; at €430/month cost, - $14K MRR covers it with 97% margin -``` - ---- - -## 20. Funding Strategy - -### Phase 0–2 β€” No Equity, No VC - -**NLnet Foundation NGI Zero Commons Fund** β€” €5K–€50K, rolling open calls, ~3 months to decision. - -Grant pitch: -> PocketStation is open-source realtime audio graph infrastructure for mobile devices, AI voice applications, accessibility tools, and creator broadcast. It eliminates the audio I/O plumbing every voice AI team builds independently. - -**Mitacs Accelerate** (if enrolled): $15K CAD / 4-month unit. -**NSERC Discovery** (if enrolled): $20–50K CAD/year. -**GitHub Sponsors:** when crates.io downloads are real. - -### Phase 3–5 β€” After Real Users - -**YC** at 1,000+ WAU or 10 developers with SDK in production. - -Pitch: -> Open source audio graph runtime every voice AI company building on mobile or desktop needs. LiveKit is the comparable infrastructure β€” $100M at $1B. We are audio-native, graph-first, Rust-core, with model routing and per-edge observability. - -**Sequoia / a16z Seed ($5–10M):** at $50K+ MRR or major AI company as design partner. - ---- - -## 21. Research Path - -IEEE, ACM, Elsevier, Springer accept independent researchers. Use "Independent Researcher" affiliation. Most venues are double-blind. Path: implement β†’ measure β†’ arXiv preprint β†’ conference/journal. - -``` -Paper 1 PocketStation: A Graph Runtime for Realtime Audio Across - Devices, Apps, and AI Models (ACM MM / ICASSP) - -Paper 2 Memory-Safe Realtime Audio Graphs: Rust Type System as - a Callback-Thread Safety Proof (USENIX ATC / EuroSys) - -Paper 3 Latency Decomposition in Cascaded Voice Agent Pipelines: - Capture, Transport, STT, LLM, and TTS (ACM IMC / Interspeech) - -Paper 4 Cross-Platform Mobile Audio Capture for Voice AI: - Constraints, Capabilities, and a Unified Abstraction (IEEE SP Magazine / ACM CSUR) - -Paper 5 Policy-Based Model Routing for Realtime Audio Agents: - Cost, Latency, and Quality Tradeoffs (ICASSP / IEEE SLT) - -Paper 6 Multi-Stem Realtime Recording for AI Voice Agent Debugging: - A Measurement Study (ACM SIGCOMM / IMC) -``` - ---- - -## 22. Competitive Landscape - -### LiveKit β€” The Benchmark - -LiveKit ships full realtime platform (video + audio + data), Go/Pion, agents SDK, $100M raised. **It has iOS and Android SDKs and powers OpenAI Voice Mode.** - -PocketStation's wedge is not "better generic LiveKit." It is a different shape: - -``` -Audio-only, leaner, smaller binary and API surface -Capture-first: SourceNode identity and adapter layering -Graph-first: programmable nodes, typed edges, policies, model routing -Observability-first: per-edge latency, cost, and drift as first-class API -Self-hostable relay: readable Go codebase, graph-aware control plane -Audio-only pricing: cleaner economics for audio-only workloads -Rust core: measurable CPU and battery advantage on mobile -``` - -Pitch: "LiveKit is full-stack realtime. PocketStation is the programmable audio graph layer for when you only need audio and you need to own the graph." - -### Vapi / Retell / Bland β€” Voice-Agent Deployment - -These own agent deployment: phone numbers, assistant config, workflows, provider integrations, analytics. That is not PocketStation's lane unless the goal is to become a full agent SaaS. - -**Better:** Vapi owns "agent deployment." PocketStation owns "audio I/O and graph control for agents." They integrate; they do not necessarily compete. - -### OpenAI / Deepgram / ElevenLabs β€” Model Providers - -These own models. PocketStation should not try to out-model them. They are graph nodes. - -> Route to the right model at the right time with the right audio source, latency budget, privacy policy, and fallback behavior. - -That is PocketStation's territory. - -### Desktop Audio Tools - -Rogue Amoeba Loopback, VoiceMeeter, BlackHole, JACK, PipeWire, Dante β€” prove demand for local routing. Most are platform-specific, local-first, manual, or pro-AV-oriented. - -PocketStation differentiation: cross-platform, remote by default, programmable, SDK/API-first, AI model nodes, graph observability, source identity, enterprise private relay path. - ---- - -## 23. Threat Analysis - -### T1 β€” Apple Policy Change (High Impact, Medium Probability) - -Apple restricts AUv3 sandbox, limits installTap, or changes AVAudioSession in a way that breaks a primary iOS SourceNode adapter. - -Mitigation: -- Android-first MVP ensures iOS is never the only source adapter path -- 5 iOS insertion points are independent adapters -- Architecture plugs in new adapters without changing the graph runtime - -### T2 β€” LiveKit Ships Audio-Only Graph SDK (Medium Probability) - -Mitigation: -- Ship the graph API before LiveKit does -- Rust core is a measurable performance moat (battery, CPU on mobile) -- Audio-only pricing structure is cleaner than video-first economics -- Self-hostable relay with readable source is a trust moat - -### T3 β€” OpenAI Ships End-to-End Audio Graph Infrastructure (Medium Probability) - -OpenAI's Realtime API handles WebRTC, voice sessions, translation, and STT. It does not yet solve per-app desktop capture, virtual drivers, creator routing, low-latency multi-device monitoring, source separation, cross-model routing, or private audio rooms. - -Mitigation: PocketStation is the routing layer between OpenAI and everything else. If OpenAI adds routing, PocketStation adds deeper OS integration and enterprise policy that OpenAI cannot own. - -### T4 β€” No Distribution (High Probability If Not Addressed) - -First public artifacts: -``` -1. Show HN: pocketstation-audio β€” zero-allocation Rust audio graph runtime -2. Blog: Why every voice AI app rebuilds the same audio capture code -3. Demo video: multi-source graph β†’ relay β†’ browser, measured per-edge latency -4. Benchmark: mobile capture latency vs. raw platform APIs -5. Direct outreach: RustAudio community, iOS music dev community, voice AI startups -``` - -### T5 β€” FFI Boundary Crashes in Production (Medium Probability) - -Mitigation: DOCS-001 defines the complete boundary contract before code. `proptest` for ring buffer invariants. DHAT in CI verifies zero allocation on hot path. Debug assertion: callback thread identity verified at session start. - -### T6 β€” Graph API Too Complex for External Developers (New, High Probability) - -Mitigation: Compatibility layer (`start_broadcast()` sugar, Β§3.8) keeps simple workflows simple. Graph templates give developers a starting point. Phase 4 exit criterion: external developer builds a graph in under 2 hours. - ---- - -## 24. Kill Criteria - -### Technical Kill / Pivot Criteria - -``` -1. P95 transport latency consistently > 500ms after optimization -2. Battery drain > 15% per hour for a broadcast session on modern iPhone -3. iOS App Store rejection for AVAudioSession misuse that cannot be resolved -4. Relay cannot maintain 99% session completion rate for 30-minute sessions -5. Graph runtime becomes too complex for external developers to understand in < 1 day -6. Cannot maintain stable source capture on at least 2 desktop OSes -7. Model routing adds too much latency to be useful in voice-agent contexts -8. Observability cannot accurately explain route or model failures for debugging -``` - -### Market Kill / Pivot Criteria - -``` -1. After 3 public demos and direct outreach, zero external developer integration requests -2. AI voice developers say source capture / routing / model switching is not painful -3. LiveKit or another platform adds equivalent source-aware graph routing before PocketStation gets traction -4. No design partner cares about graph observability -5. Developers only want a full agent SaaS, not graph infrastructure -6. No traction at 100 WAU after sustained public development -``` - -### Good Pivot Directions (If Needed) - -``` -Pivot A: Desktop/app audio capture SDK for voice AI only. -Pivot B: Realtime audio observability layer for voice agents. -Pivot C: Local creator routing + remote receiver product. -Pivot D: Private model audio gateway for enterprise voice AI. -``` - ---- - -## 25. Strategic Positioning - -### 25.1 The One Competitor That Matters - -LiveKit. Every decision answers: "Is this better than LiveKit for a developer who needs audio-only, graph-first, cross-platform infrastructure?" The answer should always be yes in at least three specific dimensions: smaller API surface, mobile capture depth, per-edge observability. - -### 25.2 The First Thing to Ship - -``` -audio-core (Rust crate, publish-ready at Phase 0 exit, published at Phase 1 exit) -+ desktop source adapters (macOS/Linux first) -+ audio-graph API (v0, typed nodes and edges) -+ Go relay as GraphSession -+ README with one working demo: multi-source graph β†’ relay β†’ browser, measured per-edge latency - -Ship this. Measure who uses it and why. -Everything else follows from what those users tell you. -``` - -### 25.3 The Permanent Architecture Principle - -The graph runtime commits to a node interface contract, not a fixed set of node types. Every new source, transform, policy, model, transport, or sink is a new adapter. The graph runtime never changes when a new adapter is added. - -### 25.4 The Question That Decides Every Feature - -> Does this feature strengthen PocketStation as the programmable realtime audio graph layer? - -If yes, build. If no, defer. - -### 25.5 The Phrase That Must Be True Everywhere - -> Any audio β†’ any graph β†’ any human, app, device, room, model, or agent. - -Not a tagline. An architectural commitment. If a proposed node type or edge contract breaks this sentence, the design is wrong. - ---- - -## 26. Open Engineering Questions - -Each open question blocks a phase exit. Each gets an ADR before code lands. - -### DOCS-004: Backpressure Policy β€” RESOLVED - -Drop newest. Β§4. - -### DOCS-005: Relay Forward-Loop Locking β€” Blocks Phase 2 - -Phase 1: `sync.RWMutex` per packet. Phase 2: `atomic.Pointer` on subscriber slice. See Β§8. - -### DOCS-006: Clock Sync β€” RESOLVED - -PI-controlled linear interpolation for voice; hook for variable-rate SRC in music/broadcast mode. - -### DOCS-007: Capability Negotiation β€” RESOLVED - -Auto-insert adapters with `negotiated: NegotiatedCapability` on the stream. - -### DOCS-008: Workspace Release Sequencing β€” Blocks Phase 1 First Publish - -See Β§14.4. `cargo-release --workspace` with sequenced publish and per-crate retry. - -### DOCS-009: Pion WriteRTP Allocation Profile β€” Blocks Phase 1 - -Does `TrackLocalStaticRTP.WriteRTP` mutate `pkt`? Need `pkt.Clone()` per bus subscriber? GC pressure at N subscribers Γ— 50 pkt/sec? Resolve before Phase 1 ships. - -### DOCS-010: JitterBuffer Algorithm β€” Blocks Phase 1 - -Adaptive (NetEQ-class) for Phase 1. Optional upgrade to RTT-variance-driven + PLC in Phase 4. - -### DOCS-011: SPSC Ring Buffer Crate β€” Blocks Phase 0 - -Default candidate: `rtrb`. Verify against criteria in Phase 0 prototype. - -### DOCS-012: Opus Frame Duration β€” RESOLVED - -20ms default, 10ms optional for voice-agent mode after Phase 1 latency benchmarks. - -### DOCS-013: Internal Sample Format β€” RESOLVED - -Interleaved f32, 48kHz, mode-dependent channel count. See Β§4.2. - -### ADR-014: Graph Manifest Format β€” Blocks Phase 1 - -``` -JSON first (Phase 0/1 readability) -Protobuf by Phase 2 (when SDKs multiply) -Hybrid: JSON for developer authoring, protobuf on the wire -``` - -**Recommended:** JSON manifest for Phase 0/1. Protobuf protocol by Phase 2 when protocol repo is created. - -### ADR-015: Node and Edge Stable IDs β€” Blocks Phase 1 - -Every node, source, bus, edge, and policy needs stable deterministic identity for telemetry and session replay. Decision needed: UUID v4 at runtime, or developer-assigned names, or both? - -### ADR-016: Clock Domains Across Graph β€” Blocks Phase 2 - -A graph spanning multiple devices and model providers has multiple clock domains. Each `AudioFrame` carries `clock_domain`. Decision needed: how are clock-domain crossings detected at graph compile time, and what adapter is inserted? - -### ADR-017: Bus vs Transport Track Boundary β€” Blocks Phase 2 - -A semantic bus (named, typed, policy-governed) becomes a transport track (RTP SSRC, WebRTC MediaStreamTrack) at the TransportNode boundary. Decision needed: exact mapping rules, and how bus_id is carried in RTCP metadata. - -### ADR-018: Model Node Privacy Contract β€” Blocks Phase 3 - -Which model nodes can send raw audio to external providers? Which require explicit developer opt-in? How does `PrivacyClass` on a SourceIdentity block a route to a cloud ModelNode? - -### ADR-019: Graph-Aware Relay Metadata β€” Blocks Phase 2 - -The relay should route by bus_id, enforce policies, and export per-bus metrics β€” without decoding audio payloads. Decision needed: what metadata travels in-band (RTP header extension? RTCP SDES? WebSocket control channel?) and what stays on the control plane. - -### ADR-020: Multi-Stem Recording Timeline β€” Blocks Phase 2 - -Independent stems can have gaps, clock drift, and different start times. Decision needed: container format (multi-track Opus in MKV? WAV stems in a zip? custom format?), gap representation, and how replay aligns stems for debugging. - -### ADR-021: Policy Execution Safety β€” Blocks Phase 2 - -Which policies run in the realtime processing path (GainNode, GateNode)? Which run on the control plane (ModelSwitch, StartRecording, TriggerWebhook)? Which require async side effects (LatencyFallback, CostCap)? A policy node must declare its execution class. - -### ADR-022: Model Fallback Contract β€” Blocks Phase 3 - -When a model node fails or exceeds latency budget: does the graph pause, reroute to fallback, emit silence, or drop frames? Decision needed per node type, per error class, with a declared fallback contract on `ModelNode::constraints()`. - ---- - -*Document version 3.0 β€” green-light version. AudioGraph is the product center. v2.3 core algorithm, hot-path rules, platform specs, and FFI boundary contracts (DOCS-001 through DOCS-013) are fully preserved. New open questions: ADR-014 through ADR-022. Next revision trigger: Phase 0 exit criteria met and first crates.io publish at Phase 1 exit.* - -*Kill criteria reviewed: 2026-06-26.* \ No newline at end of file diff --git a/docs/standards/FAKE_SCAFFOLD_INVENTORY.md b/docs/standards/FAKE_SCAFFOLD_INVENTORY.md deleted file mode 100644 index 3eafe7a..0000000 --- a/docs/standards/FAKE_SCAFFOLD_INVENTORY.md +++ /dev/null @@ -1,78 +0,0 @@ -# Fake / Scaffold Inventory - -This file lists every component in this repo that is currently mocked, stubbed, hardcoded, deferred, or otherwise not production-grade. - -**It is a living document.** Every PR that adds a scaffold appends a row. Every PR that replaces a scaffold burns the row down (delete the row in the same PR that replaces it). - -**Rule:** if a component is fake but not in this file, the PR that added it failed the production bar. Reviewers block PRs that introduce un-inventoried fakes. - ---- - -## Status column meaning - -``` -SCAFFOLD Empty placeholder, returns Default::default() or similar -MOCK Functional fake β€” tests pass against it, real impl absent -STUB Throws unimplemented! or returns hardcoded value -PARTIAL Real implementation, missing significant behavior -DEFERRED Intentionally postponed; ADR or phase plan justifies it -``` - ---- - -## Active inventory - -| Component | Status | File | What's missing | Replace by | Blocked on | -|-----------|--------|------|----------------|------------|------------| -| WebRTC transport | DEFERRED | N/A | Python SDK is WebSocket-only. No WebRTC / audio capture planned for this binding. | Not planned | Architecture decision: Python is listener/voice-agent tier only | - ---- - -## Phase 0 starter rows - -These are typical scaffolds expected at Phase 0 exit. Replace this section with the actual state when Phase 0 starts. - -| Component | Status | Repo / File | What's missing | Replace by | Blocked on | -|---|---|---|---|---|---| -| Opus encoder/decoder | MOCK | audio-core / pocketstation-codec | Real libopus bindings; current mock copies bytes | Phase 1 | PY-013 sample format finalized, libopus-sys dep approval | -| JitterBuffer | PARTIAL | audio-core / pocketstation-codec | NetEQ-class adaptive algorithm; current scaffold is fixed-delay | Phase 5 | PY-010 algorithm choice | -| ClockSync | PARTIAL | audio-core / pocketstation-bus | PI controller per PY-006; current scaffold is fixed-rate | Phase 1 | PY-006 resolution | -| DHAT allocation check | DEFERRED | audio-core / tools/pocketstation-alloccheck | Real DHAT integration; current is cargo-bloat placeholder | Phase 1 | DHAT setup in CI | - -## Phase 1 expected additions - -| Component | Status | Repo / File | What's missing | Replace by | Blocked on | -|---|---|---|---|---|---| -| Fake-source publisher | _to add_ | relay / cmd/fake-source | Real WebRTC publisher; needed for E2E smoke | Phase 1 exit | P1-PROD-003 | -| Token authority | _to add_ | api-server + relay | api-server JWTs accepted by relay (or relay owns issuance) | Phase 1 exit | P1-PROD-002 | -| Browser metrics | PARTIAL | app-web-receiver | Real RTCStats.getStats() values; current returns null | Phase 1 exit | P1-PROD-006 | -| TURN configuration | DEFERRED | relay | Production TURN credentials; STUN-only works on most networks | Phase 2 | TURN provider decision | -| SFrame E2EE | DEFERRED | relay + SDKs | Frame-layer encryption per RFC 9605 | Phase 3 | ADR for per-platform insertion point | - -## Permanent (intentional) scaffolds - -These never become production β€” they exist for testing and development. They are listed here so they're not confused with production-track components. - -| Component | Repo / File | Purpose | -|---|---|---| -| Sine wave source | audio-core / examples/sine_to_wav | Phase 0 smoke test, latency measurement | -| File output sink | audio-core / pocketstation-route | Test recording, offline verification | -| In-memory token store | api-server | Phase 1 only; Phase 2+ uses real persistence | - ---- - -## How to use this file in a PR - -When introducing a scaffold: -1. Add the row before the code lands. -2. Be specific about "what's missing" β€” "real implementation" is not enough. -3. Pick a "replace by" phase. If it's unknown, mark `DEFERRED` and link the ADR or issue tracking the decision. - -When replacing a scaffold: -1. Delete the row in the same PR that lands the real implementation. -2. The PR description references the row being removed. - -When reviewing: -1. Block any PR that introduces a fake component without adding to the table. -2. Block any PR that claims to "complete" a scaffold but doesn't burn down the row. -3. Block phase exit if the table has rows whose "replace by" matches the current phase. \ No newline at end of file diff --git a/docs/standards/PRODUCTION_ENGINEERING_BAR.md b/docs/standards/PRODUCTION_ENGINEERING_BAR.md deleted file mode 100644 index 8c50027..0000000 --- a/docs/standards/PRODUCTION_ENGINEERING_BAR.md +++ /dev/null @@ -1,236 +0,0 @@ -# Production Engineering Bar β€” PocketStation - -This document defines when a phase is *actually* done β€” not when its repos compile and unit tests pass, but when the product flow the phase promised works, is measured, and can survive failure. - -This file ships into every PocketStation repo at `docs/standards/PRODUCTION_ENGINEERING_BAR.md`. It is the third standards doc, alongside `STAFF_ENGINEERING_BAR.md` (code quality bar) and `STRUCTURE_NAMING_STYLE_THINKING.md` (structure and naming). - ---- - -## How This Bar Applies - -This bar applies **at phase exit**, not retroactively to in-progress work. - -Phase 0 produced scaffolds, types, and unit tests. That was correct for Phase 0. The bar below is what Phase 1, Phase 2, and every subsequent phase must clear *before being marked done*. - -Code already shipped under earlier phases is not failed retroactively. It is audited against this bar at the next phase-exit checkpoint, and any gaps become tasks in the next phase's hardening pass. - -The bar is not negotiable downward. It can be deferred (with an ADR) or split across phases (with explicit phase-exit criteria), but it cannot be silently lowered. - ---- - -## 1. Product-Flow Rule - -Every phase defines one real user-visible flow. The phase is not done until the flow works end-to-end against the actual code, not against mocks. - -Phase 1 flow: - -``` -room created via control plane -β†’ token issued -β†’ fake-source publisher (or real iOS source) connects via WebRTC -β†’ browser subscriber connects -β†’ RTP packets reach the browser -β†’ audio is audible / measurable in the listener -β†’ stats panel reports non-null values -β†’ disconnect + reconnect behavior is exercised and known -``` - -A repo passing tests in isolation does not prove the phase. The conductor (root-level integration runner) must execute the flow and produce a report. - -## 2. Test Pyramid Rule - -PocketStation is a protocol project. Most bugs live at integration boundaries (FFI, signaling, WebRTC negotiation, RTP semantics, jitter buffer behavior under real network), not inside individual functions. The pyramid is therefore weighted toward integration: - -``` -~50% small/unit tests allocation, ring buffer, pool, codec sanity, graph passthrough -~40% medium integration cross-service signaling, room lifecycle, publisher↔relay, - relay↔listener, FFI boundary contracts on real devices -~10% large E2E full source β†’ relay β†’ browser, 2-5 scenarios max -``` - -Standard pyramid guidance suggests ~70/20/10. PocketStation deliberately runs ~50/40/10 because the value lives in the integration tier. Don't overshoot E2E β€” 10% is a ceiling, not a target. - -## 3. CI Honesty Rule - -CI must not hide failures. - -**Forbidden in correctness checks:** - -```bash -go test -race ./... || true -pnpm test || true -cargo test || echo "tests failed" -``` - -unless the command is explicitly non-blocking (e.g. a lint check during early development that hasn't been triaged) and the override is documented in the CI file with a `# DELIBERATE NON-BLOCKING:` comment and an issue link. - -**Required:** - -``` -Rust: cargo fmt --check, cargo clippy -D warnings, cargo test, examples run, - alloccheck/criterion benchmarks where relevant -Go: gofmt, go vet, go test ./..., go test -race ./... -Web: correct package manager, typecheck, build, Playwright smoke -Mobile: simulator/emulator compile and test once SDK phases start -``` - -Status checks that lie are worse than status checks that don't exist β€” they produce false confidence. - -## 4. Integration Contract Rule - -Any cross-repo contract must be tested by a test that actually executes the contract. - -Examples for Phase 1: - -- api-server token signature must be accepted by relay (or relay must explicitly own room creation in Phase 1, documented in the relay's README) -- relay signaling message JSON must round-trip through the web receiver's TypeScript types -- fake-source publisher must complete the PUBLISH WebRTC flow against the real relay binary -- browser subscriber must complete the SUBSCRIBE WebRTC flow against the real relay binary - -Contracts that aren't tested are documentation, not contracts. - -## 5. Performance Rule - -Phase 1 must measure three hot paths, no more, no less. The list is fixed so this gate cannot expand into its own multi-week project: - -``` -Phase 1 performance gate: - 1. AudioBufferPool acquire / release / drop (Criterion bench in audio-core) - 2. JWT verify rate (Go bench in relay/auth) - 3. Pion TrackLocalStaticRTP.WriteRTP allocation profile (per PY-009) -``` - -Phase 2 hardening expands the gate to include: - -``` - FrameBus push/pop rate - Opus encode/decode per 20ms frame - Relay RTP fanout at 1 / 10 / 50 / 200 listeners - Room create/join/delete throughput - WebSocket signaling latency -``` - -The full benchmark suite isn't a Phase 1 blocker. Three measurements are. Add the rest in Phase 2 hardening when the relay grows up. - -Do not claim "low latency" anywhere in docs without numbers from a Criterion or Go benchmark in this repo. - -## 6. Load and Soak Rule - -Every phase from Phase 1 onward includes at least one local soak test. - -``` -Phase 1 minimum soak: - - 1 fake-source publisher - - 1 in-process or browser subscriber - - 5-minute run - - go test -race active - - no goroutine leak (count before/after) - - no unbounded memory growth (RSS sampled at start / 1min / 5min) - - no race-detector failures - -Phase 2 target soak: - - 1 publisher - - 50 listeners - - 30-minute run - - packets-forwarded, packets-dropped, listener errors all reported - - p50/p95/p99 forward latency captured -``` - -Soak isn't load testing. Load testing is "what's the breaking point." Soak is "does it leak when nothing exciting happens." Both matter; Phase 1 focuses on soak. - -## 7. Failure-Mode Rule - -Every real flow must include tests for predictable failure paths. - -Phase 1 failure-mode tests: - -``` -bad token β†’ request rejected with structured error -expired token β†’ request rejected with structured error -publisher disconnects β†’ relay cleans source, notifies listeners -listener disconnects β†’ relay cleans listener slot, source unaffected -ICE failure β†’ both sides report a clean error (no silent hang) -room deleted while listeners present β†’ graceful close -relay process receives SIGTERM β†’ graceful drain, no deadlock -``` - -If a failure mode silently degrades to a hang, the test must catch it before merge. - -## 8. Observability Rule - -Every service exposes structured data sufficient to debug a live session without attaching a debugger. - -Minimum fields per log/metric: - -``` -room_id -session_id -role (source | listener) -connection_state -error_code (enum, not free-text) -packets_forwarded -packets_dropped -listener_count -latency_estimate_ms (where available) -``` - -This maps to the four golden signals (latency, traffic, errors, saturation). Each Phase 1 service should be able to answer "is the system healthy right now" from these counters alone. - -## 9. Fake / Scaffold Inventory Rule - -Every active repo maintains a top-level `FAKE_SCAFFOLD_INVENTORY.md` (template ships with this standards bundle). - -The inventory lists every mock, stub, scaffold, or deferred component. Every PR that introduces a fake adds a row. Every PR that replaces a fake burns the row down (deletes it in the same PR that lands the real implementation). - -A repo whose `FAKE_SCAFFOLD_INVENTORY.md` is missing or out of date fails the production bar at phase exit. - -## 10. Phase Exit Rule - -A phase cannot be marked PASS if any of these is true: - -``` -The main user flow does not work end-to-end against real code. -CI can pass while correctness checks fail. -Cross-repo contracts are incompatible or untested. -Progress files claim completion of items that don't work. -Docs claim behavior the code doesn't support (production-ready, - low-latency, E2EE, etc.) without measurement. -The fake/scaffold inventory has rows whose "replace by" matches - this phase and they're not burned down. -The three Phase 1 hot paths (or this phase's equivalent) are - unmeasured. -A 5-minute soak has not run with race detection clean. -``` - -Phase exit requires: - -1. Integration conductor report: PASS. -2. Production audit report (re-run of the Cursor reviewer with the production bar in scope): PASS or CONDITIONAL PASS with documented follow-up. -3. `FAKE_SCAFFOLD_INVENTORY.md` reviewed; no rows assigned to this phase remain. -4. Performance and soak artifacts checked into the repo (`benches/`, `soak/results/`, etc.). - -If any of these fails, the phase is not done. Continue work in the same phase. Do not start the next phase. - ---- - -## Self-Check Before Phase Exit - -Append this block to `PHASE_PROGRESS.md` at the end of the phase: - -```md -### Production Bar Phase Exit Self-Check - -- Product flow runs end-to-end against real code: yes / no -- Pyramid coverage (small / medium / large): _% / _% / _% -- CI honest (no `|| true` on correctness): yes / no -- Cross-repo contracts tested: list contract β†’ test mapping -- Hot paths measured (this phase's required list): yes / partial / no -- Soak test run, race-clean, no leaks: yes / no -- Failure modes tested: list which ones -- Observability counters live: yes / partial / no -- FAKE_SCAFFOLD_INVENTORY.md up to date: yes / no -- Rows in inventory blocking this phase exit: list or "none" -- Remaining risk: -``` - -If any answer is unclear or "no" without an ADR or follow-up ticket, the phase is not done. Stop and produce the missing artifact. \ No newline at end of file diff --git a/docs/standards/STAFF_ENGINEERING_BAR.md b/docs/standards/STAFF_ENGINEERING_BAR.md deleted file mode 100644 index df40209..0000000 --- a/docs/standards/STAFF_ENGINEERING_BAR.md +++ /dev/null @@ -1,214 +0,0 @@ -# Staff Engineering Bar β€” PocketStation - -This document defines the minimum engineering bar for all PocketStation code. - -The goal is not clever code. The goal is boring, correct, maintainable, observable systems code that survives real production use. - -This file ships into every PocketStation repo at `docs/standards/STAFF_ENGINEERING_BAR.md`. Agents must read it before any non-trivial code change. - ---- - -## 1. General Principles - -Every change must optimize, in this order: - -1. Correctness -2. Simplicity -3. Maintainability -4. Testability -5. Observability -6. Performance β€” only where required -7. Explicit tradeoffs - -Rules: - -- Do not write clever code when clear code is enough. -- Do not introduce abstractions before at least two real use cases or an ADR. -- Do not change public APIs casually. -- Do not hide uncertainty. If a decision is provisional, mark it clearly. - -## 2. Staff-Level Code Requirements - -A PR or branch is not acceptable unless it answers: - -- What problem does this solve? -- Why is this the smallest correct design? -- What invariants does this code rely on? -- How is it tested? -- What can go wrong? -- What is intentionally not solved? -- What future phase does this unblock? - -These answers live in `PHASE_PROGRESS.md` for the current phase, not in commit messages or PR descriptions alone. - -## 3. Rust Real-Time Audio Rules - -Hot-path code (anything that runs on or near an audio callback) must not: - -- allocate after initialization -- lock -- block -- log -- panic in release builds -- call `async`/`await` -- call FFI per frame -- perform ML inference -- depend on unbounded queues - -All callback-path code must be: - -- bounded -- predictable -- measurable via metrics -- explicit about ownership and lifetimes - -## 4. Unsafe Code - -`unsafe` is permitted at FFI boundaries (cbindgen-generated bridges, JNI handoffs, raw pointers from platform callbacks like `ByteBuffer.allocateDirect()` or `AVAudioPCMBuffer`) when there is no safe alternative. - -Every `unsafe` block must have a `SAFETY:` comment that documents: - -- the invariants the caller must uphold -- why the operation is sound under those invariants -- how the surrounding code maintains the invariants - -Outside FFI, prefer safe Rust. New `unsafe` outside FFI requires an ADR. - -## 5. API Design Rules - -Rust APIs should follow the Rust API Guidelines where practical: - -- meaningful types, not naked booleans -- explicit ownership and lifetimes -- no surprising side effects -- clear error types, not stringly-typed errors -- prefer small focused traits -- document invariants in rustdoc -- make invalid states hard to represent - -Public APIs are a long-term contract. Before exposing something `pub`, ask: does another crate need this now, or could it stay private until the consumer materializes? - -## 6. Testing Bar - -Every implementation task must include at least one of: - -- unit tests -- integration tests -- invariant / property tests -- regression tests for a fixed bug -- benchmark or smoke test -- explicit explanation in the progress file why testing is not practical - -Minimum checks for `audio-core` (Phase 0): - -``` -cargo fmt --all -- --check -cargo clippy --workspace --all-targets -- -D warnings -cargo test --workspace -cargo run -p pocketstation-audio --example sine_to_wav -``` - -Minimum checks for Go services (Phase 1+): - -``` -go fmt ./... -go vet ./... -go test ./... -go test -race ./... -``` - -## 7. Review Bar - -Reviewers must block changes for: - -- architecture drift not covered by an ADR -- missing tests -- unclear ownership -- unbounded queues -- hidden allocation on hot paths -- panic paths in release builds -- premature or unjustified abstractions -- dependency creep -- public API drift -- fake completion claims in progress files - -A change that merely compiles and passes basic tests is not enough. The review asks: would a senior engineer joining the project next month understand this code, trust its invariants, and be able to change it safely? - -## 8. Commit Bar - -Each commit completes one logical step. - -Format: - -``` -type(scope): short imperative summary -``` - -Examples: - -``` -fix(frame): prevent double-release corruption -test(bus): add drop-newest invariant coverage -docs(standards): add structure naming style rules -feat(metrics): add atomic frame counters -``` - -Avoid: - -``` -update -fix -work -stuff -changes -WIP -``` - -WIP commits are fine on a working branch but must be squashed before merge. - -## 9. Architecture Document Rules - -The architecture document at `docs/architecture/pocketstation-v3.0.md` (and any successor version) is treated as the source of truth for the project's shape. - -Agents must not edit the architecture document without an ADR documenting why the change is needed. - -PY-008 through PY-013 in v3.0 are open questions; resolving them may legitimately require architecture-doc changes. When that happens: - -1. Write the ADR first. -2. Get human approval on the ADR. -3. Then edit the architecture doc to reflect the resolved decision. -4. Bump the architecture doc version (bump version in pocketstation-v3.0.md). - -Not: edit the doc speculatively, then write an ADR to justify it. - -## 10. Forbidden Agent Behavior - -Agents must not: - -- edit `docs/architecture/PocketStation-v2.X.md` without an ADR -- add dependencies without explicit human approval -- create new phases -- implement future-phase features early -- weaken or skip tests to make CI pass -- hide failing commands or suppress error output -- claim production readiness without evidence (benchmarks, tests, deployment history) -- create empty placeholder repos to claim "Phase N started" - -## 11. Self-Check Before Every Commit - -Before every commit, the agent writes this in the relevant `PHASE_PROGRESS.md`: - -```md -### Staff Bar Self-Check β€” - -- Smallest correct design: yes / no / explain -- Tests added or updated: yes / no / explain -- Hot-path safe: yes / no / not applicable -- Public API changed: yes / no -- New dependency: yes / no -- Phase scope respected: yes / no -- Unsafe added: yes / no β€” if yes, SAFETY comment present -- Remaining risk: -``` - -If any answer is unclear or "no" without justification, do not commit. Stop and ask for human input. \ No newline at end of file diff --git a/docs/standards/STRUCTURE_NAMING_STYLE_THINKING.md b/docs/standards/STRUCTURE_NAMING_STYLE_THINKING.md deleted file mode 100644 index 630d380..0000000 --- a/docs/standards/STRUCTURE_NAMING_STYLE_THINKING.md +++ /dev/null @@ -1,537 +0,0 @@ -# Structure, Naming, Style, and Thinking β€” PocketStation - -This document defines how PocketStation code, folders, names, tests, docs, comments, and implementation decisions must be structured. - -The goal: every repo looks like it was written by one senior engineering team. - -This file ships into every PocketStation repo at `docs/standards/STRUCTURE_NAMING_STYLE_THINKING.md`. Agents must read it before any non-trivial code change. - ---- - -## 1. Core Principle - -PocketStation code must be: - -- boring -- explicit -- searchable -- reviewable -- testable -- phase-scoped -- architecture-aligned -- easy for a new engineer to understand in their first week - -Do not optimize for cleverness, brevity, or agent-output speed. Optimize for long-term maintainability. - -## 2. Folder Structure - -Folders are organized by responsibility, not by random implementation detail. - -Good: - -``` -crates/pocketstation-frame/ -crates/pocketstation-bus/ -docs/adr/ -docs/standards/ -docs/architecture/ -``` - -Avoid these folder names β€” they're dumping grounds that absorb everything and reveal nothing: - -``` -utils/ -helpers/ -common/ -misc/ -stuff/ -shared/ (acceptable only when scope is genuinely shared and named in README) -core/ (acceptable only as a top-level concept, not as a misc bucket) -``` - -If a folder is hard to name, the design is probably unclear. Fix the design. - -## 3. Repo-Level Structure - -Every repo must have: - -``` -AGENTS.md -README.md -docs/REPO_CONTRACT.md -docs/architecture/ -docs/adr/ -docs/standards/ -.github/workflows/ -.github/PULL_REQUEST_TEMPLATE.md -.github/CODEOWNERS -``` - -Every phase-active repo should have: - -``` -PHASE_QUEUE.md -PHASE_PROGRESS.md -``` - -Progress files must say: - -- what is done and tested -- what is partial (and what would finish it) -- what is fake/mock/scaffold (and what would replace it) -- what is blocked (and on what) -- what needs human decision - -## 4. Rust Crate Structure - -Rust crates follow: - -``` -crates// - Cargo.toml - src/ - lib.rs - tests/ - benches/ (if applicable) -``` - -Submodules exist only when responsibilities are truly separate. - -Good module names: - -``` -buffer_pool.rs -audio_frame.rs -clock_sync.rs -jitter_buffer.rs -processor_graph.rs -``` - -Avoid these as module names β€” they describe role-in-the-abstract, not what the code does: - -``` -manager.rs -handler.rs -processor.rs (acceptable when paired with a domain, e.g. vad_processor.rs) -logic.rs -helpers.rs -``` - -If a module name ends in `manager` or `handler`, explain in a comment at the top why a more specific name was not possible. - -## 5. Rust Naming - -- Types, traits, enums: `UpperCamelCase` -- Functions, methods, modules, variables: `snake_case` -- Constants: `SCREAMING_SNAKE_CASE` -- Crates: `kebab-case` -- Features: `kebab-case` - -Acceptable abbreviations (these are domain-standard): - -``` -pcm, rtp, sdp, ffi, jni, api, vad, aec, src, dsp, opus, sfu, ice, dtls, srtp -``` - -Avoid invented abbreviations: - -``` -buf_mgr, proc_hdl, aud_st, tmp_frm, snd_eng -``` - -Readable beats short. - -## 6. Type Names β€” Pattern, Not Substring - -These type-name patterns indicate the design hasn't found its concrete responsibility yet. Block them unless the issue explicitly approves: - -- `Manager` (as the entire suffix: `AudioManager`, `RoomManager`) β€” what does it manage? -- `Handler` (as suffix without domain: `EventHandler` is fine, bare `Handler` is not) -- `System` (as suffix without domain: `RouteSystem` ok if `Route` is the domain; `AudioSystem` is too vague) -- `Magic`, `Stuff`, `Thing`, `Misc` β€” anywhere -- `Util`, `Utils`, `Helper`, `Helpers` β€” as type names or module names -- `UniversalX`, `GlobalX`, `SuperX` β€” as type names in concrete code (these may appear in vision docs) - -Note: this is a **type-name pattern** rule, not a substring blacklist. `super::` in Rust, the word "global" in metric labels, "final" as a Java keyword, etc., are fine. The rule is about names you give to types you're defining. - -Good abstractions (concrete responsibility, clear ownership): - -``` -AudioBufferPool -FrameBus -RoutePlan -SourceCapability -OutputTarget -AudioProcessorNode -``` - -## 7. Go Naming and Structure - -Go packages are small, lowercase, single-word, responsibility-based. - -Good: - -``` -room -signal -auth -relay -rtp -``` - -Avoid: - -``` -roomManager (camelCase, wrong for Go) -room_manager (snake_case, wrong for Go) -utils, common, misc -``` - -Every goroutine has a documented teardown path. -Every room/session lifecycle is explicit. -No long-running global state without a clear owner. - -## 8. Swift Naming and Structure - -Swift APIs optimize for clarity at the point of use. - -Good: - -```swift -try station.startMicrophoneStream() -try station.stopCurrentRoom() -try station.connect(to: roomToken) -``` - -Avoid: - -```swift -try station.run() -try station.doIt() -try station.process(data) -``` - -Allowed iOS source type names: - -``` -MicrophoneSource -OwnAppAudioSource -AVAudioEngineTapSource -PluginHostSource -BroadcastExtensionSource -``` - -Avoid (until they're real, working capabilities β€” see v3.0 Β§5.1): - -``` -SystemAudioSource -GlobalAudioSource -UniversalCaptureSource -``` - -## 9. TypeScript Naming and Structure - -Good names: - -``` -RoomClient -RelayConnection -SignalingMessage -ConnectionState -LatencyStats -``` - -Static web app file structure (no framework, see v3.0 Β§14.3 app-web-receiver): - -``` -src/ - main.ts - signaling.ts - webrtc.ts - ui.ts - metrics.ts -``` - -Avoid dumping grounds: - -``` -components/common/ -lib/utils/ -helpers/ -``` - -## 10. Documentation Writing Style - -Docs are direct, factual, and phase-aware. - -Good: - -> This is a Phase 1 relay scaffold. It supports room creation and signaling but does not yet implement production reconnect behavior. - -Avoid: - -> This relay is production-ready and scalable. - -Never claim, in any doc, without evidence in the repo: - -- production-ready -- secure -- low-latency -- zero-allocation -- end-to-end encrypted -- cross-platform -- universal capture - -If the repo doesn't contain tests, benchmarks, or deployment history that prove the claim, don't make the claim. - -## 11. Comment Style - -Comments explain *why*, not *what*. - -Good: - -```rust -// Drop newest instead of oldest because audio callback freshness -// matters more than completeness β€” see PY-004. -``` - -Avoid: - -```rust -// Increment i by 1. -i += 1; -``` - -Every `unsafe` block has a `SAFETY:` comment (see `STAFF_ENGINEERING_BAR.md` Β§4). - -Every `TODO` references a phase and an ADR or issue: - -```rust -// TODO(Phase 1, PY-009): measure WriteRTP allocation behavior -// before production relay. -``` - -Avoid: - -```rust -// TODO fix later -// HACK -// XXX -``` - -## 12. Error Style - -Errors are explicit and domain-specific. - -Good: - -```rust -pub enum BufferPoolError { - Exhausted, - InvalidSlot, -} -``` - -Avoid in hot-path primitives: - -```rust -Err("failed") -Err(anyhow!("oops")) -``` - -`anyhow` and similar dynamic-error crates are fine in application code (CLI, tests, examples) but not in the public API of core crates. - -## 13. Test Naming - -Good: - -```rust -#[test] -fn acquire_returns_none_when_pool_is_exhausted() {} - -#[test] -fn dropping_handle_releases_slot() {} - -#[test] -fn drop_newest_policy_preserves_existing_frames() {} -``` - -Avoid: - -```rust -#[test] -fn test1() {} - -#[test] -fn works() {} - -#[test] -fn pool_test() {} -``` - -Test names are sentences that describe the contract. - -## 14. Test Structure - -Every test follows Given / When / Then: - -```rust -#[test] -fn acquire_returns_none_when_pool_is_exhausted() { - // Given - let pool = AudioBufferPool::new_for_test(); - - // When - let handles: Vec<_> = (0..64).map(|_| pool.acquire().unwrap()).collect(); - let extra = pool.acquire(); - - // Then - assert!(extra.is_none()); - drop(handles); -} -``` - -For property tests (`proptest`, `quickcheck`), the property statement *is* the test name: - -```rust -fn ring_buffer_is_fifo_under_arbitrary_push_pop_sequences(...) {} -``` - -## 15. Design Thinking - -Before implementing non-trivial code, the agent thinks in this order: - -1. What invariant must hold? -2. What is the smallest correct design? -3. What is the ownership model? -4. What can fail? -5. What must not happen on the hot path? -6. How will this be tested? -7. What phase does this belong to? -8. What is intentionally not implemented? - -The wrong starting question: "What code can I generate fastest?" -The right starting question: "What invariant must never break?" - -## 16. Abstraction Rules - -Do not introduce an abstraction unless at least one of these is true: - -- two real implementations exist -- an ADR requires it -- a phase boundary needs it -- it removes real duplication (not speculative duplication) -- it protects a dangerous invariant - -A trait with one implementation is usually premature. Wait for the second concrete need. - -## 17. File Size - -Soft guideline: when a file passes ~500 lines, consider splitting by responsibility. - -Test files have no upper limit β€” long test files with clear `mod` organization are fine. - -Splitting a file just to satisfy a line count is anti-pattern. Splitting because two responsibilities have grown distinct is correct. - -## 18. Public API - -Before adding `pub`, ask: - -- Does another crate or external consumer need this now? -- Is this part of the documented public contract? -- Can this remain `pub(crate)` or private until the consumer materializes? - -Default to the most restrictive visibility that works. - -## 19. Dependency Rules - -No new dependency without answering: - -- Why is this needed? -- Why can `std`/`core` not solve it? -- Is it safe for the hot path (no hidden allocation, locks, panics)? -- Does it affect build time materially? -- Does it affect mobile binary size? -- Is it actively maintained? -- Can it be behind a feature flag (optional)? - -Dependency changes require explicit human approval. Document the approval in the relevant ADR or progress file. - -## 20. Phase Scope - -Every file change belongs to the current phase. - -Phase 0 (`audio-core`) may touch: - -``` -audio-core code -docs/standards -docs/adr -tests -benches -ffi placeholders -``` - -Phase 0 must not implement: - -``` -relay -iOS app -Android app -protocol repo -SFrame E2EE -OS drivers -ML processing nodes -billing -public channels -social discovery -``` - -Same pattern for every phase: stay in scope, defer everything else. - -## 21. Commit Format - -``` -type(scope): short imperative summary -``` - -Types: - -``` -feat: new functionality -fix: bug fix -test: adding or updating tests -docs: documentation only -refactor: no behavior change -perf: performance change -chore: build/tooling/dependency -``` - -Examples: - -``` -fix(frame): prevent double-release corruption -test(bus): add drop-newest invariant coverage -docs(standards): add structure naming style rules -feat(metrics): add atomic frame counters -refactor(graph): rename ProcessorGraph::process to step -``` - -## 22. Self-Check Before Commit - -Before every commit, the agent writes this in the relevant `PHASE_PROGRESS.md`: - -```md -### Staff Bar Self-Check β€” - -- Smallest correct design: yes / no / explain -- Tests added or updated: yes / no / explain -- Hot-path safe: yes / no / not applicable -- Public API changed: yes / no -- New dependency: yes / no -- Phase scope respected: yes / no -- Unsafe added: yes / no β€” if yes, SAFETY comment present -- Remaining risk: -``` - -See `STAFF_ENGINEERING_BAR.md` Β§11 for the same checklist (it lives in both documents intentionally β€” this is the one that gets read most often). \ No newline at end of file diff --git a/pocketstation/__init__.py b/pocketstation/__init__.py index 362c253..bc204f1 100644 --- a/pocketstation/__init__.py +++ b/pocketstation/__init__.py @@ -1,4 +1,4 @@ -"""PocketStation Python SDK β€” spec Β§12.1.""" +"""PocketStation Python SDK.""" from .station import PocketStation from .types import AudioFrame, AudioMode, IceServer, PocketStationError, RoomCredentials diff --git a/pocketstation/station.py b/pocketstation/station.py index da4f7ca..8a10e1d 100644 --- a/pocketstation/station.py +++ b/pocketstation/station.py @@ -1,4 +1,4 @@ -"""PocketStation session API β€” spec Β§12.1. +"""PocketStation session API. Wire format contract: - broadcast() sends raw binary PCM bytes over the WebSocket, never base64 JSON. @@ -6,8 +6,7 @@ - Errors from the WebSocket propagate to the caller; they are never swallowed. - disconnect() sends a LEAVE message before closing the WebSocket. -Phase scope: Phase 5 β€” WebSocket listener / voice-agent mode. -WebRTC transport is intentionally out of scope for this binding (see FAKE_SCAFFOLD_INVENTORY). +This preview uses a WebSocket transport and does not implement WebRTC. """ from __future__ import annotations @@ -30,7 +29,7 @@ class PocketStation: - """Voice agent / broadcast session (spec Β§12.1). + """Receive and send PCM through one PocketStation session. Lifecycle:: diff --git a/pocketstation/types.py b/pocketstation/types.py index 7ff53d3..18d440f 100644 --- a/pocketstation/types.py +++ b/pocketstation/types.py @@ -1,4 +1,4 @@ -"""PocketStation SDK type definitions. Phase 5.""" +"""Public values returned by the PocketStation Python SDK.""" from __future__ import annotations import dataclasses import enum @@ -8,7 +8,7 @@ class AudioMode(enum.Enum): - """Audio session mode (spec Β§12.1).""" + """Audio session mode.""" VOICE = "voice" VOICE_AGENT = "voice_agent" MUSIC = "music" @@ -19,7 +19,7 @@ class AudioMode(enum.Enum): class AudioFrame: """A single audio frame received from the relay. - pcm: raw PCM bytes, 48 kHz mono f32-LE (PY-013). + pcm: raw PCM bytes, 48 kHz mono f32-LE. sequence: monotonically increasing frame counter per stream. timestamp_ns: monotonic nanosecond timestamp at frame receipt. """ @@ -39,7 +39,7 @@ def samples(self) -> list[float]: @dataclass class IceServer: - """ICE server configuration (PY-023 embedded TURN).""" + """ICE server configuration returned by the control plane.""" urls: list[str] username: Optional[str] = None credential: Optional[str] = None diff --git a/tests/test_station.py b/tests/test_station.py index 222a32c..2d3209c 100644 --- a/tests/test_station.py +++ b/tests/test_station.py @@ -1,4 +1,4 @@ -"""Unit tests for pocketstation.station β€” spec Β§12.1 voice agent pattern.""" +"""Tests for the async PocketStation session client.""" from __future__ import annotations import json diff --git a/tests/test_types.py b/tests/test_types.py index 5613418..d7f16a1 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1,4 +1,4 @@ -"""Unit tests for pocketstation.types. Phase 5.""" +"""Tests for the public PocketStation values.""" import pytest from pocketstation.types import IceServer, PocketStationError, RoomCredentials