use super::app::{WhisperApp, WhisperPanel, WhisperState};
use super::visualization::{render_attention_heatmap, render_mel_spectrogram, render_waveform};
use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Cell, Paragraph, Row, Table, Tabs},
Frame,
};
fn render_data_table(
f: &mut Frame<'_>,
area: Rect,
headers: &[&str],
widths: &[u16],
rows: Vec<Row<'_>>,
) {
let header_cells: Vec<Cell<'_>> = headers.iter().map(|&h| Cell::from(h)).collect();
let header = Row::new(header_cells)
.style(Style::default().add_modifier(Modifier::BOLD))
.bottom_margin(1);
let constraints: Vec<Constraint> = widths.iter().map(|&w| Constraint::Length(w)).collect();
let table = Table::new(rows, constraints)
.header(header)
.block(Block::default());
f.render_widget(table, area);
}
fn render_empty_placeholder<T>(f: &mut Frame<'_>, area: Rect, data: &[T], message: &str) -> bool {
if data.is_empty() {
let text = Paragraph::new(message).style(Style::default().fg(Color::DarkGray));
f.render_widget(text, area);
true
} else {
false
}
}
pub fn render_whisper_dashboard(f: &mut Frame<'_>, app: &WhisperApp) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), Constraint::Min(10), Constraint::Length(3), ])
.split(f.area());
render_tabs(f, app, chunks[0]);
render_main_panel(f, app, chunks[1]);
render_status_bar(f, app, chunks[2]);
}
fn render_tabs(f: &mut Frame<'_>, app: &WhisperApp, area: Rect) {
let titles: Vec<Line<'_>> = WhisperPanel::titles()
.iter()
.map(|t| Line::from(*t))
.collect();
let tabs = Tabs::new(titles)
.block(Block::default().borders(Borders::ALL).title("Navigation"))
.highlight_style(
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
)
.select(app.current_panel.index());
f.render_widget(tabs, area);
}
fn render_main_panel(f: &mut Frame<'_>, app: &WhisperApp, area: Rect) {
match app.current_panel {
WhisperPanel::Waveform => render_waveform_panel(f, app, area),
WhisperPanel::Mel => render_mel_panel(f, app, area),
WhisperPanel::Encoder => render_encoder_panel(f, app, area),
WhisperPanel::Decoder => render_decoder_panel(f, app, area),
WhisperPanel::Attention => render_attention_panel(f, app, area),
WhisperPanel::Transcription => render_transcription_panel(f, app, area),
WhisperPanel::Metrics => render_metrics_panel(f, app, area),
WhisperPanel::Help => render_help_panel(f, app, area),
}
}
fn render_waveform_panel(f: &mut Frame<'_>, app: &WhisperApp, area: Rect) {
let block = Block::default()
.borders(Borders::ALL)
.title("WAVEFORM - Audio Signal");
let inner = block.inner(area);
f.render_widget(block, area);
if render_empty_placeholder(
f,
inner,
&app.audio_data,
"No audio loaded. Load audio to visualize waveform.",
) {
return;
}
let waveform = render_waveform(&app.audio_data, inner.width as usize, inner.height as usize);
let info = format!(
"{} samples @ {}Hz ({:.2}s)",
app.audio_data.len(),
app.sample_rate,
app.metrics.audio_duration_secs
);
let content = format!("{}\n\n{}", info, waveform);
let paragraph = Paragraph::new(content);
f.render_widget(paragraph, inner);
}
fn render_mel_panel(f: &mut Frame<'_>, app: &WhisperApp, area: Rect) {
let block = Block::default()
.borders(Borders::ALL)
.title("MEL SPECTROGRAM - 80 Mel Bins");
let inner = block.inner(area);
f.render_widget(block, area);
if render_empty_placeholder(
f,
inner,
&app.mel_data,
"No mel spectrogram computed. Compute mel to visualize.",
) {
return;
}
let heatmap = render_mel_spectrogram(
&app.mel_data,
80,
app.mel_frames,
inner.width as usize,
inner.height as usize,
);
let info = format!("80 bins x {} frames", app.mel_frames);
let content = format!("{}\n{}", info, heatmap);
let paragraph = Paragraph::new(content);
f.render_widget(paragraph, inner);
}
fn render_encoder_panel(f: &mut Frame<'_>, app: &WhisperApp, area: Rect) {
let block = Block::default()
.borders(Borders::ALL)
.title("ENCODER - Layer Activations");
let inner = block.inner(area);
f.render_widget(block, area);
if render_empty_placeholder(
f,
inner,
&app.encoder_metrics,
"No encoder data. Start encoding to see layer activations.",
) {
return;
}
let rows: Vec<Row<'_>> = app
.encoder_metrics
.iter()
.map(|m| {
Row::new(vec![
Cell::from(format!("Layer {}", m.layer)),
Cell::from(format!("{:.3}", m.mean_activation)),
Cell::from(format!("{:.3}", m.max_activation)),
Cell::from(format!("{:.3}", m.attention_entropy)),
])
})
.collect();
render_data_table(
f,
inner,
&["Layer", "Mean Act.", "Max Act.", "Attn Entropy"],
&[10, 12, 12, 14],
rows,
);
}
fn render_decoder_panel(f: &mut Frame<'_>, app: &WhisperApp, area: Rect) {
let block = Block::default()
.borders(Borders::ALL)
.title("DECODER - Token Generation");
let inner = block.inner(area);
f.render_widget(block, area);
if render_empty_placeholder(
f,
inner,
&app.decoder_tokens,
"No tokens generated. Start decoding to see tokens.",
) {
return;
}
let rows: Vec<Row<'_>> = app
.decoder_tokens
.iter()
.enumerate()
.map(|(i, t)| {
Row::new(vec![
Cell::from(format!("{}", i)),
Cell::from(format!("{}", t.id)),
Cell::from(t.text.clone()),
Cell::from(format!("{:.3}", t.log_prob)),
])
})
.collect();
render_data_table(
f,
inner,
&["Idx", "Token ID", "Text", "Log P"],
&[5, 10, 20, 10],
rows,
);
}
fn render_attention_panel(f: &mut Frame<'_>, app: &WhisperApp, area: Rect) {
let block = Block::default()
.borders(Borders::ALL)
.title("ATTENTION - Cross-Attention Weights");
let inner = block.inner(area);
f.render_widget(block, area);
if render_empty_placeholder(
f,
inner,
&app.attention_weights,
"No attention data. Decode to see cross-attention.",
) {
return;
}
let heatmap = render_attention_heatmap(
&app.attention_weights,
inner.width as usize,
inner.height as usize,
);
let info = format!(
"Cross-attention: {} tokens x {} frames",
app.attention_weights.len(),
app.attention_weights.first().map(|a| a.len()).unwrap_or(0)
);
let content = format!("{}\n{}", info, heatmap);
let paragraph = Paragraph::new(content);
f.render_widget(paragraph, inner);
}
fn render_transcription_panel(f: &mut Frame<'_>, app: &WhisperApp, area: Rect) {
let block = Block::default()
.borders(Borders::ALL)
.title("TRANSCRIPTION - Final Output");
let inner = block.inner(area);
f.render_widget(block, area);
let content = if app.transcription.is_empty() {
"No transcription yet. Complete pipeline to see result.".to_string()
} else {
let mut text = String::new();
text.push_str(&format!("Result: {}\n\n", app.transcription));
text.push_str("Token Details:\n");
for (i, token) in app.decoder_tokens.iter().enumerate() {
text.push_str(&format!(
" [{}] {} (p={:.3})\n",
i,
token.text,
token.log_prob.exp()
));
}
text
};
let paragraph = Paragraph::new(content);
f.render_widget(paragraph, inner);
}
fn render_metrics_panel(f: &mut Frame<'_>, app: &WhisperApp, area: Rect) {
let block = Block::default()
.borders(Borders::ALL)
.title("METRICS - Performance Data");
let inner = block.inner(area);
f.render_widget(block, area);
let m = &app.metrics;
let content = format!(
r#"Pipeline Performance Metrics
Audio Duration: {:.2} seconds
Sample Rate: {} Hz
Mel Frames: {}
Timing Breakdown:
Mel Compute: {:.2} ms
Encoder: {:.2} ms
Decoder: {:.2} ms
─────────────────────
Total: {:.2} ms
RTF (Real-Time Factor): {:.3}x
(< 1.0 = faster than real-time)
Tokens Generated: {}
Tokens/Second: {:.1}
"#,
m.audio_duration_secs,
app.sample_rate,
app.mel_frames,
m.mel_time_ms,
m.encoder_time_ms,
m.decoder_time_ms,
m.total_time_ms,
m.rtf,
m.tokens_generated,
if m.total_time_ms > 0.0 {
m.tokens_generated as f32 / (m.total_time_ms / 1000.0)
} else {
0.0
}
);
let paragraph = Paragraph::new(content);
f.render_widget(paragraph, inner);
}
fn render_help_panel(f: &mut Frame<'_>, _app: &WhisperApp, area: Rect) {
let block = Block::default()
.borders(Borders::ALL)
.title("HELP - Keyboard Shortcuts");
let inner = block.inner(area);
f.render_widget(block, area);
let content = r#"Keyboard Bindings
Panel Navigation:
1 Waveform panel
2 Mel spectrogram panel
3 Encoder panel
4 Decoder panel
5 Attention panel
6 Transcription panel
7 Metrics panel
? This help panel
Controls:
Space Pause/Resume
r Reset pipeline
q Quit
Scroll (where applicable):
←/→ Scroll horizontally
↑/↓ Scroll vertically
References:
Radford et al. (2022) - Whisper architecture
Davis & Mermelstein (1980) - Mel filterbank
Vaswani et al. (2017) - Transformer attention
"#;
let paragraph = Paragraph::new(content);
f.render_widget(paragraph, inner);
}
fn render_status_bar(f: &mut Frame<'_>, app: &WhisperApp, area: Rect) {
let state_color = match app.state {
WhisperState::Idle => Color::DarkGray,
WhisperState::WaveformReady => Color::Blue,
WhisperState::MelReady => Color::Cyan,
WhisperState::Encoding => Color::Yellow,
WhisperState::Decoding => Color::Magenta,
WhisperState::Complete => Color::Green,
WhisperState::Error => Color::Red,
};
let state_text = app.state_description();
let status = app.status_message.as_deref().unwrap_or("");
let paused = if app.paused { " [PAUSED]" } else { "" };
let spans = vec![
Span::styled(
format!(" {} ", state_text),
Style::default().fg(Color::Black).bg(state_color),
),
Span::raw(format!(" {} {}", status, paused)),
];
let paragraph = Paragraph::new(Line::from(spans)).block(Block::default().borders(Borders::ALL));
f.render_widget(paragraph, area);
}