7. Barge-In Interrupt Priority Queue
turn.start / turn.eager_end / turn.end turn-detection events in real time (Source: docs.cartesia.ai/get-started/realtime-text-to-speech-quickstart, cartesia.ai/blog/ink-2). A full-duplex system with real-time turn detection needs some way to let a newly detected user turn (a barge-in) preempt audio the system already had queued to play — a plausible scenario for a system like this, not a specific disclosed Cartesia scheduling implementation.Problem
A voice agent is mid-sentence when the caller starts talking over it. The moment that barge-in is detected, whatever the agent had queued up to say next should be scrapped — there's no point finishing a sentence the user has already interrupted.
The agent's response to the interruption should play as soon as it's ready, ahead of anything else. Given the full event stream of queued segments and detected interrupts, in arrival order, determine the actual final playback order.
Source: src/7_barge_in_priority_queue.py
def schedule_playback(events: list[tuple[str, str, int]]) -> list[str]:
...
Examples:
>>> schedule_playback([("play", "n1", 5), ("interrupt", "i1", 3), ("play", "n2", 5)])
["i1", "n2"]
>>> schedule_playback([("play", "n1", 1), ("play", "n2", 1)])
["n1", "n2"]
Step-by-Step Approach
- Maintain two separate FIFO structures: a
normal_queuefor regular queued segments, and aninterrupt_queuefor detected barge-ins. - Process
eventsin arrival order. A"play"event simply appends its segment id to the back ofnormal_queue. - An
"interrupt"event does two things: it clearsnormal_queueentirely (everything queued but not yet played is scrapped), and it appends its own segment id to the back ofinterrupt_queue. - Interrupts never clear each other — only normal segments get dropped by a barge-in, so
interrupt_queueonly ever grows, never gets cleared mid-stream. - After processing every event, the final playback order is deterministic: every interrupt, in the order they were detected, followed by whatever normal segments survived (necessarily all queued after the very last interrupt, since any earlier ones were wiped).
- Handle the edge cases directly: no interrupts at all just returns
normal_queueunchanged; interrupts with nothing queued between them simply accumulate back-to-back with no normal segments in between.
The key insight is that you don't need to simulate playback time or clock ticks at all — because an interrupt always wins immediately and unconditionally, the whole schedule collapses to "all interrupts, then whatever's left," which two queues and one clear operation are enough to compute.
Reference solution
def schedule_playback(events: list[tuple[str, str, int]]) -> list[str]:
normal_queue: list[str] = []
interrupt_queue: list[str] = []
for kind, segment_id, _duration in events:
if kind == "interrupt":
# a barge-in wipes out anything queued but not yet played
normal_queue.clear()
interrupt_queue.append(segment_id)
else:
normal_queue.append(segment_id)
# interrupts always win, in the order they were detected; survivors play after
return interrupt_queue + normal_queue
Key Functions & Tricks
normal_queue.clear()— the entire preemption mechanism: one call, triggered only by an interrupt event.- Two independent FIFO lists instead of one priority queue — since priority is binary (interrupt beats normal, always), a heap is unnecessary overhead; plain lists in arrival order suffice.
interrupt_queue + normal_queue— the final merge only needs to happen once, at the end, because within each queue relative order never changes after appends.- Unpacking
_durationwith a throwaway name — the field is part of the event's realistic shape but unused by this scheduling policy.
How to Recognize This Pattern
The signal is "a high-priority event arrives mid-stream and must cancel/preempt everything currently pending at lower priority" — interrupt handling, emergency dispatch reordering, or any "cancel-and-replace" queue has this shape. When there are only two priority tiers and higher priority always wins outright, two plain FIFO queues plus a clear-on-preempt rule beat a general priority queue in both simplicity and clarity. A common variation adds a true priority queue with more than two tiers (a heap keyed by priority, FIFO within a tier) when preemption isn't absolute. A common pitfall is clearing interrupt_queue as well as normal_queue on a new interrupt, which would incorrectly drop an interrupt response the system had already committed to playing.