1use crate::{
4 observation::{ObservationError, TensorObservation, TensorObservationData},
5 scheduler::{SchedulerError, SemanticStateTransaction, WorkDescriptor, WorkId},
6};
7use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9
10pub const MAX_REALTIME_FRAME_DELAY: usize = i32::MAX as usize;
16
17#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
19#[serde(rename_all = "snake_case")]
20#[non_exhaustive]
21pub enum RealtimeFrameConvention {
22 FeedbackAlignedHistory,
25 AbsoluteDelayedSlots,
28}
29
30#[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 #[allow(clippy::too_many_arguments)] 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 pub const fn total_audio_codebooks(&self) -> usize {
152 self.total_audio_codebooks
153 }
154 pub const fn input_audio_codebooks(&self) -> usize {
156 self.input_audio_codebooks
157 }
158 pub const fn generated_audio_codebooks(&self) -> usize {
160 self.generated_audio_codebooks
161 }
162 pub const fn depth_audio_codebooks(&self) -> usize {
164 self.depth_audio_codebooks
165 }
166 pub const fn text_padding_token(&self) -> i32 {
168 self.text_padding_token
169 }
170 pub const fn audio_padding_token(&self) -> i32 {
172 self.audio_padding_token
173 }
174 pub const fn frame_convention(&self) -> RealtimeFrameConvention {
176 self.frame_convention
177 }
178 pub fn delays(&self) -> &[usize] {
180 &self.delays
181 }
182 pub fn text_delay(&self) -> usize {
184 self.delays[0]
185 }
186 pub fn audio_delays(&self) -> &[usize] {
188 &self.delays[1..]
189 }
190 pub fn max_delay(&self) -> usize {
192 self.delays.iter().copied().max().unwrap_or(0)
193 }
194 pub fn max_audio_delay(&self) -> usize {
196 self.audio_delays().iter().copied().max().unwrap_or(0)
197 }
198}
199
200#[derive(Debug, Clone, PartialEq, thiserror::Error)]
202#[non_exhaustive]
203pub enum RealtimeConfigError {
204 #[error("realtime codebook geometry must be nonzero")]
206 EmptyCodebookGeometry,
207 #[error(
209 "realtime input ({input}) and generated ({generated}) codebooks do not partition total {total}"
210 )]
211 CodebookPartition {
212 total: usize,
214 input: usize,
216 generated: usize,
218 },
219 #[error(
222 "realtime generated ({generated}), depth ({depth}), and total ({total}) codebooks must satisfy 0 < generated <= depth <= total"
223 )]
224 DepthCodebookGeometry {
225 generated: usize,
227 depth: usize,
229 total: usize,
231 },
232 #[error("realtime text-plus-audio slot count overflowed")]
234 CodebookCountOverflow,
235 #[error("realtime padding tokens must be non-negative, got text={text} audio={audio}")]
237 NegativePaddingToken {
238 text: i32,
240 audio: i32,
242 },
243 #[error("realtime delay schedule has {actual} entries, expected {expected}")]
245 DelayCount {
246 expected: usize,
248 actual: usize,
250 },
251 #[error("realtime delay {delay} at slot {slot} exceeds maximum {maximum}")]
253 DelayOutOfRange {
254 slot: usize,
256 delay: usize,
258 maximum: usize,
260 },
261 #[error(
263 "realtime sampling temperatures must be finite and non-negative, got text={text} audio={audio}"
264 )]
265 SamplingTemperature {
266 text: f32,
268 audio: f32,
270 },
271 #[error(
273 "realtime sampling top-k must be positive when set, got text={text:?} audio={audio:?}"
274 )]
275 SamplingTopK {
276 text: Option<usize>,
278 audio: Option<usize>,
280 },
281}
282
283#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
285#[non_exhaustive]
286pub enum RealtimeFrameSlot {
287 Text,
289 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#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
304pub struct RealtimeSlotCoordinate {
305 position: usize,
306 slot: RealtimeFrameSlot,
307}
308
309impl RealtimeSlotCoordinate {
310 pub const fn new(position: usize, slot: RealtimeFrameSlot) -> Self {
312 Self { position, slot }
313 }
314
315 pub const fn position(self) -> usize {
317 self.position
318 }
319
320 pub const fn slot(self) -> RealtimeFrameSlot {
322 self.slot
323 }
324}
325
326#[derive(Debug, Clone, Copy, Eq, PartialEq)]
330#[non_exhaustive]
331pub enum RealtimeSlotOccupancy {
332 Input,
334 Forced,
336 Padding,
338 Sampled,
340}
341
342#[derive(Debug, Clone, Copy, Eq, PartialEq)]
344#[non_exhaustive]
345pub enum RealtimeTemporalSource {
346 Padding(RealtimeFrameSlot),
348 Occupied {
350 coordinate: RealtimeSlotCoordinate,
352 occupancy: RealtimeSlotOccupancy,
354 },
355}
356
357#[derive(Debug, Clone, Copy, Eq, PartialEq)]
359#[non_exhaustive]
360pub enum RealtimeForcedSource {
361 CurrentInput,
363 Retained,
365}
366
367#[derive(Debug, Clone, Copy, Eq, PartialEq)]
369#[non_exhaustive]
370pub enum RealtimeTargetSource {
371 Forced(RealtimeForcedSource),
373 Sampled,
375 Existing(RealtimeSlotOccupancy),
377}
378
379#[derive(Debug, Clone, Copy, Eq, PartialEq)]
381pub struct RealtimeTargetDecision {
382 slot: RealtimeFrameSlot,
383 coordinate: Option<RealtimeSlotCoordinate>,
384 source: RealtimeTargetSource,
385}
386
387impl RealtimeTargetDecision {
388 pub const fn slot(self) -> RealtimeFrameSlot {
390 self.slot
391 }
392
393 pub const fn coordinate(self) -> Option<RealtimeSlotCoordinate> {
395 self.coordinate
396 }
397
398 pub const fn source(self) -> RealtimeTargetSource {
400 self.source
401 }
402}
403
404#[derive(Debug, Clone, Eq, PartialEq)]
406pub struct RealtimeFrameForcing {
407 text: bool,
408 generated_audio: Vec<bool>,
409}
410
411impl RealtimeFrameForcing {
412 pub fn new(text: bool, generated_audio: Vec<bool>) -> Self {
414 Self {
415 text,
416 generated_audio,
417 }
418 }
419
420 pub fn none(config: &RealtimeSpeechConfig) -> Self {
422 Self::new(false, vec![false; config.generated_audio_codebooks])
423 }
424
425 pub const fn text(&self) -> bool {
427 self.text
428 }
429
430 pub fn generated_audio(&self) -> &[bool] {
432 &self.generated_audio
433 }
434}
435
436#[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 pub const fn frontier(&self) -> usize {
453 self.frontier
454 }
455
456 pub fn input_placements(&self) -> &[RealtimeSlotCoordinate] {
458 &self.input_placements
459 }
460
461 pub fn forced_placements(&self) -> &[RealtimeSlotCoordinate] {
463 &self.forced_placements
464 }
465
466 pub fn warmup_padding(&self) -> &[RealtimeSlotCoordinate] {
468 &self.warmup_padding
469 }
470
471 pub fn temporal_inputs(&self) -> &[RealtimeTemporalSource] {
473 &self.temporal_inputs
474 }
475
476 pub fn targets(&self) -> &[RealtimeTargetDecision] {
478 &self.targets
479 }
480
481 pub fn output(&self) -> Option<&[RealtimeSlotCoordinate]> {
483 self.output.as_deref()
484 }
485
486 pub const fn model_call_required(&self) -> bool {
488 self.model_call_required
489 }
490
491 pub const fn next_frontier(&self) -> usize {
493 self.next_frontier
494 }
495}
496
497#[derive(Debug, Clone, Eq, PartialEq)]
499pub struct RealtimeFrameScheduleState {
500 schedule: RealtimeSpeechConfig,
501 frontier: usize,
502 occupied: BTreeMap<RealtimeSlotCoordinate, RealtimeSlotOccupancy>,
503}
504
505impl RealtimeFrameScheduleState {
506 pub fn new(schedule: RealtimeSpeechConfig) -> Self {
508 Self {
509 schedule,
510 frontier: 0,
511 occupied: BTreeMap::new(),
512 }
513 }
514
515 pub const fn schedule(&self) -> &RealtimeSpeechConfig {
517 &self.schedule
518 }
519
520 pub const fn frontier(&self) -> usize {
522 self.frontier
523 }
524
525 pub fn occupancy(&self, coordinate: RealtimeSlotCoordinate) -> Option<RealtimeSlotOccupancy> {
527 self.occupied.get(&coordinate).copied()
528 }
529
530 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 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#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
856#[non_exhaustive]
857pub enum RealtimeScheduleError {
858 #[error("realtime frame schedule state does not match the normalized schedule")]
860 ScheduleMismatch,
861 #[error("realtime forcing mask has {actual} audio entries, expected {expected}")]
863 ForcingCount {
864 expected: usize,
866 actual: usize,
868 },
869 #[error("realtime delayed slot {coordinate:?} is not occupied")]
871 MissingSlot {
872 coordinate: RealtimeSlotCoordinate,
874 },
875 #[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#[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 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 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 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 pub const fn text_temperature(self) -> f32 {
979 self.text_temperature
980 }
981 pub const fn audio_temperature(self) -> f32 {
983 self.audio_temperature
984 }
985 pub const fn text_top_k(self) -> Option<usize> {
987 self.text_top_k
988 }
989 pub const fn audio_top_k(self) -> Option<usize> {
991 self.audio_top_k
992 }
993 pub const fn seed(self) -> u64 {
995 self.seed
996 }
997 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#[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 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 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 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 pub fn with_forced_text(mut self, tokens: Vec<i32>) -> Self {
1053 self.forced_text_tokens = Some(tokens);
1054 self
1055 }
1056
1057 pub fn with_diagnostics(mut self) -> Self {
1059 self.retain_diagnostics = true;
1060 self
1061 }
1062
1063 pub const fn batch(&self) -> usize {
1065 self.batch
1066 }
1067 pub fn input_audio_tokens(&self) -> &[i32] {
1069 &self.input_audio_tokens
1070 }
1071 pub fn forced_generated_audio_tokens(&self) -> Option<&[i32]> {
1073 self.forced_generated_audio_tokens.as_deref()
1074 }
1075 pub fn forced_generated_audio_codebooks(&self) -> Option<&[bool]> {
1077 self.forced_generated_audio_codebooks.as_deref()
1078 }
1079 pub fn forced_text_tokens(&self) -> Option<&[i32]> {
1081 self.forced_text_tokens.as_deref()
1082 }
1083 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#[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#[derive(Debug, Clone, PartialEq)]
1152pub struct RealtimeDecisionDiagnostics {
1153 prediction: usize,
1154 tensor: TensorObservation,
1155}
1156
1157impl RealtimeDecisionDiagnostics {
1158 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 pub const fn prediction(&self) -> usize {
1171 self.prediction
1172 }
1173 pub fn shape(&self) -> &[usize] {
1175 self.tensor.shape()
1176 }
1177 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 pub const fn tensor(&self) -> &TensorObservation {
1187 &self.tensor
1188 }
1189}
1190
1191#[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 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 pub const fn batch(&self) -> usize {
1223 self.batch
1224 }
1225 pub fn text_tokens(&self) -> &[i32] {
1227 &self.text_tokens
1228 }
1229 pub fn decision_audio_tokens(&self) -> &[i32] {
1231 &self.decision_audio_tokens
1232 }
1233 pub fn sampled_audio_tokens(&self) -> &[i32] {
1235 &self.sampled_audio_tokens
1236 }
1237 pub fn output_audio_tokens(&self) -> Option<&[i32]> {
1239 self.output_audio_tokens.as_deref()
1240 }
1241 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#[derive(Debug, thiserror::Error)]
1272#[non_exhaustive]
1273pub enum RealtimeError<E: std::error::Error + 'static> {
1274 #[error("realtime execution failed: {0}")]
1276 Execution(#[source] E),
1277 #[error(transparent)]
1279 Scheduler(#[from] SchedulerError),
1280 #[error("realtime model {component} does not match the scheduler model")]
1282 ModelMismatch {
1283 component: String,
1285 },
1286 #[error("realtime scheduler frame bound must be positive")]
1288 EmptyRunBound,
1289 #[error("realtime request {request} has {queued} queued frames; drain or cancel them before changing sampling")]
1291 SamplingWhileQueued {
1292 request: u64,
1294 queued: usize,
1296 },
1297 #[error("realtime work {work:?} failed asynchronously: {message}")]
1299 Asynchronous {
1300 work: WorkId,
1302 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}