File created successfully at: /home/ubuntu/training/api_results/compute_space_norm_wer.py

"""
Compute space_norm_wer for all models.

space_norm_wer: Word error rate after forgiving whitespace boundaries by doing
character alignment on space-stripped text and then counting how many reference
words are touched by real content edits.

This is NOT MER (CER on joined text). This is a word-level metric.
"""
import json
import os
import re
import unicodedata
from collections import defaultdict
from pathlib import Path

# === Normalization (same v1 pipeline) ===

ZW_CHARS = re.compile(r'[\u200b-\u200f\u2028-\u202f\ufeff\u00ad]')
PUNCT_COMMON = re.compile(r'[!"#$%&\'()*+,\-./:;<=>?@\[\\\]^_`{|}~]')
PUNCT_INDIC = re.compile(r'[\u0964\u0965\u0970\u0971]')
PUNCT_EXTENDED = re.compile(r'[\u2010-\u2027\u2030-\u205e\u2e00-\u2e4f]')
QUOTES = re.compile(r'[\u2018\u2019\u201a\u201b\u201c\u201d\u201e\u201f\u00ab\u00bb]')
ANNOTATIONS = re.compile(r'\([^)]*\)')

def norm_standard(text: str) -> str:
    text = unicodedata.normalize('NFKC', text)
    text = ZW_CHARS.sub('', text)
    text = ANNOTATIONS.sub('', text)
    text = ' '.join(text.split())
    text = QUOTES.sub("'", text)
    text = text.replace('\u2014', '-').replace('\u2013', '-').replace('\u2012', '-')
    text = PUNCT_COMMON.sub('', text)
    text = PUNCT_INDIC.sub('', text)
    text = PUNCT_EXTENDED.sub('', text)
    text = text.lower()
    return ' '.join(text.split()).strip()

# === space_norm_wer core ===

def space_norm_wer_sample(ref_norm: str, hyp_norm: str) -> tuple[int, int]:
    """
    Compute space_norm_wer for a single sample.
    Returns (error_words, total_words).
    """
    ref_words = ref_norm.split()
    if not ref_words:
        return (0, 0)

    ref_nospace = ref_norm.replace(' ', '')
    hyp_nospace = hyp_norm.replace(' ', '')

    total_words = len(ref_words)

    # Fast path: identical after space removal = pure spacing difference
    if ref_nospace == hyp_nospace:
        return (0, total_words)

    # Build char_to_word mapping: ref_nospace[i] belongs to ref_words[word_idx]
    char_to_word = []
    for word_idx, word in enumerate(ref_words):
        for _ in word:
            char_to_word.append(word_idx)

    n = len(ref_nospace)
    m = len(hyp_nospace)

    # Levenshtein DP
    dp = [[0] * (m + 1) for _ in range(n + 1)]
    for i in range(n + 1):
        dp[i][0] = i
    for j in range(m + 1):
        dp[0][j] = j

    for i in range(1, n + 1):
        for j in range(1, m + 1):
            if ref_nospace[i - 1] == hyp_nospace[j - 1]:
                dp[i][j] = dp[i - 1][j - 1]
            else:
                dp[i][j] = 1 + min(
                    dp[i - 1][j - 1],  # substitution
                    dp[i - 1][j],       # deletion from ref
                    dp[i][j - 1],       # insertion in hyp
                )

    # Backtrack to find which reference words are touched by edits
    touched_words = set()
    i, j = n, m

    while i > 0 or j > 0:
        if i > 0 and j > 0 and ref_nospace[i - 1] == hyp_nospace[j - 1]:
            # Match — no edit
            i -= 1
            j -= 1
        elif i > 0 and j > 0 and dp[i][j] == dp[i - 1][j - 1] + 1:
            # Substitution
            touched_words.add(char_to_word[i - 1])
            i -= 1
            j -= 1
        elif i > 0 and dp[i][j] == dp[i - 1][j] + 1:
            # Deletion from reference
            touched_words.add(char_to_word[i - 1])
            i -= 1
        elif j > 0 and dp[i][j] == dp[i][j - 1] + 1:
            # Insertion in hypothesis — assign to nearest reference word
            if i > 0:
                touched_words.add(char_to_word[i - 1])  # previous ref word
            elif i < n:
                touched_words.add(char_to_word[i])  # next ref word
            j -= 1
        else:
            # Fallback (shouldn't happen with correct DP)
            if j > 0:
                j -= 1
            elif i > 0:
                i -= 1

    return (len(touched_words), total_words)

# === Process all models ===

LANGUAGES = ['assamese', 'bengali', 'english', 'gujarati', 'hindi', 'kannada',
             'malayalam', 'marathi', 'odia', 'punjabi', 'tamil', 'telugu']

BASE = Path('/home/ubuntu/training/benchmark_outputs')

for model_dir in sorted(BASE.iterdir()):
    if not model_dir.is_dir():
        continue
    for ckpt_dir in sorted(model_dir.iterdir()):
        sa_path = ckpt_dir / 'sample_analysis.json'
        metrics_path = ckpt_dir / 'metrics.json'
        if not sa_path.exists() or not metrics_path.exists():
            continue

        samples = json.load(open(sa_path))
        hyp_key = 'hypothesis' if 'hypothesis' in samples[0] else 'transcript'

        by_lang = defaultdict(lambda: {'err': 0, 'total': 0})
        overall_err = 0
        overall_total = 0

        for s in samples:
            lang = s['language']
            ref = s['reference']
            hyp = s.get(hyp_key, '') or ''

            ref_norm = norm_standard(ref)
            hyp_norm = norm_standard(hyp)

            if not ref_norm or not hyp_norm:
                continue

            err, tot = space_norm_wer_sample(ref_norm, hyp_norm)
            by_lang[lang]['err'] += err
            by_lang[lang]['total'] += tot
            overall_err += err
            overall_total += tot

        # Update metrics.json
        metrics = json.load(open(metrics_path))

        for lang in LANGUAGES:
            if lang in metrics and isinstance(metrics[lang], dict):
                e = by_lang[lang]['err']
                t = by_lang[lang]['total']
                metrics[lang]['space_norm_wer'] = round((e / t * 100) if t > 0 else 0, 2)

        metrics['__overall__']['space_norm_wer'] = round((overall_err / overall_total * 100) if overall_total > 0 else 0, 2)

        # Macro avg
        lang_vals = [metrics[l]['space_norm_wer'] for l in LANGUAGES if l in metrics and isinstance(metrics[l], dict) and 'space_norm_wer' in metrics[l]]
        if lang_vals:
            metrics['__macro_avg__']['space_norm_wer'] = round(sum(lang_vals) / len(lang_vals), 2)

        json.dump(metrics, open(metrics_path, 'w'), indent=2, ensure_ascii=False)

        o_snw = metrics['__overall__']['space_norm_wer']
        o_wn = metrics['__overall__'].get('wer_norm', 0)
        o_mer = metrics['__overall__'].get('mer', 0)
        gap = o_wn - o_snw
        print(f'{model_dir.name}/{ckpt_dir.name:<15}  wer_norm={o_wn:>6.2f}  space_norm_wer={o_snw:>6.2f}  mer={o_mer:>6.2f}  wer-snw={gap:>+6.2f}')


Bash

cd /home/ubuntu/training/api_results && python3 compute_space_norm_wer.py 2>&1

Compute space_norm_wer for all models


elevenlabs-scribe-v1/baseline         wer_norm= 21.11  space_norm_wer= 15.88  mer=  6.38  wer-snw= +5.23
elevenlabs-scribe-v2/baseline         wer_norm= 17.74  space_norm_wer= 12.73  mer=  5.36  wer-snw= +5.01
gemini-2.5-flash/baseline         wer_norm= 57.71  space_norm_wer= 18.29  mer= 66.67  wer-snw=+39.42
gemini-3-flash-preview/baseline         wer_norm= 67.68  space_norm_wer= 14.04  mer=100.70  wer-snw=+53.64
gemini-3-flash-strict/baseline         wer_norm= 20.12  space_norm_wer= 14.25  mer=  5.71  wer-snw= +5.87
gemini-3.1-pro/baseline         wer_norm= 18.98  space_norm_wer= 13.12  mer=  4.80  wer-snw= +5.86
gemma3n-e2b/ckpt-10000       wer_norm= 36.24  space_norm_wer= 25.73  mer= 18.09  wer-snw=+10.51
gemma3n-e2b/ckpt-20000       wer_norm= 33.95  space_norm_wer= 25.82  mer= 16.07  wer-snw= +8.13
parakeet-tdt-1.1b-lang/ckpt-60000       wer_norm= 37.07  space_norm_wer= 28.94  mer= 11.82  wer-snw= +8.13
parakeet-tdt-1.1b-lang/ckpt-80000       wer_norm= 36.93  space_norm_wer= 28.86  mer= 11.76  wer-snw= +8.07
qwen3-asr/ckpt-100000      wer_norm= 35.98  space_norm_wer= 27.64  mer= 12.24  wer-snw= +8.34
qwen3-asr/ckpt-170000      wer_norm= 35.59  space_norm_wer= 27.27  mer= 12.05  wer-snw= +8.32
qwen3-asr/ckpt-200000      wer_norm= 35.64  space_norm_wer= 27.26  mer= 12.16  wer-snw= +8.38
qwen3-asr/ckpt-24000       wer_norm= 39.12  space_norm_wer= 30.17  mer= 13.96  wer-snw= +8.95
qwen3-asr/ckpt-250000      wer_norm= 27.18  space_norm_wer= 20.18  mer=  8.24  wer-snw= +7.00
qwen3-asr/ckpt-72000       wer_norm= 36.77  space_norm_wer= 28.18  mer= 12.61  wer-snw= +8.59
sarvam-saaras-v3/baseline         wer_norm= 18.26  space_norm_wer= 13.61  mer=  5.04  wer-snw= +4.65


All computed. Now fix permissions and add `space_norm_wer` to the dashboard:


chmod -R o+rx /home/ubuntu/training/benchmark_outputs/

Fix permissions


(Bash completed with no output)


Edit

  wer_norm_nonum?: number;
  mer_nonum?: number;
  numeric_samples_dropped?: number;
  empty_hypotheses: number;
  normalization_delta: NormalizationDelta;
}

  space_norm_wer?: number;
  wer_norm_nonum?: number;
  mer_nonum?: number;
  numeric_samples_dropped?: number;
  empty_hypotheses: number;
  normalization_delta: NormalizationDelta;
}


The file /home/ubuntu/training/asr-benchmark-dashboard/src/lib/types.ts has been updated successfully.

  space_norm_wer?: number;
  wer_norm_nonum?: number;
  mer_nonum?: number;
  numeric_samples_dropped?: number;
  empty_hypotheses: number;
  normalization_delta: NormalizationDelta;
}

// === Metric tiers ===
// wer_raw:      minimal normalization, strict fidelity
// wer_norm:     PRIMARY metric — NFKC + whitespace + punctuation + case fold
// wer_numcanon: wer_norm + number canonicalization
// cer_norm:     character-level normalized — critical for Indic scripts

export interface NormalizationDelta {
  raw_to_norm: number;       // wer_raw - wer_norm (negative = norm helped)
  norm_to_numcanon: number;  // wer_norm - wer_numcanon
  norm_to_mer?: number;      // wer_norm - mer (quantifies spacing error inflation)
}

export interface LanguageMetrics {
  n_samples: number;
  wer_raw: number;
  wer_norm: number;
  wer_numcanon: number;
  mer: number;
  cer_norm: number;
  wer_norm_nonum?: number;
  mer_nonum?: number;
  numeric_samples_dropped?: number;
  empty_hypotheses: number;
  normalization_delta: NormalizationDelta;
}

