use serde::{Deserialize, Serialize};
use super::scoring::{LevelScore, Rating};
use super::Difficulty;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LevelReport {
pub name: String,
pub difficulty: Difficulty,
pub description: String,
pub scenario_count: usize,
pub score: f64,
pub rating: Rating,
pub total_tokens: u64,
pub avg_latency_ms: f64,
pub scores: Vec<LevelScore>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BenchReport {
pub timestamp: String,
pub model: String,
pub endpoint: String,
pub levels: Vec<LevelReport>,
pub overall_score: f64,
pub overall_rating: Rating,
pub total_tokens: u64,
pub total_duration_secs: f64,
}
impl BenchReport {
pub fn to_json(&self) -> serde_json::Result<String> {
serde_json::to_string_pretty(self)
}
pub fn to_markdown(&self) -> String {
let mut md = String::new();
md.push_str(&format!("# VLM Benchmark Report — {} \n", self.model));
md.push_str(&format!(
"**Date**: {} | **Endpoint**: {}\n\n",
self.timestamp, self.endpoint
));
md.push_str("| Level | Difficulty | Score | Rating | Tokens | Avg Latency |\n");
md.push_str("|-------|-----------|-------|--------|--------|-------------|\n");
for level in &self.levels {
md.push_str(&format!(
"| {} | {} | {:.0}% | {} | {} | {:.1}s |\n",
level.name,
level.difficulty,
level.score * 100.0,
rating_with_emoji(level.rating),
format_tokens(level.total_tokens),
level.avg_latency_ms / 1000.0,
));
}
md.push_str(&format!(
"\n**Overall**: {:.0}% — {}\n",
self.overall_score * 100.0,
rating_with_emoji(self.overall_rating),
));
md.push_str(&format!(
"**Total tokens**: {} | **Duration**: {:.1}s\n",
format_tokens(self.total_tokens),
self.total_duration_secs,
));
md
}
pub fn write_to_dir(&self, dir: &std::path::Path) -> anyhow::Result<()> {
std::fs::create_dir_all(dir)?;
let json_path = dir.join("vlm_benchmark_report.json");
let md_path = dir.join("vlm_benchmark_report.md");
std::fs::write(&json_path, self.to_json()?)?;
std::fs::write(&md_path, self.to_markdown())?;
Ok(())
}
}
fn format_tokens(n: u64) -> String {
let s = n.to_string();
let mut result = String::new();
for (i, ch) in s.chars().rev().enumerate() {
if i > 0 && i % 3 == 0 {
result.push(',');
}
result.push(ch);
}
result.chars().rev().collect()
}
fn rating_with_emoji(rating: Rating) -> String {
match rating {
Rating::Bloom => "BLOOM \u{1F338}".into(),
Rating::Grow => "GROW \u{1F33F}".into(),
Rating::Wilt => "WILT \u{1F940}".into(),
Rating::Frost => "FROST \u{2744}\u{FE0F}".into(),
}
}
#[cfg(test)]
#[path = "../../tests/unit/vlm_bench/report/report_test.rs"]
mod tests;