File size: 7,758 Bytes
6eed659
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
#!/usr/bin/env python3
"""POOL INTER-MODELES sur le SHONA — transposition de ce qui vient de marcher sur
le lingala (`sub_XC010.csv` = 0.762499646, +0.000126 sur le record).

Un juge CLASSE, il ne PROPOSE pas : tous les candidats shona sortaient du beam de
`sna_ps_best` seul. On ajoute ici les hypotheses d un DONNEUR (autre distribution).

BEAUCOUP plus simple que le lingala : le shona decode en beam PUR (aucun KenLM, le LM
degradait le shona). Le score du beam est donc du CTC acoustique seul => un candidat
externe se score EXACTEMENT sur la meme echelle avec ctc_score(), sans formule de
calibration ni offset. C est precisement la calibration qui avait casse le 1er essai
lingala (-0.0091). Ici ce risque n existe pas.
Mecanisme deja valide dans le pipeline du record : `gen_sna_rescore.py` ajoute deja
le greedy et le decode-batch comme candidats supplementaires, notes par ctc_score.

GARDE-FOU DE LONGUEUR conserve (mots ET caracteres) : sur le lingala, l absence de
borne en CARACTERES suffisait a tout perdre (memes mots, formes 40 % plus longues).
"""
import csv
import json
import os
import sys

import numpy as np
import torch
from multiprocessing import Pool
from pyctcdecode import build_ctcdecoder

sys.path.insert(0, "/root")
from gen_sna_rescore import compute_logits, ctc_score, encode_for, norm  # noqa: E402

BASE = os.environ.get("BASE", "/root/sub_XC010.csv")     # record 0.762499646
LANGF = os.environ.get("LANGF", "/root/test_lang.json")
SNAM = os.environ.get("SNA_MODEL", "/root/models/sna_ps_best")
J1 = os.environ.get("J1", "/root/models/sna_r2_best")
J2 = os.environ.get("J2", "/scratch/runs/xlsrlong/checkpoint-5600")
J3 = os.environ.get("J3", "/root/models/mmsjoint_best")
DONOR = os.environ.get("DONOR", "/root/models/mmsjoint_best")
W1 = float(os.environ.get("W1", "6.0"))
W2 = float(os.environ.get("W2", "4.0"))
W3 = float(os.environ.get("W3", "2.0"))
MARGINS = [float(x) for x in os.environ.get("MARGINS", "1e9,10,5,0").split(",")]
DON_NBEST = int(os.environ.get("DON_NBEST", "5"))
RLO = float(os.environ.get("RATIO_LO", "0.85"))
RHI = float(os.environ.get("RATIO_HI", "1.15"))
GAMMA = float(os.environ.get("GAMMA", "0.0"))
NBEST = int(os.environ.get("NBEST", "50"))
BW = int(os.environ.get("BW", "256"))
TAG = os.environ.get("TAG", "SX")
AUD = os.environ.get("AUDIO_DIR", "/scratch/p2_16k")


