Skip to content
ujwalpandey1Public

About

Streaming voice turn segmentation, bounded audio buffers, barge-in handling, and generation-safe playback with inspectable timelines.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

Turncraft

Give voice agents a well-defined turn boundary.

CI Python 3.11+ Runtime dependencies: zero MIT

Streaming turn segmentation, inspectable timing, and generation-aware barge-in for voice applications.

A person starts talking while an assistant is still responding. The application must confirm the new speech, cancel the old response, ignore delayed audio chunks, and preserve the start of the incoming utterance. Turncraft makes those transitions explicit and testable.

It supplies PCM16 framing, a turn state machine, a playback generation guard, and offline WAV/event replay tools. The bundled energy detector is a baseline activity score, not a trained voice activity detector or a calibrated speech probability. Bring an external VAD through a small callback interface when actual speech discrimination matters.

The flow

flowchart TD
    A[PCM16 byte stream or mono WAV] --> B[Validate format and sequential frame positions]
    B --> C[Energy baseline or external speech probability adapter]
    C --> D{Sustained score above start threshold?}
    D -->|Not yet| E[Keep bounded pre-roll; reject short bursts]
    E --> D
    D -->|Minimum speech confirmed| F[Emit speech_start with logical and observed times]
    F --> G[Capture bounded utterance]
    G --> H{Silence hangover, duration limit, or EOF?}
    H -->|No| G
    H -->|Yes| I[Emit completed PCM segment and speech_end]
    F --> J[Invalidate active playback generation]
    J --> K[Host cancels response and flushes audio sink]
    L[Delayed response audio chunk] --> M{Current generation and next sequence?}
    M -->|No| N[Reject stale or out-of-order chunk]
    M -->|Yes| O[Host may enqueue through its coordinated sink]
    I --> P[JSON timeline, HTML inspector, WAV clips]
    N --> P
Loading

Where it fits

Use case What you get
Streaming voice agent Confirmed speech-start/end events with pre-roll and a maximum capture duration.
Barge-in handling Increasing response generations and explicit cancellation signals; late chunks are rejected.
ASR input preparation Whole PCM16 utterance clips with sample-accurate positions in the original stream.
Voice UX debugging A self-contained HTML timeline showing activity, captured spans, endpoints, and rejected playback.
VAD adapter development A simple external score/reset boundary that validates frame order before touching model state.
Regression testing Deterministic synthetic tones and event scenarios without microphones, credentials, or network calls.

Turncraft does not include ASR, TTS, echo cancellation, a microphone driver, or an audio output device. It coordinates the state surrounding those components.

Try the offline demo

Requires Python 3.11 or newer:

git clone https://github.com/ujwalpandey1/turncraft.git
cd turncraft
python3.11 -m venv .venv
source .venv/bin/activate
python -m pip install -e .
turncraft demo

Actual output:

{
  "audio_fixture": {
    "frames": 139,
    "duration_seconds": 2.78,
    "utterances": 2,
    "barge_ins": 0,
    "rejected_playback_chunks": 0
  },
  "barge_in_fixture": {
    "frames": 30,
    "duration_seconds": 0.6,
    "utterances": 1,
    "barge_ins": 1,
    "rejected_playback_chunks": 2
  }
}

The audio fixture contains 440 Hz tones and silence. The replay fixture supplies simulated speech probabilities over silent PCM. Neither is real speech, and these counts are behavior checks, not speech-detection performance measurements.

Inspect a WAV and a barge-in replay

turncraft fixtures demo-output/fixtures

turncraft segment demo-output/fixtures/tones.wav \
  --out demo-output/segmented

turncraft replay demo-output/fixtures/barge-in.jsonl \
  --out demo-output/replayed

Each output directory contains report.json, timeline.html, and numbered utterance-0001.wav clips. Open timeline.html in a browser to inspect activity and capture intervals, filter rejected playback events, or download the underlying session JSON. Reports work offline and use no external JavaScript libraries. View a saved sample inspector.

For your own audio:

turncraft segment recording.wav --out my-session \
  --min-speech-ms 100 --end-silence-ms 300 \
  --pre-roll-ms 100 --max-utterance-ms 15000

Input must be mono, uncompressed signed PCM16 WAV. Conversion and resampling are explicit caller responsibilities. The final partial frame is preserved without zero-padding. Malformed/truncated WAVs fail, and an existing report directory is never intentionally overwritten.

Streaming Python API

from turncraft import AudioFormat, PCMFramer, EnergyDetector
from turncraft import TurnSegmenter, VoicePipeline

format = AudioFormat(sample_rate=16000, frame_ms=20)
framer = PCMFramer(format)
pipeline = VoicePipeline(TurnSegmenter(format), EnergyDetector())

