Skip to content

Cascaded path: barge-in is unreachable — Interruption never delivered to process_frame, VAD gate never armed #60

Description

@rolandknight

Found while evaluating FlowCat (pinned at 37b09ba) for a self-hosted full-duplex voice assistant, running the cascaded pipeline live end-to-end: str0m WebRTC transport (aiortc client), whisper.cpp STT, OpenRouter LLM with tool calls, Kokoro TTS. Three related findings:

1. Frame::Interruption never reaches process_frame

run_processor (processor/runtime.rs) intercepts Frame::Interruption: it drains the interruptible queue and forwards the frame, but never calls process_frame with it. As a result the Frame::Interruption arms in e.g. CascadedTransportOutput/TransportOutput (send_clear, playout-estimate reset) are unreachable dead code. Frame-level barge-in therefore cannot work on the cascaded path today (the Gemini Live path is unaffected because the model handles barge-in service-side, which is presumably why this hasn't surfaced).

2. Nothing arms the VAD barge-in gate on the cascaded path

VadProcessor gates its barge-in broadcast on bot_speaking, learned from Frame::BotStartedSpeaking/BotStoppedSpeaking — but nothing in the cascaded chain emits those frames, so even with a VAD inserted, Interruption is never broadcast.

3. Even when delivered, the frame path is too slow for audible stop

With 1+2 fixed, we measured detection→sink delivery of the Interruption frame at 14 ms to 2.1 s, depending on whether an intermediate hop was mid-await (TTS synthesis, LLM stream). The biased/priority channel only helps while a task is inside its select; a busy process_frame cannot be preempted. An interrupted reply also cannot be cancelled: LlmProcessor awaits the whole stream, and AssistantContextAggregator will assemble and speak the full reply after the barge-in.

What we did about it

We have a working patch (PR to follow) validated live — barge-in stop latency ~110 µs from VAD detection, existing 302 unit tests + clippy -D warnings clean:

  • FrameProcessor::on_interruption() default-no-op hook, called by the runtime in the Interruption arm (delivery path for sinks/aggregators);
  • optional barge-in generation counter + tokio::sync::Notify on VadProcessor, bumped synchronously at detection → cooperative LLM stream cancel + an out-of-band flush reactor;
  • BotSpeakingNotifier in the cascaded sink emitting the bot-speaking edges;
  • a SpeechGate (VAD-edged segmentation with pre-roll + flush marker), because fixed-window batch STT with the turn lock removed hallucinates turns on silence and splits utterances;
  • an opt-in build_cascaded_call_duplex builder; the stock half-duplex builder is untouched.

Happy to split the PR if you'd prefer the hook/runtime fix separately from the duplex builder.

Two smaller items we hit on the same journey, mention them here rather than spamming issues — say the word and I'll file them separately:

  • factory::tts drops options.base_url for kokoro (must construct KokoroTts::with_base_url manually), and require_key rejects keyless local providers (whisper_local, kokoro) unless given a dummy key;
  • binding the media socket to 127.0.0.1 (the FLOWCAT_WEBRTC_BIND_IP default) breaks same-host clients that send ICE checks from per-interface sockets (aiortc, Chrome): replies fail with EINVAL (os error 22) and the connection never establishes. Bind wildcard + advertise loopback works.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions