#!/usr/bin/env python3 # -*- coding: utf-8 -*- """Export thinker+talker chain outputs into benchmark-specific layouts.""" from __future__ import annotations import argparse import json import os import shutil from pathlib import Path from typing import Any def read_json(path: Path) -> Any: with path.open("r", encoding="utf-8") as f: return json.load(f) def write_json(path: Path, obj: Any) -> None: path.parent.mkdir(parents=True, exist_ok=True) with path.open("w", encoding="utf-8") as f: json.dump(obj, f, ensure_ascii=False, indent=2) def read_jsonl(path: Path) -> list[dict[str, Any]]: rows = [] with path.open("r", encoding="utf-8") as f: for line in f: line = line.strip() if line: rows.append(json.loads(line)) return rows def write_jsonl(path: Path, rows: list[dict[str, Any]]) -> None: path.parent.mkdir(parents=True, exist_ok=True) with path.open("w", encoding="utf-8") as f: for row in rows: f.write(json.dumps(row, ensure_ascii=False) + "\n") def id_map(rows: list[dict[str, Any]]) -> dict[str, dict[str, Any]]: return {str(r["id"]): r for r in rows if "id" in r} def manifest_map(path: Path) -> dict[str, dict[str, Any]]: if not path.is_file(): return {} return id_map(read_jsonl(path)) def link_or_copy(src: str | Path, dst: str | Path) -> None: src_p = Path(src) dst_p = Path(dst) dst_p.parent.mkdir(parents=True, exist_ok=True) if dst_p.exists() or dst_p.is_symlink(): dst_p.unlink() try: os.symlink(src_p, dst_p) except OSError: shutil.copy2(src_p, dst_p) def chain_records(chain_input: Path, thinker_output: Path, talker_manifest: Path) -> list[dict[str, Any]]: inputs = id_map(read_jsonl(chain_input)) thinkers = id_map(read_jsonl(thinker_output)) manifests = manifest_map(talker_manifest) ids = list(inputs.keys()) out = [] for sid in ids: inp = inputs.get(sid, {}) thk = thinkers.get(sid, {}) man = manifests.get(sid, {}) out.append( { **inp, "thinker_text": thk.get("thinker_text", ""), "thinker_style": thk.get("thinker_style", ""), "struct_pass": thk.get("struct_pass", False), "wav": man.get("wav", ""), "talker_text": man.get("text", thk.get("thinker_text", "")), } ) return out def export_uro(args: argparse.Namespace) -> None: rows = chain_records(Path(args.chain_input), Path(args.thinker_output), Path(args.talker_manifest)) out_dir = Path(args.out_dir) audio_dir = out_dir / "audio" pred_rows = [] question_rows = [] gt_rows = [] for idx, row in enumerate(rows): sid = f"{idx:04d}" pred = row.get("thinker_text") or row.get("talker_text") or "" pred_rows.append({sid: pred}) question_rows.append({sid: row.get("source_text", "")}) gt_rows.append({sid: row.get("target_text", row.get("source_text", ""))}) wav = row.get("wav") if wav and Path(wav).is_file(): link_or_copy(wav, audio_dir / f"{sid}.wav") write_jsonl(out_dir / "pred_text.jsonl", pred_rows) write_jsonl(out_dir / "question_text.jsonl", question_rows) write_jsonl(out_dir / "gt_text.jsonl", gt_rows) write_jsonl(out_dir / "chain_records.jsonl", rows) print(f"[write] URO output dir={out_dir} rows={len(rows)}") def export_generic(args: argparse.Namespace) -> None: rows = chain_records(Path(args.chain_input), Path(args.thinker_output), Path(args.talker_manifest)) out_dir = Path(args.out_dir) out_dir.mkdir(parents=True, exist_ok=True) write_jsonl(out_dir / "chain_records.jsonl", rows) print(f"[write] generic output dir={out_dir} rows={len(rows)}") def export_echomind_asr(args: argparse.Namespace) -> None: rows = chain_records(Path(args.chain_input), Path(args.thinker_output), Path(args.talker_manifest)) by_case = {str(r["case_id"]): r for r in rows} root = Path(args.root_dir) input_dir = root / "dataset" / f"data_{args.data_type}" data = read_json(input_dir / f"script_info_{args.data_type}.json") for d in data: r = by_case.get(str(d.get("case_id"))) if r: d["model"] = args.model_name d["predicted_answer"] = r.get("thinker_text", "") out = root / "output" / f"output_{args.data_type}" / "asr_output" / args.model_name / f"{args.model_name}_asr.json" write_json(out, data) print(f"[write] {out} rows={len(data)}") def export_echomind_mcq(args: argparse.Namespace) -> None: rows = chain_records(Path(args.chain_input), Path(args.thinker_output), Path(args.talker_manifest)) by_qid = {str(r["question_id"]): r for r in rows} root = Path(args.root_dir) input_dir = root / "dataset" / f"data_{args.data_type}" data = read_json(input_dir / "MCQ" / args.mcq_file) for d in data: r = by_qid.get(str(d.get("question_id"))) if r: d["predicted_audio_answer"] = r.get("thinker_text", "") d["predicted_text_answer"] = r.get("thinker_text", "") d["prediected_audio_file"] = r.get("wav", "") stem = Path(args.mcq_file).stem out = ( root / "output" / f"output_{args.data_type}" / "mcq_output" / args.model_name / "audio_output_true" / f"{args.model_name}_mcq_{stem}.json" ) write_json(out, data) print(f"[write] {out} rows={len(data)}") def export_echomind_response(args: argparse.Namespace) -> None: rows = chain_records(Path(args.chain_input), Path(args.thinker_output), Path(args.talker_manifest)) by_pair = {(str(r.get("case_id")), str(r.get("voice_type"))): r for r in rows} root = Path(args.root_dir) input_dir = root / "dataset" / f"data_{args.data_type}" data = read_json(input_dir / f"script_info_{args.data_type}.json") for d in data: d["model"] = args.model_name d["ouput_prompt"] = args.system_prompt d.setdefault("output_content", {"target": {}, "neutral": {}, "alternative": {}}) for voice_type in ("target", "neutral", "alternative"): r = by_pair.get((str(d.get("case_id")), voice_type)) if not r: continue d["output_content"].setdefault(voice_type, {}) d["output_content"][voice_type]["file"] = Path(str(r.get("wav", ""))).name d["output_content"][voice_type]["response_audio_transcript"] = r.get("thinker_text", "") d["output_content"][voice_type]["response_text"] = r.get("thinker_text", "") out = ( root / "output" / f"output_{args.data_type}" / "response_output" / args.model_name / args.system_prompt / f"{args.model_name}_{args.system_prompt}_output_responses.json" ) write_json(out, data) print(f"[write] {out} rows={len(data)}") def main() -> None: ap = argparse.ArgumentParser() sub = ap.add_subparsers(dest="cmd", required=True) def add_chain_args(p: argparse.ArgumentParser) -> None: p.add_argument("--chain-input", required=True) p.add_argument("--thinker-output", required=True) p.add_argument("--talker-manifest", required=True) p = sub.add_parser("uro") add_chain_args(p) p.add_argument("--out-dir", required=True) p.set_defaults(func=export_uro) p = sub.add_parser("generic") add_chain_args(p) p.add_argument("--out-dir", required=True) p.set_defaults(func=export_generic) p = sub.add_parser("echomind-asr") add_chain_args(p) p.add_argument("--root-dir", required=True) p.add_argument("--data-type", default="synthesis") p.add_argument("--model-name", default="echoloc") p.set_defaults(func=export_echomind_asr) p = sub.add_parser("echomind-mcq") add_chain_args(p) p.add_argument("--root-dir", required=True) p.add_argument("--data-type", default="synthesis") p.add_argument("--model-name", default="echoloc") p.add_argument("--mcq-file", required=True) p.set_defaults(func=export_echomind_mcq) p = sub.add_parser("echomind-response") add_chain_args(p) p.add_argument("--root-dir", required=True) p.add_argument("--data-type", default="synthesis") p.add_argument("--model-name", default="echoloc") p.add_argument("--system-prompt", default="enhance") p.set_defaults(func=export_echomind_response) args = ap.parse_args() args.func(args) if __name__ == "__main__": main()