Skip to main content

eredu_core/
realtime.rs

1//! Portable realtime token frames, schedules, and facade errors.
2
3use crate::{
4    observation::{ObservationError, TensorObservation, TensorObservationData},
5    scheduler::{SchedulerError, SemanticStateTransaction, WorkDescriptor, WorkId},
6};
7use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9
10/// Largest admitted text or audio delay in one portable realtime schedule.
11///
12/// The bound keeps every delay representable by backends whose token and cache
13/// coordinates use signed 32-bit integers. Runtime frontier arithmetic remains
14/// checked independently.
15pub const MAX_REALTIME_FRAME_DELAY: usize = i32::MAX as usize;
16
17/// Coordinate convention used by a realtime text-plus-audio frame schedule.
18#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
19#[serde(rename_all = "snake_case")]
20#[non_exhaustive]
21pub enum RealtimeFrameConvention {
22    /// Inputs are retained as undelayed history while generated values are
23    /// written back to the history frame selected by their delay.
24    FeedbackAlignedHistory,
25    /// Every value is placed directly at its absolute `frontier + delay`
26    /// position and model inputs read the preceding absolute position.
27    AbsoluteDelayedSlots,
28}
29
30/// Static codec-token geometry shared by every session of one realtime model.
31#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
32pub struct RealtimeSpeechConfig {
33    total_audio_codebooks: usize,
34    input_audio_codebooks: usize,
35    generated_audio_codebooks: usize,
36    depth_audio_codebooks: usize,
37    text_padding_token: i32,
38    audio_padding_token: i32,
39    frame_convention: RealtimeFrameConvention,
40    delays: Vec<usize>,
41}
42
43impl<'de> Deserialize<'de> for RealtimeSpeechConfig {
44    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
45    where
46        D: serde::Deserializer<'de>,
47    {
48        #[derive(Deserialize)]
49        struct Raw {
50            total_audio_codebooks: usize,
51            input_audio_codebooks: usize,
52            generated_audio_codebooks: usize,
53            depth_audio_codebooks: usize,
54            text_padding_token: i32,
55            audio_padding_token: i32,
56            frame_convention: RealtimeFrameConvention,
57            delays: Vec<usize>,
58        }
59        let raw = Raw::deserialize(deserializer)?;
60        Self::new(
61            raw.total_audio_codebooks,
62            raw.input_audio_codebooks,
63            raw.generated_audio_codebooks,
64            raw.depth_audio_codebooks,
65            raw.text_padding_token,
66            raw.audio_padding_token,
67            raw.frame_convention,
68            raw.delays,
69        )
70        .map_err(serde::de::Error::custom)
71    }
72}
73
74impl RealtimeSpeechConfig {
75    /// Creates and validates portable realtime codec geometry.
76    #[allow(clippy::too_many_arguments)] // Public codec geometry is intentionally explicit.
77    pub fn new(
78        total_audio_codebooks: usize,
79        input_audio_codebooks: usize,
80        generated_audio_codebooks: usize,
81        depth_audio_codebooks: usize,
82        text_padding_token: i32,
83        audio_padding_token: i32,
84        frame_convention: RealtimeFrameConvention,
85        delays: Vec<usize>,
86    ) -> Result<Self, RealtimeConfigError> {
87        if total_audio_codebooks == 0
88            || generated_audio_codebooks == 0
89            || depth_audio_codebooks == 0
90        {
91            return Err(RealtimeConfigError::EmptyCodebookGeometry);
92        }
93        if input_audio_codebooks.checked_add(generated_audio_codebooks)
94            != Some(total_audio_codebooks)
95        {
96            return Err(RealtimeConfigError::CodebookPartition {
97                total: total_audio_codebooks,
98                input: input_audio_codebooks,
99                generated: generated_audio_codebooks,
100            });
101        }
102        if generated_audio_codebooks > depth_audio_codebooks
103            || depth_audio_codebooks > total_audio_codebooks
104        {
105            return Err(RealtimeConfigError::DepthCodebookGeometry {
106                generated: generated_audio_codebooks,
107                depth: depth_audio_codebooks,
108                total: total_audio_codebooks,
109            });
110        }
111        if text_padding_token < 0 || audio_padding_token < 0 {
112            return Err(RealtimeConfigError::NegativePaddingToken {
113                text: text_padding_token,
114                audio: audio_padding_token,
115            });
116        }
117        let expected_delays = total_audio_codebooks
118            .checked_add(1)
119            .ok_or(RealtimeConfigError::CodebookCountOverflow)?;
120        if delays.len() != expected_delays {
121            return Err(RealtimeConfigError::DelayCount {
122                expected: expected_delays,
123                actual: delays.len(),
124            });
125        }
126        if let Some((slot, delay)) = delays
127            .iter()
128            .copied()
129            .enumerate()
130            .find(|(_, delay)| *delay > MAX_REALTIME_FRAME_DELAY)
131        {
132            return Err(RealtimeConfigError::DelayOutOfRange {
133                slot,
134                delay,
135                maximum: MAX_REALTIME_FRAME_DELAY,
136            });
137        }
138        Ok(Self {
139            total_audio_codebooks,
140            input_audio_codebooks,
141            generated_audio_codebooks,
142            depth_audio_codebooks,
143            text_padding_token,
144            audio_padding_token,
145            frame_convention,
146            delays,
147        })
148    }
149
150    /// Total number of temporal-model audio codebooks.
151    pub const fn total_audio_codebooks(&self) -> usize {
152        self.total_audio_codebooks
153    }
154    /// Number of live input-side codebooks per frame.
155    pub const fn input_audio_codebooks(&self) -> usize {
156        self.input_audio_codebooks
157    }
158    /// Number of generated-side codebooks per frame.
159    pub const fn generated_audio_codebooks(&self) -> usize {
160        self.generated_audio_codebooks
161    }
162    /// Number of depth-transformer codebooks per frame.
163    pub const fn depth_audio_codebooks(&self) -> usize {
164        self.depth_audio_codebooks
165    }
166    /// Text token used before sampled text is available.
167    pub const fn text_padding_token(&self) -> i32 {
168        self.text_padding_token
169    }
170    /// Audio token used while delayed streams warm up.
171    pub const fn audio_padding_token(&self) -> i32 {
172        self.audio_padding_token
173    }
174    /// Explicit delayed-frame coordinate convention.
175    pub const fn frame_convention(&self) -> RealtimeFrameConvention {
176        self.frame_convention
177    }
178    /// Complete text-plus-audio delay schedule in canonical slot order.
179    pub fn delays(&self) -> &[usize] {
180        &self.delays
181    }
182    /// Leading text-stream delay.
183    pub fn text_delay(&self) -> usize {
184        self.delays[0]
185    }
186    /// Per-codebook delays following the leading text delay.
187    pub fn audio_delays(&self) -> &[usize] {
188        &self.delays[1..]
189    }
190    /// Largest delay across the complete text-plus-audio schedule.
191    pub fn max_delay(&self) -> usize {
192        self.delays.iter().copied().max().unwrap_or(0)
193    }
194    /// Largest audio delay in frames.
195    pub fn max_audio_delay(&self) -> usize {
196        self.audio_delays().iter().copied().max().unwrap_or(0)
197    }
198}
199
200/// Invalid portable realtime configuration.
201#[derive(Debug, Clone, PartialEq, thiserror::Error)]
202#[non_exhaustive]
203pub enum RealtimeConfigError {
204    /// Every realtime codebook dimension must be nonzero.
205    #[error("realtime codebook geometry must be nonzero")]
206    EmptyCodebookGeometry,
207    /// Input and generated codebooks must partition the temporal codebooks.
208    #[error(
209        "realtime input ({input}) and generated ({generated}) codebooks do not partition total {total}"
210    )]
211    CodebookPartition {
212        /// Total temporal codebooks.
213        total: usize,
214        /// Input codebooks.
215        input: usize,
216        /// Generated codebooks.
217        generated: usize,
218    },
219    /// The depth predictor must contain every generated codebook and no more
220    /// than the complete temporal audio geometry.
221    #[error(
222        "realtime generated ({generated}), depth ({depth}), and total ({total}) codebooks must satisfy 0 < generated <= depth <= total"
223    )]
224    DepthCodebookGeometry {
225        /// Generated audio codebooks.
226        generated: usize,
227        /// Depth-predictor codebooks.
228        depth: usize,
229        /// Total temporal audio codebooks.
230        total: usize,
231    },
232    /// The text-plus-audio slot count overflowed portable geometry.
233    #[error("realtime text-plus-audio slot count overflowed")]
234    CodebookCountOverflow,
235    /// Padding tokens used by realtime model inputs must be non-negative.
236    #[error("realtime padding tokens must be non-negative, got text={text} audio={audio}")]
237    NegativePaddingToken {
238        /// Invalid text padding token.
239        text: i32,
240        /// Invalid audio padding token.
241        audio: i32,
242    },
243    /// The delay schedule must describe text and every temporal audio codebook.
244    #[error("realtime delay schedule has {actual} entries, expected {expected}")]
245    DelayCount {
246        /// Expected delay count.
247        expected: usize,
248        /// Actual delay count.
249        actual: usize,
250    },
251    /// One delay exceeds the portable coordinate bound.
252    #[error("realtime delay {delay} at slot {slot} exceeds maximum {maximum}")]
253    DelayOutOfRange {
254        /// Canonical text-plus-audio slot.
255        slot: usize,
256        /// Invalid delay.
257        delay: usize,
258        /// Largest admitted delay.
259        maximum: usize,
260    },
261    /// Sampling temperatures must be finite and nonnegative.
262    #[error(
263        "realtime sampling temperatures must be finite and non-negative, got text={text} audio={audio}"
264    )]
265    SamplingTemperature {
266        /// Invalid text temperature.
267        text: f32,
268        /// Invalid audio temperature.
269        audio: f32,
270    },
271    /// Top-k truncation, when selected, must admit at least one token.
272    #[error(
273        "realtime sampling top-k must be positive when set, got text={text:?} audio={audio:?}"
274    )]
275    SamplingTopK {
276        /// Invalid text top-k value.
277        text: Option<usize>,
278        /// Invalid audio top-k value.
279        audio: Option<usize>,
280    },
281}
282
283/// Canonical stream slot in a text-plus-audio realtime schedule.
284#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
285#[non_exhaustive]
286pub enum RealtimeFrameSlot {
287    /// Text stream.
288    Text,
289    /// Zero-based temporal audio codebook.
290    Audio(usize),
291}
292
293impl RealtimeFrameSlot {
294    fn index(self) -> usize {
295        match self {
296            Self::Text => 0,
297            Self::Audio(codebook) => codebook + 1,
298        }
299    }
300}
301
302/// One absolute position in a canonical text-plus-audio slot timeline.
303#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
304pub struct RealtimeSlotCoordinate {
305    position: usize,
306    slot: RealtimeFrameSlot,
307}
308
309impl RealtimeSlotCoordinate {
310    /// Creates one portable delayed-slot coordinate.
311    pub const fn new(position: usize, slot: RealtimeFrameSlot) -> Self {
312        Self { position, slot }
313    }
314
315    /// Absolute history or delayed-timeline position.
316    pub const fn position(self) -> usize {
317        self.position
318    }
319
320    /// Text or audio stream slot.
321    pub const fn slot(self) -> RealtimeFrameSlot {
322        self.slot
323    }
324}
325
326/// Provenance of an occupied portable schedule slot.
327///
328/// This records only scheduling metadata. Token payloads remain backend-owned.
329#[derive(Debug, Clone, Copy, Eq, PartialEq)]
330#[non_exhaustive]
331pub enum RealtimeSlotOccupancy {
332    /// Live input-side audio supplied for this transition.
333    Input,
334    /// Caller-forced text or generated audio target.
335    Forced,
336    /// Convention-defined warm-up padding.
337    Padding,
338    /// A model-selected text or audio target.
339    Sampled,
340}
341
342/// Source selected for one temporal model input slot.
343#[derive(Debug, Clone, Copy, Eq, PartialEq)]
344#[non_exhaustive]
345pub enum RealtimeTemporalSource {
346    /// The model consumes the configured text or audio padding token.
347    Padding(RealtimeFrameSlot),
348    /// The model consumes the backend payload stored at this occupied slot.
349    Occupied {
350        /// Exact portable coordinate.
351        coordinate: RealtimeSlotCoordinate,
352        /// Scheduling provenance of the payload.
353        occupancy: RealtimeSlotOccupancy,
354    },
355}
356
357/// How one text or depth-codebook decision is resolved.
358#[derive(Debug, Clone, Copy, Eq, PartialEq)]
359#[non_exhaustive]
360pub enum RealtimeForcedSource {
361    /// The forcing payload belongs to the currently submitted portable frame.
362    CurrentInput,
363    /// The forcing payload was retained at the target coordinate by an earlier frame.
364    Retained,
365}
366
367/// How one text or depth-codebook decision is resolved.
368#[derive(Debug, Clone, Copy, Eq, PartialEq)]
369#[non_exhaustive]
370pub enum RealtimeTargetSource {
371    /// Caller forcing resolves this decision.
372    Forced(RealtimeForcedSource),
373    /// The model sampler resolves this decision.
374    Sampled,
375    /// An already placed input, forced value, or padding resolves the decision.
376    Existing(RealtimeSlotOccupancy),
377}
378
379/// Portable placement for one text or depth-codebook decision.
380#[derive(Debug, Clone, Copy, Eq, PartialEq)]
381pub struct RealtimeTargetDecision {
382    slot: RealtimeFrameSlot,
383    coordinate: Option<RealtimeSlotCoordinate>,
384    source: RealtimeTargetSource,
385}
386
387impl RealtimeTargetDecision {
388    /// Text or audio decision slot.
389    pub const fn slot(self) -> RealtimeFrameSlot {
390        self.slot
391    }
392
393    /// Destination coordinate, or `None` during feedback-history warm-up.
394    pub const fn coordinate(self) -> Option<RealtimeSlotCoordinate> {
395        self.coordinate
396    }
397
398    /// Forced, sampled, or already occupied resolution.
399    pub const fn source(self) -> RealtimeTargetSource {
400        self.source
401    }
402}
403
404/// Per-decision forcing mask supplied for one realtime transition.
405#[derive(Debug, Clone, Eq, PartialEq)]
406pub struct RealtimeFrameForcing {
407    text: bool,
408    generated_audio: Vec<bool>,
409}
410
411impl RealtimeFrameForcing {
412    /// Creates a forcing mask in generated-codebook order.
413    pub fn new(text: bool, generated_audio: Vec<bool>) -> Self {
414        Self {
415            text,
416            generated_audio,
417        }
418    }
419
420    /// No forced text or generated-audio decisions.
421    pub fn none(config: &RealtimeSpeechConfig) -> Self {
422        Self::new(false, vec![false; config.generated_audio_codebooks])
423    }
424
425    /// Whether the text target is forced.
426    pub const fn text(&self) -> bool {
427        self.text
428    }
429
430    /// Per-generated-codebook forcing mask.
431    pub fn generated_audio(&self) -> &[bool] {
432        &self.generated_audio
433    }
434}
435
436/// Complete portable scheduling decision for one accepted input-side frame.
437#[derive(Debug, Clone, Eq, PartialEq)]
438pub struct RealtimeFrameTransition {
439    frontier: usize,
440    input_placements: Vec<RealtimeSlotCoordinate>,
441    forced_placements: Vec<RealtimeSlotCoordinate>,
442    warmup_padding: Vec<RealtimeSlotCoordinate>,
443    temporal_inputs: Vec<RealtimeTemporalSource>,
444    targets: Vec<RealtimeTargetDecision>,
445    output: Option<Vec<RealtimeSlotCoordinate>>,
446    model_call_required: bool,
447    next_frontier: usize,
448}
449
450impl RealtimeFrameTransition {
451    /// Committed frontier before this transition.
452    pub const fn frontier(&self) -> usize {
453        self.frontier
454    }
455
456    /// Coordinates receiving live input-side audio payloads.
457    pub fn input_placements(&self) -> &[RealtimeSlotCoordinate] {
458        &self.input_placements
459    }
460
461    /// Coordinates receiving caller-forced text or generated audio payloads.
462    pub fn forced_placements(&self) -> &[RealtimeSlotCoordinate] {
463        &self.forced_placements
464    }
465
466    /// Coordinates initialized to the configured warm-up padding token.
467    pub fn warmup_padding(&self) -> &[RealtimeSlotCoordinate] {
468        &self.warmup_padding
469    }
470
471    /// Temporal text-plus-audio inputs in canonical slot order.
472    pub fn temporal_inputs(&self) -> &[RealtimeTemporalSource] {
473        &self.temporal_inputs
474    }
475
476    /// Ordered text followed by depth-codebook decisions.
477    pub fn targets(&self) -> &[RealtimeTargetDecision] {
478        &self.targets
479    }
480
481    /// Delay-aligned generated-audio frame, in generated-codebook order.
482    pub fn output(&self) -> Option<&[RealtimeSlotCoordinate]> {
483        self.output.as_deref()
484    }
485
486    /// Whether this transition requires temporal/depth model execution.
487    pub const fn model_call_required(&self) -> bool {
488        self.model_call_required
489    }
490
491    /// Frontier published when the transition branch commits.
492    pub const fn next_frontier(&self) -> usize {
493        self.next_frontier
494    }
495}
496
497/// Portable delayed-frame schedule state with no token or backend payloads.
498#[derive(Debug, Clone, Eq, PartialEq)]
499pub struct RealtimeFrameScheduleState {
500    schedule: RealtimeSpeechConfig,
501    frontier: usize,
502    occupied: BTreeMap<RealtimeSlotCoordinate, RealtimeSlotOccupancy>,
503}
504
505impl RealtimeFrameScheduleState {
506    /// Creates an empty state bound to one exact normalized schedule.
507    pub fn new(schedule: RealtimeSpeechConfig) -> Self {
508        Self {
509            schedule,
510            frontier: 0,
511            occupied: BTreeMap::new(),
512        }
513    }
514
515    /// Exact normalized schedule identity carried by this state.
516    pub const fn schedule(&self) -> &RealtimeSpeechConfig {
517        &self.schedule
518    }
519
520    /// Next input-side frame coordinate to accept.
521    pub const fn frontier(&self) -> usize {
522        self.frontier
523    }
524
525    /// Returns scheduling provenance for one retained coordinate.
526    pub fn occupancy(&self, coordinate: RealtimeSlotCoordinate) -> Option<RealtimeSlotOccupancy> {
527        self.occupied.get(&coordinate).copied()
528    }
529
530    /// Rejects state handoff to any materially different normalized schedule.
531    pub fn validate_schedule(
532        &self,
533        schedule: &RealtimeSpeechConfig,
534    ) -> Result<(), RealtimeScheduleError> {
535        if &self.schedule == schedule {
536            Ok(())
537        } else {
538            Err(RealtimeScheduleError::ScheduleMismatch)
539        }
540    }
541
542    /// Accepts one input frame, resolves all portable coordinates, records
543    /// target occupancy, and advances this transaction-local branch.
544    ///
545    /// The update is atomic: malformed masks, missing history, and arithmetic
546    /// overflow leave `self` unchanged.
547    pub fn advance(
548        &mut self,
549        schedule: &RealtimeSpeechConfig,
550        forcing: &RealtimeFrameForcing,
551    ) -> Result<RealtimeFrameTransition, RealtimeScheduleError> {
552        self.validate_schedule(schedule)?;
553        if forcing.generated_audio.len() != schedule.generated_audio_codebooks {
554            return Err(RealtimeScheduleError::ForcingCount {
555                expected: schedule.generated_audio_codebooks,
556                actual: forcing.generated_audio.len(),
557            });
558        }
559        let mut branch = self.clone();
560        let transition = match schedule.frame_convention {
561            RealtimeFrameConvention::FeedbackAlignedHistory => branch.advance_feedback(forcing)?,
562            RealtimeFrameConvention::AbsoluteDelayedSlots => branch.advance_absolute(forcing)?,
563        };
564        *self = branch;
565        Ok(transition)
566    }
567
568    fn advance_feedback(
569        &mut self,
570        forcing: &RealtimeFrameForcing,
571    ) -> Result<RealtimeFrameTransition, RealtimeScheduleError> {
572        let schedule = &self.schedule;
573        let frontier = self.frontier;
574        let next_frontier = checked_add(frontier, 1)?;
575        let generated = schedule.generated_audio_codebooks;
576        let mut input_placements = Vec::with_capacity(schedule.input_audio_codebooks);
577        for codebook in generated..schedule.total_audio_codebooks {
578            let coordinate = coordinate(frontier, RealtimeFrameSlot::Audio(codebook));
579            self.occupied
580                .insert(coordinate, RealtimeSlotOccupancy::Input);
581            input_placements.push(coordinate);
582        }
583
584        let mut forced_placements = Vec::new();
585        for (codebook, forced) in forcing.generated_audio.iter().copied().enumerate() {
586            if forced {
587                let coordinate = coordinate(frontier, RealtimeFrameSlot::Audio(codebook));
588                self.occupied
589                    .insert(coordinate, RealtimeSlotOccupancy::Forced);
590                forced_placements.push(coordinate);
591            }
592        }
593
594        let mut temporal_inputs = Vec::with_capacity(schedule.delays.len());
595        for slot in slots(schedule.total_audio_codebooks) {
596            let delay = schedule.delays[slot.index()];
597            let source = frontier
598                .checked_sub(1)
599                .and_then(|position| position.checked_sub(delay));
600            match source {
601                None => temporal_inputs.push(RealtimeTemporalSource::Padding(slot)),
602                Some(position) => {
603                    let coordinate = coordinate(position, slot);
604                    let occupancy = self.required_occupancy(coordinate)?;
605                    temporal_inputs.push(RealtimeTemporalSource::Occupied {
606                        coordinate,
607                        occupancy,
608                    });
609                }
610            }
611        }
612
613        let mut targets = Vec::with_capacity(1 + schedule.depth_audio_codebooks);
614        let text_coordinate = frontier
615            .checked_sub(schedule.text_delay())
616            .map(|position| coordinate(position, RealtimeFrameSlot::Text));
617        let text_source = if forcing.text {
618            RealtimeTargetSource::Forced(RealtimeForcedSource::CurrentInput)
619        } else {
620            RealtimeTargetSource::Sampled
621        };
622        if let Some(coordinate) = text_coordinate {
623            self.occupied.insert(
624                coordinate,
625                if forcing.text {
626                    RealtimeSlotOccupancy::Forced
627                } else {
628                    RealtimeSlotOccupancy::Sampled
629                },
630            );
631            if forcing.text {
632                forced_placements.push(coordinate);
633            }
634        }
635        targets.push(RealtimeTargetDecision {
636            slot: RealtimeFrameSlot::Text,
637            coordinate: text_coordinate,
638            source: text_source,
639        });
640        for codebook in 0..schedule.depth_audio_codebooks {
641            let slot = RealtimeFrameSlot::Audio(codebook);
642            if codebook < generated {
643                let target_coordinate = frontier
644                    .checked_sub(schedule.audio_delays()[codebook])
645                    .map(|position| coordinate(position, slot));
646                let forced = forcing.generated_audio[codebook];
647                if let Some(coordinate) = target_coordinate {
648                    self.occupied.insert(
649                        coordinate,
650                        if forced {
651                            RealtimeSlotOccupancy::Forced
652                        } else {
653                            RealtimeSlotOccupancy::Sampled
654                        },
655                    );
656                }
657                targets.push(RealtimeTargetDecision {
658                    slot,
659                    coordinate: target_coordinate,
660                    source: if forced {
661                        RealtimeTargetSource::Forced(RealtimeForcedSource::CurrentInput)
662                    } else {
663                        RealtimeTargetSource::Sampled
664                    },
665                });
666            } else {
667                let input_coordinate = coordinate(frontier, slot);
668                let occupancy = self.required_occupancy(input_coordinate)?;
669                targets.push(RealtimeTargetDecision {
670                    slot,
671                    coordinate: Some(input_coordinate),
672                    source: RealtimeTargetSource::Existing(occupancy),
673                });
674            }
675        }
676
677        let output = frontier
678            .checked_sub(schedule.max_delay())
679            .map(|position| self.output_at_same_position(position))
680            .transpose()?;
681        self.frontier = next_frontier;
682        self.prune_before(frontier.saturating_sub(schedule.max_delay()));
683        Ok(RealtimeFrameTransition {
684            frontier,
685            input_placements,
686            forced_placements,
687            warmup_padding: Vec::new(),
688            temporal_inputs,
689            targets,
690            output,
691            model_call_required: true,
692            next_frontier,
693        })
694    }
695
696    fn advance_absolute(
697        &mut self,
698        forcing: &RealtimeFrameForcing,
699    ) -> Result<RealtimeFrameTransition, RealtimeScheduleError> {
700        let schedule = &self.schedule;
701        let frontier = self.frontier;
702        let next_frontier = checked_add(frontier, 1)?;
703        let generated = schedule.generated_audio_codebooks;
704        let mut input_placements = Vec::with_capacity(schedule.input_audio_codebooks);
705        for codebook in generated..schedule.total_audio_codebooks {
706            let position = checked_add(frontier, schedule.audio_delays()[codebook])?;
707            let coordinate = coordinate(position, RealtimeFrameSlot::Audio(codebook));
708            self.occupied
709                .insert(coordinate, RealtimeSlotOccupancy::Input);
710            input_placements.push(coordinate);
711        }
712
713        let mut forced_placements = Vec::new();
714        if forcing.text {
715            let position = checked_add(frontier, schedule.text_delay())?;
716            let coordinate = coordinate(position, RealtimeFrameSlot::Text);
717            self.occupied
718                .insert(coordinate, RealtimeSlotOccupancy::Forced);
719            forced_placements.push(coordinate);
720        }
721        for (codebook, forced) in forcing.generated_audio.iter().copied().enumerate() {
722            if forced {
723                let position = checked_add(frontier, schedule.audio_delays()[codebook])?;
724                let coordinate = coordinate(position, RealtimeFrameSlot::Audio(codebook));
725                self.occupied
726                    .insert(coordinate, RealtimeSlotOccupancy::Forced);
727                forced_placements.push(coordinate);
728            }
729        }
730
731        let mut warmup_padding = Vec::new();
732        for slot in slots(schedule.total_audio_codebooks) {
733            if frontier <= schedule.delays[slot.index()] {
734                let coordinate = coordinate(frontier, slot);
735                self.occupied
736                    .insert(coordinate, RealtimeSlotOccupancy::Padding);
737                warmup_padding.push(coordinate);
738            }
739        }
740
741        let mut temporal_inputs = Vec::new();
742        let mut targets = Vec::new();
743        let output = if frontier == 0 {
744            None
745        } else {
746            let input_position = frontier - 1;
747            temporal_inputs.reserve(schedule.delays.len());
748            for slot in slots(schedule.total_audio_codebooks) {
749                let coordinate = coordinate(input_position, slot);
750                let occupancy = self.required_occupancy(coordinate)?;
751                temporal_inputs.push(RealtimeTemporalSource::Occupied {
752                    coordinate,
753                    occupancy,
754                });
755            }
756            targets.reserve(1 + schedule.depth_audio_codebooks);
757            for slot in std::iter::once(RealtimeFrameSlot::Text)
758                .chain((0..schedule.depth_audio_codebooks).map(RealtimeFrameSlot::Audio))
759            {
760                let coordinate = coordinate(frontier, slot);
761                let (source, occupancy) = match self.occupied.get(&coordinate).copied() {
762                    Some(RealtimeSlotOccupancy::Forced) => (
763                        RealtimeTargetSource::Forced(RealtimeForcedSource::Retained),
764                        RealtimeSlotOccupancy::Forced,
765                    ),
766                    Some(occupancy) => (RealtimeTargetSource::Existing(occupancy), occupancy),
767                    None => (
768                        RealtimeTargetSource::Sampled,
769                        RealtimeSlotOccupancy::Sampled,
770                    ),
771                };
772                self.occupied.insert(coordinate, occupancy);
773                targets.push(RealtimeTargetDecision {
774                    slot,
775                    coordinate: Some(coordinate),
776                    source,
777                });
778            }
779            if frontier <= schedule.max_delay() {
780                None
781            } else {
782                let base = frontier - schedule.max_delay();
783                let coordinates = (0..generated)
784                    .map(|codebook| {
785                        let position = checked_add(base, schedule.audio_delays()[codebook])?;
786                        let coordinate = coordinate(position, RealtimeFrameSlot::Audio(codebook));
787                        self.required_occupancy(coordinate)?;
788                        Ok(coordinate)
789                    })
790                    .collect::<Result<Vec<_>, RealtimeScheduleError>>()?;
791                Some(coordinates)
792            }
793        };
794
795        self.frontier = next_frontier;
796        self.prune_before(next_frontier.saturating_sub(schedule.max_delay().saturating_add(1)));
797        Ok(RealtimeFrameTransition {
798            frontier,
799            input_placements,
800            forced_placements,
801            warmup_padding,
802            temporal_inputs,
803            targets,
804            output,
805            model_call_required: frontier != 0,
806            next_frontier,
807        })
808    }
809
810    fn required_occupancy(
811        &self,
812        coordinate: RealtimeSlotCoordinate,
813    ) -> Result<RealtimeSlotOccupancy, RealtimeScheduleError> {
814        self.occupied
815            .get(&coordinate)
816            .copied()
817            .ok_or(RealtimeScheduleError::MissingSlot { coordinate })
818    }
819
820    fn output_at_same_position(
821        &self,
822        position: usize,
823    ) -> Result<Vec<RealtimeSlotCoordinate>, RealtimeScheduleError> {
824        (0..self.schedule.generated_audio_codebooks)
825            .map(|codebook| {
826                let coordinate = coordinate(position, RealtimeFrameSlot::Audio(codebook));
827                self.required_occupancy(coordinate)?;
828                Ok(coordinate)
829            })
830            .collect()
831    }
832
833    fn prune_before(&mut self, minimum: usize) {
834        self.occupied
835            .retain(|coordinate, _| coordinate.position >= minimum);
836    }
837}
838
839impl SemanticStateTransaction for RealtimeFrameScheduleState {
840    type Branch = Self;
841    type Error = RealtimeScheduleError;
842
843    fn branch(&self) -> Result<Self::Branch, Self::Error> {
844        Ok(self.clone())
845    }
846
847    fn commit_branch(&mut self, branch: Self::Branch) -> Result<(), Self::Error> {
848        self.validate_schedule(&branch.schedule)?;
849        *self = branch;
850        Ok(())
851    }
852}
853
854/// Invalid portable frame-schedule transition or state handoff.
855#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
856#[non_exhaustive]
857pub enum RealtimeScheduleError {
858    /// State belongs to a different normalized schedule.
859    #[error("realtime frame schedule state does not match the normalized schedule")]
860    ScheduleMismatch,
861    /// Per-generated-codebook forcing cardinality is invalid.
862    #[error("realtime forcing mask has {actual} audio entries, expected {expected}")]
863    ForcingCount {
864        /// Expected generated-codebook count.
865        expected: usize,
866        /// Actual mask length.
867        actual: usize,
868    },
869    /// A required delayed slot has not been initialized.
870    #[error("realtime delayed slot {coordinate:?} is not occupied")]
871    MissingSlot {
872        /// Missing coordinate.
873        coordinate: RealtimeSlotCoordinate,
874    },
875    /// Frontier plus delay exceeded portable integer coordinates.
876    #[error("realtime frame coordinate overflowed")]
877    CoordinateOverflow,
878}
879
880fn checked_add(left: usize, right: usize) -> Result<usize, RealtimeScheduleError> {
881    left.checked_add(right)
882        .ok_or(RealtimeScheduleError::CoordinateOverflow)
883}
884
885fn coordinate(position: usize, slot: RealtimeFrameSlot) -> RealtimeSlotCoordinate {
886    RealtimeSlotCoordinate::new(position, slot)
887}
888
889fn slots(total_audio_codebooks: usize) -> impl Iterator<Item = RealtimeFrameSlot> {
890    std::iter::once(RealtimeFrameSlot::Text)
891        .chain((0..total_audio_codebooks).map(RealtimeFrameSlot::Audio))
892}
893
894/// Portable sampling controls for one realtime request.
895#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
896pub struct RealtimeSampling {
897    text_temperature: f32,
898    audio_temperature: f32,
899    text_top_k: Option<usize>,
900    audio_top_k: Option<usize>,
901    seed: u64,
902}
903
904impl<'de> Deserialize<'de> for RealtimeSampling {
905    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
906    where
907        D: serde::Deserializer<'de>,
908    {
909        #[derive(Deserialize)]
910        struct Raw {
911            text_temperature: f32,
912            audio_temperature: f32,
913            text_top_k: Option<usize>,
914            audio_top_k: Option<usize>,
915            seed: u64,
916        }
917        let raw = Raw::deserialize(deserializer)?;
918        Self::new(raw.text_temperature, raw.audio_temperature, raw.seed)
919            .and_then(|sampling| sampling.with_top_k(raw.text_top_k, raw.audio_top_k))
920            .map_err(serde::de::Error::custom)
921    }
922}
923
924impl RealtimeSampling {
925    /// Creates validated request-local controls.
926    pub fn new(
927        text_temperature: f32,
928        audio_temperature: f32,
929        seed: u64,
930    ) -> Result<Self, RealtimeConfigError> {
931        if !text_temperature.is_finite()
932            || text_temperature < 0.0
933            || !audio_temperature.is_finite()
934            || audio_temperature < 0.0
935        {
936            return Err(RealtimeConfigError::SamplingTemperature {
937                text: text_temperature,
938                audio: audio_temperature,
939            });
940        }
941        Ok(Self {
942            text_temperature,
943            audio_temperature,
944            text_top_k: None,
945            audio_top_k: None,
946            seed,
947        })
948    }
949
950    /// Applies optional top-k truncation independently to text and audio decisions.
951    pub fn with_top_k(
952        mut self,
953        text_top_k: Option<usize>,
954        audio_top_k: Option<usize>,
955    ) -> Result<Self, RealtimeConfigError> {
956        if text_top_k == Some(0) || audio_top_k == Some(0) {
957            return Err(RealtimeConfigError::SamplingTopK {
958                text: text_top_k,
959                audio: audio_top_k,
960            });
961        }
962        self.text_top_k = text_top_k;
963        self.audio_top_k = audio_top_k;
964        Ok(self)
965    }
966
967    /// Deterministic greedy sampling.
968    pub const fn greedy() -> Self {
969        Self {
970            text_temperature: 0.0,
971            audio_temperature: 0.0,
972            text_top_k: None,
973            audio_top_k: None,
974            seed: 0,
975        }
976    }
977    /// Text sampling temperature.
978    pub const fn text_temperature(self) -> f32 {
979        self.text_temperature
980    }
981    /// Audio sampling temperature.
982    pub const fn audio_temperature(self) -> f32 {
983        self.audio_temperature
984    }
985    /// Optional number of highest-scoring text tokens admitted for sampling.
986    pub const fn text_top_k(self) -> Option<usize> {
987        self.text_top_k
988    }
989    /// Optional number of highest-scoring audio tokens admitted for sampling.
990    pub const fn audio_top_k(self) -> Option<usize> {
991        self.audio_top_k
992    }
993    /// Deterministic root seed interpreted by the selected backend.
994    pub const fn seed(self) -> u64 {
995        self.seed
996    }
997    /// Whether either stream requires stochastic sampling.
998    pub const fn is_stochastic(self) -> bool {
999        self.text_temperature != 0.0 || self.audio_temperature != 0.0
1000    }
1001}
1002
1003impl Default for RealtimeSampling {
1004    fn default() -> Self {
1005        Self::greedy()
1006    }
1007}
1008
1009/// Portable host representation of one realtime input frame.
1010#[derive(Debug, Clone, Eq, PartialEq)]
1011pub struct RealtimeInputFrame {
1012    batch: usize,
1013    input_audio_tokens: Vec<i32>,
1014    forced_generated_audio_tokens: Option<Vec<i32>>,
1015    forced_generated_audio_codebooks: Option<Vec<bool>>,
1016    forced_text_tokens: Option<Vec<i32>>,
1017    retain_diagnostics: bool,
1018}
1019
1020impl RealtimeInputFrame {
1021    /// Creates one batch-major encoded input-audio frame.
1022    pub fn new(batch: usize, input_audio_tokens: Vec<i32>) -> Self {
1023        Self {
1024            batch,
1025            input_audio_tokens,
1026            forced_generated_audio_tokens: None,
1027            forced_generated_audio_codebooks: None,
1028            forced_text_tokens: None,
1029            retain_diagnostics: false,
1030        }
1031    }
1032
1033    /// Forces every generated-audio decision from batch-major token values.
1034    pub fn with_forced_generated_audio(mut self, tokens: Vec<i32>) -> Self {
1035        self.forced_generated_audio_tokens = Some(tokens);
1036        self.forced_generated_audio_codebooks = None;
1037        self
1038    }
1039
1040    /// Forces selected generated-audio codebooks from batch-major token values.
1041    pub fn with_partially_forced_generated_audio(
1042        mut self,
1043        tokens: Vec<i32>,
1044        codebooks: Vec<bool>,
1045    ) -> Self {
1046        self.forced_generated_audio_tokens = Some(tokens);
1047        self.forced_generated_audio_codebooks = Some(codebooks);
1048        self
1049    }
1050
1051    /// Forces one text decision per batch row.
1052    pub fn with_forced_text(mut self, tokens: Vec<i32>) -> Self {
1053        self.forced_text_tokens = Some(tokens);
1054        self
1055    }
1056
1057    /// Requests complete decision logits in the observed step output.
1058    pub fn with_diagnostics(mut self) -> Self {
1059        self.retain_diagnostics = true;
1060        self
1061    }
1062
1063    /// Batch dimension.
1064    pub const fn batch(&self) -> usize {
1065        self.batch
1066    }
1067    /// Batch-major input-audio tokens.
1068    pub fn input_audio_tokens(&self) -> &[i32] {
1069        &self.input_audio_tokens
1070    }
1071    /// Optional batch-major generated-audio forcing tokens.
1072    pub fn forced_generated_audio_tokens(&self) -> Option<&[i32]> {
1073        self.forced_generated_audio_tokens.as_deref()
1074    }
1075    /// Optional generated-codebook forcing mask.
1076    pub fn forced_generated_audio_codebooks(&self) -> Option<&[bool]> {
1077        self.forced_generated_audio_codebooks.as_deref()
1078    }
1079    /// Optional forced text tokens, one per batch row.
1080    pub fn forced_text_tokens(&self) -> Option<&[i32]> {
1081        self.forced_text_tokens.as_deref()
1082    }
1083    /// Whether complete decision diagnostics were requested.
1084    pub const fn retains_diagnostics(&self) -> bool {
1085        self.retain_diagnostics
1086    }
1087}
1088
1089impl WorkDescriptor for RealtimeInputFrame {
1090    type Error = RealtimeInputDescriptorError;
1091
1092    fn encode_descriptor(&self, output: &mut Vec<u32>) -> Result<(), Self::Error> {
1093        output.push(descriptor_len(self.batch)?);
1094        encode_i32_descriptor(&self.input_audio_tokens, output)?;
1095        encode_optional_i32_descriptor(self.forced_generated_audio_tokens.as_deref(), output)?;
1096        match self.forced_generated_audio_codebooks.as_deref() {
1097            Some(mask) => {
1098                output.push(1);
1099                output.push(descriptor_len(mask.len())?);
1100                output.extend(mask.iter().copied().map(u32::from));
1101            }
1102            None => output.push(0),
1103        }
1104        encode_optional_i32_descriptor(self.forced_text_tokens.as_deref(), output)?;
1105        output.push(u32::from(self.retain_diagnostics));
1106        Ok(())
1107    }
1108}
1109
1110fn encode_i32_descriptor(
1111    values: &[i32],
1112    output: &mut Vec<u32>,
1113) -> Result<(), RealtimeInputDescriptorError> {
1114    output.push(descriptor_len(values.len())?);
1115    output.extend(
1116        values
1117            .iter()
1118            .map(|value| u32::from_ne_bytes(value.to_ne_bytes())),
1119    );
1120    Ok(())
1121}
1122
1123fn encode_optional_i32_descriptor(
1124    values: Option<&[i32]>,
1125    output: &mut Vec<u32>,
1126) -> Result<(), RealtimeInputDescriptorError> {
1127    match values {
1128        Some(values) => {
1129            output.push(1);
1130            encode_i32_descriptor(values, output)
1131        }
1132        None => {
1133            output.push(0);
1134            Ok(())
1135        }
1136    }
1137}
1138
1139fn descriptor_len(value: usize) -> Result<u32, RealtimeInputDescriptorError> {
1140    u32::try_from(value).map_err(|_| RealtimeInputDescriptorError { value })
1141}
1142
1143/// A portable realtime work descriptor exceeded its stable wire representation.
1144#[derive(Debug, Clone, Copy, Eq, PartialEq, thiserror::Error)]
1145#[error("realtime input descriptor length {value} exceeds the u32 wire range")]
1146pub struct RealtimeInputDescriptorError {
1147    value: usize,
1148}
1149
1150/// Materialized logits for one ordered realtime decision.
1151#[derive(Debug, Clone, PartialEq)]
1152pub struct RealtimeDecisionDiagnostics {
1153    prediction: usize,
1154    tensor: TensorObservation,
1155}
1156
1157impl RealtimeDecisionDiagnostics {
1158    /// Creates one portable diagnostic observation.
1159    pub fn new(
1160        prediction: usize,
1161        shape: Vec<usize>,
1162        logits: Vec<f32>,
1163    ) -> Result<Self, ObservationError> {
1164        Ok(Self {
1165            prediction,
1166            tensor: TensorObservation::new(shape, TensorObservationData::F32(logits))?,
1167        })
1168    }
1169    /// Decision ordinal in text-then-depth order.
1170    pub const fn prediction(&self) -> usize {
1171        self.prediction
1172    }
1173    /// Complete materialized logits shape.
1174    pub fn shape(&self) -> &[usize] {
1175        self.tensor.shape()
1176    }
1177    /// Complete row-major logits values.
1178    pub fn logits(&self) -> &[f32] {
1179        let TensorObservationData::F32(values) = self.tensor.data() else {
1180            unreachable!("realtime diagnostics are constructed from F32 values")
1181        };
1182        values
1183    }
1184
1185    /// General portable tensor observation for this decision.
1186    pub const fn tensor(&self) -> &TensorObservation {
1187        &self.tensor
1188    }
1189}
1190
1191/// Portable host observation of one completed realtime output frame.
1192#[derive(Debug, Clone, PartialEq)]
1193pub struct RealtimeOutputFrame {
1194    batch: usize,
1195    text_tokens: Vec<i32>,
1196    decision_audio_tokens: Vec<i32>,
1197    sampled_audio_tokens: Vec<i32>,
1198    output_audio_tokens: Option<Vec<i32>>,
1199    diagnostics: Vec<RealtimeDecisionDiagnostics>,
1200}
1201
1202impl RealtimeOutputFrame {
1203    /// Creates a completed host observation in batch-major order.
1204    pub fn new(
1205        batch: usize,
1206        text_tokens: Vec<i32>,
1207        decision_audio_tokens: Vec<i32>,
1208        sampled_audio_tokens: Vec<i32>,
1209        output_audio_tokens: Option<Vec<i32>>,
1210        diagnostics: Vec<RealtimeDecisionDiagnostics>,
1211    ) -> Self {
1212        Self {
1213            batch,
1214            text_tokens,
1215            decision_audio_tokens,
1216            sampled_audio_tokens,
1217            output_audio_tokens,
1218            diagnostics,
1219        }
1220    }
1221    /// Batch dimension.
1222    pub const fn batch(&self) -> usize {
1223        self.batch
1224    }
1225    /// One sampled text token per batch row.
1226    pub fn text_tokens(&self) -> &[i32] {
1227        &self.text_tokens
1228    }
1229    /// Batch-major audio tokens resolved at every ordered depth decision.
1230    pub fn decision_audio_tokens(&self) -> &[i32] {
1231        &self.decision_audio_tokens
1232    }
1233    /// Batch-major generated-audio tokens resolved for this frame.
1234    pub fn sampled_audio_tokens(&self) -> &[i32] {
1235        &self.sampled_audio_tokens
1236    }
1237    /// Optional batch-major delay-aligned output-audio tokens.
1238    pub fn output_audio_tokens(&self) -> Option<&[i32]> {
1239        self.output_audio_tokens.as_deref()
1240    }
1241    /// Ordered decision diagnostics, empty unless explicitly requested.
1242    pub fn diagnostics(&self) -> &[RealtimeDecisionDiagnostics] {
1243        &self.diagnostics
1244    }
1245}
1246
1247impl WorkDescriptor for RealtimeOutputFrame {
1248    type Error = RealtimeInputDescriptorError;
1249
1250    fn encode_descriptor(&self, output: &mut Vec<u32>) -> Result<(), Self::Error> {
1251        output.push(descriptor_len(self.batch)?);
1252        encode_i32_descriptor(&self.text_tokens, output)?;
1253        encode_i32_descriptor(&self.decision_audio_tokens, output)?;
1254        encode_i32_descriptor(&self.sampled_audio_tokens, output)?;
1255        encode_optional_i32_descriptor(self.output_audio_tokens.as_deref(), output)?;
1256        output.push(descriptor_len(self.diagnostics.len())?);
1257        for diagnostic in &self.diagnostics {
1258            output.push(descriptor_len(diagnostic.prediction())?);
1259            output.push(descriptor_len(diagnostic.shape().len())?);
1260            for &dimension in diagnostic.shape() {
1261                output.push(descriptor_len(dimension)?);
1262            }
1263            output.push(descriptor_len(diagnostic.logits().len())?);
1264            output.extend(diagnostic.logits().iter().map(|value| value.to_bits()));
1265        }
1266        Ok(())
1267    }
1268}
1269
1270/// Realtime coordination failure with structured execution context.
1271#[derive(Debug, thiserror::Error)]
1272#[non_exhaustive]
1273pub enum RealtimeError<E: std::error::Error + 'static> {
1274    /// Selected mechanisms rejected model, session, input, or execution.
1275    #[error("realtime execution failed: {0}")]
1276    Execution(#[source] E),
1277    /// Generic scheduler lifecycle or capacity failure.
1278    #[error(transparent)]
1279    Scheduler(#[from] SchedulerError),
1280    /// A runtime or released session belongs to a different selected realization.
1281    #[error("realtime model {component} does not match the scheduler model")]
1282    ModelMismatch {
1283        /// Backend-defined identity component that differs.
1284        component: String,
1285    },
1286    /// A bounded drain must permit at least one frame.
1287    #[error("realtime scheduler frame bound must be positive")]
1288    EmptyRunBound,
1289    /// Sampling cannot change while accepted frames still use the prior state.
1290    #[error("realtime request {request} has {queued} queued frames; drain or cancel them before changing sampling")]
1291    SamplingWhileQueued {
1292        /// Request identity.
1293        request: u64,
1294        /// Accepted queued frames.
1295        queued: usize,
1296    },
1297    /// At least one submitted transition failed asynchronously.
1298    #[error("realtime work {work:?} failed asynchronously: {message}")]
1299    Asynchronous {
1300        /// Failed work identity.
1301        work: WorkId,
1302        /// Scheduler-provided failure context.
1303        message: String,
1304    },
1305}
1306
1307#[cfg(test)]
1308mod tests {
1309    use super::*;
1310    #[test]
1311    fn sampling_and_speech_config_validate_portably() {
1312        assert!(RealtimeSampling::new(f32::NAN, 0.0, 0).is_err());
1313        let config = RealtimeSpeechConfig::new(
1314            4,
1315            2,
1316            2,
1317            3,
1318            11,
1319            12,
1320            RealtimeFrameConvention::AbsoluteDelayedSlots,
1321            vec![2, 0, 1, 2, 3],
1322        )
1323        .unwrap();
1324        assert_eq!(config.max_audio_delay(), 3);
1325        assert_eq!(config.max_delay(), 3);
1326        assert_eq!(config.text_delay(), 2);
1327        assert_eq!(config.generated_audio_codebooks(), 2);
1328        assert_eq!(
1329            serde_json::from_str::<RealtimeSpeechConfig>(&serde_json::to_string(&config).unwrap())
1330                .unwrap(),
1331            config
1332        );
1333        let sampling = RealtimeSampling::new(0.7, 0.9, 42)
1334            .unwrap()
1335            .with_top_k(Some(25), Some(250))
1336            .unwrap();
1337        assert_eq!(sampling.text_top_k(), Some(25));
1338        assert_eq!(sampling.audio_top_k(), Some(250));
1339        assert!(RealtimeSampling::greedy()
1340            .with_top_k(Some(0), None)
1341            .is_err());
1342        assert_eq!(
1343            serde_json::from_str::<RealtimeSampling>(&serde_json::to_string(&sampling).unwrap())
1344                .unwrap(),
1345            sampling
1346        );
1347        let frame = RealtimeInputFrame::new(1, vec![1, 2])
1348            .with_forced_generated_audio(vec![3, 4])
1349            .with_forced_text(vec![5])
1350            .with_diagnostics();
1351        assert_eq!(frame.input_audio_tokens(), [1, 2]);
1352        assert_eq!(frame.forced_generated_audio_tokens(), Some(&[3, 4][..]));
1353        assert_eq!(frame.forced_text_tokens(), Some(&[5][..]));
1354        assert!(frame.retains_diagnostics());
1355        assert!(RealtimeSpeechConfig::new(
1356            4,
1357            1,
1358            1,
1359            1,
1360            0,
1361            0,
1362            RealtimeFrameConvention::FeedbackAlignedHistory,
1363            vec![0; 5],
1364        )
1365        .is_err());
1366        assert!(RealtimeSpeechConfig::new(
1367            1,
1368            0,
1369            1,
1370            1,
1371            -1,
1372            0,
1373            RealtimeFrameConvention::FeedbackAlignedHistory,
1374            vec![0; 2],
1375        )
1376        .is_err());
1377        assert!(RealtimeSpeechConfig::new(
1378            1,
1379            0,
1380            1,
1381            1,
1382            0,
1383            0,
1384            RealtimeFrameConvention::FeedbackAlignedHistory,
1385            vec![0, MAX_REALTIME_FRAME_DELAY + 1],
1386        )
1387        .is_err());
1388    }
1389
1390    fn released_schedule(
1391        convention: RealtimeFrameConvention,
1392        depth_audio_codebooks: usize,
1393    ) -> RealtimeSpeechConfig {
1394        RealtimeSpeechConfig::new(
1395            16,
1396            8,
1397            8,
1398            depth_audio_codebooks,
1399            32_000,
1400            2_048,
1401            convention,
1402            vec![0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1],
1403        )
1404        .unwrap()
1405    }
1406
1407    #[test]
1408    fn released_feedback_schedule_covers_warmup_forcing_and_output_alignment() {
1409        let config = released_schedule(RealtimeFrameConvention::FeedbackAlignedHistory, 8);
1410        let mut state = RealtimeFrameScheduleState::new(config.clone());
1411        let first = state
1412            .advance(&config, &RealtimeFrameForcing::none(&config))
1413            .unwrap();
1414        assert!(first.model_call_required());
1415        assert_eq!(first.input_placements().len(), 8);
1416        assert_eq!(first.temporal_inputs().len(), 17);
1417        assert!(first
1418            .temporal_inputs()
1419            .iter()
1420            .all(|source| matches!(source, RealtimeTemporalSource::Padding(_))));
1421        assert_eq!(first.targets().len(), 9);
1422        assert!(first.output().is_none());
1423
1424        let forcing = RealtimeFrameForcing::new(
1425            true,
1426            vec![true, false, false, false, false, false, false, false],
1427        );
1428        let second = state.advance(&config, &forcing).unwrap();
1429        assert_eq!(second.frontier(), 1);
1430        assert_eq!(second.next_frontier(), 2);
1431        assert_eq!(second.output().unwrap().len(), 8);
1432        assert_eq!(
1433            second.targets()[0].source(),
1434            RealtimeTargetSource::Forced(RealtimeForcedSource::CurrentInput)
1435        );
1436        assert_eq!(
1437            second.targets()[1].source(),
1438            RealtimeTargetSource::Forced(RealtimeForcedSource::CurrentInput)
1439        );
1440        assert_eq!(second.targets()[2].source(), RealtimeTargetSource::Sampled);
1441        assert!(matches!(
1442            second.temporal_inputs()[2],
1443            RealtimeTemporalSource::Padding(RealtimeFrameSlot::Audio(1))
1444        ));
1445        assert_eq!(
1446            second.output().unwrap()[0],
1447            RealtimeSlotCoordinate::new(0, RealtimeFrameSlot::Audio(0))
1448        );
1449    }
1450
1451    #[test]
1452    fn released_absolute_schedule_has_initialization_step_and_absolute_targets() {
1453        let config = released_schedule(RealtimeFrameConvention::AbsoluteDelayedSlots, 16);
1454        let mut state = RealtimeFrameScheduleState::new(config.clone());
1455        let initialization = state
1456            .advance(&config, &RealtimeFrameForcing::none(&config))
1457            .unwrap();
1458        assert!(!initialization.model_call_required());
1459        assert!(initialization.temporal_inputs().is_empty());
1460        assert!(initialization.targets().is_empty());
1461        assert_eq!(initialization.warmup_padding().len(), 17);
1462        assert!(initialization.output().is_none());
1463
1464        let forcing = RealtimeFrameForcing::new(
1465            true,
1466            vec![true, true, false, false, false, false, false, false],
1467        );
1468        let first_model = state.advance(&config, &forcing).unwrap();
1469        assert!(first_model.model_call_required());
1470        assert_eq!(first_model.temporal_inputs().len(), 17);
1471        assert!(first_model.temporal_inputs().iter().all(|source| matches!(
1472            source,
1473            RealtimeTemporalSource::Occupied {
1474                occupancy: RealtimeSlotOccupancy::Padding,
1475                ..
1476            }
1477        )));
1478        assert_eq!(first_model.targets().len(), 17);
1479        assert_eq!(
1480            first_model.targets()[0].source(),
1481            RealtimeTargetSource::Forced(RealtimeForcedSource::Retained)
1482        );
1483        assert_eq!(
1484            first_model.targets()[1].source(),
1485            RealtimeTargetSource::Forced(RealtimeForcedSource::Retained)
1486        );
1487        assert_eq!(
1488            first_model.targets()[2].source(),
1489            RealtimeTargetSource::Existing(RealtimeSlotOccupancy::Padding)
1490        );
1491        assert!(first_model.output().is_none());
1492
1493        let second_model = state
1494            .advance(&config, &RealtimeFrameForcing::none(&config))
1495            .unwrap();
1496        assert_eq!(second_model.output().unwrap().len(), 8);
1497        assert_eq!(
1498            second_model.output().unwrap()[0],
1499            RealtimeSlotCoordinate::new(1, RealtimeFrameSlot::Audio(0))
1500        );
1501        assert_eq!(
1502            second_model.output().unwrap()[1],
1503            RealtimeSlotCoordinate::new(2, RealtimeFrameSlot::Audio(1))
1504        );
1505    }
1506
1507    #[test]
1508    fn frame_schedule_branch_commit_and_rollback_are_atomic() {
1509        let config = released_schedule(RealtimeFrameConvention::FeedbackAlignedHistory, 8);
1510        let mut state = RealtimeFrameScheduleState::new(config.clone());
1511        let mut discarded = state.branch().unwrap();
1512        discarded
1513            .advance(&config, &RealtimeFrameForcing::none(&config))
1514            .unwrap();
1515        assert_eq!(state.frontier(), 0);
1516        RealtimeFrameScheduleState::discard_branch(discarded).unwrap();
1517
1518        let mut committed = state.branch().unwrap();
1519        committed
1520            .advance(&config, &RealtimeFrameForcing::none(&config))
1521            .unwrap();
1522        state.commit_branch(committed).unwrap();
1523        assert_eq!(state.frontier(), 1);
1524
1525        let other = released_schedule(RealtimeFrameConvention::AbsoluteDelayedSlots, 16);
1526        assert_eq!(
1527            state.validate_schedule(&other),
1528            Err(RealtimeScheduleError::ScheduleMismatch)
1529        );
1530    }
1531
1532    #[test]
1533    fn frame_schedule_rejects_masks_missing_history_and_coordinate_overflow_atomically() {
1534        let config = released_schedule(RealtimeFrameConvention::AbsoluteDelayedSlots, 16);
1535        let mut state = RealtimeFrameScheduleState::new(config.clone());
1536        assert!(matches!(
1537            state.advance(&config, &RealtimeFrameForcing::new(false, vec![false; 7])),
1538            Err(RealtimeScheduleError::ForcingCount { .. })
1539        ));
1540        assert_eq!(state.frontier(), 0);
1541
1542        let before = state.clone();
1543        state.frontier = usize::MAX;
1544        let overflow_before = state.clone();
1545        assert_eq!(
1546            state.advance(&config, &RealtimeFrameForcing::none(&config)),
1547            Err(RealtimeScheduleError::CoordinateOverflow)
1548        );
1549        assert_eq!(state, overflow_before);
1550
1551        let mut missing = before;
1552        missing.frontier = 1;
1553        let missing_before = missing.clone();
1554        assert!(matches!(
1555            missing.advance(&config, &RealtimeFrameForcing::none(&config)),
1556            Err(RealtimeScheduleError::MissingSlot { .. })
1557        ));
1558        assert_eq!(missing, missing_before);
1559    }
1560
1561    #[test]
1562    fn portable_input_frame_has_one_exact_scheduler_descriptor() {
1563        let frame = RealtimeInputFrame::new(1, vec![-1])
1564            .with_partially_forced_generated_audio(vec![5], vec![true])
1565            .with_forced_text(vec![3])
1566            .with_diagnostics();
1567        let mut descriptor = Vec::new();
1568        frame.encode_descriptor(&mut descriptor).unwrap();
1569        assert_eq!(
1570            descriptor,
1571            vec![1, 1, u32::MAX, 1, 1, 5, 1, 1, 1, 1, 1, 3, 1,]
1572        );
1573
1574        let mut changed = Vec::new();
1575        RealtimeInputFrame::new(1, vec![0])
1576            .encode_descriptor(&mut changed)
1577            .unwrap();
1578        assert_ne!(descriptor, changed);
1579    }
1580}