export interface AggregateMetrics {
  n_samples: number;
  wer_raw: number;
  wer_norm: number;
  wer_numcanon: number;
  mer: number;
  cer_norm: number;
  wer_norm_nonum?: number;
  mer_nonum?: number;
  numeric_samples_dropped?: number;
}

export interface MacroAvgMetrics {
  n_languages: number;
  wer_raw: number;
  wer_norm: number;
  wer_numcanon: number;
  mer: number;
  cer_norm: number;
}

export interface MetaMeta {
  checkpoint: string;
  checkpoint_name: string;
  model_id: string;
  model_type?: string;
  dataset: string;
  batch_size?: number;
  force_language?: boolean;
  inference_time_sec: number;
  total_audio_sec: number;
  rtf: number;
  timestamp?: string;
  gpu?: string;
  framework?: string;
  normalization_version?: string;
  jiwer_version?: string;
}

export type MetricsFile = {
  [language: string]: LanguageMetrics | AggregateMetrics | MacroAvgMetrics | MetaMeta;
} & {
  __overall__: AggregateMetrics;
  __macro_avg__: MacroAvgMetrics;
  __meta__: MetaMeta;
};

export interface SampleAnalysis {
  id: string;
  language: string;
  reference: string;
  hypothesis: string;
  ref_norm?: string;
  hyp_norm?: string;
  ref_numcanon?: string;
  hyp_numcanon?: string;
  detected_language?: string;
  wer_raw?: number;
  wer_norm?: number;
  flags?: string[];
}

export interface ErrorBuckets {
  numeric_mismatch_count: number;
  punctuation_only_count: number;
  spacing_tokenization_count: number;
  entity_mismatch_count: number;
  script_confusion_count: number;
  empty_hypothesis_count: number;
}

export interface SubstitutionEntry {
  ref: string;
  hyp: string;
  count: number;
}

export interface WordCountEntry {
  word: string;
  count: number;
}

export interface LanguageErrorAnalysis {
  top_substitutions: SubstitutionEntry[];
  top_insertions: WordCountEntry[];
  top_deletions: WordCountEntry[];
  error_buckets: ErrorBuckets;
  examples: {
    worst_samples: string[];
    best_samples: string[];
    numeric_mismatch_samples: string[];
    entity_mismatch_samples: string[];
  };
}

export interface ErrorAnalysisSummary {
  model_diagnosis: "recognition-limited" | "formatting-limited" | "numeric-limited" | "mixed";
  primary_error_source: string;
  numeric_verbalization_impact: string;
  formatting_impact: string;
  worst_languages: string[];
  best_languages: string[];
}

export type ErrorAnalysisFile = {
  [language: string]: LanguageErrorAnalysis | ErrorAnalysisSummary;
} & {
  __summary__: ErrorAnalysisSummary;
};

export interface ModelInfo {
  id: string;
  displayName: string;
  checkpoints: string[];
}

export interface CheckpointComparison {
  language: string;
  [key: string]: number | string;
}

// === Constants ===

export const MODEL_REGISTRY: Record<string, string> = {
  "qwen3-asr": "Qwen3-ASR-1.7B",
  "gemma3n-e2b": "Gemma-3n-E2B-ASR",
  "parakeet-1.1b": "Parakeet-1.1B-Language-Guided",
  "parakeet-tdt-1.1b-lang": "Parakeet-TDT-1.1B-Lang",
  "elevenlabs-scribe-v2": "ElevenLabs Scribe v2",
  "elevenlabs-scribe-v1": "ElevenLabs Scribe v1",
  "gemini-3-flash-strict": "Gemini 3 Flash (Strict)",
  "gemini-3.1-pro": "Gemini 3.1 Pro",
  "sarvam-saaras-v3": "Sarvam Saaras v3",
};

// Models grouped by category
export const OUR_MODELS = ["qwen3-asr", "gemma3n-e2b", "parakeet-1.1b", "parakeet-tdt-1.1b-lang"] as const;
export const BASELINE_MODELS = [
  "elevenlabs-scribe-v2", "elevenlabs-scribe-v1",
  "gemini-3-flash-strict", "gemini-3.1-pro",
  "sarvam-saaras-v3",
] as const;

export const LANGUAGES = [
  "assamese", "bengali", "english", "gujarati", "hindi", "kannada",
  "malayalam", "marathi", "odia", "punjabi", "tamil", "telugu",
] as const;

export const LANGUAGE_CODES: Record<string, string> = {
  assamese: "as", bengali: "bn", english: "en", gujarati: "gu",
  hindi: "hi", kannada: "kn", malayalam: "ml", marathi: "mr",
  odia: "or", punjabi: "pa", tamil: "ta", telugu: "te",
};

export const METRIC_TIERS = ["wer_raw", "wer_norm", "wer_numcanon", "mer", "cer_norm", "wer_norm_nonum", "mer_nonum"] as const;
export type MetricTier = typeof METRIC_TIERS[number];

export const METRIC_LABELS: Record<MetricTier, string> = {
  wer_raw: "WER Raw",
  wer_norm: "WER Norm",
  wer_numcanon: "WER NumCanon",
  mer: "MER",
  cer_norm: "CER Norm",
  wer_norm_nonum: "WER (no num)",
  mer_nonum: "MER (no num)",
};

export const METRIC_DESCRIPTIONS: Record<MetricTier, string> = {
  wer_raw: "Strict transcript fidelity",
  wer_norm: "Primary ASR metric",
  wer_numcanon: "Numeric robustness",
  mer: "Meaningful Error Rate (space-normalized)",
  cer_norm: "Script-sensitive (Indic)",
  wer_norm_nonum: "WER excluding numeric samples",
  mer_nonum: "MER excluding numeric samples",
};

// Primary tiers shown in the main metric selector (exclude nonum variants)
export const PRIMARY_METRIC_TIERS = ["wer_raw", "wer_norm", "wer_numcanon", "mer", "cer_norm"] as const;

export const CHART_COLORS = [
  "#6366f1", "#8b5cf6", "#a78bfa", "#c084fc",
  "#e879f9", "#f472b6", "#fb7185", "#f87171",
  "#fb923c", "#fbbf24", "#a3e635", "#34d399",
];

export const MODEL_COLORS: Record<string, string> = {
  "qwen3-asr": "#6366f1",
  "gemma3n-e2b": "#8b5cf6",
  "parakeet-1.1b": "#34d399",
  "parakeet-tdt-1.1b-lang": "#2dd4bf",
  "elevenlabs-scribe-v2": "#f472b6",
  "elevenlabs-scribe-v1": "#fb7185",
  "gemini-3-flash-strict": "#fbbf24",
  "gemini-3.1-pro": "#a3e635",
  "sarvam-saaras-v3": "#22d3ee",
};

// === Backward compatibility ===
// Converts old-schema metrics into new-schema shape so dashboard works
// with both old data (pre-migration) and new data.

export interface OldLanguageMetrics {
  n_samples: number;
  wer: number;
  cer: number;
  wer_normalized: number;
  cer_normalized: number;
  empty_hypotheses: number;
}

export function isOldSchema(obj: Record<string, unknown>): boolean {
  return "wer" in obj && !("wer_raw" in obj);
}

export function migrateLanguageMetrics(old: OldLanguageMetrics): LanguageMetrics {
  return {
    n_samples: old.n_samples,
    wer_raw: old.wer,
    wer_norm: old.wer_normalized,
    wer_numcanon: old.wer_normalized,
    mer: old.cer_normalized, // fallback: use CER as rough MER proxy
    cer_norm: old.cer_normalized,
    empty_hypotheses: old.empty_hypotheses,
    normalization_delta: {
      raw_to_norm: old.wer - old.wer_normalized,
      norm_to_numcanon: 0,
      norm_to_mer: 0,
    },
  };
}

wer_numcanon: number;
mer: number;
cer_norm: number;
+  space_norm_wer?: number;
wer_norm_nonum?: number;
mer_nonum?: number;
numeric_samples_dropped?: number;


  wer_norm_nonum?: number;
  mer_nonum?: number;
  numeric_samples_dropped?: number;
}

export interface MacroAvgMetrics {

  space_norm_wer?: number;
  wer_norm_nonum?: number;
  mer_nonum?: number;
  numeric_samples_dropped?: number;
}

export interface MacroAvgMetrics {


  space_norm_wer?: number;
  wer_norm_nonum?: number;
  mer_nonum?: number;
  numeric_samples_dropped?: number;
}

