python3 << 'PYEOF'
import json, re, os
from collections import defaultdict
from jiwer import wer as compute_wer, cer as compute_cer
import unicodedata

# Same normalization functions
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'\([^)]*\)')

DIGIT_PATTERN = re.compile(r'\d+')
INDIC_DIGIT = re.compile(r'[\u0966-\u096F\u09E6-\u09EF\u0A66-\u0A6F\u0AE6-\u0AEF\u0B66-\u0B6F\u0BE6-\u0BEF\u0C66-\u0C6F\u0CE6-\u0CEF\u0D66-\u0D6F]+')
NUM_WORDS_EN = re.compile(r'\b(zero|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety|hundred|thousand|million|billion|lakh|crore)\b', re.I)

def has_numeric(ref, hyp, lang):
    """Check if either ref or hyp contains numeric content."""
    if DIGIT_PATTERN.search(ref) or DIGIT_PATTERN.search(hyp):
        return True
    if INDIC_DIGIT.search(ref) or INDIC_DIGIT.search(hyp):
        return True
    if lang == 'english' and (NUM_WORDS_EN.search(ref) or NUM_WORDS_EN.search(hyp)):
        return True
    return False

def norm_standard(text):
    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()
    text = ' '.join(text.split()).strip()
    return text

def norm_raw(text):
    return unicodedata.normalize('NFC', text).strip()

def norm_mer(text):
    return norm_standard(text).replace(' ', '')

# Process our checkpoints
OUR_CKPTS = [
    ('qwen3-asr', 'ckpt-24000'), ('qwen3-asr', 'ckpt-72000'),
    ('qwen3-asr', 'ckpt-100000'), ('qwen3-asr', 'ckpt-170000'), ('qwen3-asr', 'ckpt-200000'),
    ('gemma3n-e2b', 'ckpt-10000'), ('gemma3n-e2b', 'ckpt-20000'),
]

for model_id, ckpt in OUR_CKPTS:
    sa_path = f'{model_id}/{ckpt}/sample_analysis.json'
    if not os.path.isfile(sa_path):
        continue

    samples = json.load(open(sa_path))

    by_lang_all = defaultdict(list)
    by_lang_nonum = defaultdict(list)
    dropped_by_lang = defaultdict(int)

    for s in samples:
        lang = s['language']
        ref = s['reference']
        hyp = s['hypothesis']
        by_lang_all[lang].append(s)

        if has_numeric(ref, hyp, lang):
            dropped_by_lang[lang] += 1
        else:
            by_lang_nonum[lang].append(s)

    total_dropped = sum(dropped_by_lang.values())
    total_kept = len(samples) - total_dropped

    # Compute WER for all and no-numeric
    print(f'\n{"="*70}')
    print(f'{model_id}/{ckpt}  —  dropped {total_dropped} numeric samples, kept {total_kept}')
    print(f'{"="*70}')
    print(f'{"Language":<12} {"All":>4} {"Kept":>4} {"Drop":>4} {"WER_norm":>9} {"noNum":>9} {"Delta":>7} {"MER":>7} {"noNum_MER":>9}')
    print('-' * 85)

    overall_ref_norm_all, overall_hyp_norm_all = [], []
    overall_ref_norm_nn, overall_hyp_norm_nn = [], []
    overall_ref_mer_all, overall_hyp_mer_all = [], []
    overall_ref_mer_nn, overall_hyp_mer_nn = [], []

    for lang in sorted(by_lang_all.keys()):
        # All samples
        refs_all = [norm_standard(s['reference']) or '<empty>' for s in by_lang_all[lang]]
        hyps_all = [norm_standard(s['hypothesis']) or '<empty>' for s in by_lang_all[lang]]
        refs_mer_a = [norm_mer(s['reference']) or '<empty>' for s in by_lang_all[lang]]
        hyps_mer_a = [norm_mer(s['hypothesis']) or '<empty>' for s in by_lang_all[lang]]

        wer_all = compute_wer(refs_all, hyps_all) * 100
        mer_all = compute_cer(refs_mer_a, hyps_mer_a) * 100

        # No-numeric samples
        nn_samples = by_lang_nonum[lang]
        if nn_samples:
            refs_nn = [norm_standard(s['reference']) or '<empty>' for s in nn_samples]
            hyps_nn = [norm_standard(s['hypothesis']) or '<empty>' for s in nn_samples]
            refs_mer_n = [norm_mer(s['reference']) or '<empty>' for s in nn_samples]
            hyps_mer_n = [norm_mer(s['hypothesis']) or '<empty>' for s in nn_samples]
            wer_nn = compute_wer(refs_nn, hyps_nn) * 100
            mer_nn = compute_cer(refs_mer_n, hyps_mer_n) * 100
        else:
            refs_nn, hyps_nn = [], []
            refs_mer_n, hyps_mer_n = [], []
            wer_nn = 0
            mer_nn = 0

        delta = wer_all - wer_nn
        dropped = dropped_by_lang[lang]

        print(f'{lang:<12} {len(by_lang_all[lang]):>4} {len(nn_samples):>4} {dropped:>4} {wer_all:>8.2f}% {wer_nn:>8.2f}% {delta:>+6.2f} {mer_all:>6.2f}% {mer_nn:>8.2f}%')

        overall_ref_norm_all.extend(refs_all)
        overall_hyp_norm_all.extend(hyps_all)
        overall_ref_norm_nn.extend(refs_nn)
        overall_hyp_norm_nn.extend(hyps_nn)
        overall_ref_mer_all.extend(refs_mer_a)
        overall_hyp_mer_all.extend(hyps_mer_a)
        overall_ref_mer_nn.extend(refs_mer_n)
        overall_hyp_mer_nn.extend(hyps_mer_n)

    wer_overall_all = compute_wer(overall_ref_norm_all, overall_hyp_norm_all) * 100
    wer_overall_nn = compute_wer(overall_ref_norm_nn, overall_hyp_norm_nn) * 100
    mer_overall_all = compute_cer(overall_ref_mer_all, overall_hyp_mer_all) * 100
    mer_overall_nn = compute_cer(overall_ref_mer_nn, overall_hyp_mer_nn) * 100

    print('-' * 85)
    print(f'{"OVERALL":<12} {len(samples):>4} {total_kept:>4} {total_dropped:>4} {wer_overall_all:>8.2f}% {wer_overall_nn:>8.2f}% {wer_overall_all-wer_overall_nn:>+6.2f} {mer_overall_all:>6.2f}% {mer_overall_nn:>8.2f}%')

