EchoLoc / data_construction /export_query_tts_controls.py
zsy814's picture
Initial EchoLoc code release
d8bfe4a verified
Raw
History Blame Contribute Delete
4.75 kB
#!/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())