export interface MacroAvgMetrics {

// === Metric tiers ===
// wer_raw:      minimal normalization, strict fidelity
// wer_norm:     PRIMARY metric — NFKC + whitespace + punctuation + case fold
// wer_numcanon: wer_norm + number canonicalization
// cer_norm:     character-level normalized — critical for Indic scripts

export interface NormalizationDelta {
  raw_to_norm: number;       // wer_raw - wer_norm (negative = norm helped)
  norm_to_numcanon: number;  // wer_norm - wer_numcanon
  norm_to_mer?: number;      // wer_norm - mer (quantifies spacing error inflation)
}

export interface LanguageMetrics {
  n_samples: number;
  wer_raw: number;
  wer_norm: number;
  wer_numcanon: number;
  mer: number;
  cer_norm: number;
  space_norm_wer?: number;
  wer_norm_nonum?: number;
  mer_nonum?: number;
  numeric_samples_dropped?: number;
  empty_hypotheses: number;
  normalization_delta: NormalizationDelta;
}

export interface AggregateMetrics {
  n_samples: number;
  wer_raw: number;
  wer_norm: number;
  wer_numcanon: number;
  mer: number;
  cer_norm: number;
  wer_norm_nonum?: number;
  mer_nonum?: number;
  numeric_samples_dropped?: number;
}

export interface MacroAvgMetrics {
  n_languages: number;
  wer_raw: number;
  wer_norm: number;
  wer_numcanon: number;
  mer: number;
  cer_norm: number;
}

export interface MetaMeta {
  checkpoint: string;
  checkpoint_name: string;
  model_id: string;
  model_type?: string;
  dataset: string;
  batch_size?: number;
  force_language?: boolean;
  inference_time_sec: number;
  total_audio_sec: number;
  rtf: number;
  timestamp?: string;
  gpu?: string;
  framework?: string;
  normalization_version?: string;
  jiwer_version?: string;
}

export type MetricsFile = {
  [language: string]: LanguageMetrics | AggregateMetrics | MacroAvgMetrics | MetaMeta;
} & {
  __overall__: AggregateMetrics;
  __macro_avg__: MacroAvgMetrics;
  __meta__: MetaMeta;
};

export interface SampleAnalysis {
  id: string;
  language: string;
  reference: string;
  hypothesis: string;
  ref_norm?: string;
  hyp_norm?: string;
  ref_numcanon?: string;
  hyp_numcanon?: string;
  detected_language?: string;
  wer_raw?: number;
  wer_norm?: number;
  flags?: string[];
}

export interface ErrorBuckets {
  numeric_mismatch_count: number;
  punctuation_only_count: number;
  spacing_tokenization_count: number;
  entity_mismatch_count: number;
  script_confusion_count: number;
  empty_hypothesis_count: number;
}

export interface SubstitutionEntry {
  ref: string;
  hyp: string;
  count: number;
}

export interface WordCountEntry {
  word: string;
  count: number;
}

export interface LanguageErrorAnalysis {
  top_substitutions: SubstitutionEntry[];
  top_insertions: WordCountEntry[];
  top_deletions: WordCountEntry[];
  error_buckets: ErrorBuckets;
  examples: {
    worst_samples: string[];
    best_samples: string[];
    numeric_mismatch_samples: string[];
    entity_mismatch_samples: string[];
  };
}

export interface ErrorAnalysisSummary {
  model_diagnosis: "recognition-limited" | "formatting-limited" | "numeric-limited" | "mixed";
  primary_error_source: string;
  numeric_verbalization_impact: string;
  formatting_impact: string;
  worst_languages: string[];
  best_languages: string[];
}

export type ErrorAnalysisFile = {
  [language: string]: LanguageErrorAnalysis | ErrorAnalysisSummary;
} & {
  __summary__: ErrorAnalysisSummary;
};

export interface ModelInfo {
  id: string;
  displayName: string;
  checkpoints: string[];
}

export interface CheckpointComparison {
  language: string;
  [key: string]: number | string;
}

// === Constants ===

export const MODEL_REGISTRY: Record<string, string> = {
  "qwen3-asr": "Qwen3-ASR-1.7B",
  "gemma3n-e2b": "Gemma-3n-E2B-ASR",
  "parakeet-1.1b": "Parakeet-1.1B-Language-Guided",
  "parakeet-tdt-1.1b-lang": "Parakeet-TDT-1.1B-Lang",
  "elevenlabs-scribe-v2": "ElevenLabs Scribe v2",
  "elevenlabs-scribe-v1": "ElevenLabs Scribe v1",
  "gemini-3-flash-strict": "Gemini 3 Flash (Strict)",
  "gemini-3.1-pro": "Gemini 3.1 Pro",
  "sarvam-saaras-v3": "Sarvam Saaras v3",
};

// Models grouped by category
export const OUR_MODELS = ["qwen3-asr", "gemma3n-e2b", "parakeet-1.1b", "parakeet-tdt-1.1b-lang"] as const;
export const BASELINE_MODELS = [
  "elevenlabs-scribe-v2", "elevenlabs-scribe-v1",
  "gemini-3-flash-strict", "gemini-3.1-pro",
  "sarvam-saaras-v3",
] as const;

export const LANGUAGES = [
  "assamese", "bengali", "english", "gujarati", "hindi", "kannada",
  "malayalam", "marathi", "odia", "punjabi", "tamil", "telugu",
] as const;

export const LANGUAGE_CODES: Record<string, string> = {
  assamese: "as", bengali: "bn", english: "en", gujarati: "gu",
  hindi: "hi", kannada: "kn", malayalam: "ml", marathi: "mr",
  odia: "or", punjabi: "pa", tamil: "ta", telugu: "te",
};

export const METRIC_TIERS = ["wer_raw", "wer_norm", "wer_numcanon", "mer", "cer_norm", "wer_norm_nonum", "mer_nonum"] as const;
export type MetricTier = typeof METRIC_TIERS[number];

export const METRIC_LABELS: Record<MetricTier, string> = {
  wer_raw: "WER Raw",
  wer_norm: "WER Norm",
  wer_numcanon: "WER NumCanon",
  mer: "MER",
  cer_norm: "CER Norm",
  wer_norm_nonum: "WER (no num)",
  mer_nonum: "MER (no num)",
};

export const METRIC_DESCRIPTIONS: Record<MetricTier, string> = {
  wer_raw: "Strict transcript fidelity",
  wer_norm: "Primary ASR metric",
  wer_numcanon: "Numeric robustness",
  mer: "Meaningful Error Rate (space-normalized)",
  cer_norm: "Script-sensitive (Indic)",
  wer_norm_nonum: "WER excluding numeric samples",
  mer_nonum: "MER excluding numeric samples",
};

// Primary tiers shown in the main metric selector (exclude nonum variants)
export const PRIMARY_METRIC_TIERS = ["wer_raw", "wer_norm", "wer_numcanon", "mer", "cer_norm"] as const;

export const CHART_COLORS = [
  "#6366f1", "#8b5cf6", "#a78bfa", "#c084fc",
  "#e879f9", "#f472b6", "#fb7185", "#f87171",
  "#fb923c", "#fbbf24", "#a3e635", "#34d399",
];

export const MODEL_COLORS: Record<string, string> = {
  "qwen3-asr": "#6366f1",
  "gemma3n-e2b": "#8b5cf6",
  "parakeet-1.1b": "#34d399",
  "parakeet-tdt-1.1b-lang": "#2dd4bf",
  "elevenlabs-scribe-v2": "#f472b6",
  "elevenlabs-scribe-v1": "#fb7185",
  "gemini-3-flash-strict": "#fbbf24",
  "gemini-3.1-pro": "#a3e635",
  "sarvam-saaras-v3": "#22d3ee",
};

// === Backward compatibility ===
// Converts old-schema metrics into new-schema shape so dashboard works
// with both old data (pre-migration) and new data.

export interface OldLanguageMetrics {
  n_samples: number;
  wer: number;
  cer: number;
  wer_normalized: number;
  cer_normalized: number;
  empty_hypotheses: number;
}

export function isOldSchema(obj: Record<string, unknown>): boolean {
  return "wer" in obj && !("wer_raw" in obj);
}

export function migrateLanguageMetrics(old: OldLanguageMetrics): LanguageMetrics {
  return {
    n_samples: old.n_samples,
    wer_raw: old.wer,
    wer_norm: old.wer_normalized,
    wer_numcanon: old.wer_normalized,
    mer: old.cer_normalized, // fallback: use CER as rough MER proxy
    cer_norm: old.cer_normalized,
    empty_hypotheses: old.empty_hypotheses,
    normalization_delta: {
      raw_to_norm: old.wer - old.wer_normalized,
      norm_to_numcanon: 0,
      norm_to_mer: 0,
    },
  };
}


Now add it to the metric tiers:


export const METRIC_TIERS = ["wer_raw", "wer_norm", "wer_numcanon", "mer", "cer_norm", "wer_norm_nonum", "mer_nonum"] as const;

export const METRIC_TIERS = ["wer_raw", "wer_norm", "wer_numcanon", "space_norm_wer", "mer", "cer_norm", "wer_norm_nonum", "mer_nonum"] as const;


// === Metric tiers ===
// wer_raw:      minimal normalization, strict fidelity
// wer_norm:     PRIMARY metric — NFKC + whitespace + punctuation + case fold
// wer_numcanon: wer_norm + number canonicalization
// cer_norm:     character-level normalized — critical for Indic scripts

export interface NormalizationDelta {
  raw_to_norm: number;       // wer_raw - wer_norm (negative = norm helped)
  norm_to_numcanon: number;  // wer_norm - wer_numcanon
  norm_to_mer?: number;      // wer_norm - mer (quantifies spacing error inflation)
}

export interface LanguageMetrics {
  n_samples: number;
  wer_raw: number;
  wer_norm: number;
  wer_numcanon: number;
  mer: number;
  cer_norm: number;
  space_norm_wer?: number;
  wer_norm_nonum?: number;
  mer_nonum?: number;
  numeric_samples_dropped?: number;
  empty_hypotheses: number;
  normalization_delta: NormalizationDelta;
}

export interface AggregateMetrics {
  n_samples: number;
  wer_raw: number;
  wer_norm: number;
  wer_numcanon: number;
  mer: number;
  cer_norm: number;
  space_norm_wer?: number;
  wer_norm_nonum?: number;
  mer_nonum?: number;
  numeric_samples_dropped?: number;
}

export interface MacroAvgMetrics {
  n_languages: number;
  wer_raw: number;
  wer_norm: number;
  wer_numcanon: number;
  mer: number;
  cer_norm: number;
}

export interface MetaMeta {
  checkpoint: string;
  checkpoint_name: string;
  model_id: string;
  model_type?: string;
  dataset: string;
  batch_size?: number;
  force_language?: boolean;
  inference_time_sec: number;
  total_audio_sec: number;
  rtf: number;
  timestamp?: string;
  gpu?: string;
  framework?: string;
  normalization_version?: string;
  jiwer_version?: string;
}

export type MetricsFile = {
  [language: string]: LanguageMetrics | AggregateMetrics | MacroAvgMetrics | MetaMeta;
} & {
  __overall__: AggregateMetrics;
  __macro_avg__: MacroAvgMetrics;
  __meta__: MetaMeta;
};

export interface SampleAnalysis {
  id: string;
  language: string;
  reference: string;
  hypothesis: string;
  ref_norm?: string;
  hyp_norm?: string;
  ref_numcanon?: string;
  hyp_numcanon?: string;
  detected_language?: string;
  wer_raw?: number;
  wer_norm?: number;
  flags?: string[];
}

export interface ErrorBuckets {
  numeric_mismatch_count: number;
  punctuation_only_count: number;
  spacing_tokenization_count: number;
  entity_mismatch_count: number;
  script_confusion_count: number;
  empty_hypothesis_count: number;
}

export interface SubstitutionEntry {
  ref: string;
  hyp: string;
  count: number;
}

export interface WordCountEntry {
  word: string;
  count: number;
}

export interface LanguageErrorAnalysis {
  top_substitutions: SubstitutionEntry[];
  top_insertions: WordCountEntry[];
  top_deletions: WordCountEntry[];
  error_buckets: ErrorBuckets;
  examples: {
    worst_samples: string[];
    best_samples: string[];
    numeric_mismatch_samples: string[];
    entity_mismatch_samples: string[];
  };
}

export interface ErrorAnalysisSummary {
  model_diagnosis: "recognition-limited" | "formatting-limited" | "numeric-limited" | "mixed";
  primary_error_source: string;
  numeric_verbalization_impact: string;
  formatting_impact: string;
  worst_languages: string[];
  best_languages: string[];
}

export type ErrorAnalysisFile = {
  [language: string]: LanguageErrorAnalysis | ErrorAnalysisSummary;
} & {
  __summary__: ErrorAnalysisSummary;
};

export interface ModelInfo {
  id: string;
  displayName: string;
  checkpoints: string[];
}

export interface CheckpointComparison {
  language: string;
  [key: string]: number | string;
}

// === Constants ===

export const MODEL_REGISTRY: Record<string, string> = {
  "qwen3-asr": "Qwen3-ASR-1.7B",
  "gemma3n-e2b": "Gemma-3n-E2B-ASR",
  "parakeet-1.1b": "Parakeet-1.1B-Language-Guided",
  "parakeet-tdt-1.1b-lang": "Parakeet-TDT-1.1B-Lang",
  "elevenlabs-scribe-v2": "ElevenLabs Scribe v2",
  "elevenlabs-scribe-v1": "ElevenLabs Scribe v1",
  "gemini-3-flash-strict": "Gemini 3 Flash (Strict)",
  "gemini-3.1-pro": "Gemini 3.1 Pro",
  "sarvam-saaras-v3": "Sarvam Saaras v3",
};

// Models grouped by category
export const OUR_MODELS = ["qwen3-asr", "gemma3n-e2b", "parakeet-1.1b", "parakeet-tdt-1.1b-lang"] as const;
export const BASELINE_MODELS = [
  "elevenlabs-scribe-v2", "elevenlabs-scribe-v1",
  "gemini-3-flash-strict", "gemini-3.1-pro",
  "sarvam-saaras-v3",
] as const;

export const LANGUAGES = [
  "assamese", "bengali", "english", "gujarati", "hindi", "kannada",
  "malayalam", "marathi", "odia", "punjabi", "tamil", "telugu",
] as const;

export const LANGUAGE_CODES: Record<string, string> = {
  assamese: "as", bengali: "bn", english: "en", gujarati: "gu",
  hindi: "hi", kannada: "kn", malayalam: "ml", marathi: "mr",
  odia: "or", punjabi: "pa", tamil: "ta", telugu: "te",
};

export const METRIC_TIERS = ["wer_raw", "wer_norm", "wer_numcanon", "mer", "cer_norm", "wer_norm_nonum", "mer_nonum"] as const;
export type MetricTier = typeof METRIC_TIERS[number];

export const METRIC_LABELS: Record<MetricTier, string> = {
  wer_raw: "WER Raw",
  wer_norm: "WER Norm",
  wer_numcanon: "WER NumCanon",
  mer: "MER",
  cer_norm: "CER Norm",
  wer_norm_nonum: "WER (no num)",
  mer_nonum: "MER (no num)",
};

export const METRIC_DESCRIPTIONS: Record<MetricTier, string> = {
  wer_raw: "Strict transcript fidelity",
  wer_norm: "Primary ASR metric",
  wer_numcanon: "Numeric robustness",
  mer: "Meaningful Error Rate (space-normalized)",
  cer_norm: "Script-sensitive (Indic)",
  wer_norm_nonum: "WER excluding numeric samples",
  mer_nonum: "MER excluding numeric samples",
};

// Primary tiers shown in the main metric selector (exclude nonum variants)
export const PRIMARY_METRIC_TIERS = ["wer_raw", "wer_norm", "wer_numcanon", "mer", "cer_norm"] as const;

export const CHART_COLORS = [
  "#6366f1", "#8b5cf6", "#a78bfa", "#c084fc",
  "#e879f9", "#f472b6", "#fb7185", "#f87171",
  "#fb923c", "#fbbf24", "#a3e635", "#34d399",
];

export const MODEL_COLORS: Record<string, string> = {
  "qwen3-asr": "#6366f1",
  "gemma3n-e2b": "#8b5cf6",
  "parakeet-1.1b": "#34d399",
  "parakeet-tdt-1.1b-lang": "#2dd4bf",
  "elevenlabs-scribe-v2": "#f472b6",
  "elevenlabs-scribe-v1": "#fb7185",
  "gemini-3-flash-strict": "#fbbf24",
  "gemini-3.1-pro": "#a3e635",
  "sarvam-saaras-v3": "#22d3ee",
};

// === Backward compatibility ===
// Converts old-schema metrics into new-schema shape so dashboard works
// with both old data (pre-migration) and new data.

export interface OldLanguageMetrics {
  n_samples: number;
  wer: number;
  cer: number;
  wer_normalized: number;
  cer_normalized: number;
  empty_hypotheses: number;
}

export function isOldSchema(obj: Record<string, unknown>): boolean {
  return "wer" in obj && !("wer_raw" in obj);
}

export function migrateLanguageMetrics(old: OldLanguageMetrics): LanguageMetrics {
  return {
    n_samples: old.n_samples,
    wer_raw: old.wer,
    wer_norm: old.wer_normalized,
    wer_numcanon: old.wer_normalized,
    mer: old.cer_normalized, // fallback: use CER as rough MER proxy
    cer_norm: old.cer_normalized,
    empty_hypotheses: old.empty_hypotheses,
    normalization_delta: {
      raw_to_norm: old.wer - old.wer_normalized,
      norm_to_numcanon: 0,
      norm_to_mer: 0,
    },
  };
}

-export const METRIC_TIERS = ["wer_raw", "wer_norm", "wer_numcanon", "mer", "cer_norm", "wer_norm_nonum", "mer_nonum"] as const;

+export const METRIC_TIERS = ["wer_raw", "wer_norm", "wer_numcanon", "space_norm_wer", "mer", "cer_norm", "wer_norm_nonum", "mer_nonum"] as const;


  wer_numcanon: "WER NumCanon",
  mer: "MER",

  wer_numcanon: "WER NumCanon",
  space_norm_wer: "Space-Norm WER",
  mer: "MER",


  wer_numcanon: "WER NumCanon",
  space_norm_wer: "Space-Norm WER",
  mer: "MER",

// === Metric tiers ===
// wer_raw:      minimal normalization, strict fidelity
// wer_norm:     PRIMARY metric — NFKC + whitespace + punctuation + case fold
// wer_numcanon: wer_norm + number canonicalization
// cer_norm:     character-level normalized — critical for Indic scripts

export interface NormalizationDelta {
  raw_to_norm: number;       // wer_raw - wer_norm (negative = norm helped)
  norm_to_numcanon: number;  // wer_norm - wer_numcanon
  norm_to_mer?: number;      // wer_norm - mer (quantifies spacing error inflation)
}

export interface LanguageMetrics {
  n_samples: number;
  wer_raw: number;
  wer_norm: number;
  wer_numcanon: number;
  mer: number;
  cer_norm: number;
  space_norm_wer?: number;
  wer_norm_nonum?: number;
  mer_nonum?: number;
  numeric_samples_dropped?: number;
  empty_hypotheses: number;
  normalization_delta: NormalizationDelta;
}

export interface AggregateMetrics {
  n_samples: number;
  wer_raw: number;
  wer_norm: number;
  wer_numcanon: number;
  mer: number;
  cer_norm: number;
  space_norm_wer?: number;
  wer_norm_nonum?: number;
  mer_nonum?: number;
  numeric_samples_dropped?: number;
}

export interface MacroAvgMetrics {
  n_languages: number;
  wer_raw: number;
  wer_norm: number;
  wer_numcanon: number;
  mer: number;
  cer_norm: number;
}

export interface MetaMeta {
  checkpoint: string;
  checkpoint_name: string;
  model_id: string;
  model_type?: string;
  dataset: string;
  batch_size?: number;
  force_language?: boolean;
  inference_time_sec: number;
  total_audio_sec: number;
  rtf: number;
  timestamp?: string;
  gpu?: string;
  framework?: string;
  normalization_version?: string;
  jiwer_version?: string;
}

export type MetricsFile = {
  [language: string]: LanguageMetrics | AggregateMetrics | MacroAvgMetrics | MetaMeta;
} & {
  __overall__: AggregateMetrics;
  __macro_avg__: MacroAvgMetrics;
  __meta__: MetaMeta;
};

export interface SampleAnalysis {
  id: string;
  language: string;
  reference: string;
  hypothesis: string;
  ref_norm?: string;
  hyp_norm?: string;
  ref_numcanon?: string;
  hyp_numcanon?: string;
  detected_language?: string;
  wer_raw?: number;
  wer_norm?: number;
  flags?: string[];
}

export interface ErrorBuckets {
  numeric_mismatch_count: number;
  punctuation_only_count: number;
  spacing_tokenization_count: number;
  entity_mismatch_count: number;
  script_confusion_count: number;
  empty_hypothesis_count: number;
}

export interface SubstitutionEntry {
  ref: string;
  hyp: string;
  count: number;
}

export interface WordCountEntry {
  word: string;
  count: number;
}

export interface LanguageErrorAnalysis {
  top_substitutions: SubstitutionEntry[];
  top_insertions: WordCountEntry[];
  top_deletions: WordCountEntry[];
  error_buckets: ErrorBuckets;
  examples: {
    worst_samples: string[];
    best_samples: string[];
    numeric_mismatch_samples: string[];
    entity_mismatch_samples: string[];
  };
}

export interface ErrorAnalysisSummary {
  model_diagnosis: "recognition-limited" | "formatting-limited" | "numeric-limited" | "mixed";
  primary_error_source: string;
  numeric_verbalization_impact: string;
  formatting_impact: string;
  worst_languages: string[];
  best_languages: string[];
}

export type ErrorAnalysisFile = {
  [language: string]: LanguageErrorAnalysis | ErrorAnalysisSummary;
} & {
  __summary__: ErrorAnalysisSummary;
};

export interface ModelInfo {
  id: string;
  displayName: string;
  checkpoints: string[];
}

export interface CheckpointComparison {
  language: string;
  [key: string]: number | string;
}

// === Constants ===

export const MODEL_REGISTRY: Record<string, string> = {
  "qwen3-asr": "Qwen3-ASR-1.7B",
  "gemma3n-e2b": "Gemma-3n-E2B-ASR",
  "parakeet-1.1b": "Parakeet-1.1B-Language-Guided",
  "parakeet-tdt-1.1b-lang": "Parakeet-TDT-1.1B-Lang",
  "elevenlabs-scribe-v2": "ElevenLabs Scribe v2",
  "elevenlabs-scribe-v1": "ElevenLabs Scribe v1",
  "gemini-3-flash-strict": "Gemini 3 Flash (Strict)",
  "gemini-3.1-pro": "Gemini 3.1 Pro",
  "sarvam-saaras-v3": "Sarvam Saaras v3",
};

// Models grouped by category
export const OUR_MODELS = ["qwen3-asr", "gemma3n-e2b", "parakeet-1.1b", "parakeet-tdt-1.1b-lang"] as const;
export const BASELINE_MODELS = [
  "elevenlabs-scribe-v2", "elevenlabs-scribe-v1",
  "gemini-3-flash-strict", "gemini-3.1-pro",
  "sarvam-saaras-v3",
] as const;

export const LANGUAGES = [
  "assamese", "bengali", "english", "gujarati", "hindi", "kannada",
  "malayalam", "marathi", "odia", "punjabi", "tamil", "telugu",
] as const;

export const LANGUAGE_CODES: Record<string, string> = {
  assamese: "as", bengali: "bn", english: "en", gujarati: "gu",
  hindi: "hi", kannada: "kn", malayalam: "ml", marathi: "mr",
  odia: "or", punjabi: "pa", tamil: "ta", telugu: "te",
};

export const METRIC_TIERS = ["wer_raw", "wer_norm", "wer_numcanon", "space_norm_wer", "mer", "cer_norm", "wer_norm_nonum", "mer_nonum"] as const;
export type MetricTier = typeof METRIC_TIERS[number];

export const METRIC_LABELS: Record<MetricTier, string> = {
  wer_raw: "WER Raw",
  wer_norm: "WER Norm",
  wer_numcanon: "WER NumCanon",
  mer: "MER",
  cer_norm: "CER Norm",
  wer_norm_nonum: "WER (no num)",
  mer_nonum: "MER (no num)",
};

export const METRIC_DESCRIPTIONS: Record<MetricTier, string> = {
  wer_raw: "Strict transcript fidelity",
  wer_norm: "Primary ASR metric",
  wer_numcanon: "Numeric robustness",
  mer: "Meaningful Error Rate (space-normalized)",
  cer_norm: "Script-sensitive (Indic)",
  wer_norm_nonum: "WER excluding numeric samples",
  mer_nonum: "MER excluding numeric samples",
};

// Primary tiers shown in the main metric selector (exclude nonum variants)
export const PRIMARY_METRIC_TIERS = ["wer_raw", "wer_norm", "wer_numcanon", "mer", "cer_norm"] as const;

export const CHART_COLORS = [
  "#6366f1", "#8b5cf6", "#a78bfa", "#c084fc",
  "#e879f9", "#f472b6", "#fb7185", "#f87171",
  "#fb923c", "#fbbf24", "#a3e635", "#34d399",
];

export const MODEL_COLORS: Record<string, string> = {
  "qwen3-asr": "#6366f1",
  "gemma3n-e2b": "#8b5cf6",
  "parakeet-1.1b": "#34d399",
  "parakeet-tdt-1.1b-lang": "#2dd4bf",
  "elevenlabs-scribe-v2": "#f472b6",
  "elevenlabs-scribe-v1": "#fb7185",
  "gemini-3-flash-strict": "#fbbf24",
  "gemini-3.1-pro": "#a3e635",
  "sarvam-saaras-v3": "#22d3ee",
};

// === Backward compatibility ===
// Converts old-schema metrics into new-schema shape so dashboard works
// with both old data (pre-migration) and new data.

export interface OldLanguageMetrics {
  n_samples: number;
  wer: number;
  cer: number;
  wer_normalized: number;
  cer_normalized: number;
  empty_hypotheses: number;
}

export function isOldSchema(obj: Record<string, unknown>): boolean {
  return "wer" in obj && !("wer_raw" in obj);
}

export function migrateLanguageMetrics(old: OldLanguageMetrics): LanguageMetrics {
  return {
    n_samples: old.n_samples,
    wer_raw: old.wer,
    wer_norm: old.wer_normalized,
    wer_numcanon: old.wer_normalized,
    mer: old.cer_normalized, // fallback: use CER as rough MER proxy
    cer_norm: old.cer_normalized,
    empty_hypotheses: old.empty_hypotheses,
    normalization_delta: {
      raw_to_norm: old.wer - old.wer_normalized,
      norm_to_numcanon: 0,
      norm_to_mer: 0,
    },
  };
}

wer_raw: "WER Raw",
wer_norm: "WER Norm",
wer_numcanon: "WER NumCanon",
+  space_norm_wer: "Space-Norm WER",
mer: "MER",
cer_norm: "CER Norm",
wer_norm_nonum: "WER (no num)",


