entrenar 0.7.11

Training & Optimization library with autograd, LoRA, quantization, and model merging
Documentation
//! Model card generation for HuggingFace Hub
//!
//! Generates HF-compatible README.md with YAML front matter for model
//! metadata, metrics, and training details.

use crate::eval::evaluator::{EvalResult, Metric};

/// Model card for HuggingFace Hub (README.md with YAML front matter)
#[derive(Clone, Debug)]
pub struct ModelCard {
    /// Model name
    pub model_name: String,
    /// Model description
    pub description: String,
    /// License identifier (SPDX)
    pub license: Option<String>,
    /// Language codes (e.g., "en", "multilingual")
    pub language: Vec<String>,
    /// Tags for discoverability
    pub tags: Vec<String>,
    /// Evaluation metrics (name → value)
    pub metrics: Vec<(String, f64)>,
    /// Training details (free-form markdown)
    pub training_details: Option<String>,
    /// Base model repository ID
    pub base_model: Option<String>,
}

impl ModelCard {
    /// Create a model card from an `EvalResult`
    #[must_use]
    pub fn from_eval_result(result: &EvalResult) -> Self {
        let metrics: Vec<(String, f64)> = result
            .scores
            .iter()
            .map(|(metric, &value)| (metric_to_yaml_key(metric), value))
            .collect();

        Self {
            model_name: result.model_name.clone(),
            description: format!("Model: {}", result.model_name),
            license: None,
            language: Vec::new(),
            tags: vec!["entrenar".to_string()],
            metrics,
            training_details: None,
            base_model: None,
        }
    }

    /// Render the model card as markdown with YAML front matter
    #[must_use]
    pub fn to_markdown(&self) -> String {
        let mut out = String::new();
        out.push_str("---\n");

        // YAML front matter
        if let Some(license) = &self.license {
            out.push_str(&format!("license: {license}\n"));
        }

        if !self.language.is_empty() {
            out.push_str("language:\n");
            for lang in &self.language {
                out.push_str(&format!("- {lang}\n"));
            }
        }

        if !self.tags.is_empty() {
            out.push_str("tags:\n");
            for tag in &self.tags {
                out.push_str(&format!("- {tag}\n"));
            }
        }

        if let Some(base) = &self.base_model {
            out.push_str(&format!("base_model: {base}\n"));
        }

        if !self.metrics.is_empty() {
            out.push_str("model-index:\n");
            out.push_str(&format!("- name: {}\n", self.model_name));
            out.push_str("  results:\n");
            out.push_str("  - metrics:\n");
            for (name, value) in &self.metrics {
                out.push_str(&format!("    - type: {name}\n"));
                out.push_str(&format!("      value: {value}\n"));
            }
        }

        out.push_str("---\n\n");

        // Markdown body
        out.push_str(&format!("# {}\n\n", self.model_name));
        out.push_str(&format!("{}\n\n", self.description));

        if !self.metrics.is_empty() {
            out.push_str("## Evaluation Results\n\n");
            out.push_str("| Metric | Value |\n");
            out.push_str("|--------|-------|\n");
            for (name, value) in &self.metrics {
                out.push_str(&format!("| {name} | {value:.4} |\n"));
            }
            out.push('\n');
        }

        if let Some(details) = &self.training_details {
            out.push_str("## Training Details\n\n");
            out.push_str(details);
            out.push('\n');
        }

        out.push_str("\n---\n*Generated by [entrenar](https://github.com/paiml/entrenar)*\n");

        out
    }
}

/// Convert a `Metric` to a YAML-friendly key name
fn metric_to_yaml_key(metric: &Metric) -> String {
    match metric {
        Metric::WER => "wer".to_string(),
        Metric::RTFx => "rtfx".to_string(),
        Metric::BLEU => "bleu".to_string(),
        Metric::ROUGE(v) => format!("{v}").to_lowercase().replace('-', "_"),
        Metric::Perplexity => "perplexity".to_string(),
        Metric::MMLUAccuracy => "mmlu_accuracy".to_string(),
        Metric::PassAtK(k) => format!("pass_at_{k}"),
        Metric::NDCGAtK(k) => format!("ndcg_at_{k}"),
        other => other.name().to_lowercase(),
    }
}