PYEOF

Recalculate WER excluding numeric samples


======================================================================
qwen3-asr/ckpt-24000  —  dropped 292 numeric samples, kept 5708
======================================================================
Language      All Kept Drop  WER_norm     noNum   Delta     MER noNum_MER
-------------------------------------------------------------------------------------
assamese      500  500    0    56.71%    56.71%  +0.00  28.35%    28.35%
bengali       500  457   43    34.07%    32.50%  +1.56  11.84%    10.47%
english       500  402   98    14.55%     8.80%  +5.75   8.89%     3.63%
gujarati      500  474   26    28.19%    26.55%  +1.65   9.62%     8.19%
hindi         500  493    7    14.12%    13.65%  +0.48   5.58%     5.23%
kannada       500  488   12    49.57%    49.29%  +0.28  13.76%    13.26%
malayalam     500  494    6    56.77%    56.41%  +0.36  15.42%    15.36%
marathi       500  449   51    41.49%    38.95%  +2.54  13.58%    11.43%
odia          500  468   32    43.83%    42.88%  +0.95  15.13%    14.16%
punjabi       500  496    4    29.43%    29.31%  +0.12  11.83%    11.64%
tamil         500  493    7    52.90%    52.57%  +0.33  15.46%    15.26%
telugu        500  494    6    44.16%    44.07%  +0.09  11.49%    11.38%
-------------------------------------------------------------------------------------
OVERALL      6000 5708  292    39.12%    38.61%  +0.51  13.96%    13.25%