  wer_numcanon: "Numeric robustness",
  mer: "Meaningful Error Rate (space-normalized)",

  wer_numcanon: "Numeric robustness",
  space_norm_wer: "Word-level errors after space-insensitive char alignment",
  mer: "Meaningful Error Rate (space-normalized)",


  wer_numcanon: "Numeric robustness",
  space_norm_wer: "Word-level errors after space-insensitive char alignment",
  mer: "Meaningful Error Rate (space-normalized)",

// === Metric tiers ===
// wer_raw:      minimal normalization, strict fidelity
// wer_norm:     PRIMARY metric — NFKC + whitespace + punctuation + case fold
// wer_numcanon: wer_norm + number canonicalization
// cer_norm:     character-level normalized — critical for Indic scripts

export interface NormalizationDelta {
  raw_to_norm: number;       // wer_raw - wer_norm (negative = norm helped)
  norm_to_numcanon: number;  // wer_norm - wer_numcanon
  norm_to_mer?: number;      // wer_norm - mer (quantifies spacing error inflation)
}

export interface LanguageMetrics {
  n_samples: number;
  wer_raw: number;
  wer_norm: number;
  wer_numcanon: number;
  mer: number;
  cer_norm: number;
  space_norm_wer?: number;
  wer_norm_nonum?: number;
  mer_nonum?: number;
  numeric_samples_dropped?: number;
  empty_hypotheses: number;
  normalization_delta: NormalizationDelta;
}

export interface AggregateMetrics {
  n_samples: number;
  wer_raw: number;
  wer_norm: number;
  wer_numcanon: number;
  mer: number;
  cer_norm: number;
  space_norm_wer?: number;
  wer_norm_nonum?: number;
  mer_nonum?: number;
  numeric_samples_dropped?: number;
}

export interface MacroAvgMetrics {
  n_languages: number;
  wer_raw: number;
  wer_norm: number;
  wer_numcanon: number;
  mer: number;
  cer_norm: number;
}

export interface MetaMeta {
  checkpoint: string;
  checkpoint_name: string;
  model_id: string;
  model_type?: string;
  dataset: string;
  batch_size?: number;
  force_language?: boolean;
  inference_time_sec: number;
  total_audio_sec: number;
  rtf: number;
  timestamp?: string;
  gpu?: string;
  framework?: string;
  normalization_version?: string;
  jiwer_version?: string;
}

export type MetricsFile = {
  [language: string]: LanguageMetrics | AggregateMetrics | MacroAvgMetrics | MetaMeta;
} & {
  __overall__: AggregateMetrics;
  __macro_avg__: MacroAvgMetrics;
  __meta__: MetaMeta;
};

export interface SampleAnalysis {
  id: string;
  language: string;
  reference: string;
  hypothesis: string;
  ref_norm?: string;
  hyp_norm?: string;
  ref_numcanon?: string;
  hyp_numcanon?: string;
  detected_language?: string;
  wer_raw?: number;
  wer_norm?: number;
  flags?: string[];
}

export interface ErrorBuckets {
  numeric_mismatch_count: number;
  punctuation_only_count: number;
  spacing_tokenization_count: number;
  entity_mismatch_count: number;
  script_confusion_count: number;
  empty_hypothesis_count: number;
}

export interface SubstitutionEntry {
  ref: string;
  hyp: string;
  count: number;
}

export interface WordCountEntry {
  word: string;
  count: number;
}

export interface LanguageErrorAnalysis {
  top_substitutions: SubstitutionEntry[];
  top_insertions: WordCountEntry[];
  top_deletions: WordCountEntry[];
  error_buckets: ErrorBuckets;
  examples: {
    worst_samples: string[];
    best_samples: string[];
    numeric_mismatch_samples: string[];
    entity_mismatch_samples: string[];
  };
}

export interface ErrorAnalysisSummary {
  model_diagnosis: "recognition-limited" | "formatting-limited" | "numeric-limited" | "mixed";
  primary_error_source: string;
  numeric_verbalization_impact: string;
  formatting_impact: string;
  worst_languages: string[];
  best_languages: string[];
}

export type ErrorAnalysisFile = {
  [language: string]: LanguageErrorAnalysis | ErrorAnalysisSummary;
} & {
  __summary__: ErrorAnalysisSummary;
};

export interface ModelInfo {
  id: string;
  displayName: string;
  checkpoints: string[];
}

export interface CheckpointComparison {
  language: string;
  [key: string]: number | string;
}

// === Constants ===

export const MODEL_REGISTRY: Record<string, string> = {
  "qwen3-asr": "Qwen3-ASR-1.7B",
  "gemma3n-e2b": "Gemma-3n-E2B-ASR",
  "parakeet-1.1b": "Parakeet-1.1B-Language-Guided",
  "parakeet-tdt-1.1b-lang": "Parakeet-TDT-1.1B-Lang",
  "elevenlabs-scribe-v2": "ElevenLabs Scribe v2",
  "elevenlabs-scribe-v1": "ElevenLabs Scribe v1",
  "gemini-3-flash-strict": "Gemini 3 Flash (Strict)",
  "gemini-3.1-pro": "Gemini 3.1 Pro",
  "sarvam-saaras-v3": "Sarvam Saaras v3",
};

// Models grouped by category
export const OUR_MODELS = ["qwen3-asr", "gemma3n-e2b", "parakeet-1.1b", "parakeet-tdt-1.1b-lang"] as const;
export const BASELINE_MODELS = [
  "elevenlabs-scribe-v2", "elevenlabs-scribe-v1",
  "gemini-3-flash-strict", "gemini-3.1-pro",
  "sarvam-saaras-v3",
] as const;

export const LANGUAGES = [
  "assamese", "bengali", "english", "gujarati", "hindi", "kannada",
  "malayalam", "marathi", "odia", "punjabi", "tamil", "telugu",
] as const;

export const LANGUAGE_CODES: Record<string, string> = {
  assamese: "as", bengali: "bn", english: "en", gujarati: "gu",
  hindi: "hi", kannada: "kn", malayalam: "ml", marathi: "mr",
  odia: "or", punjabi: "pa", tamil: "ta", telugu: "te",
};

export const METRIC_TIERS = ["wer_raw", "wer_norm", "wer_numcanon", "space_norm_wer", "mer", "cer_norm", "wer_norm_nonum", "mer_nonum"] as const;
export type MetricTier = typeof METRIC_TIERS[number];

export const METRIC_LABELS: Record<MetricTier, string> = {
  wer_raw: "WER Raw",
  wer_norm: "WER Norm",
  wer_numcanon: "WER NumCanon",
  space_norm_wer: "Space-Norm WER",
  mer: "MER",
  cer_norm: "CER Norm",
  wer_norm_nonum: "WER (no num)",
  mer_nonum: "MER (no num)",
};

export const METRIC_DESCRIPTIONS: Record<MetricTier, string> = {
  wer_raw: "Strict transcript fidelity",
  wer_norm: "Primary ASR metric",
  wer_numcanon: "Numeric robustness",
  mer: "Meaningful Error Rate (space-normalized)",
  cer_norm: "Script-sensitive (Indic)",
  wer_norm_nonum: "WER excluding numeric samples",
  mer_nonum: "MER excluding numeric samples",
};

// Primary tiers shown in the main metric selector (exclude nonum variants)
export const PRIMARY_METRIC_TIERS = ["wer_raw", "wer_norm", "wer_numcanon", "mer", "cer_norm"] as const;

export const CHART_COLORS = [
  "#6366f1", "#8b5cf6", "#a78bfa", "#c084fc",
  "#e879f9", "#f472b6", "#fb7185", "#f87171",
  "#fb923c", "#fbbf24", "#a3e635", "#34d399",
];

export const MODEL_COLORS: Record<string, string> = {
  "qwen3-asr": "#6366f1",
  "gemma3n-e2b": "#8b5cf6",
  "parakeet-1.1b": "#34d399",
  "parakeet-tdt-1.1b-lang": "#2dd4bf",
  "elevenlabs-scribe-v2": "#f472b6",
  "elevenlabs-scribe-v1": "#fb7185",
  "gemini-3-flash-strict": "#fbbf24",
  "gemini-3.1-pro": "#a3e635",
  "sarvam-saaras-v3": "#22d3ee",
};

// === Backward compatibility ===
// Converts old-schema metrics into new-schema shape so dashboard works
// with both old data (pre-migration) and new data.

export interface OldLanguageMetrics {
  n_samples: number;
  wer: number;
  cer: number;
  wer_normalized: number;
  cer_normalized: number;
  empty_hypotheses: number;
}

export function isOldSchema(obj: Record<string, unknown>): boolean {
  return "wer" in obj && !("wer_raw" in obj);
}

export function migrateLanguageMetrics(old: OldLanguageMetrics): LanguageMetrics {
  return {
    n_samples: old.n_samples,
    wer_raw: old.wer,
    wer_norm: old.wer_normalized,
    wer_numcanon: old.wer_normalized,
    mer: old.cer_normalized, // fallback: use CER as rough MER proxy
    cer_norm: old.cer_normalized,
    empty_hypotheses: old.empty_hypotheses,
    normalization_delta: {
      raw_to_norm: old.wer - old.wer_normalized,
      norm_to_numcanon: 0,
      norm_to_mer: 0,
    },
  };
}

+  space_norm_wer: "Word-level errors after space-insensitive char alignment",


Add to PRIMARY_METRIC_TIERS:


export const PRIMARY_METRIC_TIERS = ["wer_raw", "wer_norm", "wer_numcanon", "mer", "cer_norm"] as const;

export const PRIMARY_METRIC_TIERS = ["wer_raw", "wer_norm", "wer_numcanon", "space_norm_wer", "mer", "cer_norm"] as const;


// === Metric tiers ===
// wer_raw:      minimal normalization, strict fidelity
// wer_norm:     PRIMARY metric — NFKC + whitespace + punctuation + case fold
// wer_numcanon: wer_norm + number canonicalization
// cer_norm:     character-level normalized — critical for Indic scripts

export interface NormalizationDelta {
  raw_to_norm: number;       // wer_raw - wer_norm (negative = norm helped)
  norm_to_numcanon: number;  // wer_norm - wer_numcanon
  norm_to_mer?: number;      // wer_norm - mer (quantifies spacing error inflation)
}

export interface LanguageMetrics {
  n_samples: number;
  wer_raw: number;
  wer_norm: number;
  wer_numcanon: number;
  mer: number;
  cer_norm: number;
  space_norm_wer?: number;
  wer_norm_nonum?: number;
  mer_nonum?: number;
  numeric_samples_dropped?: number;
  empty_hypotheses: number;
  normalization_delta: NormalizationDelta;
}

export interface AggregateMetrics {
  n_samples: number;
  wer_raw: number;
  wer_norm: number;
  wer_numcanon: number;
  mer: number;
  cer_norm: number;
  space_norm_wer?: number;
  wer_norm_nonum?: number;
  mer_nonum?: number;
  numeric_samples_dropped?: number;
}

export interface MacroAvgMetrics {
  n_languages: number;
  wer_raw: number;
  wer_norm: number;
  wer_numcanon: number;
  mer: number;
  cer_norm: number;
}

export interface MetaMeta {
  checkpoint: string;
  checkpoint_name: string;
  model_id: string;
  model_type?: string;
  dataset: string;
  batch_size?: number;
  force_language?: boolean;
  inference_time_sec: number;
  total_audio_sec: number;
  rtf: number;
  timestamp?: string;
  gpu?: string;
  framework?: string;
  normalization_version?: string;
  jiwer_version?: string;
}

export type MetricsFile = {
  [language: string]: LanguageMetrics | AggregateMetrics | MacroAvgMetrics | MetaMeta;
} & {
  __overall__: AggregateMetrics;
  __macro_avg__: MacroAvgMetrics;
  __meta__: MetaMeta;
};

export interface SampleAnalysis {
  id: string;
  language: string;
  reference: string;
  hypothesis: string;
  ref_norm?: string;
  hyp_norm?: string;
  ref_numcanon?: string;
  hyp_numcanon?: string;
  detected_language?: string;
  wer_raw?: number;
  wer_norm?: number;
  flags?: string[];
}

export interface ErrorBuckets {
  numeric_mismatch_count: number;
  punctuation_only_count: number;
  spacing_tokenization_count: number;
  entity_mismatch_count: number;
  script_confusion_count: number;
  empty_hypothesis_count: number;
}

export interface SubstitutionEntry {
  ref: string;
  hyp: string;
  count: number;
}

export interface WordCountEntry {
  word: string;
  count: number;
}

export interface LanguageErrorAnalysis {
  top_substitutions: SubstitutionEntry[];
  top_insertions: WordCountEntry[];
  top_deletions: WordCountEntry[];
  error_buckets: ErrorBuckets;
  examples: {
    worst_samples: string[];
    best_samples: string[];
    numeric_mismatch_samples: string[];
    entity_mismatch_samples: string[];
  };
}

export interface ErrorAnalysisSummary {
  model_diagnosis: "recognition-limited" | "formatting-limited" | "numeric-limited" | "mixed";
  primary_error_source: string;
  numeric_verbalization_impact: string;
  formatting_impact: string;
  worst_languages: string[];
  best_languages: string[];
}

export type ErrorAnalysisFile = {
  [language: string]: LanguageErrorAnalysis | ErrorAnalysisSummary;
} & {
  __summary__: ErrorAnalysisSummary;
};

export interface ModelInfo {
  id: string;
  displayName: string;
  checkpoints: string[];
}

export interface CheckpointComparison {
  language: string;
  [key: string]: number | string;
}

// === Constants ===

export const MODEL_REGISTRY: Record<string, string> = {
  "qwen3-asr": "Qwen3-ASR-1.7B",
  "gemma3n-e2b": "Gemma-3n-E2B-ASR",
  "parakeet-1.1b": "Parakeet-1.1B-Language-Guided",
  "parakeet-tdt-1.1b-lang": "Parakeet-TDT-1.1B-Lang",
  "elevenlabs-scribe-v2": "ElevenLabs Scribe v2",
  "elevenlabs-scribe-v1": "ElevenLabs Scribe v1",
  "gemini-3-flash-strict": "Gemini 3 Flash (Strict)",
  "gemini-3.1-pro": "Gemini 3.1 Pro",
  "sarvam-saaras-v3": "Sarvam Saaras v3",
};

// Models grouped by category
export const OUR_MODELS = ["qwen3-asr", "gemma3n-e2b", "parakeet-1.1b", "parakeet-tdt-1.1b-lang"] as const;
export const BASELINE_MODELS = [
  "elevenlabs-scribe-v2", "elevenlabs-scribe-v1",
  "gemini-3-flash-strict", "gemini-3.1-pro",
  "sarvam-saaras-v3",
] as const;

export const LANGUAGES = [
  "assamese", "bengali", "english", "gujarati", "hindi", "kannada",
  "malayalam", "marathi", "odia", "punjabi", "tamil", "telugu",
] as const;

export const LANGUAGE_CODES: Record<string, string> = {
  assamese: "as", bengali: "bn", english: "en", gujarati: "gu",
  hindi: "hi", kannada: "kn", malayalam: "ml", marathi: "mr",
  odia: "or", punjabi: "pa", tamil: "ta", telugu: "te",
};

export const METRIC_TIERS = ["wer_raw", "wer_norm", "wer_numcanon", "space_norm_wer", "mer", "cer_norm", "wer_norm_nonum", "mer_nonum"] as const;
export type MetricTier = typeof METRIC_TIERS[number];

export const METRIC_LABELS: Record<MetricTier, string> = {
  wer_raw: "WER Raw",
  wer_norm: "WER Norm",
  wer_numcanon: "WER NumCanon",
  space_norm_wer: "Space-Norm WER",
  mer: "MER",
  cer_norm: "CER Norm",
  wer_norm_nonum: "WER (no num)",
  mer_nonum: "MER (no num)",
};

export const METRIC_DESCRIPTIONS: Record<MetricTier, string> = {
  wer_raw: "Strict transcript fidelity",
  wer_norm: "Primary ASR metric",
  wer_numcanon: "Numeric robustness",
  space_norm_wer: "Word-level errors after space-insensitive char alignment",
  mer: "Meaningful Error Rate (space-normalized)",
  cer_norm: "Script-sensitive (Indic)",
  wer_norm_nonum: "WER excluding numeric samples",
  mer_nonum: "MER excluding numeric samples",
};

// Primary tiers shown in the main metric selector (exclude nonum variants)
export const PRIMARY_METRIC_TIERS = ["wer_raw", "wer_norm", "wer_numcanon", "mer", "cer_norm"] as const;

export const CHART_COLORS = [
  "#6366f1", "#8b5cf6", "#a78bfa", "#c084fc",
  "#e879f9", "#f472b6", "#fb7185", "#f87171",
  "#fb923c", "#fbbf24", "#a3e635", "#34d399",
];

export const MODEL_COLORS: Record<string, string> = {
  "qwen3-asr": "#6366f1",
  "gemma3n-e2b": "#8b5cf6",
  "parakeet-1.1b": "#34d399",
  "parakeet-tdt-1.1b-lang": "#2dd4bf",
  "elevenlabs-scribe-v2": "#f472b6",
  "elevenlabs-scribe-v1": "#fb7185",
  "gemini-3-flash-strict": "#fbbf24",
  "gemini-3.1-pro": "#a3e635",
  "sarvam-saaras-v3": "#22d3ee",
};

// === Backward compatibility ===
// Converts old-schema metrics into new-schema shape so dashboard works
// with both old data (pre-migration) and new data.

export interface OldLanguageMetrics {
  n_samples: number;
  wer: number;
  cer: number;
  wer_normalized: number;
  cer_normalized: number;
  empty_hypotheses: number;
}

export function isOldSchema(obj: Record<string, unknown>): boolean {
  return "wer" in obj && !("wer_raw" in obj);
}

export function migrateLanguageMetrics(old: OldLanguageMetrics): LanguageMetrics {
  return {
    n_samples: old.n_samples,
    wer_raw: old.wer,
    wer_norm: old.wer_normalized,
    wer_numcanon: old.wer_normalized,
    mer: old.cer_normalized, // fallback: use CER as rough MER proxy
    cer_norm: old.cer_normalized,
    empty_hypotheses: old.empty_hypotheses,
    normalization_delta: {
      raw_to_norm: old.wer - old.wer_normalized,
      norm_to_numcanon: 0,
      norm_to_mer: 0,
    },
  };
}

-export const PRIMARY_METRIC_TIERS = ["wer_raw", "wer_norm", "wer_numcanon", "mer", "cer_norm"] as const;

+export const PRIMARY_METRIC_TIERS = ["wer_raw", "wer_norm", "wer_numcanon", "space_norm_wer", "mer", "cer_norm"] as const;


Now update the compare API to include space_norm_wer:


