Skip to main content

eredu_evaluation/
realtime.rs

1//! Backend-neutral execution and observation of realtime traces.
2
3use eredu_core::{
4    ObservationSet, ObservationValue, RealtimeInputFrame, RealtimeOutputFrame, RealtimeSampling,
5    RealtimeSpeechConfig, TensorObservation, TensorObservationData,
6};
7use eredu_nn::Tensor;
8use std::error::Error;
9
10use crate::{observe_i32_tensor, EvidenceError};
11
12/// Evaluation-owned execution seam for one portable realtime trace.
13///
14/// Implementations are composition adapters: they may drive a native or
15/// reference executable, but evaluation only observes portable frames and
16/// sampling controls. In particular, this contract does not make a concrete
17/// tensor backend responsible for model, session, or scheduling policy.
18pub trait RealtimeEvaluationDriver {
19    /// Driver-specific execution failure.
20    type Error: Error + Send + Sync + 'static;
21
22    /// Portable token geometry used by this executable.
23    fn speech_config(&self) -> &RealtimeSpeechConfig;
24
25    /// Starts a fresh request-local trace with the supplied sampling controls.
26    fn start_trace(&mut self, sampling: RealtimeSampling) -> Result<(), Self::Error>;
27
28    /// Executes and observes one portable input frame.
29    fn evaluate_frame(
30        &mut self,
31        frame: RealtimeInputFrame,
32    ) -> Result<RealtimeOutputFrame, Self::Error>;
33
34    /// Finishes the active trace and releases its request-local state.
35    fn finish_trace(&mut self) -> Result<(), Self::Error>;
36}
37
38/// Completed portable outputs from one realtime request.
39#[derive(Debug, Clone)]
40pub struct RealtimeTrace {
41    batch: usize,
42    generated_audio_codebooks: usize,
43    frames: Vec<RealtimeOutputFrame>,
44}
45
46impl RealtimeTrace {
47    /// Stable batch dimension.
48    pub const fn batch(&self) -> usize {
49        self.batch
50    }
51
52    /// Generated audio codebooks per output frame.
53    pub const fn generated_audio_codebooks(&self) -> usize {
54        self.generated_audio_codebooks
55    }
56
57    /// Completed frames in submission order.
58    pub fn frames(&self) -> &[RealtimeOutputFrame] {
59        &self.frames
60    }
61
62    /// Converts common token streams and per-decision diagnostics to evidence.
63    pub fn observations(&self) -> Result<ObservationSet, RealtimeTraceError> {
64        let mut observations = ObservationSet::new();
65        observations.insert(
66            "trace.text_tokens",
67            ObservationValue::Tensor(integer_tensor(
68                vec![self.batch, self.frames.len()],
69                transpose_frame_values(
70                    self.frames.iter().map(RealtimeOutputFrame::text_tokens),
71                    self.batch,
72                    1,
73                )?,
74            )?),
75        )?;
76        observations.insert(
77            "trace.sampled_audio_tokens",
78            ObservationValue::Tensor(integer_tensor(
79                vec![
80                    self.batch,
81                    self.generated_audio_codebooks,
82                    self.frames.len(),
83                ],
84                transpose_frame_values(
85                    self.frames
86                        .iter()
87                        .map(RealtimeOutputFrame::sampled_audio_tokens),
88                    self.batch,
89                    self.generated_audio_codebooks,
90                )?,
91            )?),
92        )?;
93        let emitted = self
94            .frames
95            .iter()
96            .filter_map(RealtimeOutputFrame::output_audio_tokens)
97            .collect::<Vec<_>>();
98        observations.insert(
99            "trace.output_audio_tokens",
100            ObservationValue::Tensor(integer_tensor(
101                vec![self.batch, self.generated_audio_codebooks, emitted.len()],
102                transpose_frame_values(
103                    emitted.iter().copied(),
104                    self.batch,
105                    self.generated_audio_codebooks,
106                )?,
107            )?),
108        )?;
109        for (frame, output) in self.frames.iter().enumerate() {
110            for diagnostic in output.diagnostics() {
111                observations.insert(
112                    format!(
113                        "frames.{frame}.decisions.{}.logits",
114                        diagnostic.prediction()
115                    ),
116                    ObservationValue::Tensor(diagnostic.tensor().clone()),
117                )?;
118            }
119        }
120        Ok(observations)
121    }
122
123    /// Stacks text followed by sampled-audio tokens as `[batch, width, frames]`.
124    pub fn combined_sampled_tokens(
125        &self,
126        skip_frames: usize,
127    ) -> Result<TensorObservation, RealtimeTraceError> {
128        let frames = self.frames.get(skip_frames..).unwrap_or_default();
129        let width = self.generated_audio_codebooks + 1;
130        let mut values = Vec::with_capacity(self.batch * width * frames.len());
131        for batch_index in 0..self.batch {
132            for value_index in 0..width {
133                for frame in frames {
134                    let value = if value_index == 0 {
135                        *frame.text_tokens().get(batch_index).ok_or(
136                            RealtimeTraceError::FrameWidth {
137                                batch: self.batch,
138                                width: 1,
139                                values: frame.text_tokens().len(),
140                            },
141                        )?
142                    } else {
143                        *frame
144                            .sampled_audio_tokens()
145                            .get(batch_index * self.generated_audio_codebooks + value_index - 1)
146                            .ok_or(RealtimeTraceError::FrameWidth {
147                                batch: self.batch,
148                                width: self.generated_audio_codebooks,
149                                values: frame.sampled_audio_tokens().len(),
150                            })?
151                    };
152                    values.push(i64::from(value));
153                }
154            }
155        }
156        integer_tensor(vec![self.batch, width, frames.len()], values)
157    }
158
159    /// Frame indices at which delay-aligned output audio was emitted.
160    pub fn emitted_frame_indices(&self) -> Result<TensorObservation, RealtimeTraceError> {
161        let values = self
162            .frames
163            .iter()
164            .enumerate()
165            .filter_map(|(index, frame)| frame.output_audio_tokens().is_some().then_some(index))
166            .map(|index| i64::try_from(index).map_err(|_| RealtimeTraceError::IndexOverflow(index)))
167            .collect::<Result<Vec<_>, _>>()?;
168        integer_tensor(vec![values.len()], values)
169    }
170}
171
172/// Converts neutral `[batch, codebooks, frames]` integer tokens to frame inputs.
173pub fn encoded_audio_frames<T: Tensor>(
174    tokens: &T,
175    context: &T::Context,
176) -> Result<Vec<RealtimeInputFrame>, RealtimeTraceError> {
177    let observed = observe_i32_tensor(tokens, context)?;
178    let [batch, codebooks, frames] = observed.shape() else {
179        return Err(RealtimeTraceError::InputShape(observed.shape().to_vec()));
180    };
181    let TensorObservationData::I64(values) = observed.data() else {
182        unreachable!("observe_i32_tensor always produces I64 host values")
183    };
184    (0..*frames)
185        .map(|frame| {
186            let mut frame_tokens = Vec::with_capacity(batch * codebooks);
187            for batch_index in 0..*batch {
188                for codebook in 0..*codebooks {
189                    let index = (batch_index * codebooks + codebook) * frames + frame;
190                    frame_tokens.push(
191                        i32::try_from(values[index])
192                            .map_err(|_| RealtimeTraceError::TokenRange(values[index]))?,
193                    );
194                }
195            }
196            Ok(RealtimeInputFrame::new(*batch, frame_tokens))
197        })
198        .collect()
199}
200
201/// Executes portable encoded frames through an evaluation driver.
202pub fn run_realtime_trace<D>(
203    driver: &mut D,
204    inputs: impl IntoIterator<Item = RealtimeInputFrame>,
205    sampling: RealtimeSampling,
206) -> Result<RealtimeTrace, Box<dyn std::error::Error + Send + Sync>>
207where
208    D: RealtimeEvaluationDriver,
209{
210    driver
211        .start_trace(sampling)
212        .map_err(boxed_driver_error::<D::Error>)?;
213    let mut batch = None;
214    let mut frames = Vec::new();
215    for frame in inputs {
216        match batch {
217            Some(expected) if expected != frame.batch() => {
218                return Err(Box::new(RealtimeTraceError::BatchChanged {
219                    expected,
220                    actual: frame.batch(),
221                }));
222            }
223            None => batch = Some(frame.batch()),
224            _ => {}
225        }
226        frames.push(
227            driver
228                .evaluate_frame(frame)
229                .map_err(boxed_driver_error::<D::Error>)?,
230        );
231    }
232    let generated_audio_codebooks = driver.speech_config().generated_audio_codebooks();
233    driver
234        .finish_trace()
235        .map_err(boxed_driver_error::<D::Error>)?;
236    Ok(RealtimeTrace {
237        batch: batch.unwrap_or(1),
238        generated_audio_codebooks,
239        frames,
240    })
241}
242
243fn boxed_driver_error<E>(error: E) -> Box<dyn Error + Send + Sync>
244where
245    E: Error + Send + Sync + 'static,
246{
247    Box::new(error)
248}
249
250fn transpose_frame_values<'a>(
251    frames: impl IntoIterator<Item = &'a [i32]>,
252    batch: usize,
253    width: usize,
254) -> Result<Vec<i64>, RealtimeTraceError> {
255    let frames = frames.into_iter().collect::<Vec<_>>();
256    for values in &frames {
257        if values.len() != batch.saturating_mul(width) {
258            return Err(RealtimeTraceError::FrameWidth {
259                batch,
260                width,
261                values: values.len(),
262            });
263        }
264    }
265    let mut output = Vec::with_capacity(batch * width * frames.len());
266    for batch_index in 0..batch {
267        for value_index in 0..width {
268            for frame in &frames {
269                output.push(i64::from(frame[batch_index * width + value_index]));
270            }
271        }
272    }
273    Ok(output)
274}
275
276fn integer_tensor(
277    shape: Vec<usize>,
278    values: Vec<i64>,
279) -> Result<TensorObservation, RealtimeTraceError> {
280    Ok(TensorObservation::new(
281        shape,
282        TensorObservationData::I64(values),
283    )?)
284}
285
286/// Invalid portable realtime trace evidence.
287#[derive(Debug, thiserror::Error)]
288pub enum RealtimeTraceError {
289    /// Tensor host observation failed.
290    #[error(transparent)]
291    Evidence(#[from] EvidenceError),
292    /// Encoded input must be `[batch, codebooks, frames]`.
293    #[error("encoded realtime input must have shape [batch, codebooks, frames], got {0:?}")]
294    InputShape(Vec<usize>),
295    /// One portable token does not fit the realtime I32 domain.
296    #[error("encoded realtime token {0} does not fit I32")]
297    TokenRange(i64),
298    /// A frame ordinal cannot be represented in evidence.
299    #[error("realtime frame index {0} does not fit I64")]
300    IndexOverflow(usize),
301    /// Input batch changed within one request.
302    #[error("realtime trace batch changed from {expected} to {actual}")]
303    BatchChanged {
304        /// Initial batch.
305        expected: usize,
306        /// Later batch.
307        actual: usize,
308    },
309    /// A completed frame has incompatible token geometry.
310    #[error("realtime frame has {values} values for batch {batch} and width {width}")]
311    FrameWidth {
312        /// Trace batch.
313        batch: usize,
314        /// Expected values per batch row.
315        width: usize,
316        /// Observed values.
317        values: usize,
318    },
319    /// Portable observation construction failed.
320    #[error(transparent)]
321    Observation(#[from] eredu_core::ObservationError),
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327    use eredu_core::RealtimeFrameConvention;
328
329    #[derive(Debug)]
330    struct RecordingDriver {
331        config: RealtimeSpeechConfig,
332        sampling: Option<RealtimeSampling>,
333        inputs: Vec<RealtimeInputFrame>,
334        finishes: usize,
335    }
336
337    impl RecordingDriver {
338        fn new() -> Self {
339            Self {
340                config: RealtimeSpeechConfig::new(
341                    4,
342                    2,
343                    2,
344                    2,
345                    0,
346                    0,
347                    RealtimeFrameConvention::FeedbackAlignedHistory,
348                    vec![0; 5],
349                )
350                .unwrap(),
351                sampling: None,
352                inputs: Vec::new(),
353                finishes: 0,
354            }
355        }
356    }
357
358    impl RealtimeEvaluationDriver for RecordingDriver {
359        type Error = std::io::Error;
360
361        fn speech_config(&self) -> &RealtimeSpeechConfig {
362            &self.config
363        }
364
365        fn start_trace(&mut self, sampling: RealtimeSampling) -> Result<(), Self::Error> {
366            self.sampling = Some(sampling);
367            Ok(())
368        }
369
370        fn evaluate_frame(
371            &mut self,
372            frame: RealtimeInputFrame,
373        ) -> Result<RealtimeOutputFrame, Self::Error> {
374            let value = i32::try_from(self.inputs.len()).unwrap();
375            let batch = frame.batch();
376            self.inputs.push(frame);
377            Ok(RealtimeOutputFrame::new(
378                batch,
379                vec![value; batch],
380                vec![value; batch * 2],
381                vec![value; batch * 2],
382                Some(vec![value; batch * 2]),
383                Vec::new(),
384            ))
385        }
386
387        fn finish_trace(&mut self) -> Result<(), Self::Error> {
388            self.finishes += 1;
389            Ok(())
390        }
391    }
392
393    #[test]
394    fn trace_runner_uses_only_the_portable_evaluation_driver() {
395        let sampling = RealtimeSampling::new(0.7, 0.8, 42).unwrap();
396        let mut driver = RecordingDriver::new();
397        let trace = run_realtime_trace(
398            &mut driver,
399            [
400                RealtimeInputFrame::new(1, vec![10, 11]),
401                RealtimeInputFrame::new(1, vec![12, 13]),
402            ],
403            sampling,
404        )
405        .unwrap();
406
407        assert_eq!(driver.sampling, Some(sampling));
408        assert_eq!(driver.inputs.len(), 2);
409        assert_eq!(driver.finishes, 1);
410        assert_eq!(trace.batch(), 1);
411        assert_eq!(trace.generated_audio_codebooks(), 2);
412        assert_eq!(trace.frames()[1].text_tokens(), [1]);
413    }
414
415    #[test]
416    fn trace_observations_transpose_frame_major_tokens() {
417        let trace = RealtimeTrace {
418            batch: 1,
419            generated_audio_codebooks: 2,
420            frames: vec![
421                RealtimeOutputFrame::new(1, vec![1], vec![2, 3], vec![2, 3], None, Vec::new()),
422                RealtimeOutputFrame::new(
423                    1,
424                    vec![4],
425                    vec![5, 6],
426                    vec![5, 6],
427                    Some(vec![7, 8]),
428                    Vec::new(),
429                ),
430            ],
431        };
432        let observations = trace.observations().unwrap();
433        let Some(ObservationValue::Tensor(text)) = observations.get("trace.text_tokens") else {
434            panic!("text trace must be a tensor");
435        };
436        assert_eq!(text.shape(), [1, 2]);
437        assert_eq!(text.data(), &TensorObservationData::I64(vec![1, 4]));
438    }
439}