Spaces:
Running on Zero
Running on Zero
| """ | |
| 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() | |