File size: 5,630 Bytes
f1ef7e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
Phase 0 context-engineering: turn our per-channel transcript into the
evaluation INPUT PACKET the LLM will consume.

Steps (deterministic, no LLM):
  1. Role mapping  -- channel identity IS the speaker (AGENT=ch1, CUSTOMER=ch2).
                      No diarization, no "who spoke first" heuristic -> 100% correct.
  2. Turn ordering -- merge both channels' segments by start time into a single
                      chronological [AGENT]/[CUSTOMER] turn list with mm:ss stamps.
  3. Metadata injection -- prepend call metadata (domain as "Call Type" lens,
                      duration, etc.) so the LLM judges with the right context.

Output: a single text packet (+ saved .txt) ready to drop into the Phase 1 prompt.

Usage:
  python assemble.py --result ../../data/na_testset/results_channels/en-CA/en_CA_Banking_1592237.json
  python assemble.py --call_id en_CA_Banking_1592237   # auto-locates result + manifest
"""
import os, json, argparse

import paths

DATA = str(paths.NA_TESTSET)
MANIFEST = str(paths.MANIFEST)


def mmss(seconds):
    seconds = int(round(seconds))
    return f"{seconds // 60:02d}:{seconds % 60:02d}"


def load_manifest():
    """Dataset manifest merged with the runtime registry of ingested calls.

    Runtime entries win on call_id collision so a re-ingested call can carry
    corrected metadata without touching the dataset manifest.
    """
    with open(MANIFEST, encoding="utf-8") as f:
        manifest = {m["call_id"]: m for m in json.load(f)}
    if paths.RUNTIME_MANIFEST.exists():
        with open(paths.RUNTIME_MANIFEST, encoding="utf-8") as f:
            manifest.update({m["call_id"]: m for m in json.load(f)})
    return manifest


def assemble_turns(result):
    """Merge agent+customer segments into one time-ordered turn list."""
    turns = []
    segs = result.get("segments", {})
    if segs and segs.get("agent") is not None:
        for speaker, key in (("AGENT", "agent"), ("CUSTOMER", "customer")):
            for s in segs.get(key, []):
                text = s.get("text", "").strip()
                if text:
                    turns.append({"start": s["start"], "speaker": speaker, "text": text})
    else:
        # fallback: no segments -> group words by speaker flips
        merged = []
        for speaker, key in (("AGENT", "agent"), ("CUSTOMER", "customer")):
            for w in result.get(key, []):
                merged.append((w["start"], speaker, w["word"]))
        merged.sort()
        cur_sp, cur_words, cur_start = None, [], None
        for start, sp, word in merged:
            if sp != cur_sp and cur_words:
                turns.append({"start": cur_start, "speaker": cur_sp, "text": " ".join(cur_words)})
                cur_words = []
            if sp != cur_sp:
                cur_sp, cur_start = sp, start
            cur_words.append(word)
        if cur_words:
            turns.append({"start": cur_start, "speaker": cur_sp, "text": " ".join(cur_words)})

    turns.sort(key=lambda t: t["start"])
    return turns


def estimate_duration(result):
    last = 0.0
    for key in ("agent", "customer"):
        for w in result.get(key, []):
            last = max(last, w.get("end", w.get("start", 0)))
    return round(last, 1)


def build_packet(result, meta):
    turns = assemble_turns(result)
    dur = estimate_duration(result)

    # ── metadata header (the "lens") ──────────────────────────────────────────
    header = [
        "=" * 70,
        "CALL METADATA",
        "=" * 70,
        f"Call ID      : {result.get('call_id', meta.get('call_id', '?'))}",
        f"Call Type    : {result.get('domain', meta.get('domain', 'unknown'))}",
        f"Duration     : {mmss(dur)} ({dur:.0f}s)",
        f"Transcript   : {result.get('model', 'unknown')} (per-channel; AGENT=ch1, CUSTOMER=ch2)",
        f"Turns        : {len(turns)}",
        "",
        "=" * 70,
        "ROLE-MAPPED TRANSCRIPT  (chronological; [SPEAKER mm:ss])",
        "=" * 70,
    ]

    body = [f"[{t['speaker']} {mmss(t['start'])}] {t['text']}" for t in turns]
    return "\n".join(header) + "\n" + "\n".join(body), {"turns": len(turns), "duration_s": dur}


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--result", help="path to a per-channel result JSON")
    ap.add_argument("--call_id", help="call id (auto-locates result via manifest)")
    ap.add_argument("--results_dir", default="results_channels",
                    help="result dir under na_testset (default: results_channels)")
    ap.add_argument("--out", default=None, help="output .txt path (default: evaluation/packets/<id>.txt)")
    args = ap.parse_args()

    manifest = load_manifest()

    if args.result:
        path = args.result
    elif args.call_id:
        meta = manifest[args.call_id]
        path = os.path.join(DATA, args.results_dir, meta["accent"], args.call_id + ".json")
    else:
        raise SystemExit("Provide --result or --call_id")

    with open(path, encoding="utf-8") as f:
        result = json.load(f)

    cid = result.get("call_id")
    meta = manifest.get(cid, {})
    packet, stats = build_packet(result, meta)

    here = os.path.dirname(os.path.abspath(__file__))
    out = args.out or os.path.join(here, "packets", f"{cid}.txt")
    os.makedirs(os.path.dirname(out), exist_ok=True)
    with open(out, "w", encoding="utf-8") as f:
        f.write(packet)

    print(packet)
    print("\n" + "-" * 70)
    print(f"Saved packet -> {out}  ({stats['turns']} turns, {mmss(stats['duration_s'])})")


if __name__ == "__main__":
    main()