"""2.3 (English edition): a names file, sent with every correction call, vs. no names file.
Input: plain Grok transcripts of the 38 synthetic memos (corpus/transcripts_grok.json), language=en.
Each memo is corrected in its own call (stateless), so the names file must be attached every time. temperature 0.
Scored with corpus/score_names.py over the 28 W+L memos: 31 name mentions, 22 term mentions (case-insensitive, whole
word; the case-sensitive term count is reported alongside).
Every word-level change the model made (transcript -> output) is written to diffs_<model>_<cond>.md, so confident wrong
substitutions can be quoted verbatim.

Usage: run.py [model ...]     (existing out_*.json files are reused)   run.py --score   (re-score only)"""
import difflib, json, pathlib, re, sys
from concurrent.futures import ThreadPoolExecutor
HERE = pathlib.Path(__file__).resolve().parent
ROOT = HERE.parent
sys.path.insert(0, str(ROOT)); sys.path.insert(0, str(ROOT / 'corpus'))
from common import chat, dump, log_cost
from score_names import score, NAMES, TERMS

d = json.load(open(ROOT / 'corpus/memos.json', encoding='utf-8'))
src = json.load(open(ROOT / 'corpus/transcripts_grok.json', encoding='utf-8'))
WL = {m['id'] for m in d['memos'] if m['set'] in 'WL'}
names_md = ('\n'.join(f'- {k}: {v}' for k, v in d['people'].items()) + '\n\nCommon terms: ' + ', '.join(d['terms']))
(HERE / 'names.md').write_text('# People I mention often\n\n' + names_md + '\n', encoding='utf-8')
BASE = ('Below is an automatic transcript of a voice memo. It may contain recognition errors. '
        'Fix the recognition errors and output only the corrected full text, with no explanation.')
conds = {'no_list': lambda t: BASE + '\n\nTranscript:\n' + t,
         'with_list': lambda t: BASE + '\n\nThese are the people and terms I mention often. Wherever the transcript sounds '
                                       'like one of them, use the spelling here:\n' + names_md + '\n\nTranscript:\n' + t}
MODELS = ['deepseek-v4.1-flash', 'gemini-3.8-flash']


def changes(a, b):
    """Word-level replacements/insertions/deletions from transcript a to output b (punctuation-insensitive tokens)."""
    tok = lambda s: re.findall(r"[\w'’-]+", s)
    A, B = tok(a), tok(b)
    out = []
    for op, i1, i2, j1, j2 in difflib.SequenceMatcher(None, [x.lower() for x in A], [x.lower() for x in B], autojunk=False).get_opcodes():
        if op != 'equal':
            out.append((' '.join(A[i1:i2]), ' '.join(B[j1:j2])))
    return out


def write_diffs(model, c, outs):
    lines = [f'# {model} / {c}: every word-level change, transcript -> output\n']
    for mid in sorted(outs):
        ch = changes(src[mid], outs[mid])
        if ch:
            lines.append(f'- **{mid}**: ' + '; '.join(f'"{a}" → "{b}"' for a, b in ch))
    (HERE / f'diffs_{model}_{c}.md').write_text('\n'.join(lines) + '\n', encoding='utf-8')


def score_all():
    res = {}
    for label, t in [('asr_grok', src)] + [(f.stem[4:], json.load(open(f, encoding='utf-8'))) for f in sorted(HERE.glob('out_*.json'))]:
        nt, nh, nm, ns = score(t, NAMES, ids=WL)
        tt, th, tm, _ = score(t, TERMS, ids=WL)
        _, ths, _, _ = score(t, TERMS, flags=0, ids=WL)
        res[label] = {'names': f'{nh}/{nt}', 'terms': f'{th}/{tt}', 'terms_case_sensitive': f'{ths}/{tt}',
                      'name_misses': nm, 'names_right_by_person': ns, 'term_misses': tm}
        print(f'{label:36s} names {nh}/{nt}  terms {th}/{tt} (cs {ths})  name_misses={nm}  term_misses={tm}')
    for f in ('transcripts_compat.json', 'transcripts_grok_terms.json'):
        t = json.load(open(ROOT / 'corpus' / f, encoding='utf-8'))
        nt, nh, _, _ = score(t, NAMES, ids=WL); tt, th, _, _ = score(t, TERMS, ids=WL); _, ths, _, _ = score(t, TERMS, flags=0, ids=WL)
        res['asr_' + f[12:-5]] = {'names': f'{nh}/{nt}', 'terms': f'{th}/{tt}', 'terms_case_sensitive': f'{ths}/{tt}'}
    # Hand check (2026-10-14): the scorer counts mentions per memo, so a name written in the wrong place still counts.
    # One such case: deepseek with the list left "Source says" in W07 and instead turned the later "I asked her" into
    # "I asked Saoirse". The spoken mention is still wrong, so the strict count is one lower. No other output turns a
    # pronoun into a name (checked by comparing he/she/her/him/his counts against the transcript).
    res['hand_check'] = {'deepseek-v4.1-flash_with_list': {
        'names_strict': '24/31', 'scorer': res.get('deepseek-v4.1-flash_with_list', {}).get('names'),
        'why': 'W07: "Source says" left as is; "I asked her" changed to "I asked Saoirse". Counted by the scorer, not a real fix.'}}
    dump(res, HERE / 'results.json')


if __name__ == '__main__':
    if '--score' in sys.argv:
        score_all(); sys.exit()
    for model in sys.argv[1:] or MODELS:
        for c, mk in conds.items():
            f = HERE / f'out_{model}_{c}.json'
            if f.exists():
                outs = json.load(open(f, encoding='utf-8'))
            else:
                def one(mid):
                    out, u, _ = chat([{'role': 'user', 'content': mk(src[mid])}], model, temperature=0)
                    return mid, out.strip()
                with ThreadPoolExecutor(8) as ex:
                    outs = dict(ex.map(one, src))
                dump(outs, f)
            write_diffs(model, c, outs)
    log_cost('2_3_names/run.py ' + ' '.join(sys.argv[1:] or MODELS))
    score_all()