def main():
    base = {r["ID"]: r["Target"] for r in csv.DictReader(open(BASE, encoding="utf-8"))}
    lang = json.load(open(LANGF))
    sna_ids = [k for k in base if lang.get(k) == "sna"]
    print("base %d | sna %d | lin intouches %d"
          % (len(base), len(sna_ids), len(base) - len(sna_ids)), flush=True)
    files = [os.path.join(AUD, k + ".wav") for k in sna_ids]
    assert all(os.path.exists(f) for f in files), "audio manquant"

    proc, L1 = compute_logits(SNAM, files)
    tok = proc.tokenizer
    v = tok.get_vocab()
    lab = [None] * len(v)
    for t, i in v.items():
        lab[i] = t
    lab[tok.word_delimiter_token_id] = " "
    lab[tok.unk_token_id] = "⁇"
    lab[tok.pad_token_id] = ""
    greedy = [norm(tok.decode(l.argmax(-1))) for l in L1]

    dec = build_ctcdecoder(lab)                 # beam PUR, aucun LM
    with Pool(8) as p:
        allbeams = dec.decode_beams_batch(p, L1, beam_width=BW)
    with Pool(8) as p:
        db = [norm(x) for x in dec.decode_batch(p, L1, beam_width=BW)]

    cands, AC1, NW, ISEXT = [], [], [], []
    for i, bs in enumerate(allbeams):
        c = [norm(b[0]) for b in bs[:NBEST]]
        a = [(b[3] if len(b) > 3 else 0.0) for b in bs[:NBEST]]
        for extra in (db[i], greedy[i]):
            if extra and extra not in c:
                c.append(extra)
                a.append(ctc_score(L1[i], encode_for(tok, extra), tok.pad_token_id))
        cands.append(c)
        AC1.append(list(a))
        ISEXT.append([False] * len(c))
    print("n-best du modele principal construit", flush=True)

    # ---------- candidats DONNEURS ----------
    dproc, DL = compute_logits(DONOR, files)
    dtok = dproc.tokenizer
    dv = dtok.get_vocab()
    dlab = [None] * len(dv)
    for t, i in dv.items():
        dlab[i] = t
    dlab[dtok.word_delimiter_token_id] = " "
    dlab[dtok.unk_token_id] = "⁇"
    dlab[dtok.pad_token_id] = ""
    ddec = build_ctcdecoder(dlab)
    with Pool(8) as p:
        dbeams = ddec.decode_beams_batch(p, DL, beam_width=64, prune_history=True)
    del ddec, DL
    torch.cuda.empty_cache()
    nadd = 0
    for i in range(len(sna_ids)):
        seen = set(cands[i])
        for b in dbeams[i][:DON_NBEST]:
            h = norm(b[0])
            if h and h not in seen:
                seen.add(h)
                cands[i].append(h)
                # meme echelle EXACTE : le beam shona est du CTC pur, sans LM
                AC1[i].append(ctc_score(L1[i], encode_for(tok, h), tok.pad_token_id))
                ISEXT[i].append(True)
                nadd += 1
    del dbeams, L1
    print("donneur %s : %d hypotheses NOUVELLES (%.1f/clip)"
          % (os.path.basename(DONOR), nadd, nadd / max(len(sna_ids), 1)), flush=True)

    AC1 = [np.array(x) for x in AC1]
    ISEXT = [np.array(x) for x in ISEXT]
    NW = [np.array([float(len(x.split())) for x in c]) for c in cands]

    def judge(path):
        pr, LG = compute_logits(path, files)
        t = pr.tokenizer
        R = [np.array([ctc_score(LG[i], encode_for(t, x), t.pad_token_id)
                       for x in cands[i]]) for i in range(len(sna_ids))]
        del LG
        torch.cuda.empty_cache()
        print("juge %s OK" % os.path.basename(path), flush=True)
        return R

    R1, R2, R3 = judge(J1), judge(J2), judge(J3)

    from huggingface_hub import HfApi
    api = HfApi(token=open(os.path.expanduser("~/.cache/huggingface/token")).read().strip())
    for mg in MARGINS:
        out = dict(base)
        nchg = next_ = 0
        for i, k in enumerate(sna_ids):
            tot = AC1[i] + W1 * R1[i] + W2 * R2[i] + W3 * R3[i] + GAMMA * NW[i]
            bm = ~ISEXT[i]
            jb = int(np.argmax(np.where(bm, tot, -1e18)))
            nref, cref = len(cands[i][jb].split()), len(cands[i][jb])
            ct = tot.copy()
            ct[ISEXT[i]] -= mg
            for j2 in np.flatnonzero(ISEXT[i]):
                r = len(cands[i][j2].split()) / max(nref, 1)
                rc = len(cands[i][j2]) / max(cref, 1)
                if r < RLO or r > RHI or rc < RLO or rc > RHI:
                    ct[j2] = -1e18
            j = int(np.argmax(ct))
            if ISEXT[i][j]:
                next_ += 1
            pick = cands[i][j] or greedy[i] or "a"
            if pick != base[k]:
                nchg += 1
            out[k] = pick
        empt = sum(1 for x in out.values() if not str(x).strip())
        dlin = sum(1 for k in base if lang.get(k) != "sna" and out[k] != base[k])
        tag = "%s%s" % (TAG, ("CTL" if mg > 1e8 else "%03d" % round(mg)))
        OUT = "/root/sub_%s.csv" % tag
        with open(OUT, "w", newline="", encoding="utf-8") as f:
            wr = csv.writer(f)
            wr.writerow(["ID", "Target"])
            for k in base:
                wr.writerow([k, out[k] or "a"])
        assert len(out) == 892 and empt == 0 and dlin == 0, "%s INVALIDE" % tag
        if mg <= 1e8:
            api.upload_file(path_or_fileobj=OUT,
                            path_in_repo="phase2_corrected/sub_%s.csv" % tag,
                            repo_id="Pricile/waxal2026-backup", repo_type="model")
        flag = "  <-- CONTROLE : doit etre 0" if mg > 1e8 else ""
        print("%-8s MARGIN=%8.1f | sna modifies %3d/%d | externes retenus %3d | lin intouche%s"
              % (tag, mg, nchg, len(sna_ids), next_, flag), flush=True)
    print("CROSSPOOL_SNA_DONE", flush=True)


if __name__ == "__main__":
    main()