Phase 2 QC found three explicit blockers + a broader sweep needed:
Explicit (QC-named):
- crates/mcp/src/lib.rs:15 — SERVER_NAME public MCP wire identity
- crates/transcription/build.rs:59 — panic message prefix
- crates/llm/tests/content_tags_smoke.rs:7 — docstring -p flag
Swept (string literals + dev env vars + doc comments + test fixtures):
- crates/mcp/src/main.rs — eprintln log prefixes
- src-tauri/src/commands/diagnostics.rs — diagnostic filename + MAGNOTIA_VERSION
- src-tauri/src/commands/audio.rs — recording filename pattern lumotia-<secs>-...wav
- src-tauri/src/commands/fs.rs — test placeholder path
- crates/transcription/src/model_manager.rs — .lumotia-verified marker
- crates/storage/src/database.rs — lumotia-storage-ro-<pid> temp dirs + doc comments
- crates/cloud-providers/src/keystore.rs — LUMOTIA_API_KEY_<PROVIDER> env var
- crates/audio/src/{wav,decode}.rs — lumotia_test_* / lumotia_decode_* test fixtures
- crates/core/src/tuning.rs — LUMOTIA_INFERENCE_THREADS env var
- All MAGNOTIA_LLM_TEST_MODEL / MAGNOTIA_WHISPER_TEST_* env vars
- Doc comments referencing crate names
Excluded (intentional Phase 4/5 scope):
- magnotia_preferences, magnotia_history, magnotia_morning_triage_last_shown
DB setting keys (Phase 5 paths.rs migration)
- magnotia_startup tracing target (Phase 4)
- crates/core/src/paths.rs (Phase 5 wholesale rewrite + migration shim)
cargo build --workspace passes. cargo test --workspace: 330 pass, 0 fail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
152 lines
5.3 KiB
Rust
152 lines
5.3 KiB
Rust
//! Benchmark: load the JFK WAV from disk, transcribe it via whisper-rs.
|
|
//! Reports cold-load time, transcribe time, RTF, peak RSS.
|
|
//!
|
|
//! Gated on env vars so it never runs in CI without setup:
|
|
//! LUMOTIA_WHISPER_TEST_MODEL=/path/to/ggml-tiny.bin
|
|
//! LUMOTIA_WHISPER_TEST_AUDIO=/path/to/jfk.wav
|
|
|
|
use std::env;
|
|
use std::time::Instant;
|
|
|
|
#[test]
|
|
fn jfk_transcription_benchmark() {
|
|
let Ok(model_path) = env::var("LUMOTIA_WHISPER_TEST_MODEL") else {
|
|
eprintln!("LUMOTIA_WHISPER_TEST_MODEL not set — skipping");
|
|
return;
|
|
};
|
|
let Ok(audio_path) = env::var("LUMOTIA_WHISPER_TEST_AUDIO") else {
|
|
eprintln!("LUMOTIA_WHISPER_TEST_AUDIO not set — skipping");
|
|
return;
|
|
};
|
|
|
|
use whisper_rs::{FullParams, SamplingStrategy, WhisperContext, WhisperContextParameters};
|
|
|
|
eprintln!("[bench] loading WAV: {audio_path}");
|
|
let bytes = std::fs::read(&audio_path).expect("read wav");
|
|
// Minimal RIFF/WAV parse: skip the 44-byte canonical header for PCM-16-mono-16kHz.
|
|
// Sanity-check magic bytes + format.
|
|
assert_eq!(&bytes[0..4], b"RIFF", "expected RIFF");
|
|
assert_eq!(&bytes[8..12], b"WAVE", "expected WAVE");
|
|
let sample_rate = u32::from_le_bytes(bytes[24..28].try_into().unwrap());
|
|
let channels = u16::from_le_bytes(bytes[22..24].try_into().unwrap());
|
|
let bits = u16::from_le_bytes(bytes[34..36].try_into().unwrap());
|
|
eprintln!(
|
|
"[bench] wav spec: {} Hz, {} ch, {}-bit",
|
|
sample_rate, channels, bits
|
|
);
|
|
assert_eq!(sample_rate, 16_000, "expected 16 kHz wav");
|
|
assert_eq!(channels, 1, "expected mono");
|
|
assert_eq!(bits, 16, "expected 16-bit PCM");
|
|
|
|
let pcm = &bytes[44..];
|
|
let samples: Vec<f32> = pcm
|
|
.chunks_exact(2)
|
|
.map(|c| i16::from_le_bytes([c[0], c[1]]) as f32 / 32768.0)
|
|
.collect();
|
|
let audio_secs = samples.len() as f64 / sample_rate as f64;
|
|
eprintln!(
|
|
"[bench] audio length: {} samples = {:.2}s",
|
|
samples.len(),
|
|
audio_secs
|
|
);
|
|
|
|
let rss_before_load_kb = read_rss_kb();
|
|
eprintln!(
|
|
"[bench] RSS before model load: {} MB",
|
|
rss_before_load_kb / 1024
|
|
);
|
|
|
|
let load_start = Instant::now();
|
|
let ctx = WhisperContext::new_with_params(&model_path, WhisperContextParameters::default())
|
|
.expect("whisper model load");
|
|
let load_dur = load_start.elapsed();
|
|
eprintln!("[bench] model load: {:.2}s", load_dur.as_secs_f64());
|
|
|
|
let rss_after_load_kb = read_rss_kb();
|
|
eprintln!(
|
|
"[bench] RSS after model load: {} MB (delta +{} MB)",
|
|
rss_after_load_kb / 1024,
|
|
(rss_after_load_kb.saturating_sub(rss_before_load_kb)) / 1024
|
|
);
|
|
|
|
let mut state = ctx.create_state().expect("whisper state");
|
|
let mut params = FullParams::new(SamplingStrategy::Greedy { best_of: 1 });
|
|
params.set_language(Some("en"));
|
|
params.set_n_threads(6);
|
|
params.set_print_special(false);
|
|
params.set_print_progress(false);
|
|
params.set_print_realtime(false);
|
|
|
|
// Cold transcription (first run)
|
|
let cold_start = Instant::now();
|
|
state.full(params, &samples).expect("transcribe cold");
|
|
let cold_dur = cold_start.elapsed();
|
|
|
|
let n = state.full_n_segments();
|
|
let mut full_text = String::new();
|
|
for i in 0..n {
|
|
let seg = state.get_segment(i).expect("get_segment");
|
|
full_text.push_str(seg.to_str().unwrap_or(""));
|
|
}
|
|
eprintln!(
|
|
"[bench] cold transcribe: {:.2}s ({} segments, RTF={:.3})",
|
|
cold_dur.as_secs_f64(),
|
|
n,
|
|
cold_dur.as_secs_f64() / audio_secs
|
|
);
|
|
eprintln!("[bench] transcript: {}", full_text.trim());
|
|
let rss_after_cold_kb = read_rss_kb();
|
|
eprintln!("[bench] RSS after cold xc: {} MB", rss_after_cold_kb / 1024);
|
|
|
|
// Warm transcription (second run, same state)
|
|
let mut state2 = ctx.create_state().expect("whisper state 2");
|
|
let mut params2 = FullParams::new(SamplingStrategy::Greedy { best_of: 1 });
|
|
params2.set_language(Some("en"));
|
|
params2.set_n_threads(6);
|
|
params2.set_print_special(false);
|
|
params2.set_print_progress(false);
|
|
params2.set_print_realtime(false);
|
|
let warm_start = Instant::now();
|
|
state2.full(params2, &samples).expect("transcribe warm");
|
|
let warm_dur = warm_start.elapsed();
|
|
eprintln!(
|
|
"[bench] warm transcribe: {:.2}s (RTF={:.3})",
|
|
warm_dur.as_secs_f64(),
|
|
warm_dur.as_secs_f64() / audio_secs
|
|
);
|
|
|
|
let rss_final_kb = read_rss_kb();
|
|
eprintln!("[bench] RSS final: {} MB", rss_final_kb / 1024);
|
|
|
|
eprintln!();
|
|
eprintln!("=== SUMMARY ===");
|
|
eprintln!("audio: {:.2}s", audio_secs);
|
|
eprintln!("model_load: {:.2}s", load_dur.as_secs_f64());
|
|
eprintln!(
|
|
"cold xc: {:.2}s RTF={:.3}",
|
|
cold_dur.as_secs_f64(),
|
|
cold_dur.as_secs_f64() / audio_secs
|
|
);
|
|
eprintln!(
|
|
"warm xc: {:.2}s RTF={:.3}",
|
|
warm_dur.as_secs_f64(),
|
|
warm_dur.as_secs_f64() / audio_secs
|
|
);
|
|
eprintln!("RSS peak: {} MB", rss_final_kb / 1024);
|
|
}
|
|
|
|
fn read_rss_kb() -> u64 {
|
|
let pid = std::process::id();
|
|
let s = std::fs::read_to_string(format!("/proc/{pid}/status")).unwrap_or_default();
|
|
for line in s.lines() {
|
|
if let Some(rest) = line.strip_prefix("VmRSS:") {
|
|
return rest
|
|
.split_whitespace()
|
|
.next()
|
|
.and_then(|n| n.parse::<u64>().ok())
|
|
.unwrap_or(0);
|
|
}
|
|
}
|
|
0
|
|
}
|