#!/usr/bin/env python3 # -*- coding: utf-8 -*- """Export query candidates into control2instruct-like JSON for query-audio TTS.""" from __future__ import annotations import argparse import json import re from pathlib import Path from typing import Any, Dict, Iterable, List DEFAULT_INPUT = "/workspace/echoloc/Dataset/Novel/query_data/v2_2000/query_candidates.jsonl" DEFAULT_OUT_DIR = "/workspace/echoloc/Dataset/Novel/query_data/v2_2000/query_tts_controls" def sanitize_id(qid: str) -> str: return re.sub(r"[^A-Za-z0-9_.-]+", "_", qid)[:180] def iter_items(path: Path) -> Iterable[Dict[str, Any]]: with path.open("r", encoding="utf-8") as f: for line in f: if line.strip(): yield json.loads(line) def query_voice_to_instruct(item: Dict[str, Any]) -> str: qtype = item.get("query_type") voice = item.get("query_voice") or {} text = item.get("visible_query", {}).get("text", "") if qtype == "instruction": return "请用标准中性、清晰自然的普通话嗓音,语速适中、音量平稳地读出用户指令文本,不额外加入情绪表演。" parts = [] gender = voice.get("speaker_gender") age = voice.get("speaker_age") timbre = voice.get("timbre") emotion = voice.get("emotion") intensity = voice.get("emotion_intensity") delivery = voice.get("delivery") vad = voice.get("vad") or {} gender_map = {"male": "男性", "female": "女性", "neutral": "中性"} age_map = { "child": "儿童", "teen": "青少年", "young": "年轻", "middle_aged": "中年", "elderly": "老年", } speaker_desc = f"{age_map.get(str(age), str(age) if age else '')}{gender_map.get(str(gender), str(gender) if gender else '')}" if age or gender or timbre: parts.append(f"请以{speaker_desc or '自然'}说话人的声音,音色{timbre or '自然清晰'}") if emotion: parts.append(f"表达{emotion},情绪强度约为{intensity}") if vad: parts.append( f"VAD倾向为 valence={vad.get('valence')}, arousal={vad.get('arousal')}, dominance={vad.get('dominance')}" ) if delivery: parts.append(f"具体发声方式:{delivery}") parts.append("只朗读用户原话,不添加解释或回应。") return ",".join(parts).replace(",,", ",") def build_control(item: Dict[str, Any]) -> Dict[str, Any]: text = item.get("visible_query", {}).get("text", "").strip() instruct = query_voice_to_instruct(item) return { "segments": [ { "instruct": instruct, "txt": text, } ], "combined": { "instruct": instruct, "txt": text, }, "combined_no_speaker": { "instruct": instruct, "txt": text, }, "meta": { "qid": item.get("qid"), "query_type": item.get("query_type"), "source_event": item.get("source_event"), "query_voice": item.get("query_voice"), }, } def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("--input", default=DEFAULT_INPUT) parser.add_argument("--out_dir", default=DEFAULT_OUT_DIR) parser.add_argument("--limit", type=int, default=0) parser.add_argument("--overwrite", action="store_true") return parser.parse_args() def main() -> int: args = parse_args() input_path = Path(args.input) out_dir = Path(args.out_dir) out_dir.mkdir(parents=True, exist_ok=True) manifest = out_dir / "manifest.jsonl" if args.overwrite and manifest.exists(): manifest.unlink() count = 0 with manifest.open("a", encoding="utf-8") as mf: for item in iter_items(input_path): if args.limit > 0 and count >= args.limit: break qid = item.get("qid", "") if not qid: continue item_dir = out_dir / sanitize_id(qid) item_dir.mkdir(parents=True, exist_ok=True) control_path = item_dir / "control2instruct.json" if control_path.exists() and not args.overwrite: count += 1 continue control = build_control(item) control_path.write_text(json.dumps(control, ensure_ascii=False, indent=2), encoding="utf-8") mf.write(json.dumps({"qid": qid, "query_type": item.get("query_type"), "control_path": str(control_path)}, ensure_ascii=False) + "\n") count += 1 print(f"[DONE] exported={count} out_dir={out_dir}", flush=True) return 0 if __name__ == "__main__": raise SystemExit(main())