Skip to content
13 changes: 13 additions & 0 deletions crates/app/src/bin/mic_probe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ use coldvox_app::probes::{
};
use std::path::PathBuf;
use std::time::Duration;
use tracing_subscriber::{fmt, prelude::*, EnvFilter};

fn init_logging() -> Result<(), Box<dyn std::error::Error>> {
let log_level = std::env::var("RUST_LOG").unwrap_or_else(|_| "debug".to_string());
let env_filter = EnvFilter::try_new(log_level).unwrap_or_else(|_| EnvFilter::new("debug"));

tracing_subscriber::registry()
.with(env_filter)
.with(fmt::layer().with_writer(std::io::stderr))
.init();
Ok(())
}

#[derive(Parser)]
#[command(name = "mic-probe")]
Expand Down Expand Up @@ -49,6 +61,7 @@ enum Commands {
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
init_logging()?;

match cli.command {
Commands::MicCapture => run_single_test(&cli, TestType::MicCapture).await,
Expand Down
9 changes: 7 additions & 2 deletions crates/app/src/bin/tui_dashboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ async fn run_app(
activation_mode: state.activation_mode,
resampler_quality: state.resampler_quality,
stt_selection: Some(coldvox_stt::plugin::PluginSelectionConfig::default()),
enable_device_monitor: false,
#[cfg(feature = "text-injection")]
injection: None,
};
Expand Down Expand Up @@ -1123,7 +1124,7 @@ fn draw_logs(f: &mut Frame, area: Rect, state: &DashboardState) {
f.render_widget(paragraph, inner);
}

fn draw_plugins(f: &mut Frame, area: Rect, state: &DashboardState) {
fn draw_plugins(f: &mut Frame, area: Rect, #[allow(unused_variables)] state: &DashboardState) {
let block = Block::default()
.title("Available Plugins")
.borders(Borders::ALL);
Expand Down Expand Up @@ -1168,7 +1169,11 @@ fn draw_plugins(f: &mut Frame, area: Rect, state: &DashboardState) {
f.render_widget(paragraph, inner);
}

fn draw_plugin_status(f: &mut Frame, area: Rect, state: &DashboardState) {
fn draw_plugin_status(
f: &mut Frame,
area: Rect,
#[allow(unused_variables)] state: &DashboardState,
) {
let block = Block::default()
.title("Plugin Status")
.borders(Borders::ALL);
Expand Down
3 changes: 2 additions & 1 deletion crates/app/src/probes/mic_capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ impl MicCaptureCheck {
// Prepare ring buffer and spawn capture thread
let rb = AudioRingBuffer::new(16_384);
let (audio_producer, audio_consumer) = rb.split();
let audio_producer = Arc::new(parking_lot::Mutex::new(audio_producer));
let (capture_thread, dev_cfg, _config_rx, _device_event_rx) =
AudioCaptureThread::spawn(config, audio_producer, device_name).map_err(|e| {
AudioCaptureThread::spawn(config, audio_producer, device_name, false).map_err(|e| {
TestError {
kind: match e {
AudioError::DeviceNotFound { .. } => TestErrorKind::Device,
Expand Down
71 changes: 54 additions & 17 deletions crates/app/src/probes/vad_mic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,20 @@ pub struct VadMicCheck;

impl VadMicCheck {
pub async fn run(ctx: &TestContext) -> Result<LiveTestResult, TestError> {
let device_name = ctx.device.clone();
// HARDCODED: Always use HyperX QuadCast for now to bypass broken device detection
let device_name = Some("HyperX QuadCast".to_string());
Copy link

Copilot AI Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded device name creates a maintenance burden and will fail on systems without this specific device. Consider adding a fallback mechanism or making this configurable through environment variables or test parameters.

Copilot uses AI. Check for mistakes.
let duration = ctx.duration;

Comment on lines 23 to 33

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Ignore user-specified microphone in VAD test

The VAD microphone test now unconditionally assigns device_name to the string "HyperX QuadCast" and logs this choice, bypassing ctx.device which is provided from the CLI or caller. After this change, running mic-probe --device <name> cannot target the requested device; the code will always prefer the hard-coded model and only fall back to the internal priority list if that device cannot be opened. This makes the probe unusable on systems without the specified microphone and prevents testing arbitrary devices. The test should respect ctx.device and only fall back to defaults when no device is supplied or when the requested device fails.

Useful? React with 👍 / 👎.

tracing::info!("VAD Mic Test: Hardcoded to use HyperX QuadCast device");

let config = AudioConfig::default();

// Prepare ring buffer and spawn capture thread
let rb = AudioRingBuffer::new(16_384);
let (audio_producer, audio_consumer) = rb.split();
let (capture_thread, dev_cfg, _config_rx, _device_event_rx) =
AudioCaptureThread::spawn(config, audio_producer, device_name).map_err(|e| {
let audio_producer = Arc::new(parking_lot::Mutex::new(audio_producer));
let (capture_thread, dev_cfg, device_cfg_rx, _device_event_rx) =
AudioCaptureThread::spawn(config, audio_producer, device_name, false).map_err(|e| {
TestError {
kind: TestErrorKind::Setup,
message: format!("Failed to create audio capture thread: {}", e),
Expand All @@ -42,10 +46,10 @@ impl VadMicCheck {
// Create metrics for this test instance
let metrics = Arc::new(PipelineMetrics::default());

// Periodic metrics logging every 30s
// Periodic metrics logging every 2s (short tests)
let metrics_clone = metrics.clone();
let log_handle = tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(30));
let mut interval = tokio::time::interval(Duration::from_secs(2));
Copy link

Copilot AI Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logging interval change from 30 seconds to 2 seconds significantly increases log output frequency. This magic number should be configurable or documented as to why 2 seconds is optimal for short tests.

Copilot uses AI. Check for mistakes.
loop {
interval.tick().await;
let cap_fps = metrics_clone
Expand Down Expand Up @@ -89,14 +93,18 @@ impl VadMicCheck {
Some(metrics.clone()),
);
let chunker = AudioChunker::new(frame_reader, audio_tx.clone(), chunker_cfg)
.with_metrics(metrics.clone());
.with_metrics(metrics.clone())
.with_device_config(device_cfg_rx);
let chunker_handle = chunker.spawn();

let vad_cfg = UnifiedVadConfig {
mode: VadMode::Silero,
silero: coldvox_vad::config::SileroConfig {
threshold: 0.2,
..Default::default()
},
frame_size_samples: 512,
sample_rate_hz: 16000, // Silero requires 16kHz - resampler will handle conversion
..Default::default()
};

let vad_audio_rx = audio_tx.subscribe();
Expand Down Expand Up @@ -149,35 +157,64 @@ impl VadMicCheck {
let elapsed = start_time.elapsed();

// Calculate metrics
let mut metrics = HashMap::new();
metrics.insert("vad_events_count".to_string(), json!(vad_events.len()));
metrics.insert("speech_segments".to_string(), json!(speech_segments));
metrics.insert(
let mut result_metrics = HashMap::new();
result_metrics.insert("vad_events_count".to_string(), json!(vad_events.len()));
result_metrics.insert("speech_segments".to_string(), json!(speech_segments));
result_metrics.insert(
"total_speech_duration_ms".to_string(),
json!(total_speech_duration_ms),
);
metrics.insert(
result_metrics.insert(
"test_duration_secs".to_string(),
json!(elapsed.as_secs_f64()),
);
metrics.insert("device_sample_rate".to_string(), json!(dev_cfg.sample_rate));
metrics.insert("device_channels".to_string(), json!(dev_cfg.channels));
result_metrics.insert("device_sample_rate".to_string(), json!(dev_cfg.sample_rate));
result_metrics.insert("device_channels".to_string(), json!(dev_cfg.channels));
// Runtime FPS/buffer metrics snapshot
result_metrics.insert(
"capture_fps".to_string(),
json!(metrics
.capture_fps
.load(std::sync::atomic::Ordering::Relaxed)),
);
result_metrics.insert(
"chunker_fps".to_string(),
json!(metrics
.chunker_fps
.load(std::sync::atomic::Ordering::Relaxed)),
);
result_metrics.insert(
"vad_fps".to_string(),
json!(metrics.vad_fps.load(std::sync::atomic::Ordering::Relaxed)),
);
result_metrics.insert(
"capture_buffer_fill".to_string(),
json!(metrics
.capture_buffer_fill
.load(std::sync::atomic::Ordering::Relaxed)),
);
result_metrics.insert(
"chunker_buffer_fill".to_string(),
json!(metrics
.chunker_buffer_fill
.load(std::sync::atomic::Ordering::Relaxed)),
);

// Calculate speech ratio
let speech_ratio = if elapsed.as_millis() > 0 {
total_speech_duration_ms as f64 / elapsed.as_millis() as f64
} else {
0.0
};
metrics.insert("speech_ratio".to_string(), json!(speech_ratio));
result_metrics.insert("speech_ratio".to_string(), json!(speech_ratio));

// Evaluate results
let (pass, notes) = evaluate_vad_performance(&metrics, &vad_events);
let (pass, notes) = evaluate_vad_performance(&result_metrics, &vad_events);

Ok(LiveTestResult {
test: "vad_mic".to_string(),
pass,
metrics,
metrics: result_metrics,
notes: Some(notes),
artifacts: vec![],
})
Expand Down
2 changes: 2 additions & 0 deletions crates/coldvox-telemetry/src/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ impl SttMetricsManager {
.record_engine_processing_time(engine_processing_time);
self.metrics.record_transcription_success();
self.metrics.record_final_transcription();
self.metrics.increment_total_requests();

if let Some(confidence) = confidence_score {
self.metrics.record_confidence_score(confidence);
Expand All @@ -119,6 +120,7 @@ impl SttMetricsManager {
pub fn record_failed_transcription(&self, error_latency: Option<Duration>) {
self.metrics.record_transcription_failure();
self.metrics.record_error();
self.metrics.increment_total_requests();

if let Some(latency) = error_latency {
self.metrics.record_end_to_end_latency(latency);
Expand Down
6 changes: 6 additions & 0 deletions crates/coldvox-telemetry/src/stt_metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,12 @@ impl SttPerformanceMetrics {
.fetch_add(1, Ordering::Relaxed);
}

pub fn increment_total_requests(&self) {
self.operational
.requests_per_second
.fetch_add(1, Ordering::Relaxed);
}

/// Record processing error
pub fn record_error(&self) {
self.operational
Expand Down
Loading