======================================================================
qwen3-asr/ckpt-72000  —  dropped 281 numeric samples, kept 5719
======================================================================
Language      All Kept Drop  WER_norm     noNum   Delta     MER noNum_MER
-------------------------------------------------------------------------------------
assamese      500  500    0    52.78%    52.78%  +0.00  25.48%    25.48%
bengali       500  459   41    32.42%    30.95%  +1.47  11.20%     9.96%
english       500  402   98    14.63%     8.70%  +5.92   8.83%     3.50%
gujarati      500  476   24    26.58%    25.22%  +1.36   8.75%     7.64%
hindi         500  493    7    13.93%    13.44%  +0.50   5.42%     5.04%
kannada       500  489   11    46.40%    46.02%  +0.37  12.68%    12.26%
malayalam     500  497    3    52.34%    52.17%  +0.16  12.79%    12.75%
marathi       500  452   48    39.91%    37.38%  +2.52  13.27%    11.27%
odia          500  462   38    40.91%    40.11%  +0.80  13.42%    12.33%
punjabi       500  497    3    28.53%    28.37%  +0.16  12.18%    12.01%
tamil         500  496    4    49.16%    48.90%  +0.25  13.32%    13.19%
telugu        500  496    4    40.24%    40.24%  +0.01   9.72%     9.71%
-------------------------------------------------------------------------------------
OVERALL      6000 5719  281    36.77%    36.25%  +0.52  12.61%    11.93%

======================================================================
qwen3-asr/ckpt-100000  —  dropped 257 numeric samples, kept 5743
======================================================================
Language      All Kept Drop  WER_norm     noNum   Delta     MER noNum_MER
-------------------------------------------------------------------------------------
assamese      500  500    0    50.32%    50.32%  +0.00  24.32%    24.32%
bengali       500  465   35    31.18%    30.27%  +0.91  10.32%     9.30%
english       500  402   98    14.18%     8.31%  +5.87   8.63%     3.42%
gujarati      500  478   22    26.40%    25.01%  +1.39   8.97%     7.82%
hindi         500  494    6    13.93%    13.43%  +0.50   5.35%     4.97%
kannada       500  489   11    45.98%    45.67%  +0.31  12.05%    11.60%
malayalam     500  497    3    51.29%    51.12%  +0.17  12.60%    12.56%
marathi       500  457   43    40.47%    38.15%  +2.32  13.95%    12.29%
odia          500  469   31    40.87%    40.17%  +0.70  13.27%    12.23%
punjabi       500  497    3    27.07%    26.83%  +0.24  11.11%    10.93%
tamil         500  498    2    47.52%    47.41%  +0.11  12.47%    12.44%
telugu        500  497    3    39.59%    39.53%  +0.06   9.65%     9.61%
-------------------------------------------------------------------------------------
OVERALL      6000 5743  257    35.98%    35.51%  +0.47  12.24%    11.59%

======================================================================
qwen3-asr/ckpt-170000  —  dropped 257 numeric samples, kept 5743
======================================================================
Language      All Kept Drop  WER_norm     noNum   Delta     MER noNum_MER
-------------------------------------------------------------------------------------
assamese      500  500    0    49.04%    49.04%  +0.00  23.87%    23.87%
bengali       500  466   34    30.79%    29.75%  +1.04  10.17%     9.15%
english       500  402   98    14.27%     8.46%  +5.81   8.69%     3.44%
gujarati      500  477   23    26.36%    24.87%  +1.49   8.90%     7.70%
hindi         500  494    6    14.14%    13.68%  +0.46   5.35%     5.02%
kannada       500  489   11    45.25%    44.92%  +0.33  11.80%    11.32%
malayalam     500  497    3    50.57%    50.40%  +0.17  12.36%    12.32%
marathi       500  459   41    38.67%    36.61%  +2.07  12.69%    11.17%
odia          500  468   32    40.06%    39.29%  +0.78  12.91%    11.90%
punjabi       500  497    3    28.46%    28.23%  +0.23  12.63%    12.46%
tamil         500  497    3    47.17%    46.98%  +0.19  12.23%    12.14%
telugu        500  497    3    39.54%    39.48%  +0.06   9.52%     9.47%
-------------------------------------------------------------------------------------
OVERALL      6000 5743  257    35.59%    35.11%  +0.47  12.05%    11.41%

======================================================================
qwen3-asr/ckpt-200000  —  dropped 276 numeric samples, kept 5724
======================================================================
Language      All Kept Drop  WER_norm     noNum   Delta     MER noNum_MER
-------------------------------------------------------------------------------------
assamese      500  500    0    48.93%    48.93%  +0.00  24.04%    24.04%
bengali       500  459   41    31.31%    29.92%  +1.39  10.50%     9.24%
english       500  402   98    14.12%     8.21%  +5.91   8.57%     3.45%
gujarati      500  475   25    26.26%    24.73%  +1.53   8.98%     7.71%
hindi         500  493    7    13.97%    13.49%  +0.48   5.45%     5.11%
kannada       500  488   12    45.65%    45.26%  +0.39  12.02%    11.53%
malayalam     500  496    4    50.88%    50.72%  +0.16  12.38%    12.35%
marathi       500  452   48    38.57%    36.17%  +2.40  12.72%    10.73%
odia          500  468   32    40.43%    39.64%  +0.79  13.34%    12.32%
punjabi       500  497    3    27.87%    27.64%  +0.23  11.86%    11.68%
tamil         500  497    3    47.97%    47.77%  +0.20  12.73%    12.64%
telugu        500  497    3    39.15%    39.07%  +0.08   9.47%     9.42%
-------------------------------------------------------------------------------------
OVERALL      6000 5724  276    35.64%    35.10%  +0.54  12.16%    11.47%

======================================================================
gemma3n-e2b/ckpt-10000  —  dropped 164 numeric samples, kept 5836
======================================================================
Language      All Kept Drop  WER_norm     noNum   Delta     MER noNum_MER
-------------------------------------------------------------------------------------
assamese      500  500    0    36.13%    36.13%  +0.00  19.34%    19.34%
bengali       500  494    6    33.10%    33.16%  -0.06  15.50%    15.55%
english       500  401   99    15.11%    10.98%  +4.14   9.70%     5.51%
gujarati      500  496    4    21.98%    21.69%  +0.29   7.19%     6.99%
hindi         500  500    0    14.81%    14.81%  +0.00   5.93%     5.93%
kannada       500  498    2    42.58%    42.52%  +0.06  15.14%    15.13%
malayalam     500  500    0    45.42%    45.42%  +0.00  10.77%    10.77%
marathi       500  499    1    35.05%    35.10%  -0.05  12.22%    12.23%
odia          500  450   50    82.51%    73.69%  +8.82  58.40%    54.30%
punjabi       500  499    1    31.13%    31.09%  +0.03  44.12%    44.13%
tamil         500  500    0    47.89%    47.89%  +0.00  17.73%    17.73%
telugu        500  499    1    31.79%    30.89%  +0.89   9.59%     9.07%
-------------------------------------------------------------------------------------
OVERALL      6000 5836  164    36.24%    35.25%  +0.98  18.09%    17.34%

======================================================================
gemma3n-e2b/ckpt-20000  —  dropped 167 numeric samples, kept 5833
======================================================================
Language      All Kept Drop  WER_norm     noNum   Delta     MER noNum_MER
-------------------------------------------------------------------------------------
assamese      500  496    4    37.46%    37.03%  +0.43  20.49%    20.24%
bengali       500  492    8    26.03%    25.71%  +0.32   9.88%     9.67%
english       500  399  101    17.94%    13.17%  +4.77  12.68%     8.04%
gujarati      500  496    4    22.27%    22.05%  +0.22   7.53%     7.42%
hindi         500  500    0    14.54%    14.54%  +0.00   4.98%     4.98%
kannada       500  493    7    43.47%    42.49%  +0.98  17.77%    17.20%
malayalam     500  500    0    44.67%    44.67%  +0.00  10.67%    10.67%
marathi       500  495    5    30.51%    30.26%  +0.25   9.29%     9.11%
odia          500  469   31    73.08%    73.52%  -0.45  55.90%    54.01%
punjabi       500  497    3    26.69%    26.57%  +0.13  15.88%    14.79%
tamil         500  499    1    41.38%    41.35%  +0.03  17.53%    17.02%
telugu        500  497    3    30.62%    30.33%  +0.29   9.70%     8.32%
-------------------------------------------------------------------------------------
OVERALL      6000 5833  167    33.95%    33.57%  +0.38  16.07%    15.20%


Here's the summary:

## Numeric Samples Impact

**~260-290 samples dropped per Qwen checkpoint, ~165 per Gemma** (out of 6000). English has the most (98 consistently) since its references actually contain digits. Bengali and Marathi have 35-50 each where the model hallucinated digits.

### Overall WER with vs without numeric samples

| Model / Checkpoint | WER Norm (all) | WER Norm (no num) | Delta | MER (all) | MER (no num) |
|---|---|---|---|---|---|
| qwen3-asr/ckpt-24000 | 39.12% | 38.61% | -0.51 | 13.96% | 13.25% |
| qwen3-asr/ckpt-72000 | 36.77% | 36.25% | -0.52 | 12.61% | 11.93% |
| qwen3-asr/ckpt-100000 | 35.98% | 35.51% | -0.47 | 12.24% | 11.59% |
| qwen3-asr/ckpt-170000 | 35.59% | 35.11% | -0.47 | 12.05% | 11.41% |
| qwen3-asr/ckpt-200000 | 35.64% | 35.10% | -0.54 | 12.16% | 11.47% |
| gemma3n-e2b/ckpt-10000 | 36.24% | 35.25% | -0.98 | 18.09% | 17.34% |
| gemma3n-e2b/ckpt-20000 | 33.95% | 33.57% | -0.38 | 16.07% | 15.20% |

### Key takeaways

- **Numerics add ~0.4-0.5% to overall WER for Qwen, ~0.4-1.0% for Gemma** — not huge overall
- **English is hit hardest**: WER drops 5.8-5.9 points when numeric samples are removed (14.1% → 8.2%). The model is verbalizing digits instead of keeping them as digits
- **Marathi next**: ~2.4 point drop — model outputs "1960" when ref says "एक हजार नऊ शे साठ"
- **Bengali**: ~1.4 point drop — same digit-for-word pattern
- **Dravidian languages** (Malayalam, Tamil, Telugu, Kannada): barely affected (<0.4 points) — almost no numeric content in those samples
- **MER is barely affected** by removing numerics (~0.7 point drop), confirming that the numeric errors are mostly format conflicts (digit vs word), not content errors


can you also do this calculation for all mdoels and update as a column or section in dashboard


Bash

python3 << 'PYEOF'
import json, re, os, unicodedata
from collections import defaultdict
from jiwer import wer as compute_wer, cer as compute_cer

# Normalization
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'\([^)]*\)')
DIGIT_PATTERN = re.compile(r'\d+')
INDIC_DIGIT = re.compile(r'[\u0966-\u096F\u09E6-\u09EF\u0A66-\u0A6F\u0AE6-\u0AEF\u0B66-\u0B6F\u0BE6-\u0BEF\u0C66-\u0C6F\u0CE6-\u0CEF\u0D66-\u0D6F]+')
NUM_WORDS_EN = re.compile(r'\b(zero|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety|hundred|thousand|million|billion|lakh|crore)\b', re.I)

def has_numeric(ref, hyp, lang):
    if DIGIT_PATTERN.search(ref) or DIGIT_PATTERN.search(hyp): return True
    if INDIC_DIGIT.search(ref) or INDIC_DIGIT.search(hyp): return True
    if lang == 'english' and (NUM_WORDS_EN.search(ref) or NUM_WORDS_EN.search(hyp)): return True
    return False

def norm_standard(text):
    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()

def norm_mer(text):
    return norm_standard(text).replace(' ', '')

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

# Process every model/checkpoint
for model_dir in sorted(os.listdir('.')):
    if not os.path.isdir(model_dir): continue
    for ckpt in sorted(os.listdir(model_dir)):
        sa_path = f'{model_dir}/{ckpt}/sample_analysis.json'
        metrics_path = f'{model_dir}/{ckpt}/metrics.json'
        if not os.path.isfile(sa_path) or not os.path.isfile(metrics_path): continue

        samples = json.load(open(sa_path))
        # Get ref/hyp field names (old schema uses 'hypothesis', some use 'transcript')
        hyp_key = 'hypothesis' if 'hypothesis' in samples[0] else 'transcript'

        by_lang_nn = defaultdict(list)
        dropped_by_lang = defaultdict(int)
        for s in samples:
            lang = s['language']
            ref = s['reference']
            hyp = s.get(hyp_key, '') or ''
            if has_numeric(ref, hyp, lang):
                dropped_by_lang[lang] += 1
            else:
                by_lang_nn[lang].append(s)

        total_dropped = sum(dropped_by_lang.values())
        total_kept = len(samples) - total_dropped

        # Compute per-language no-numeric metrics
        metrics = json.load(open(metrics_path))

        all_ref_norm, all_hyp_norm = [], []
        all_ref_mer, all_hyp_mer = [], []

        for lang in LANGUAGES:
            nn = by_lang_nn.get(lang, [])
            if not nn:
                if lang in metrics and isinstance(metrics[lang], dict):
                    metrics[lang]['wer_norm_nonum'] = metrics[lang].get('wer_norm', 0)
                    metrics[lang]['mer_nonum'] = metrics[lang].get('mer', 0)
                    metrics[lang]['numeric_samples_dropped'] = 0
                continue

            refs = [norm_standard(s['reference']) or '<e>' for s in nn]
            hyps = [norm_standard(s.get(hyp_key, '') or '') or '<e>' for s in nn]
            rmer = [norm_mer(s['reference']) or '<e>' for s in nn]
            hmer = [norm_mer(s.get(hyp_key, '') or '') or '<e>' for s in nn]

            wn = compute_wer(refs, hyps) * 100
            mn = compute_cer(rmer, hmer) * 100

            if lang in metrics and isinstance(metrics[lang], dict):
                metrics[lang]['wer_norm_nonum'] = round(wn, 2)
                metrics[lang]['mer_nonum'] = round(mn, 2)
                metrics[lang]['numeric_samples_dropped'] = dropped_by_lang.get(lang, 0)

            all_ref_norm.extend(refs); all_hyp_norm.extend(hyps)
            all_ref_mer.extend(rmer); all_hyp_mer.extend(hmer)

        # Overall no-numeric
        if all_ref_norm:
            o_wn = compute_wer(all_ref_norm, all_hyp_norm) * 100
            o_mn = compute_cer(all_ref_mer, all_hyp_mer) * 100
        else:
            o_wn = 0; o_mn = 0

        metrics['__overall__']['wer_norm_nonum'] = round(o_wn, 2)
        metrics['__overall__']['mer_nonum'] = round(o_mn, 2)
        metrics['__overall__']['numeric_samples_dropped'] = total_dropped

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

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

        delta = metrics['__overall__'].get('wer_norm', 0) - o_wn
        print(f'{model_dir}/{ckpt:<15}  dropped={total_dropped:>3}  wer_norm={metrics["__overall__"].get("wer_norm",0):>6.2f}%  nonum={o_wn:>6.2f}%  delta={delta:>+5.2f}')

PYEOF

Compute no-numeric WER for ALL models and write to metrics.json


elevenlabs-scribe-v1/baseline         dropped=128  wer_norm= 21.11%  nonum= 20.80%  delta=+0.31
elevenlabs-scribe-v2/baseline         dropped=100  wer_norm= 17.74%  nonum= 17.55%  delta=+0.19
gemini-2.5-flash/baseline         dropped=468  wer_norm= 57.71%  nonum= 59.61%  delta=-1.90
gemini-3-flash-preview/baseline         dropped=326  wer_norm= 67.68%  nonum= 18.85%  delta=+48.83
gemini-3-flash-strict/baseline         dropped=345  wer_norm= 20.12%  nonum= 19.27%  delta=+0.85
gemini-3.1-pro/baseline         dropped=352  wer_norm= 18.98%  nonum= 18.10%  delta=+0.88
gemma3n-e2b/ckpt-10000       dropped=164  wer_norm= 36.24%  nonum= 35.25%  delta=+0.99
gemma3n-e2b/ckpt-20000       dropped=167  wer_norm= 33.95%  nonum= 33.57%  delta=+0.38
maya-asr-tdt-1.1b/ckpt-60000       dropped=261  wer_norm= 37.07%  nonum= 36.71%  delta=+0.36
maya-asr-tdt-1.1b/ckpt-80000       dropped=261  wer_norm= 36.93%  nonum= 36.59%  delta=+0.34
qwen3-asr/ckpt-100000      dropped=257  wer_norm= 35.98%  nonum= 35.51%  delta=+0.47
qwen3-asr/ckpt-170000      dropped=257  wer_norm= 35.59%  nonum= 35.11%  delta=+0.48
qwen3-asr/ckpt-200000      dropped=276  wer_norm= 35.64%  nonum= 35.10%  delta=+0.54
qwen3-asr/ckpt-24000       dropped=292  wer_norm= 39.12%  nonum= 38.61%  delta=+0.51
qwen3-asr/ckpt-72000       dropped=281  wer_norm= 36.77%  nonum= 36.25%  delta=+0.52
sarvam-saaras-v3/baseline         dropped=750  wer_norm= 18.26%  nonum= 16.52%  delta=+1.74


All metrics.json files updated with `wer_norm_nonum`, `mer_nonum`, and `numeric_samples_dropped`. Now let me fix permissions and update the dashboard.


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

