feat(playrecord): concurrent-capture mode + stopplay (content-gated barge-in) - #168
Merged
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a content-gated barge-in primitive to
playrecord— a "middle mode" between full-prompt playback and energyinterrupt. 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
interruptis 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 byrecord.startabovepoweruntil the caller actually speaks. Itsmin/maxdurations are measured from speech-start (unchanged), so opening early is safe and the ~2s power warm-up conveniently elapses during the prompt. NoBargeInStateis created — inbound energy never interrupts the prompt. Mutually exclusive withinterrupt(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" →finishedeventmin/maxduration → floor/ceiling on the answerOnly 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 secondplayrecord(which would rebuild thePlayerand 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.jsproxy +node.jschannelmap/scrub). The existing hard-interruptpath is unchanged.Testing
cargo fmt+clippy -D warningsclean.stopplay()ends the prompt (reason "stopped") with the recording preserved; a mid-promptrecord()re-arms a fresh capture without restarting the player (exactly oneplay/start, naturalplay/end "completed").Release
🤖 Generated with Claude Code