File size: 4,749 Bytes
d8bfe4a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/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())