Fix permissions


(Bash completed with no output)


Now update the dashboard. I'll add `wer_norm_nonum` and `mer_nonum` to the types, the compare API, and add a "No-Numeric" toggle in the Baselines view:


Edit

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

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;
}


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

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;
}

// === 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;
  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;
}

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",
  "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"] 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"] 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",
};

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)",
};

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",
  "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 interface AggregateMetrics {
  n_samples: number;
  wer_raw: number;
  wer_norm: number;
  wer_numcanon: number;
  mer: number;
  cer_norm: number;
}

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 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;
}

// === 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;
}

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",
  "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"] 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"] 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",
};

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)",
};

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",
  "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;
+  wer_norm_nonum?: number;
+  mer_nonum?: number;
+  numeric_samples_dropped?: number;
}
export interface MacroAvgMetrics {


Add nonum metric tiers:


export const METRIC_TIERS = ["wer_raw", "wer_norm", "wer_numcanon", "mer", "cer_norm"] 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",
};

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)",
};

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 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;

// === 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",
  "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"] 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"] 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",
};

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)",
};

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",
  "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"] as const;

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

+  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;


Update the compare API to include nonum fields:


     1→import { getMetrics, getModels } from "@/lib/data";
     2→import { LANGUAGES, LanguageMetrics } from "@/lib/types";
     3→
     4→export async function GET() {
     5→  const models = await getModels();
     6→  const result: Record<string, Record<string, Record<string, Record<string, number>>>> = {};
     7→
     8→  for (const model of models) {
     9→    result[model.id] = {};
    10→    for (const ckpt of model.checkpoints) {
    11→      const metrics = await getMetrics(model.id, ckpt);
    12→      if (!metrics) continue;
    13→      const langMetrics: Record<string, Record<string, number>> = {};
    14→      for (const lang of LANGUAGES) {
    15→        const lm = metrics[lang] as LanguageMetrics | undefined;
    16→        if (lm && typeof lm === "object" && "wer_norm" in lm) {
    17→          langMetrics[lang] = {
    18→            wer_raw: lm.wer_raw,
    19→            wer_norm: lm.wer_norm,
    20→            wer_numcanon: lm.wer_numcanon,
    21→            mer: lm.mer ?? 0,
    22→            cer_norm: lm.cer_norm,
    23→          };
    24→        }
    25→      }
    26→      langMetrics["__overall__"] = {
    27→        wer_raw: metrics.__overall__?.wer_raw ?? 0,
    28→        wer_norm: metrics.__overall__?.wer_norm ?? 0,
    29→        wer_numcanon: metrics.__overall__?.wer_numcanon ?? 0,
    30→        mer: metrics.__overall__?.mer ?? 0,
    31→        cer_norm: metrics.__overall__?.cer_norm ?? 0,
    32→      };
    33→      result[model.id][ckpt] = langMetrics;
    34→    }
    35→  }
    36→
    37→  return Response.json(result);
    38→}
    39→

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,
          };
        }
      }
      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,
      };
      result[model.id][ckpt] = langMetrics;
    }
  }

  return Response.json(result);
}


Write

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);
}


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

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);
}

+            wer_norm_nonum: lm.wer_norm_nonum ?? lm.wer_norm,

+            mer_nonum: lm.mer_nonum ?? lm.mer ?? 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,
            mer: lm.mer ?? 0,
            cer_norm: lm.cer_norm,
          };
        }
      }
      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,
      };
      result[model.id][ckpt] = langMetrics;
    }
  }

  return Response.json(result);
}


