File size: 7,431 Bytes
0cfc3a7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8d3d08e
 
0cfc3a7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env python3
"""Benchmark the phoneme recognition on the lab's labelled recordings, with the current scorer, against their stored scores.

Usage:  python scripts/bench.py                        # every lab recording
        python scripts/bench.py --speaker "מרב"         # one speaker's
        python scripts/bench.py --flags                # also: are the confirmed flags' false issues still flagged?
        python scripts/bench.py --no-baseline          # without the native TTS baseline (the acoustic model alone)
        python scripts/bench.py --json out.json        # every recording's old and new result

Reads SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY and SUPABASE_SECRET_KEY (as the server does), MODEL_ID and, for the
baseline, SAWTAK_API_KEY. The lab (מעבדה, see app/lab.py) labels each recording said right or with one planted
mistake, so per sound:  frr = said right but flagged / said right;  detection = planted mistake flagged / planted.
Run it before shipping a scoring or model change: frr should go down without detection going down with it.
"stored" is the result kept when the recording was made, by the code of that day: to measure a change, save a run
of the code before it (--json) and compare.
"""
from __future__ import annotations

import argparse
import json
import os
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

from dotenv import load_dotenv  # noqa: E402

from app import lab  # noqa: E402
from app.auth import AuthUnavailable, load_auth  # noqa: E402
from app.reference import References  # noqa: E402
from app.scorer import decode_audio, load_scorer  # noqa: E402
from app.tts import VOICES, load_tts  # noqa: E402


def pct(x: float | None) -> str:
    return "  –  " if x is None else f"{100 * x:4.0f}%"


def change(old: float | None, new: float | None) -> str:
    return f"{pct(old)} -> {pct(new)}" if old != new else f"{pct(new)}        "


def main() -> int:
    ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    ap.add_argument("--speaker", default="", help="only this speaker's recordings")
    ap.add_argument("--flags", action="store_true", help="also rescore the confirmed flags")
    ap.add_argument("--no-baseline", action="store_true", help="score without the native TTS baseline")
    ap.add_argument("--json", type=Path, help="write every recording's old and new result here")
    args = ap.parse_args()

    load_dotenv()
    auth = load_auth()
    if auth is None or not auth.secret:
        print("needs SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY and SUPABASE_SECRET_KEY", file=sys.stderr)
        return 2
    try:
        samples = [s for s in auth.lab_samples(full=True) if s.get("audio")
                   and (not args.speaker or (s.get("speaker") or "") == args.speaker)]
    except AuthUnavailable as e:
        print(f"the lab's recordings are not available ({e}); is the table lab_samples created? see the README",
              file=sys.stderr)
        return 2
    if not samples and not args.flags:
        print("no lab recordings yet: record some in the lab (מעבדה), or run with --flags", file=sys.stderr)
        return 1

    model = os.environ.get("MODEL_ID", "facebook/wav2vec2-xlsr-53-espeak-cv-ft")
    scorer = load_scorer(model)
    tts = None if args.no_baseline else load_tts()
    refs = References(scorer, tts, list(VOICES.values())) if tts else None

    def score(audio: bytes, phrase: dict, key) -> dict:
        return scorer.score(decode_audio(audio), phrase, refs.get(key, phrase) if refs else [])
    new: dict[int, dict] = {}
    for k, s in enumerate(samples, 1):
        phrase = lab.sample_phrase(s)
        if phrase is None:   # a legacy row without enough information to reconstruct its phrase
            continue
        try:
            found = auth.lab_audio(s["id"])
            if found:
                new[s["id"]] = score(found[0], phrase, phrase["key"])
        except (AuthUnavailable, ValueError) as e:
            print(f"sample {s['id']}: skipped ({e})", file=sys.stderr)
        print(f"\r{k}/{len(samples)} rescored", end="", file=sys.stderr)
    print(file=sys.stderr)
    scored = [s for s in samples if s["id"] in new]
    reports = auth.list_flags("confirmed")
    before, after = lab.benchmark(scored, reports=reports), lab.benchmark(scored, new, reports)

    t0, t1 = before["totals"], after["totals"]
    print(f"model {model}{' (no baseline)' if args.no_baseline else ''} · {t1['recordings']} recordings "
          f"({t1['correct']} said right, {t1['mistake']} with a planted mistake, {t1['unrelated']} unrelated)")
    print(f"false rejections {change(t0['frr'], t1['frr'])}   detection {change(t0['detection'], t1['detection'])}   "
          f"said right scoring >= {lab.GOOD_SCORE}: {change(t0['good_share'], t1['good_share'])}")
    if t1["unplaced"]:
        print(f"{t1['unplaced']} planted mistakes could not be placed on a sound of the reading the scorer chose")
    if not scored:
        print("(no lab recordings yet)")
    print(f"\n{'sound':<6} {'right':>5}  {'false rejections (stored -> now)':<32} {'planted':>7}  "
          f"{'detection':<18} {'mean':>5}  {'reports':>7}  heard instead")
    old = {r["sound"]: r for r in before["sounds"]}
    for r in after["sounds"]:
        o = old.get(r["sound"], {})
        heard = " ".join(f"{h}×{n}" for h, n in list(r["confusions"].items())[:4])
        print(f"{r['sound']:<6} {r['right']:>5}  {r['false_flags']:>3} {change(o.get('frr'), r['frr']):<28} "
              f"{r['planted']:>7}  {change(o.get('detection'), r['detection']):<18} "
              f"{'' if r['mean'] is None else r['mean']:>5}  {r['reports']:>7}  {heard}")

    rows = []
    if args.flags:
        fixed = still = 0
        print("\nconfirmed flags: the sounds a teacher marked as wrongly flagged, with the current scorer")
        for f in reports:
            if not f.get("audio") or not f.get("false_issues"):
                continue
            try:
                found = auth.flag_audio(f["id"])
                result = score(found[0], lab.flag_phrase(f), ("flag", f.get("phrase"))) if found else None
            except (AuthUnavailable, ValueError) as e:
                print(f"flag {f['id']}: skipped ({e})", file=sys.stderr)
                continue
            if not result or result.get("error"):
                continue
            marks = [(i, lab.still_flagged(result, i)) for i in f["false_issues"]]
            still += sum(s for _, s in marks); fixed += sum(not s for _, s in marks)
            print(f"{f['id']:>6}  {f.get('score')!s:>3} -> {result['score']:>3}  "
                  + " ".join(f"{i['expected']}>{i.get('heard') or '_'}:{'still' if s else 'fixed'}" for i, s in marks)
                  + f"  {f.get('he') or f.get('phrase')}")
            rows.append({"flag": f["id"], "old": f.get("result"), "new": result})
        print(f"{fixed} fixed, {still} still flagged")

    if args.json:
        rows += [{"sample": s["id"], "item": s["item"], "speaker": s.get("speaker"), "label": s["label"],
                  "mistake": s.get("mistake"), "old": s.get("result"), "new": new[s["id"]]} for s in scored]
        args.json.write_text(json.dumps({"before": before, "after": after, "rows": rows}, ensure_ascii=False, indent=1),
                             encoding="utf-8")
    return 0


if __name__ == "__main__":
    sys.exit(main())