#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum WhisperPanel {
#[default]
Waveform,
Mel,
Encoder,
Decoder,
Attention,
Transcription,
Metrics,
Help,
}
impl WhisperPanel {
pub fn titles() -> Vec<&'static str> {
vec![
"Waveform [1]",
"Mel [2]",
"Encoder [3]",
"Decoder [4]",
"Attention [5]",
"Transcription [6]",
"Metrics [7]",
"Help [?]",
]
}
pub fn index(self) -> usize {
match self {
Self::Waveform => 0,
Self::Mel => 1,
Self::Encoder => 2,
Self::Decoder => 3,
Self::Attention => 4,
Self::Transcription => 5,
Self::Metrics => 6,
Self::Help => 7,
}
}
pub fn from_index(index: usize) -> Self {
match index {
0 => Self::Waveform,
1 => Self::Mel,
2 => Self::Encoder,
3 => Self::Decoder,
4 => Self::Attention,
5 => Self::Transcription,
6 => Self::Metrics,
_ => Self::Help,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum WhisperState {
#[default]
Idle,
WaveformReady,
MelReady,
Encoding,
Decoding,
Complete,
Error,
}
#[derive(Debug, Clone, Default)]
pub struct EncoderLayerMetrics {
pub layer: usize,
pub mean_activation: f32,
pub max_activation: f32,
pub attention_entropy: f32,
}
#[derive(Debug, Clone)]
pub struct DecoderToken {
pub id: u32,
pub text: String,
pub log_prob: f32,
pub attention_weights: Vec<f32>,
}
#[derive(Debug, Clone, Default)]
pub struct PipelineMetrics {
pub audio_duration_secs: f32,
pub mel_time_ms: f32,
pub encoder_time_ms: f32,
pub decoder_time_ms: f32,
pub total_time_ms: f32,
pub rtf: f32,
pub tokens_generated: usize,
pub memory_bytes: usize,
}
impl PipelineMetrics {
pub fn compute_rtf(&mut self) {
if self.audio_duration_secs > 0.0 {
self.rtf = (self.total_time_ms / 1000.0) / self.audio_duration_secs;
}
}
}
#[derive(Debug, Clone, Default)]
pub struct JidokaAlerts {
pub drift_ms: f32,
pub min_log_prob: f32,
pub max_amplitude: f32,
}
impl JidokaAlerts {
pub fn is_high_drift(&self) -> bool {
self.drift_ms > 500.0
}
pub fn is_low_confidence(&self) -> bool {
self.min_log_prob < -1.0
}
pub fn is_clipping(&self) -> bool {
self.max_amplitude > 1.0
}
pub fn has_alerts(&self) -> bool {
self.is_high_drift() || self.is_low_confidence() || self.is_clipping()
}
}
#[derive(Debug, Clone)]
#[allow(clippy::struct_excessive_bools)] pub struct WhisperApp {
pub current_panel: WhisperPanel,
pub state: WhisperState,
pub should_quit: bool,
pub paused: bool,
pub audio_data: Vec<f32>,
pub sample_rate: u32,
pub mel_data: Vec<f32>,
pub mel_frames: usize,
pub encoder_metrics: Vec<EncoderLayerMetrics>,
pub decoder_tokens: Vec<DecoderToken>,
pub attention_weights: Vec<Vec<f32>>,
pub transcription: String,
pub metrics: PipelineMetrics,
pub alerts: JidokaAlerts,
pub error_message: Option<String>,
pub status_message: Option<String>,
pub scroll_x: usize,
pub scroll_y: usize,
pub selected_layer: usize,
pub show_trace_overlay: bool,
pub show_vad_overlay: bool,
}
impl Default for WhisperApp {
fn default() -> Self {
Self::new()
}
}
impl WhisperApp {
pub fn new() -> Self {
Self {
current_panel: WhisperPanel::Waveform,
state: WhisperState::Idle,
should_quit: false,
paused: false,
audio_data: Vec::new(),
sample_rate: 16000,
mel_data: Vec::new(),
mel_frames: 0,
encoder_metrics: Vec::new(),
decoder_tokens: Vec::new(),
attention_weights: Vec::new(),
transcription: String::new(),
metrics: PipelineMetrics::default(),
alerts: JidokaAlerts::default(),
error_message: None,
status_message: None,
scroll_x: 0,
scroll_y: 0,
selected_layer: 0,
show_trace_overlay: false,
show_vad_overlay: false,
}
}
pub fn set_panel(&mut self, panel: WhisperPanel) {
self.current_panel = panel;
}
fn transition(&mut self, state: WhisperState, message: String) {
self.state = state;
self.status_message = Some(message);
}
fn elapsed_ms(start: std::time::Instant) -> f32 {
start.elapsed().as_secs_f32() * 1000.0
}
pub fn handle_key(&mut self, key: char) {
match key {
'1' => self.current_panel = WhisperPanel::Waveform,
'2' => self.current_panel = WhisperPanel::Mel,
'3' => self.current_panel = WhisperPanel::Encoder,
'4' => self.current_panel = WhisperPanel::Decoder,
'5' => self.current_panel = WhisperPanel::Attention,
'6' => self.current_panel = WhisperPanel::Transcription,
'7' => self.current_panel = WhisperPanel::Metrics,
'?' => self.current_panel = WhisperPanel::Help,
' ' => self.paused = !self.paused,
't' => self.show_trace_overlay = !self.show_trace_overlay,
'v' => self.show_vad_overlay = !self.show_vad_overlay,
'r' => self.reset(),
'q' => self.should_quit = true,
_ => {}
}
}
#[allow(clippy::no_effect_underscore_binding)]
pub fn reset(&mut self) {
let _span = crate::trace_enter!("tui.reset");
self.audio_data.clear();
self.mel_data.clear();
self.mel_frames = 0;
self.encoder_metrics.clear();
self.decoder_tokens.clear();
self.attention_weights.clear();
self.transcription.clear();
self.metrics = PipelineMetrics::default();
self.alerts = JidokaAlerts::default();
self.error_message = None;
self.scroll_x = 0;
self.scroll_y = 0;
self.selected_layer = 0;
self.show_trace_overlay = false;
self.show_vad_overlay = false;
self.transition(WhisperState::Idle, "Reset to idle".to_string());
}
#[allow(clippy::no_effect_underscore_binding)]
pub fn load_audio(&mut self, audio: &[f32]) {
let _span = crate::trace_enter!("tui.load_audio");
self.audio_data = audio.to_vec();
self.metrics.audio_duration_secs = audio.len() as f32 / self.sample_rate as f32;
self.alerts.max_amplitude = audio.iter().map(|s| s.abs()).fold(0.0_f32, f32::max);
self.transition(
WhisperState::WaveformReady,
format!(
"Loaded {} samples ({:.2}s)",
audio.len(),
self.metrics.audio_duration_secs
),
);
}
#[allow(clippy::no_effect_underscore_binding)]
pub fn compute_mel(&mut self) {
const N_MELS: usize = 80;
const HOP_LENGTH: usize = 160;
let _span = crate::trace_enter!("tui.compute_mel");
if self.audio_data.is_empty() {
self.error_message = Some("No audio loaded".to_string());
self.transition(WhisperState::Error, "No audio loaded".to_string());
return;
}
let start = std::time::Instant::now();
let n_frames = (self.audio_data.len() / HOP_LENGTH).max(1);
self.mel_data = vec![0.0; N_MELS * n_frames];
for frame in 0..n_frames {
for mel_bin in 0..N_MELS {
let frame_energy = self
.audio_data
.get(frame * HOP_LENGTH..(frame + 1) * HOP_LENGTH)
.map_or(0.0, |s| s.iter().map(|x| x.powi(2)).sum::<f32>());
let log_energy = (frame_energy + 1e-10).ln();
let freq_weight = 1.0 - (mel_bin as f32 / N_MELS as f32);
self.mel_data[frame * N_MELS + mel_bin] = log_energy * freq_weight;
}
}
self.mel_frames = n_frames;
self.metrics.mel_time_ms = Self::elapsed_ms(start);
self.transition(
WhisperState::MelReady,
format!(
"Computed {n_frames} mel frames in {:.2}ms",
self.metrics.mel_time_ms
),
);
}
#[allow(clippy::no_effect_underscore_binding)]
pub fn start_encoding(&mut self) {
let _span = crate::trace_enter!("tui.start_encoding");
if self.state != WhisperState::MelReady {
return;
}
let start = std::time::Instant::now();
self.encoder_metrics = (0..4)
.map(|layer| EncoderLayerMetrics {
layer,
mean_activation: 0.5 + (layer as f32 * 0.1),
max_activation: 2.0 + (layer as f32 * 0.2),
attention_entropy: 3.5 - (layer as f32 * 0.3),
})
.collect();
self.metrics.encoder_time_ms = Self::elapsed_ms(start);
self.transition(
WhisperState::Encoding,
format!(
"Encoded through {} layers in {:.2}ms",
self.encoder_metrics.len(),
self.metrics.encoder_time_ms,
),
);
}
#[allow(clippy::no_effect_underscore_binding)]
pub fn start_decoding(&mut self) {
let _span = crate::trace_enter!("tui.start_decoding");
if self.state != WhisperState::Encoding {
return;
}
let start = std::time::Instant::now();
let sample_tokens = [
"<|startoftranscript|>",
"<|en|>",
"Hello",
",",
" world",
".",
"<|endoftext|>",
];
self.decoder_tokens = sample_tokens
.iter()
.enumerate()
.map(|(i, text)| {
let attention = (0..self.mel_frames.max(10))
.map(|f| {
let peak = (i as f32 / sample_tokens.len() as f32) * self.mel_frames as f32;
let dist = (f as f32 - peak).abs();
(-dist / 10.0).exp()
})
.collect();
DecoderToken {
id: i as u32 + 50000,
text: text.to_string(),
log_prob: -0.1 - (i as f32 * 0.05),
attention_weights: attention,
}
})
.collect();
self.attention_weights = self
.decoder_tokens
.iter()
.map(|t| t.attention_weights.clone())
.collect();
self.metrics.decoder_time_ms = Self::elapsed_ms(start);
self.metrics.tokens_generated = self.decoder_tokens.len();
self.transition(
WhisperState::Decoding,
format!(
"Generated {} tokens in {:.2}ms",
self.decoder_tokens.len(),
self.metrics.decoder_time_ms,
),
);
}
#[allow(clippy::no_effect_underscore_binding)]
pub fn complete(&mut self) {
let _span = crate::trace_enter!("tui.complete");
if self.state != WhisperState::Decoding {
return;
}
self.transcription = self
.decoder_tokens
.iter()
.filter(|t| !t.text.starts_with("<|"))
.map(|t| t.text.as_str())
.collect::<Vec<_>>()
.join("");
self.metrics.total_time_ms =
self.metrics.mel_time_ms + self.metrics.encoder_time_ms + self.metrics.decoder_time_ms;
self.metrics.compute_rtf();
self.transition(
WhisperState::Complete,
format!(
"Complete: '{}' (RTF: {:.2}x)",
self.transcription.trim(),
self.metrics.rtf,
),
);
}
pub fn state_description(&self) -> &'static str {
match self.state {
WhisperState::Idle => "Idle - Load audio to begin",
WhisperState::WaveformReady => "Waveform ready - Compute mel spectrogram",
WhisperState::MelReady => "Mel ready - Start encoding",
WhisperState::Encoding => "Encoding audio features",
WhisperState::Decoding => "Decoding to text",
WhisperState::Complete => "Transcription complete",
WhisperState::Error => "Error occurred",
}
}
}