| |
| """Run the fine-tuned text->gloss model over EVERY Full_TriVis clip. |
| |
| `train_text2gloss.py` only emitted the test split (`pred_test.json`); the composed |
| word-level evaluation wants predictions for every clip, plus the per-split scores in |
| one place. Model: `output_vsl/text2gloss/best` (BARTpho-syllable fine-tuned on the |
| `Sentence` -> `Sign_sentence` pairs of the same split). |
| |
| Generation is done once per UNIQUE sentence and fanned back out to clips, since the |
| CSV repeats each sentence across signers/sessions (24k rows, ~12k unique pairs). |
| |
| Output JSON: {clip_name: {split, sentence, pred_gloss, pred_signs, ref_gloss, ref_signs}} |
| where `pred_gloss` keeps the `|` sign separators and `pred_signs` is the split list the |
| pose models consume. |
| |
| NOTE on honesty: the model was trained on the train split, so train/val gloss quality |
| is optimistic. Only the `test` rows are leak-free -- report those. |
| """ |
| import argparse |
| import csv |
| import json |
| import os |
|
|
| import numpy as np |
| import torch |
|
|
| from train_text2gloss import norm_gloss, score, signs |
|
|
| REPO = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..') |
|
|
|
|
| def main(): |
| ap = argparse.ArgumentParser() |
| ap.add_argument('--model', default='output_vsl/text2gloss/best') |
| ap.add_argument('--csv', default=os.path.join(REPO, 'Full_TriVis', 'split_lab_front.csv')) |
| ap.add_argument('--splits', default='train,val,test') |
| ap.add_argument('--num-beams', type=int, default=4) |
| ap.add_argument('--batch-size', type=int, default=64) |
| ap.add_argument('--device', default='cuda') |
| ap.add_argument('--out-json', default='output_vsl/text2gloss/pred_full_trivis.json') |
| ap.add_argument('--metrics-json', default='output_vsl/text2gloss/metrics_full_trivis.json') |
| args = ap.parse_args() |
|
|
| want = set(args.splits.split(',')) |
| rows = [] |
| with open(args.csv, newline='', encoding='utf-8') as f: |
| for r in csv.DictReader(f): |
| if r['split'] not in want: |
| continue |
| rows.append({'name': os.path.splitext(os.path.basename(r['npz_path']))[0], |
| 'split': r['split'], |
| 'sentence': r['Sentence'].strip(), |
| 'gloss': r['Sign_sentence'].strip()}) |
| print(f'{len(rows)} clips over splits {sorted({r["split"] for r in rows})}') |
|
|
| uniq = sorted({r['sentence'] for r in rows}) |
| print(f'{len(uniq)} unique sentences to translate') |
|
|
| from transformers import AutoModelForSeq2SeqLM, AutoTokenizer |
| device = torch.device(args.device) |
| tok = AutoTokenizer.from_pretrained(args.model) |
| model = AutoModelForSeq2SeqLM.from_pretrained(args.model).eval().to(device) |
|
|
| preds = {} |
| with torch.no_grad(): |
| for i in range(0, len(uniq), args.batch_size): |
| chunk = uniq[i:i + args.batch_size] |
| x = tok(chunk, padding=True, truncation=True, max_length=64, |
| return_tensors='pt').to(device) |
| g = model.generate(**x, num_beams=args.num_beams, max_length=64, |
| early_stopping=True) |
| for s, h in zip(chunk, tok.batch_decode(g, skip_special_tokens=True)): |
| preds[s] = h |
| print(f' {min(i + args.batch_size, len(uniq))}/{len(uniq)}', end='\r', flush=True) |
| print() |
|
|
| out = {} |
| for r in rows: |
| h = preds[r['sentence']] |
| out[r['name']] = {'split': r['split'], 'sentence': r['sentence'], |
| 'pred_gloss': h, 'pred_signs': signs(h), |
| 'ref_gloss': r['gloss'], 'ref_signs': signs(r['gloss'])} |
| os.makedirs(os.path.dirname(args.out_json) or '.', exist_ok=True) |
| with open(args.out_json, 'w', encoding='utf-8') as f: |
| json.dump(out, f, ensure_ascii=False) |
| print(f'wrote {args.out_json} ({len(out)} clips)') |
|
|
| |
| metrics = {} |
| for sp in sorted({r['split'] for r in rows}): |
| pairs = {(r['sentence'], r['gloss']) for r in rows if r['split'] == sp} |
| refs = [g for _, g in pairs] |
| hyps = [preds[s] for s, _ in pairs] |
| m = score(refs, hyps) |
| m['n_pairs'] = len(pairs) |
| m['n_clips'] = sum(r['split'] == sp for r in rows) |
| m['mean_ref_signs'] = float(np.mean([len(signs(g)) for g in refs])) |
| m['mean_hyp_signs'] = float(np.mean([len(signs(h)) for h in hyps])) |
| m['seen_in_training'] = (sp == 'train') |
| metrics[sp] = m |
| print(f"[{sp}] WER {m['wer']:.4f} EM {m['exact_match']:.4f} F1 {m['f1']:.4f} " |
| f"signs ref {m['mean_ref_signs']:.2f} / hyp {m['mean_hyp_signs']:.2f} " |
| f"({m['n_pairs']} pairs, {m['n_clips']} clips)" |
| + (' [SEEN IN TRAINING -- optimistic]' if m['seen_in_training'] else '')) |
|
|
| with open(args.metrics_json, 'w', encoding='utf-8') as f: |
| json.dump({'model': args.model, 'num_beams': args.num_beams, |
| 'per_split': metrics}, f, indent=2, ensure_ascii=False) |
| print(f'wrote {args.metrics_json}') |
|
|
|
|
| if __name__ == '__main__': |
| main() |
|
|