          langMetrics[lang] = {
            wer_raw: lm.wer_raw,
            wer_norm: lm.wer_norm,
            wer_numcanon: lm.wer_numcanon,
            mer: lm.mer ?? 0,
            cer_norm: lm.cer_norm,
            wer_norm_nonum: lm.wer_norm_nonum ?? lm.wer_norm,
            mer_nonum: lm.mer_nonum ?? lm.mer ?? 0,
          };

          langMetrics[lang] = {
            wer_raw: lm.wer_raw,
            wer_norm: lm.wer_norm,
            wer_numcanon: lm.wer_numcanon,
            space_norm_wer: lm.space_norm_wer ?? 0,
            mer: lm.mer ?? 0,
            cer_norm: lm.cer_norm,
            wer_norm_nonum: lm.wer_norm_nonum ?? lm.wer_norm,
            mer_nonum: lm.mer_nonum ?? lm.mer ?? 0,
          };


The file /home/ubuntu/training/asr-benchmark-dashboard/src/app/api/compare/route.ts has been updated successfully.

          langMetrics[lang] = {
            wer_raw: lm.wer_raw,
            wer_norm: lm.wer_norm,
            wer_numcanon: lm.wer_numcanon,
            space_norm_wer: lm.space_norm_wer ?? 0,
            mer: lm.mer ?? 0,
            cer_norm: lm.cer_norm,
            wer_norm_nonum: lm.wer_norm_nonum ?? lm.wer_norm,
            mer_nonum: lm.mer_nonum ?? lm.mer ?? 0,
          };

import { getMetrics, getModels } from "@/lib/data";
import { LANGUAGES, LanguageMetrics } from "@/lib/types";

export async function GET() {
  const models = await getModels();
  const result: Record<string, Record<string, Record<string, Record<string, number>>>> = {};

  for (const model of models) {
    result[model.id] = {};
    for (const ckpt of model.checkpoints) {
      const metrics = await getMetrics(model.id, ckpt);
      if (!metrics) continue;
      const langMetrics: Record<string, Record<string, number>> = {};
      for (const lang of LANGUAGES) {
        const lm = metrics[lang] as LanguageMetrics | undefined;
        if (lm && typeof lm === "object" && "wer_norm" in lm) {
          langMetrics[lang] = {
            wer_raw: lm.wer_raw,
            wer_norm: lm.wer_norm,
            wer_numcanon: lm.wer_numcanon,
            mer: lm.mer ?? 0,
            cer_norm: lm.cer_norm,
            wer_norm_nonum: lm.wer_norm_nonum ?? lm.wer_norm,
            mer_nonum: lm.mer_nonum ?? lm.mer ?? 0,
          };
        }
      }
      langMetrics["__overall__"] = {
        wer_raw: metrics.__overall__?.wer_raw ?? 0,
        wer_norm: metrics.__overall__?.wer_norm ?? 0,
        wer_numcanon: metrics.__overall__?.wer_numcanon ?? 0,
        mer: metrics.__overall__?.mer ?? 0,
        cer_norm: metrics.__overall__?.cer_norm ?? 0,
        wer_norm_nonum: metrics.__overall__?.wer_norm_nonum ?? metrics.__overall__?.wer_norm ?? 0,
        mer_nonum: metrics.__overall__?.mer_nonum ?? metrics.__overall__?.mer ?? 0,
      };
      result[model.id][ckpt] = langMetrics;
    }
  }

  return Response.json(result);
}

+            space_norm_wer: lm.space_norm_wer ?? 0,


      langMetrics["__overall__"] = {
        wer_raw: metrics.__overall__?.wer_raw ?? 0,
        wer_norm: metrics.__overall__?.wer_norm ?? 0,
        wer_numcanon: metrics.__overall__?.wer_numcanon ?? 0,
        mer: metrics.__overall__?.mer ?? 0,
        cer_norm: metrics.__overall__?.cer_norm ?? 0,
        wer_norm_nonum: metrics.__overall__?.wer_norm_nonum ?? metrics.__overall__?.wer_norm ?? 0,
        mer_nonum: metrics.__overall__?.mer_nonum ?? metrics.__overall__?.mer ?? 0,
      };

      langMetrics["__overall__"] = {
        wer_raw: metrics.__overall__?.wer_raw ?? 0,
        wer_norm: metrics.__overall__?.wer_norm ?? 0,
        wer_numcanon: metrics.__overall__?.wer_numcanon ?? 0,
        space_norm_wer: metrics.__overall__?.space_norm_wer ?? 0,
        mer: metrics.__overall__?.mer ?? 0,
        cer_norm: metrics.__overall__?.cer_norm ?? 0,
        wer_norm_nonum: metrics.__overall__?.wer_norm_nonum ?? metrics.__overall__?.wer_norm ?? 0,
        mer_nonum: metrics.__overall__?.mer_nonum ?? metrics.__overall__?.mer ?? 0,
      };


      langMetrics["__overall__"] = {
        wer_raw: metrics.__overall__?.wer_raw ?? 0,
        wer_norm: metrics.__overall__?.wer_norm ?? 0,
        wer_numcanon: metrics.__overall__?.wer_numcanon ?? 0,
        space_norm_wer: metrics.__overall__?.space_norm_wer ?? 0,
        mer: metrics.__overall__?.mer ?? 0,
        cer_norm: metrics.__overall__?.cer_norm ?? 0,
        wer_norm_nonum: metrics.__overall__?.wer_norm_nonum ?? metrics.__overall__?.wer_norm ?? 0,
        mer_nonum: metrics.__overall__?.mer_nonum ?? metrics.__overall__?.mer ?? 0,
      };

import { getMetrics, getModels } from "@/lib/data";
import { LANGUAGES, LanguageMetrics } from "@/lib/types";

export async function GET() {
  const models = await getModels();
  const result: Record<string, Record<string, Record<string, Record<string, number>>>> = {};

  for (const model of models) {
    result[model.id] = {};
    for (const ckpt of model.checkpoints) {
      const metrics = await getMetrics(model.id, ckpt);
      if (!metrics) continue;
      const langMetrics: Record<string, Record<string, number>> = {};
      for (const lang of LANGUAGES) {
        const lm = metrics[lang] as LanguageMetrics | undefined;
        if (lm && typeof lm === "object" && "wer_norm" in lm) {
          langMetrics[lang] = {
            wer_raw: lm.wer_raw,
            wer_norm: lm.wer_norm,
            wer_numcanon: lm.wer_numcanon,
            space_norm_wer: lm.space_norm_wer ?? 0,
            mer: lm.mer ?? 0,
            cer_norm: lm.cer_norm,
            wer_norm_nonum: lm.wer_norm_nonum ?? lm.wer_norm,
            mer_nonum: lm.mer_nonum ?? lm.mer ?? 0,
          };
        }
      }
      langMetrics["__overall__"] = {
        wer_raw: metrics.__overall__?.wer_raw ?? 0,
        wer_norm: metrics.__overall__?.wer_norm ?? 0,
        wer_numcanon: metrics.__overall__?.wer_numcanon ?? 0,
        mer: metrics.__overall__?.mer ?? 0,
        cer_norm: metrics.__overall__?.cer_norm ?? 0,
        wer_norm_nonum: metrics.__overall__?.wer_norm_nonum ?? metrics.__overall__?.wer_norm ?? 0,
        mer_nonum: metrics.__overall__?.mer_nonum ?? metrics.__overall__?.mer ?? 0,
      };
      result[model.id][ckpt] = langMetrics;
    }
  }

  return Response.json(result);
}

+        space_norm_wer: metrics.__overall__?.space_norm_wer ?? 0,


Now add space_norm_wer to the overview metric cards and the WER analysis table:


            {/* Metric Cards — all five tiers */}
            <div className="grid grid-cols-5 gap-3">
              <MetricCard
                label="WER Raw"
                value={overall?.wer_raw ?? 0}
                subtitle="Strict fidelity"
              />
              <MetricCard
                label="WER Norm"
                value={overall?.wer_norm ?? 0}
                subtitle="Primary metric"
              />
              <MetricCard
                label="WER NumCanon"
                value={overall?.wer_numcanon ?? 0}
                subtitle="Numeric robustness"
              />
              <MetricCard
                label="MER"
                value={overall?.mer ?? 0}
                subtitle="Space-normalized"
              />
              <MetricCard
                label="CER Norm"
                value={overall?.cer_norm ?? 0}
                subtitle="Script-sensitive"
              />
            </div>

{/* Metric Cards — all tiers */}

              <MetricCard
                label="WER Raw"
                value={overall?.wer_raw ?? 0}
                subtitle="Strict fidelity"
              />
              <MetricCard
                label="WER Norm"
                value={overall?.wer_norm ?? 0}
                subtitle="Primary metric"
              />
              <MetricCard
                label="WER NumCanon"
                value={overall?.wer_numcanon ?? 0}
                subtitle="Numeric robustness"
              />

              <MetricCard
                label="Space-Norm WER"
                value={overall?.space_norm_wer ?? 0}
                subtitle="Word-level, space-forgiven"
              />
              <MetricCard
                label="MER"
                value={overall?.mer ?? 0}
                subtitle="Char-level, space-stripped"
              />
              <MetricCard
                label="CER Norm"
                value={overall?.cer_norm ?? 0}
                subtitle="Script-sensitive"
              />


The file /home/ubuntu/training/asr-benchmark-dashboard/src/app/page.tsx has been updated successfully.

            {/* Metric Cards — all five tiers */}
            <div className="grid grid-cols-5 gap-3">
              <MetricCard
                label="WER Raw"
                value={overall?.wer_raw ?? 0}
                subtitle="Strict fidelity"
              />
              <MetricCard
                label="WER Norm"
                value={overall?.wer_norm ?? 0}
                subtitle="Primary metric"
              />
              <MetricCard
                label="WER NumCanon"
                value={overall?.wer_numcanon ?? 0}
                subtitle="Numeric robustness"
              />
              <MetricCard
                label="MER"
                value={overall?.mer ?? 0}
                subtitle="Space-normalized"
              />
              <MetricCard
                label="CER Norm"
                value={overall?.cer_norm ?? 0}
                subtitle="Script-sensitive"
              />
            </div>

{/* Metric Cards — all tiers */}

              <MetricCard
                label="WER Raw"
                value={overall?.wer_raw ?? 0}
                subtitle="Strict fidelity"
              />
              <MetricCard
                label="WER Norm"
                value={overall?.wer_norm ?? 0}
                subtitle="Primary metric"
              />
              <MetricCard
                label="WER NumCanon"
                value={overall?.wer_numcanon ?? 0}
                subtitle="Numeric robustness"
              />

              <MetricCard
                label="Space-Norm WER"
                value={overall?.space_norm_wer ?? 0}
                subtitle="Word-level, space-forgiven"
              />
              <MetricCard
                label="MER"
                value={overall?.mer ?? 0}
                subtitle="Char-level, space-stripped"
              />
              <MetricCard
                label="CER Norm"
                value={overall?.cer_norm ?? 0}
                subtitle="Script-sensitive"
              />

"use client";

import { useState, useEffect, useCallback } from "react";
import Sidebar, { ViewMode } from "@/components/Sidebar";
import MetricCard from "@/components/MetricCard";
import LanguageBarChart from "@/components/LanguageBarChart";
import CheckpointLineChart from "@/components/CheckpointLineChart";
import CrossModelChart from "@/components/CrossModelChart";
import LanguageHeatmap from "@/components/LanguageHeatmap";
import SampleTable from "@/components/SampleTable";
import BaselinesView from "@/components/BaselinesView";
import {
  ModelInfo, LANGUAGES, MODEL_REGISTRY,
  MetricTier, METRIC_LABELS, METRIC_DESCRIPTIONS,
  LanguageMetrics, AggregateMetrics, PRIMARY_METRIC_TIERS,
} from "@/lib/types";

interface MetaInfo {
  checkpoint?: string;
  checkpoint_name?: string;
  model_id?: string;
  dataset?: string;
  inference_time_sec?: number;
  total_audio_sec?: number;
  rtf?: number;
  timestamp?: string;
  normalization_version?: string;
}

interface MetricsData {
  [key: string]: unknown;
  __overall__: AggregateMetrics;
  __meta__: MetaInfo;
  __macro_avg__: { n_languages?: number; wer_raw?: number; wer_norm?: number; wer_numcanon?: number; cer_norm?: number };
}

interface SampleData {
  id: string;
  language: string;
  reference: string;
  hypothesis: string;
  ref_norm?: string;
  hyp_norm?: string;
  detected_language?: string;
  flags?: string[];
}

interface PaginationData {
  page: number;
  pageSize: number;
  total: number;
  totalPages: number;
}

// Compare data now has nested metric tiers per language
type CompareData = Record<string, Record<string, Record<string, Record<string, number>>>>;

export default function Dashboard() {
  const [models, setModels] = useState<ModelInfo[]>([]);
  const [selectedModel, setSelectedModel] = useState<string | null>(null);
  const [selectedCheckpoint, setSelectedCheckpoint] = useState<string | null>(null);
  const [activeView, setActiveView] = useState<ViewMode>("overview");
  const [metrics, setMetrics] = useState<MetricsData | null>(null);
  const [compareData, setCompareData] = useState<CompareData>({});
  const [samples, setSamples] = useState<SampleData[]>([]);
  const [pagination, setPagination] = useState<PaginationData>({ page: 1, pageSize: 20, total: 0, totalPages: 0 });
  const [sampleLanguage, setSampleLanguage] = useState<string>("all");
  const [samplePage, setSamplePage] = useState(1);
  const [samplesLoading, setSamplesLoading] = useState(false);
  const [compareCheckpoints, setCompareCheckpoints] = useState<Record<string, string>>({});
  const [selectedMetric, setSelectedMetric] = useState<MetricTier>("wer_norm");

  useEffect(() => {
    fetch("/api/models")
      .then((r) => r.json())
      .then((data: ModelInfo[]) => {
        setModels(data);
        const firstWithCkpts = data.find((m) => m.checkpoints.length > 0);
        if (firstWithCkpts) {
          setSelectedModel(firstWithCkpts.id);
          setSelectedCheckpoint(firstWithCkpts.checkpoints[firstWithCkpts.checkpoints.length - 1]);
        }
        const defaults: Record<string, string> = {};
        for (const m of data) {
          if (m.checkpoints.length > 0) {
            defaults[m.id] = m.checkpoints[m.checkpoints.length - 1];
          }
        }
        setCompareCheckpoints(defaults);
      });
  }, []);

  useEffect(() => {
    fetch("/api/compare")
      .then((r) => r.json())
      .then(setCompareData);
  }, []);

  useEffect(() => {
    if (!selectedModel || !selectedCheckpoint) return;
    fetch(`/api/metrics?model=${selectedModel}&checkpoint=${selectedCheckpoint}`)
      .then((r) => r.json())
      .then(setMetrics);
  }, [selectedModel, selectedCheckpoint]);

  const loadSamples = useCallback(() => {
    if (!selectedModel || !selectedCheckpoint) return;
    setSamplesLoading(true);
    const langParam = sampleLanguage === "all" ? "" : `&language=${sampleLanguage}`;
    fetch(`/api/samples?model=${selectedModel}&checkpoint=${selectedCheckpoint}&page=${samplePage}&pageSize=20${langParam}`)
      .then((r) => r.json())
      .then((data) => {
        setSamples(data.samples || []);
        setPagination(data.pagination || { page: 1, pageSize: 20, total: 0, totalPages: 0 });
        setSamplesLoading(false);
      })
      .catch(() => setSamplesLoading(false));
  }, [selectedModel, selectedCheckpoint, samplePage, sampleLanguage]);

  useEffect(() => {
    loadSamples();
  }, [loadSamples]);

  // Build language data for bar charts
  const languageData = metrics
    ? LANGUAGES.map((lang) => {
        const lm = metrics[lang] as LanguageMetrics | undefined;
        return {
          language: lang,
          wer_raw: lm?.wer_raw ?? 0,
          wer_norm: lm?.wer_norm ?? 0,
          wer_numcanon: lm?.wer_numcanon ?? 0,
          cer_norm: lm?.cer_norm ?? 0,
        };
      })
    : [];

  const overall = metrics?.__overall__;
  const meta = metrics?.__meta__;

  // Build checkpoint progression for heatmap/line chart — extract selected metric tier
  const ckptProgressionData: Record<string, Record<string, number>> = {};
  if (selectedModel && compareData[selectedModel]) {
    for (const [ckpt, langData] of Object.entries(compareData[selectedModel])) {
      const flat: Record<string, number> = {};
      for (const [lang, tierObj] of Object.entries(langData)) {
        flat[lang] = (tierObj as Record<string, number>)?.[selectedMetric] ?? 0;
      }
      ckptProgressionData[ckpt] = flat;
    }
  }

  // Build cross-model compare data flattened by selected metric
  const crossModelFlat: Record<string, Record<string, Record<string, number>>> = {};
  for (const [modelId, ckpts] of Object.entries(compareData)) {
    crossModelFlat[modelId] = {};
    for (const [ckpt, langData] of Object.entries(ckpts)) {
      const flat: Record<string, number> = {};
      for (const [lang, tierObj] of Object.entries(langData)) {
        flat[lang] = (tierObj as Record<string, number>)?.[selectedMetric] ?? 0;
      }
      crossModelFlat[modelId][ckpt] = flat;
    }
  }

  // Metric tier selector component
  const MetricSelector = () => (

      {PRIMARY_METRIC_TIERS.map((tier) => (
        <button
          key={tier}
          onClick={() => setSelectedMetric(tier)}
          className={`px-3 py-1.5 rounded text-[10px] transition-colors ${
            selectedMetric === tier
              ? "bg-[var(--color-card-hover)] text-[var(--color-foreground)]"
              : "text-[var(--color-muted)] hover:text-[var(--color-foreground)]"
          }`}
          title={METRIC_DESCRIPTIONS[tier]}
        >
          {METRIC_LABELS[tier]}
        </button>
      ))}

  );

  return (

      <Sidebar
        models={models}
        selectedModel={selectedModel}
        selectedCheckpoint={selectedCheckpoint}
        activeView={activeView}
        onSelectModel={setSelectedModel}
        onSelectCheckpoint={(ckpt) => {
          setSelectedCheckpoint(ckpt);
          setSamplePage(1);
        }}
        onViewChange={setActiveView}
      />

        {!selectedModel || !selectedCheckpoint ? (

Select a model and checkpoint to begin.

        ) : activeView === "overview" ? (

            {/* Header */}

                  {MODEL_REGISTRY[selectedModel] || selectedModel}

                  {selectedCheckpoint} · {(meta?.dataset as string) || "indic-asr-benchmark-6k"}
                  {meta?.normalization_version && <> · norm {meta.normalization_version as string}</>}

                <MetricSelector />

                  RTF: {typeof meta?.rtf === "number" ? meta.rtf.toFixed(4) : "—"} · Inference: {typeof meta?.inference_time_sec === "number" ? `${(meta.inference_time_sec / 60).toFixed(1)}m` : "—"}

            {/* Metric Cards — all five tiers */}

              <MetricCard
                label="WER Raw"
                value={overall?.wer_raw ?? 0}
                subtitle="Strict fidelity"
              />
              <MetricCard
                label="WER Norm"
                value={overall?.wer_norm ?? 0}
                subtitle="Primary metric"
              />
              <MetricCard
                label="WER NumCanon"
                value={overall?.wer_numcanon ?? 0}
                subtitle="Numeric robustness"
              />
              <MetricCard
                label="MER"
                value={overall?.mer ?? 0}
                subtitle="Space-normalized"
              />
              <MetricCard
                label="CER Norm"
                value={overall?.cer_norm ?? 0}
                subtitle="Script-sensitive"
              />

            {/* Normalization impact strip */}
            {overall && overall.wer_raw !== overall.wer_norm && (

                  Raw → Norm
                  <span className={`text-xs font-medium tabular-nums ${(overall.wer_raw - overall.wer_norm) > 0 ? "text-[var(--color-success)]" : "text-[var(--color-danger)]"}`}>
                    {(overall.wer_raw - overall.wer_norm) > 0 ? "-" : "+"}{Math.abs(overall.wer_raw - overall.wer_norm).toFixed(2)}%

                  Norm → NumCanon
                  <span className={`text-xs font-medium tabular-nums ${(overall.wer_norm - overall.wer_numcanon) > 0 ? "text-[var(--color-success)]" : "text-[var(--color-muted)]"}`}>
                    {(overall.wer_norm - overall.wer_numcanon) > 0 ? "-" : "+"}{Math.abs(overall.wer_norm - overall.wer_numcanon).toFixed(2)}%

                  Spacing Inflation
                  <span className={`text-xs font-medium tabular-nums ${(overall.wer_norm - (overall.mer ?? 0)) > 0 ? "text-[var(--color-warning)]" : "text-[var(--color-muted)]"}`}>
                    +{Math.abs(overall.wer_norm - (overall.mer ?? 0)).toFixed(2)}%

            )}

            {/* Bar Charts — selected metric */}

              <LanguageBarChart
                data={languageData}
                metric={selectedMetric}
                title={`${METRIC_LABELS[selectedMetric]} by Language`}
              />
              <LanguageBarChart
                data={languageData}
                metric={selectedMetric === "cer_norm" ? "wer_norm" : "cer_norm"}
                title={`${selectedMetric === "cer_norm" ? METRIC_LABELS.wer_norm : METRIC_LABELS.cer_norm} by Language`}
              />

            {/* Checkpoint Progression */}
            {Object.keys(ckptProgressionData).length > 1 && (
              <CheckpointLineChart
                data={ckptProgressionData}
                languages={[...LANGUAGES]}
                title={`${METRIC_LABELS[selectedMetric]} Progression — ${MODEL_REGISTRY[selectedModel]}`}
              />
            )}

            {/* Cross-Model Comparison */}
            {Object.keys(compareCheckpoints).length > 1 && (
              <>

                    Cross-Model Comparison

                  {models
                    .filter((m) => m.checkpoints.length > 0)
                    .map((m) => (
                      <select
                        key={m.id}
                        value={compareCheckpoints[m.id] || ""}
                        onChange={(e) =>
                          setCompareCheckpoints((prev) => ({ ...prev, [m.id]: e.target.value }))
                        }
                        className="bg-[var(--color-card)] border border-[var(--color-border)] rounded px-2 py-1 text-[10px] text-[var(--color-foreground)]"
                      >
                        {m.checkpoints.map((ckpt) => (
                          <option key={ckpt} value={ckpt}>
                            {MODEL_REGISTRY[m.id]?.split("-")[0]} — {ckpt}
                          </option>
                        ))}
                      </select>
                    ))}

                <CrossModelChart
                  data={crossModelFlat}
                  selectedCheckpoints={compareCheckpoints}
                  title={`${METRIC_LABELS[selectedMetric]} Comparison Across Models`}
                />
              </>
            )}

        ) : activeView === "wer-analysis" ? (
          /* WER Analysis View */

                  WER Analysis — {MODEL_REGISTRY[selectedModel] || selectedModel}

                  Language-wise comparison across checkpoints

              <MetricSelector />

            {/* Heatmap */}
            {Object.keys(ckptProgressionData).length > 0 && (
              <LanguageHeatmap data={ckptProgressionData} title={`${METRIC_LABELS[selectedMetric]} Heatmap — All Checkpoints`} />
            )}

            {/* Checkpoint Line Chart */}
            {Object.keys(ckptProgressionData).length > 1 && (
              <CheckpointLineChart
                data={ckptProgressionData}
                languages={[...LANGUAGES]}
                title={`${METRIC_LABELS[selectedMetric]} Trend by Language`}
                height={400}
              />
            )}

            {/* Sample Predictions */}

                  Sample Predictions — {selectedCheckpoint}

                <select
                  value={sampleLanguage}
                  onChange={(e) => {
                    setSampleLanguage(e.target.value);
                    setSamplePage(1);
                  }}
                  className="bg-[var(--color-card)] border border-[var(--color-border)] rounded px-2 py-1 text-[10px] text-[var(--color-foreground)]"
                >
                  <option value="all">All Languages</option>
                  {LANGUAGES.map((lang) => (
                    <option key={lang} value={lang}>
                      {lang.charAt(0).toUpperCase() + lang.slice(1)}
                    </option>
                  ))}
                </select>

              <SampleTable
                samples={samples}
                pagination={pagination}
                onPageChange={setSamplePage}
                loading={samplesLoading}
              />

            {/* Detailed Metrics Table — all four tiers */}
            {metrics && (

                  All Metric Tiers — {selectedCheckpoint}

                      Language
                      N
                      WER Raw
                      WER Norm
                      WER Num
                      MER
                      CER Norm
                      Spacing
                      Empty

                    {LANGUAGES.map((lang) => {
                      const lm = metrics[lang] as LanguageMetrics | undefined;
                      if (!lm || !lm.wer_norm) return null;
                      const delta_rn = lm.normalization_delta?.raw_to_norm ?? (lm.wer_raw - lm.wer_norm);
                      const delta_nn = lm.normalization_delta?.norm_to_numcanon ?? (lm.wer_norm - lm.wer_numcanon);
                      return (

                          {lang}
                          {lm.n_samples}
                          {lm.wer_raw.toFixed(2)}%

                             40 ? "text-[var(--color-danger)]" : lm.wer_norm < 20 ? "text-[var(--color-success)]" : "text-[var(--color-foreground)]"}>
                              {lm.wer_norm.toFixed(2)}%

                          {lm.wer_numcanon.toFixed(2)}%

                            <span className={(lm.mer ?? 0) < lm.wer_norm * 0.6 ? "text-[var(--color-success)]" : "text-[var(--color-foreground)]"}>
                              {(lm.mer ?? 0).toFixed(2)}%

                          {lm.cer_norm.toFixed(2)}%

                             10 ? "text-[var(--color-warning)]" : "text-[var(--color-muted)]"}>
                              +{(lm.wer_norm - (lm.mer ?? 0)).toFixed(1)}

                          {lm.empty_hypotheses ?? 0}

                      );
                    })}

            )}

        ) : activeView === "baselines" ? (
          <BaselinesView models={models} compareData={compareData} selectedMetric={selectedMetric} />
        ) : null}

  );
}

-            {/* Metric Cards — all five tiers */}

-            <div className="grid grid-cols-5 gap-3">

+            {/* Metric Cards — all tiers */}

+            <div className="grid grid-cols-3 gap-3">

+                value={overall?.space_norm_wer ?? 0}

+                subtitle="Word-level, space-forgiven"

-                subtitle="Space-normalized"

+                subtitle="Char-level, space-stripped"


Now update the normalization impact strip to show the WER→SNW gap:


                  Spacing Inflation
                  <span className={`text-xs font-medium tabular-nums ${(overall.wer_norm - (overall.mer ?? 0)) > 0 ? "text-[var(--color-warning)]" : "text-[var(--color-muted)]"}`}>
                    +{Math.abs(overall.wer_norm - (overall.mer ?? 0)).toFixed(2)}%

                  Norm → SNW
                  <span className={`text-xs font-medium tabular-nums ${(overall.wer_norm - (overall.space_norm_wer ?? 0)) > 0 ? "text-[var(--color-success)]" : "text-[var(--color-muted)]"}`}>
                    -{Math.abs(overall.wer_norm - (overall.space_norm_wer ?? 0)).toFixed(2)}%

                  SNW → MER
                  <span className={`text-xs font-medium tabular-nums ${((overall.space_norm_wer ?? 0) - (overall.mer ?? 0)) > 0 ? "text-[var(--color-warning)]" : "text-[var(--color-muted)]"}`}>
                    -{Math.abs((overall.space_norm_wer ?? 0) - (overall.mer ?? 0)).toFixed(2)}%