Skip to content

feat(playrecord): concurrent-capture mode + stopplay (content-gated barge-in) - #168

Merged
tinpotnick merged 2 commits into
mainfrom
feat/concurrent-capture-stopplay
Jul 23, 2026
Merged

feat(playrecord): concurrent-capture mode + stopplay (content-gated barge-in)#168
tinpotnick merged 2 commits into
mainfrom
feat/concurrent-capture-stopplay

Conversation

@tinpotnick

Copy link
Copy Markdown
Collaborator

Summary

Adds a content-gated barge-in primitive to playrecord — a "middle mode" between full-prompt playback and energy interrupt. Instead of inbound energy cutting the prompt on any sound, the caller's answer is captured while the prompt keeps playing, and the controller decides when to interrupt.

Motivated by the multilingual language gate (babble-sip): with N languages a caller must currently sit through the whole greeting before answering, and the existing energy interrupt is too trigger-happy (a cough cuts the prompt, then a failed turn replays everything).

What's new

playrecord({ concurrent: true }) — the recorder opens immediately and runs alongside the player, staying gated by record.startabovepower until the caller actually speaks. Its min/max durations are measured from speech-start (unchanged), so opening early is safe and the ~2s power warm-up conveniently elapses during the prompt. No BargeInState is created — inbound energy never interrupts the prompt. Mutually exclusive with interrupt (concurrent wins).

channel.stopplay() / Command::StopPlay — stop the active player now (play/end reason="stopped"), leaving any running recorder untouched. This is how the controller "fires" the interrupt once it has an acceptable answer.

The recorder's own power thresholds still drive capture in concurrent mode:

  • startabovepower → when capture begins (waits for speech)
  • finishbelowpower → when the answer is "full" → finished event
  • min/maxduration → floor/ceiling on the answer

Only the prompt-interrupt decision is handed to the controller.

Re-arm without replaying the prompt

Key point for the gate's retry path: when an attempt fails and the prompt is still playing, re-listen with a bare record() (which never touches the player) — not a second playrecord (which would rebuild the Player and restart the prompt). Covered by a dedicated interface test.

Scope

Wired through both local and mixed (mixer) command handlers, the napi facade, and the JS control layer (server.js proxy + node.js channelmap/scrub). The existing hard-interrupt path is unchanged.

Testing

  • Rust: 118 lib tests (new: concurrent opens the recorder at command-accept time — the inverse of the deferred path). cargo fmt + clippy -D warnings clean.
  • Interface (real RTP): 3 new cases — a loud tone during a concurrent prompt does not interrupt; stopplay() ends the prompt (reason "stopped") with the recording preserved; a mid-prompt record() re-arms a fresh capture without restarting the player (exactly one play/start, natural play/end "completed").

Release

  • README documents the two barge-in modes.
  • Version 3.3.0 → 3.4.0 (additive, backward-compatible).

🤖 Generated with Claude Code

Nick Knight and others added 2 commits July 23, 2026 11:05
…arge-in)

Adds the engine primitive for a "middle mode" between full-prompt playback
and energy barge-in: capture the caller's answer *while the prompt keeps
playing*, then let the controller interrupt explicitly once it has an
acceptable answer — instead of energy cutting the prompt on any sound.

Two decoupled pieces:

- playrecord `{ concurrent: true }`: the recorder is opened immediately and
  runs alongside the player, staying gated by startabovepower until the
  caller actually speaks (min/max durations are measured from speech-start,
  so opening early is safe and the power warm-up elapses during the prompt).
  No BargeInState is created, so inbound energy never interrupts the prompt.
  Mutually exclusive with interrupt (concurrent wins).

- `channel.stopplay()` / Command::StopPlay: stop the active player now,
  emitting play/end reason="stopped", leaving any running recorder untouched
  (a still-deferred pending recorder is activated for parity with play-end).

Wired through both local and mixed (mixer) command handlers, the napi facade,
and the JS control layer (server.js proxy + node.js channelmap/scrub).
Existing hard-interrupt behaviour is unchanged.

Tests: new interface test proves a loud tone during a concurrent prompt does
NOT interrupt and that stopplay() ends it (reason "stopped") with the
recording preserved. 117 lib tests + playrecord interface suite green;
clippy -D warnings and fmt clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mp 3.4.0

Completes the concurrent-capture feature for release:

- Rust unit test: concurrent mode opens the recorder at command-accept time
  (Record::Recording precedes Play::End) — the inverse of the deferred path.
- Handle::stop_play() helper for symmetry with the other command helpers.
- Interface test: a bare record() issued mid-prompt during a concurrent
  playrecord re-arms a fresh capture WITHOUT restarting or replacing the
  player — exactly one play/start and a natural play/end reason "completed".
  This is the "attempt 1 failed, listen again without replaying the greeting"
  path the gate relies on: re-arm with record(), never a second playrecord.
- README: document the two barge-in modes (energy interrupt vs concurrent).
- Version 3.3.0 -> 3.4.0 (additive, backward-compatible feature).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tinpotnick
tinpotnick merged commit fe7f009 into main Jul 23, 2026
3 checks passed
@tinpotnick
tinpotnick deleted the feat/concurrent-capture-stopplay branch July 23, 2026 10:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant