1use 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
12pub trait RealtimeEvaluationDriver {
19 type Error: Error + Send + Sync + 'static;
21
22 fn speech_config(&self) -> &RealtimeSpeechConfig;
24
25 fn start_trace(&mut self, sampling: RealtimeSampling) -> Result<(), Self::Error>;
27
28 fn evaluate_frame(
30 &mut self,
31 frame: RealtimeInputFrame,
32 ) -> Result<RealtimeOutputFrame, Self::Error>;
33
34 fn finish_trace(&mut self) -> Result<(), Self::Error>;
36}
37
38#[derive(Debug, Clone)]
40pub struct RealtimeTrace {
41 batch: usize,
42 generated_audio_codebooks: usize,
43 frames: Vec<RealtimeOutputFrame>,
44}
45
46impl RealtimeTrace {
47 pub const fn batch(&self) -> usize {
49 self.batch
50 }
51
52 pub const fn generated_audio_codebooks(&self) -> usize {
54 self.generated_audio_codebooks
55 }
56
57 pub fn frames(&self) -> &[RealtimeOutputFrame] {
59 &self.frames
60 }
61
62 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 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 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
172pub 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
201pub 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#[derive(Debug, thiserror::Error)]
288pub enum RealtimeTraceError {
289 #[error(transparent)]
291 Evidence(#[from] EvidenceError),
292 #[error("encoded realtime input must have shape [batch, codebooks, frames], got {0:?}")]
294 InputShape(Vec<usize>),
295 #[error("encoded realtime token {0} does not fit I32")]
297 TokenRange(i64),
298 #[error("realtime frame index {0} does not fit I64")]
300 IndexOverflow(usize),
301 #[error("realtime trace batch changed from {expected} to {actual}")]
303 BatchChanged {
304 expected: usize,
306 actual: usize,
308 },
309 #[error("realtime frame has {values} values for batch {batch} and width {width}")]
311 FrameWidth {
312 batch: usize,
314 width: usize,
316 values: usize,
318 },
319 #[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}