# Feed bytes from a mono PCM16 capture source. Chunk boundaries may split samples.
for chunk in microphone_pcm_chunks:  # your capture iterator
    for frame in framer.feed(chunk):
        update = pipeline.push(frame)
        for event in update.turn.events:
            print(event.kind, event.sample, event.observed_sample)
        for utterance in update.turn.utterances:
            send_to_asr(utterance.pcm)  # your downstream consumer
        for cancellation in update.cancellations:
            cancel_response_and_flush_sink(cancellation.cancelled_generation)

for frame in framer.finish():
    update = pipeline.push(frame)
    # Dispatch this final update through the same event/utterance/cancellation handlers.
for utterance in pipeline.finish().utterances:
    send_to_asr(utterance.pcm)

The capture and ASR names above are integration placeholders. A complete runnable streaming example dispatches both ordinary and final partial frames with the same handler.

Barge-in and stale playback

from turncraft import PlaybackGate

gate = PlaybackGate()
generation = gate.start_response()    # attach this ID to every async TTS chunk
assert gate.accept(generation, 0).accepted

cancellation = gate.cancel("barge_in")
assert gate.accept(generation, 1).reason == "stale_generation"

next_generation = gate.start_response()
assert next_generation > generation
assert gate.accept(next_generation, 0).accepted

VoicePipeline calls its gate's cancel() when minimum speech is confirmed and returns the cancellation. Sequence numbers start at zero per response. A gap or duplicate is rejected without consuming the expected sequence. Reset invalidates old IDs and never returns the generation counter to zero.

The gate cannot retract audio already accepted, queued, or played. Its lock protects generation/sequence state only. Your host must coordinate accept with enqueue/dequeue, cancellation, and flushing the real playback sink. Check at the final controllable playback boundary, cancel the asynchronous response producer, and flush queued chunks when signaled. See integration design for the race boundary and a host-owned queue pattern.

Timing rules

Setting Default Meaning
Frame duration 20 ms One score per frame, except a shorter final frame.
Start threshold 0.65 Consecutive frames at or above this score begin a candidate.
Minimum speech 80 ms Candidate must persist before speech_start is emitted.
End threshold 0.35 Active frames at or below this score count toward silence.
End silence 200 ms Consecutive low-scoring audio required to finish a turn.
Pre-roll 100 ms Retained audio before the first candidate frame.
Maximum utterance 10,000 ms Hard bound on captured PCM, including pre-roll and hangover.

Thresholds between end and start keep an active turn alive but cannot start an idle one. Duration settings must align to the frame duration. sample is the logical boundary; observed_sample is when enough evidence arrived to emit the event. This exposes start-confirmation and end-hangover delays rather than hiding them.

After a hard duration split, continuing activity must satisfy the minimum speech rule again. A final unconfirmed tail is discarded. finish() emits an active utterance with reason end_of_stream; it never promotes an unconfirmed candidate.

Plug in an external VAD

from turncraft import ExternalDetector

detector = ExternalDetector(
    score=adapter.score_pcm_frame,  # return a finite float from 0 to 1
    reset=adapter.reset,            # clear model and window-buffer state
)

The adapter owns conversion, supported sample rates, model window sizes, and state. Turncraft's 20 ms frame is not a promise that any particular model accepts a 20 ms input. A model may need an internal buffer and an explicit policy for aligning probabilities to frames. No pretrained model or adapter-specific accuracy claim is bundled. Integration notes explain model-state failures and resets.

Verify and extend

python -m unittest discover -s tests -v
turncraft demo
python examples/streaming.py

The 41 tests cover arbitrary byte chunks, exact boundaries, partial/truncated audio, pre-roll, short bursts, midband hysteresis, maximum capture duration, reset, out-of-order input, concurrent playback checks, stale generations, malformed replay, external detector failure, and report escaping. CI runs Python 3.11 and 3.12.

Read architecture, CLI and data reference, and contributing. This is an early, local toolkit. Streaming active-turn buffers are bounded; the offline report builder retains frame diagnostics and completed clips in memory, so long files need a streaming consumer or sampled diagnostics.

Research context

The Silero VAD FAQ explains that its models keep state, should receive sequential chunks, and provide reset behavior. It also discusses supported sample rates and tuning speech/silence duration thresholds. Those concerns inform Turncraft's adapter boundary and ordering checks; Turncraft does not implement Silero, bundle its weights, or reproduce its performance results.

MIT license · Issues

About

Streaming voice turn segmentation, bounded audio buffers, barge-in handling, and generation-safe playback with inspectable timelines.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages