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.
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
| 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.
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 demoActual 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.
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/replayedEach 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 15000Input 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.
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.
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).acceptedVoicePipeline 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.
| 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.
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.
python -m unittest discover -s tests -v
turncraft demo
python examples/streaming.pyThe 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.
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.