| |
| |
| """Export aligned inputs for response TTS, talker inference, and e2e Omni. |
| |
| The query construction pipeline produces three related artifacts: |
| 1. response_controls.jsonl: response-side Global/Control, used as oracle TTS. |
| 2. thinker_targets.jsonl: constructed thinker output, used as talker input. |
| 3. query audio dirs: synthesized user-query audio, used as full-Omni input. |
| |
| This script keeps qid as the stable join key and writes compact jsonl files for |
| the next inference stages. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| import glob |
| import json |
| import os |
| from pathlib import Path |
| from typing import Any, Dict, Iterable, List, Optional, Tuple |
|
|
|
|
| BASE_DIR = Path("/workspace/echoloc/Dataset/Novel/query_data/v2_2000") |
| DEFAULT_RESPONSE_CONTROLS = BASE_DIR / "thinker_targets/response_controls.jsonl" |
| DEFAULT_THINKER_TARGETS = BASE_DIR / "thinker_targets/thinker_targets.jsonl" |
| DEFAULT_QUERY_QWEN3_DIR = BASE_DIR / "query_audio/qwen3tts_structured_zh_serena_a800" |
| DEFAULT_QUERY_INDEXTTS_DIR = BASE_DIR / "query_audio/indextts_structured_zh_serena_a800_floatsave" |
| DEFAULT_OUT_DIR = BASE_DIR / "eval_inputs" |
|
|
|
|
| def iter_jsonl(path: Path) -> Iterable[Dict[str, Any]]: |
| with path.open("r", encoding="utf-8") as f: |
| for line in f: |
| line = line.strip() |
| if line: |
| yield json.loads(line) |
|
|
|
|
| def write_jsonl(path: Path, rows: Iterable[Dict[str, Any]]) -> int: |
| path.parent.mkdir(parents=True, exist_ok=True) |
| n = 0 |
| with path.open("w", encoding="utf-8") as f: |
| for row in rows: |
| f.write(json.dumps(row, ensure_ascii=False) + "\n") |
| n += 1 |
| return n |
|
|
|
|
| def load_by_qid(path: Path) -> Dict[str, Dict[str, Any]]: |
| out: Dict[str, Dict[str, Any]] = {} |
| for row in iter_jsonl(path): |
| qid = row.get("qid") |
| if qid: |
| out[str(qid)] = row |
| return out |
|
|
|
|
| def _read_instruct_json(path: str) -> Optional[Dict[str, Any]]: |
| try: |
| with open(path, "r", encoding="utf-8") as f: |
| return json.load(f) |
| except Exception: |
| return None |
|
|
|
|
| def meta_language(meta: Dict[str, Any], qid: str) -> str: |
| language = str(meta.get("language") or "").strip().lower() |
| if language: |
| return language |
| qid = str(qid or "") |
| if qid.startswith("vstyle_en_") or "_en_" in qid: |
| return "en" |
| return "zh" |
|
|
|
|
| def discover_query_audio(qwen3_dir: Path, indextts_dir: Path) -> Dict[str, Dict[str, Any]]: |
| """Map qid -> synthesized query audio paths. |
| |
| The TTS scripts use line_idx subdirectories, but each Qwen3TTS subdir keeps a |
| *_instruct.json containing the original qid/instruct_id. We use that to join |
| against response_controls and thinker_targets. |
| """ |
| out: Dict[str, Dict[str, Any]] = {} |
| for subdir in sorted(qwen3_dir.glob("*")): |
| if not subdir.is_dir(): |
| continue |
| line_idx = subdir.name |
| instruct_paths = list(subdir.glob("*_instruct.json")) |
| if not instruct_paths: |
| continue |
| meta = _read_instruct_json(str(instruct_paths[0])) or {} |
| qid = meta.get("instruct_id") |
| if not qid: |
| continue |
| language = meta_language(meta, str(qid)) |
|
|
| qwen3_vd = subdir / f"{line_idx}_vd_{language}.wav" |
| qwen3_cv = subdir / f"{line_idx}_cv_serena_{language}.wav" |
| if language != "zh": |
| qwen3_vd_zh = subdir / f"{line_idx}_vd_zh.wav" |
| qwen3_cv_zh = subdir / f"{line_idx}_cv_serena_zh.wav" |
| else: |
| qwen3_vd_zh = qwen3_vd |
| qwen3_cv_zh = qwen3_cv |
| indextts_wav = indextts_dir / line_idx / f"{line_idx}_indextts_spk-zh_emo-zh_serena.wav" |
| indextts_control = indextts_dir / line_idx / f"{line_idx}_indextts_spk-zh_emo-zh_serena_control.json" |
|
|
| query_audio_path = "" |
| query_audio_control_path = "" |
| if indextts_wav.exists(): |
| query_audio_path = str(indextts_wav) |
| query_audio_control_path = str(indextts_control) if indextts_control.exists() else "" |
| elif qwen3_cv.exists(): |
| query_audio_path = str(qwen3_cv) |
| elif qwen3_vd.exists(): |
| query_audio_path = str(qwen3_vd) |
| elif language != "zh" and qwen3_cv_zh.exists(): |
| query_audio_path = str(qwen3_cv_zh) |
| elif language != "zh" and qwen3_vd_zh.exists(): |
| query_audio_path = str(qwen3_vd_zh) |
|
|
| if not query_audio_path: |
| continue |
|
|
| out[str(qid)] = { |
| "qid": str(qid), |
| "language": language, |
| "query_audio_line_idx": line_idx, |
| "query_audio_path": query_audio_path, |
| "query_audio_control_path": query_audio_control_path, |
| "query_qwen3_vd_path": str(qwen3_vd) if qwen3_vd.exists() else "", |
| "query_qwen3_cv_path": str(qwen3_cv) if qwen3_cv.exists() else "", |
| "query_qwen3_vd_zh_path": str(qwen3_vd_zh) if qwen3_vd_zh.exists() else "", |
| "query_qwen3_cv_zh_path": str(qwen3_cv_zh) if qwen3_cv_zh.exists() else "", |
| "query_tts_meta": meta, |
| } |
| return out |
|
|
|
|
| def combined_text(target: Dict[str, Any]) -> Tuple[str, str]: |
| combined = target.get("combined") or {} |
| style = str(combined.get("instruct") or "").strip() |
| text = str(combined.get("txt") or "").strip() |
| return style, text |
|
|
|
|
| def make_talker_row(target: Dict[str, Any], aligned: bool) -> Dict[str, Any]: |
| style, text = combined_text(target) |
| qid = str(target["qid"]) |
| return { |
| "id": qid, |
| "qid": qid, |
| "language": target.get("language", "zh"), |
| "ability": "constructed_thinker_to_talker", |
| "query_type": target.get("query_type"), |
| "thinker_style": style, |
| "thinker_text": text, |
| "need_emochange": target.get("need_emochange"), |
| "source_query": target.get("source_query"), |
| "aligned_with_query_audio": aligned, |
| } |
|
|
|
|
| def make_e2e_row( |
| target: Dict[str, Any], |
| response: Dict[str, Any], |
| audio: Dict[str, Any], |
| ) -> Dict[str, Any]: |
| style, text = combined_text(target) |
| qid = str(target["qid"]) |
| visible_query = ( |
| response.get("visible_query") |
| or (response.get("source_query_candidate") or {}).get("visible_query") |
| or target.get("source_query") |
| or {} |
| ) |
| return { |
| "id": qid, |
| "qid": qid, |
| "query_type": target.get("query_type") or response.get("query_type"), |
| "language": target.get("language", "zh"), |
| "query_text": visible_query.get("text", ""), |
| "query_audio_path": audio["query_audio_path"], |
| "query_audio_control_path": audio.get("query_audio_control_path", ""), |
| "oracle_thinker_style": style, |
| "oracle_thinker_text": text, |
| "oracle_response_text": response.get("audio_content", text.replace(" <|EMO_CHANGE|> ", "")), |
| "response_control": response.get("final_generated_control") or response.get("response_generated_control"), |
| "source_event": response.get("source_event"), |
| } |
|
|
|
|
| def make_manifest_row( |
| target: Dict[str, Any], |
| response: Dict[str, Any], |
| audio: Dict[str, Any], |
| ) -> Dict[str, Any]: |
| e2e = make_e2e_row(target, response, audio) |
| return { |
| **e2e, |
| "talker_input_id": target["qid"], |
| "response_tts_input_qid": response["qid"], |
| } |
|
|
|
|
| def main() -> None: |
| ap = argparse.ArgumentParser() |
| ap.add_argument("--response_controls", type=Path, default=DEFAULT_RESPONSE_CONTROLS) |
| ap.add_argument("--thinker_targets", type=Path, default=DEFAULT_THINKER_TARGETS) |
| ap.add_argument("--query_qwen3_dir", type=Path, default=DEFAULT_QUERY_QWEN3_DIR) |
| ap.add_argument("--query_indextts_dir", type=Path, default=DEFAULT_QUERY_INDEXTTS_DIR) |
| ap.add_argument("--out_dir", type=Path, default=DEFAULT_OUT_DIR) |
| args = ap.parse_args() |
|
|
| responses = load_by_qid(args.response_controls) |
| targets = load_by_qid(args.thinker_targets) |
| query_audio = discover_query_audio(args.query_qwen3_dir, args.query_indextts_dir) |
|
|
| qids_target = set(targets) |
| qids_response = set(responses) |
| qids_audio = set(query_audio) |
| aligned_qids = sorted(qids_target & qids_response & qids_audio) |
|
|
| args.out_dir.mkdir(parents=True, exist_ok=True) |
|
|
| talker_all = [ |
| make_talker_row(targets[qid], aligned=(qid in aligned_qids)) |
| for qid in sorted(qids_target) |
| ] |
| talker_aligned = [row for row in talker_all if row["aligned_with_query_audio"]] |
| e2e_rows = [ |
| make_e2e_row(targets[qid], responses[qid], query_audio[qid]) |
| for qid in aligned_qids |
| ] |
| manifest_rows = [ |
| make_manifest_row(targets[qid], responses[qid], query_audio[qid]) |
| for qid in aligned_qids |
| ] |
| response_aligned = [responses[qid] for qid in aligned_qids] |
|
|
| counts = { |
| "response_controls": len(responses), |
| "thinker_targets": len(targets), |
| "query_audio": len(query_audio), |
| "aligned": len(aligned_qids), |
| "missing_response_for_target": len(qids_target - qids_response), |
| "missing_query_audio_for_target": len(qids_target - qids_audio), |
| } |
|
|
| written = { |
| "talker_input_all": write_jsonl(args.out_dir / "talker_input_all.jsonl", talker_all), |
| "talker_input_aligned": write_jsonl(args.out_dir / "talker_input_aligned.jsonl", talker_aligned), |
| "e2e_input_aligned": write_jsonl(args.out_dir / "e2e_input_aligned.jsonl", e2e_rows), |
| "aligned_manifest": write_jsonl(args.out_dir / "aligned_manifest.jsonl", manifest_rows), |
| "response_tts_input_aligned": write_jsonl(args.out_dir / "response_tts_input_aligned.jsonl", response_aligned), |
| } |
|
|
| with (args.out_dir / "export_summary.json").open("w", encoding="utf-8") as f: |
| json.dump({"counts": counts, "written": written}, f, ensure_ascii=False, indent=2) |
|
|
| print(json.dumps({"counts": counts, "written": written, "out_dir": str(args.out_dir)}, ensure_ascii=False, indent=2)) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|