Now update the Overview page to use `PRIMARY_METRIC_TIERS` for the main selector (so it doesn't clutter with nonum variants there), and update the Baselines view to show the nonum comparison:


    12→import {
    13→  ModelInfo, LANGUAGES, MODEL_REGISTRY,
    14→  MetricTier, METRIC_TIERS, METRIC_LABELS, METRIC_DESCRIPTIONS,
    15→  LanguageMetrics, AggregateMetrics,
    16→} from "@/lib/types";

import {
  ModelInfo, LANGUAGES, MODEL_REGISTRY,
  MetricTier, METRIC_TIERS, METRIC_LABELS, METRIC_DESCRIPTIONS,
  LanguageMetrics, AggregateMetrics,
} from "@/lib/types";


Edit

  ModelInfo, LANGUAGES, MODEL_REGISTRY,
  MetricTier, METRIC_TIERS, METRIC_LABELS, METRIC_DESCRIPTIONS,
  LanguageMetrics, AggregateMetrics,

  ModelInfo, LANGUAGES, MODEL_REGISTRY,
  MetricTier, METRIC_LABELS, METRIC_DESCRIPTIONS,
  LanguageMetrics, AggregateMetrics, PRIMARY_METRIC_TIERS,


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

  ModelInfo, LANGUAGES, MODEL_REGISTRY,
  MetricTier, METRIC_LABELS, METRIC_DESCRIPTIONS,
  LanguageMetrics, AggregateMetrics, PRIMARY_METRIC_TIERS,

"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_TIERS, METRIC_LABELS, METRIC_DESCRIPTIONS,
  LanguageMetrics, AggregateMetrics,
} 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 = () => (

      {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}

  );
}

-  MetricTier, METRIC_TIERS, METRIC_LABELS, METRIC_DESCRIPTIONS,

+  MetricTier, METRIC_LABELS, METRIC_DESCRIPTIONS,

+  LanguageMetrics, AggregateMetrics, PRIMARY_METRIC_TIERS,


Now update the MetricSelector in page.tsx to use PRIMARY_METRIC_TIERS:


169:      {METRIC_TIERS.map((tier) => (

169:      {METRIC_TIERS.map((tier) => (


    <div className="flex items-center gap-1 bg-[var(--color-card)] border border-[var(--color-border)] rounded-lg p-0.5">
      {METRIC_TIERS.map((tier) => (

    <div className="flex items-center gap-1 bg-[var(--color-card)] border border-[var(--color-border)] rounded-lg p-0.5">
      {PRIMARY_METRIC_TIERS.map((tier) => (


    <div className="flex items-center gap-1 bg-[var(--color-card)] border border-[var(--color-border)] rounded-lg p-0.5">
      {METRIC_TIERS.map((tier) => (

    <div className="flex items-center gap-1 bg-[var(--color-card)] border border-[var(--color-border)] rounded-lg p-0.5">
      {PRIMARY_METRIC_TIERS.map((tier) => (

"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 = () => (

      {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}

  );
}

+      {PRIMARY_METRIC_TIERS.map((tier) => (


Now update the Baselines view to include nonum tiers in its own selector and add a numeric impact section:


     1→"use client";
     2→
     3→import { useState } from "react";
     4→import {
     5→  BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip,
     6→  ResponsiveContainer, Legend, Cell,
     7→} from "recharts";
     8→import {
     9→  ModelInfo, LANGUAGES, MODEL_REGISTRY, MODEL_COLORS,
    10→  OUR_MODELS, BASELINE_MODELS,
    11→  MetricTier, METRIC_TIERS, METRIC_LABELS,
    12→} from "@/lib/types";
    13→
    14→type CompareData = Record<string, Record<string, Record<string, Record<string, number>>>>;
    15→
    16→interface BaselinesViewProps {
    17→  models: ModelInfo[];
    18→  compareData: CompareData;
    19→  selectedMetric: MetricTier;
    20→}

"use client";

import { useState } from "react";
import {
  BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip,
  ResponsiveContainer, Legend, Cell,
} from "recharts";
import {
  ModelInfo, LANGUAGES, MODEL_REGISTRY, MODEL_COLORS,
  OUR_MODELS, BASELINE_MODELS,
  MetricTier, METRIC_TIERS, METRIC_LABELS,
} from "@/lib/types";

type CompareData = Record<string, Record<string, Record<string, Record<string, number>>>>;

interface BaselinesViewProps {
  models: ModelInfo[];
  compareData: CompareData;
  selectedMetric: MetricTier;
}


import {
  ModelInfo, LANGUAGES, MODEL_REGISTRY, MODEL_COLORS,
  OUR_MODELS, BASELINE_MODELS,
  MetricTier, METRIC_TIERS, METRIC_LABELS,
} from "@/lib/types";

import {
  ModelInfo, LANGUAGES, MODEL_REGISTRY, MODEL_COLORS,
  OUR_MODELS, BASELINE_MODELS,
  MetricTier, METRIC_TIERS, METRIC_LABELS,
  PRIMARY_METRIC_TIERS,
} from "@/lib/types";