| |
| """Decodage Phase 2 ROUTE PAR LANGUE (100% modeles pre-entraines, 0 dataset externe). |
| LID (mms-lid-256, contraint a lin/lug/sna) -> lug=Sunbird Whisper-salt ; lin/sna=notre MMS CTC. |
| Garde-fous anti-hallucination Whisper -> fallback MMS. Sortie CSV Zindi. |
| """ |
| import argparse, csv, glob, os |
| from collections import Counter |
| import soundfile as sf, torch |
| from transformers import (AutoModelForCTC, AutoProcessor, |
| WhisperForConditionalGeneration, WhisperProcessor, |
| AutoModelForAudioClassification, AutoFeatureExtractor) |
| SR = 16000 |
|
|
|
|
| def norm(s): |
| return " ".join(str(s).replace("|", " ").split()) |
|
|
|
|
| def is_halluc(hyp, dur): |
| w = hyp.split() |
| if not w: |
| return True |
| if len(w) >= 6 and Counter(w).most_common(1)[0][1] / len(w) > 0.5: |
| return True |
| if dur > 0 and len(w) / dur > 6.0: |
| return True |
| return False |
|
|
|
|
| def main(): |
| ap = argparse.ArgumentParser() |
| ap.add_argument("--whisper_model", default="Sunbird/asr-whisper-large-v3-salt") |
| ap.add_argument("--mms_model", required=True) |
| ap.add_argument("--lid_model", default="facebook/mms-lid-256") |
| ap.add_argument("--audio_dir", default="/root/phase2_audio/audio") |
| ap.add_argument("--test_csv", default="/root/Test_phase2.csv") |
| ap.add_argument("--out", required=True) |
| ap.add_argument("--lang_token", type=int, default=50355) |
| ap.add_argument("--targets", default="lin,lug,sna") |
| ap.add_argument("--chunk_s", type=float, default=28.0) |
| ap.add_argument("--beams", type=int, default=1) |
| ap.add_argument("--no_repeat", type=int, default=0) |
| a = ap.parse_args() |
| dev = "cuda" |
|
|
| lid = AutoModelForAudioClassification.from_pretrained(a.lid_model).to(dev).eval() |
| lidfe = AutoFeatureExtractor.from_pretrained(a.lid_model) |
| id2label = lid.config.id2label |
| label2id = {v: k for k, v in id2label.items()} |
| tgt = [t for t in a.targets.split(",")] |
| missing = [t for t in tgt if t not in label2id] |
| assert not missing, f"LID labels manquants {missing} ; ex labels={list(id2label.values())[:8]}" |
| tgt_ids = torch.tensor([label2id[t] for t in tgt]) |
|
|
| mms = AutoModelForCTC.from_pretrained(a.mms_model, torch_dtype=torch.float16).to(dev).eval() |
| mmsp = AutoProcessor.from_pretrained(a.mms_model) |
| wp = WhisperProcessor.from_pretrained(a.whisper_model) |
| wm = WhisperForConditionalGeneration.from_pretrained(a.whisper_model, torch_dtype=torch.float16).to(dev).eval() |
| tr = wp.tokenizer.convert_tokens_to_ids("<|transcribe|>") |
| nt = wp.tokenizer.convert_tokens_to_ids("<|notimestamps|>") |
| forced = [[1, a.lang_token], [2, tr], [3, nt]] |
|
|
| test_ids = [r["ID"] for r in csv.DictReader(open(a.test_csv, encoding="utf-8"))] |
|
|
| def mms_decode(au): |
| x = mmsp(au, sampling_rate=SR, return_tensors="pt") |
| x = {k: (v.to(dev, torch.float16) if v.dtype == torch.float32 else v.to(dev)) for k, v in x.items()} |
| ids = mms(**x).logits.argmax(-1).cpu().numpy() |
| return norm(mmsp.batch_decode(ids)[0]) |
|
|
| def whisper_decode(au): |
| win = int(a.chunk_s * SR) |
| pieces = [au[j:j + win] for j in range(0, len(au), win)] or [au] |
| out = [] |
| for p in pieces: |
| f = wp(p, sampling_rate=SR, return_tensors="pt").input_features.to(dev, torch.float16) |
| ids = wm.generate(f, forced_decoder_ids=forced, max_new_tokens=220, num_beams=a.beams, no_repeat_ngram_size=a.no_repeat) |
| out.append(wp.batch_decode(ids, skip_special_tokens=True)[0]) |
| return norm(" ".join(out)) |
|
|
| out = {}; nlug = 0; nfb = 0; langcount = Counter() |
| with torch.inference_mode(): |
| for i, tid in enumerate(test_ids): |
| f = os.path.join(a.audio_dir, tid + ".wav") |
| if not os.path.exists(f): |
| cand = glob.glob(os.path.join(a.audio_dir, tid + ".*")) |
| f = cand[0] if cand else None |
| if not f: |
| out[tid] = "a"; continue |
| au = sf.read(f, dtype="float32")[0]; dur = len(au) / SR |
| inp = lidfe(au[:SR * 20], sampling_rate=SR, return_tensors="pt") |
| inp = {k: v.to(dev) for k, v in inp.items()} |
| logits = lid(**inp).logits[0] |
| lang = tgt[int(torch.argmax(logits[tgt_ids]).item())] |
| langcount[lang] += 1 |
| if lang == "lug": |
| nlug += 1 |
| h = whisper_decode(au) |
| if is_halluc(h, dur): |
| h = mms_decode(au); nfb += 1 |
| else: |
| h = mms_decode(au) |
| out[tid] = h or "a" |
| if (i + 1) % 200 == 0: |
| print(f"{i+1}/{len(test_ids)} langs={dict(langcount)} fb={nfb}", flush=True) |
| empt = sum(1 for t in test_ids if not out.get(t, "").strip()) |
| with open(a.out, "w", newline="", encoding="utf-8") as fo: |
| w = csv.writer(fo); w.writerow(["ID", "Target"]) |
| for t in test_ids: |
| w.writerow([t, out.get(t) or "a"]) |
| print(f"ROUTED_DONE {a.out} | {len(test_ids)} IDs | langs={dict(langcount)} " |
| f"halluc_fallback={nfb} vides={empt}", flush=True) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|