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