main
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3import random
4from collections import defaultdict
5
6from config import TTS
7
8ENGINES = [
9 # Gemini
10 {"name": "Achernar", "desc": "Soft", "engine": "gemini", "sex": "male"},
11 {"name": "Achird", "desc": "Friendly", "engine": "gemini", "sex": "female"},
12 {"name": "Algenib", "desc": "Gravelly", "engine": "gemini", "sex": "female"},
13 {"name": "Algieba", "desc": "Smooth", "engine": "gemini", "sex": "male"},
14 {"name": "Alnilam", "desc": "Firm", "engine": "gemini", "sex": "male"},
15 {"name": "Aoede", "desc": "Breezy", "engine": "gemini", "sex": "female"},
16 {"name": "Autonoe", "desc": "Bright", "engine": "gemini", "sex": "male"},
17 {"name": "Callirrhoe", "desc": "Easy-going", "engine": "gemini", "sex": "female"},
18 {"name": "Charon", "desc": "Informative", "engine": "gemini", "sex": "male"},
19 {"name": "Despina", "desc": "Smooth", "engine": "gemini", "sex": "female"},
20 {"name": "Enceladus", "desc": "Breathy", "engine": "gemini", "sex": "male"},
21 {"name": "Erinome", "desc": "Clear", "engine": "gemini", "sex": "female"},
22 {"name": "Fenrir", "desc": "Excitable", "engine": "gemini", "sex": "male"},
23 {"name": "Gacrux", "desc": "Mature", "engine": "gemini", "sex": "male"},
24 {"name": "Iapetus", "desc": "Clear", "engine": "gemini", "sex": "male"},
25 {"name": "Kore", "desc": "Firm", "engine": "gemini", "sex": "female"},
26 {"name": "Laomedeia", "desc": "Upbeat", "engine": "gemini", "sex": "female"},
27 {"name": "Leda", "desc": "Youthful", "engine": "gemini", "sex": "female"},
28 {"name": "Orus", "desc": "Firm", "engine": "gemini", "sex": "male"},
29 {"name": "Puck", "desc": "Upbeat", "engine": "gemini", "sex": "male"},
30 {"name": "Pulcherrima", "desc": "Forward", "engine": "gemini", "sex": "female"},
31 {"name": "Rasalgethi", "desc": "Informative", "engine": "gemini", "sex": "male"},
32 {"name": "Sadachbia", "desc": "Lively", "engine": "gemini", "sex": "male"},
33 {"name": "Sadaltager", "desc": "Knowledgeable", "engine": "gemini", "sex": "male"},
34 {"name": "Schedar", "desc": "Even", "engine": "gemini", "sex": "male"},
35 {"name": "Sulafat", "desc": "Warm", "engine": "gemini", "sex": "female"},
36 {"name": "Umbriel", "desc": "Easy-going", "engine": "gemini", "sex": "male"},
37 {"name": "Vindemiatrix", "desc": "Gentle", "engine": "gemini", "sex": "female"},
38 {"name": "Zephyr", "desc": "Bright", "engine": "gemini", "sex": "female"},
39 {"name": "Zubenelgenubi", "desc": "Casual", "engine": "gemini", "sex": "male"},
40 # Qwen
41 {"name": "Chelsie", "desc": "圆润、甜美", "engine": "qwen", "sex": "female"},
42 {"name": "Cherry", "desc": "元气少女", "engine": "qwen", "sex": "female"},
43 {"name": "Ethan", "desc": "年轻、清亮", "engine": "qwen", "sex": "male"},
44 {"name": "Serena", "desc": "甜美、活泼", "engine": "qwen", "sex": "female"},
45 {"name": "Dylan", "desc": "【方言】北京话", "engine": "qwen", "sex": "male"},
46 {"name": "Jada", "desc": "【方言】吴语", "engine": "qwen", "sex": "female"},
47 {"name": "Sunny", "desc": "【方言】四川话", "engine": "qwen", "sex": "female"},
48 # Sambert
49 {"name": "知楠", "desc": "通用场景, 广告男声", "engine": "sambert", "sex": "male"},
50 {"name": "知琪", "desc": "通用场景, 温柔女声", "engine": "sambert", "sex": "female"},
51 {"name": "知厨", "desc": "新闻播报, 舌尖男声", "engine": "sambert", "sex": "male"},
52 {"name": "知德", "desc": "新闻播报, 新闻男声", "engine": "sambert", "sex": "male"},
53 {"name": "知佳", "desc": "新闻播报, 标准女声", "engine": "sambert", "sex": "female"},
54 {"name": "知茹", "desc": "新闻播报, 新闻女声", "engine": "sambert", "sex": "female"},
55 {"name": "知倩", "desc": "配音解说, 资讯女声", "engine": "sambert", "sex": "female"},
56 {"name": "知祥", "desc": "配音解说, 磁性男声", "engine": "sambert", "sex": "male"},
57 {"name": "知薇", "desc": "产品简介, 萝莉女声", "engine": "sambert", "sex": "female"},
58 {"name": "知浩", "desc": "通用场景, 咨询男声", "engine": "sambert", "sex": "male"},
59 {"name": "知婧", "desc": "通用场景, 严厉女声", "engine": "sambert", "sex": "female"},
60 {"name": "知茗", "desc": "通用场景, 诙谐男声", "engine": "sambert", "sex": "male"},
61 {"name": "知墨", "desc": "通用场景, 情感男声", "engine": "sambert", "sex": "male"},
62 {"name": "知娜", "desc": "通用场景, 浙普女声", "engine": "sambert", "sex": "female"},
63 {"name": "知树", "desc": "通用场景, 资讯男声", "engine": "sambert", "sex": "male"},
64 {"name": "知莎", "desc": "通用场景, 知性女声", "engine": "sambert", "sex": "female"},
65 {"name": "知婷", "desc": "通用场景, 电台女声", "engine": "sambert", "sex": "female"},
66 {"name": "知笑", "desc": "通用场景, 资讯女声", "engine": "sambert", "sex": "female"},
67 {"name": "知雅", "desc": "通用场景, 严厉女声", "engine": "sambert", "sex": "female"},
68 {"name": "知晔", "desc": "通用场景, 青年男声", "engine": "sambert", "sex": "male"},
69 {"name": "知颖", "desc": "通用场景, 软萌童声", "engine": "sambert", "sex": "male"},
70 {"name": "知媛", "desc": "通用场景, 知心姐姐", "engine": "sambert", "sex": "female"},
71 {"name": "知悦", "desc": "客服, 温柔女声", "engine": "sambert", "sex": "female"},
72 {"name": "知柜", "desc": "阅读产品简介, 直播女声", "engine": "sambert", "sex": "female"},
73 {"name": "知硕", "desc": "数字人, 自然男声", "engine": "sambert", "sex": "male"},
74 {"name": "知妙", "desc": "产品简介、数字人、直播", "engine": "sambert", "sex": "female"},
75 {"name": "知猫", "desc": "产品简介、数字人、直播", "engine": "sambert", "sex": "female"},
76 {"name": "知伦", "desc": "配音解说, 悬疑解说", "engine": "sambert", "sex": "male"},
77 {"name": "知飞", "desc": "配音解说, 激昂解说", "engine": "sambert", "sex": "male"},
78 {"name": "知达", "desc": "新闻播报, 标准男声", "engine": "sambert", "sex": "male"},
79 # EdgeTTS
80 {"name": "晓晓", "desc": "温柔", "engine": "edge", "sex": "female"},
81 {"name": "晓伊", "desc": "甜美", "engine": "edge", "sex": "female"},
82 {"name": "晓辰", "desc": "知性", "engine": "edge", "sex": "female"},
83 {"name": "晓涵", "desc": "优雅", "engine": "edge", "sex": "female"},
84 {"name": "晓梦", "desc": "梦幻", "engine": "edge", "sex": "female"},
85 {"name": "晓墨", "desc": "文艺", "engine": "edge", "sex": "female"},
86 {"name": "晓秋", "desc": "成熟", "engine": "edge", "sex": "female"},
87 {"name": "晓睿", "desc": "智慧", "engine": "edge", "sex": "female"},
88 {"name": "晓双", "desc": "活泼", "engine": "edge", "sex": "female"},
89 {"name": "晓萱", "desc": "清新", "engine": "edge", "sex": "female"},
90 {"name": "晓颜", "desc": "柔美", "engine": "edge", "sex": "female"},
91 {"name": "晓悠", "desc": "悠扬", "engine": "edge", "sex": "female"},
92 {"name": "晓甄", "desc": "端庄", "engine": "edge", "sex": "female"},
93 {"name": "云希", "desc": "清朗", "engine": "edge", "sex": "male"},
94 {"name": "云扬", "desc": "阳光", "engine": "edge", "sex": "male"},
95 {"name": "云健", "desc": "稳重", "engine": "edge", "sex": "male"},
96 {"name": "云枫", "desc": "磁性", "engine": "edge", "sex": "male"},
97 {"name": "云皓", "desc": "豪迈", "engine": "edge", "sex": "male"},
98 {"name": "云夏", "desc": "热情", "engine": "edge", "sex": "male"},
99 {"name": "云野", "desc": "野性", "engine": "edge", "sex": "male"},
100 {"name": "云泽", "desc": "深沉", "engine": "edge", "sex": "male"},
101]
102
103LIMIT_FOR_MODEL = {
104 "Dylan": ["qwen-tts-latest", "qwen-tts-2025-05-22"],
105 "Jada": ["qwen-tts-latest", "qwen-tts-2025-05-22"],
106 "Sunny": ["qwen-tts-latest", "qwen-tts-2025-05-22"],
107 "知楠": ["sambert-zhinan-v1"],
108 "知琪": ["sambert-zhiqi-v1"],
109 "知厨": ["sambert-zhichu-v1"],
110 "知德": ["sambert-zhide-v1"],
111 "知佳": ["sambert-zhijia-v1"],
112 "知茹": ["sambert-zhiru-v1"],
113 "知倩": ["sambert-zhiqian-v1"],
114 "知祥": ["sambert-zhixiang-v1"],
115 "知薇": ["sambert-zhiwei-v1"],
116 "知浩": ["sambert-zhihao-v1"],
117 "知婧": ["sambert-zhijing-v1"],
118 "知茗": ["sambert-zhiming-v1"],
119 "知墨": ["sambert-zhimo-v1"],
120 "知娜": ["sambert-zhina-v1"],
121 "知树": ["sambert-zhishu-v1"],
122 "知莎": ["sambert-zhistella-v1"],
123 "知婷": ["sambert-zhiting-v1"],
124 "知笑": ["sambert-zhixiao-v1"],
125 "知雅": ["sambert-zhiya-v1"],
126 "知晔": ["sambert-zhiye-v1"],
127 "知颖": ["sambert-zhiying-v1"],
128 "知媛": ["sambert-zhiyuan-v1"],
129 "知悦": ["sambert-zhiyue-v1"],
130 "知柜": ["sambert-zhigui-v1"],
131 "知硕": ["sambert-zhishuo-v1"],
132 "知妙": ["sambert-zhimiao-emo-v1"],
133 "知猫": ["sambert-zhimao-v1"],
134 "知伦": ["sambert-zhilun-v1"],
135 "知飞": ["sambert-zhifei-v1"],
136 "知达": ["sambert-zhida-v1"],
137 "晓晓": ["zh-CN-XiaoxiaoNeural"],
138 "晓伊": ["zh-CN-XiaoyiNeural"],
139 "晓辰": ["zh-CN-XiaochenNeural"],
140 "晓涵": ["zh-CN-XiaohanNeural"],
141 "晓梦": ["zh-CN-XiaomengNeural"],
142 "晓墨": ["zh-CN-XiaomoNeural"],
143 "晓秋": ["zh-CN-XiaoqiuNeural"],
144 "晓睿": ["zh-CN-XiaoruiNeural"],
145 "晓双": ["zh-CN-XiaoshuangNeural"],
146 "晓萱": ["zh-CN-XiaoxuanNeural"],
147 "晓颜": ["zh-CN-XiaoyanNeural"],
148 "晓悠": ["zh-CN-XiaoyouNeural"],
149 "晓甄": ["zh-CN-XiaozhenNeural"],
150 "云希": ["zh-CN-YunxiNeural"],
151 "云扬": ["zh-CN-YunyangNeural"],
152 "云健": ["zh-CN-YunjianNeural"],
153 "云枫": ["zh-CN-YunfengNeural"],
154 "云皓": ["zh-CN-YunhaoNeural"],
155 "云夏": ["zh-CN-YunxiaNeural"],
156 "云野": ["zh-CN-YunyeNeural"],
157 "云泽": ["zh-CN-YunzeNeural"],
158}
159
160
161def get_random_one(engine: str = "", sex: str = "") -> dict:
162 available = ENGINES
163 if engine:
164 available = [x for x in available if x["engine"] == engine]
165 if sex:
166 available = [x for x in available if x["sex"] == sex]
167 if not available:
168 return random.choice(ENGINES)
169 return random.choice(available)
170
171
172def sex_emoji(name: str) -> str:
173 info = next((x for x in ENGINES if x["name"].lower() == name.lower()), None)
174 if not info:
175 return "❓"
176 return "🚹" if info["sex"] == "male" else "🚺"
177
178
179def list_engines() -> str:
180 texts = "👤音色名称: 描述\n"
181 groupped_by_engine = defaultdict(list)
182 for x in ENGINES:
183 groupped_by_engine[x["engine"]].append(x)
184 for engine, item_list in groupped_by_engine.items():
185 texts += f"🏷️提供商: **{engine.capitalize()}**\n"
186 for x in sorted(item_list, key=lambda x: x["sex"]): # groupped by sex
187 texts += f"{sex_emoji(x['name'])} `{x['name'].capitalize()}`: {x['desc']}\n"
188
189 return texts
190
191
192def get_tts_config(texts: str) -> tuple[str, str, str, str]:
193 """Get TTS config from texts.
194
195 Examples:
196 >>> get_tts_config("@Cherry 你好")
197 ("Cherry", "qwen", "qwen-tts", "你好")
198
199 Args:
200 texts (str): Texts to parse.
201
202 Returns:
203 (voice_name, engine, model, texts)
204 """
205 engine = TTS.DEFAULT_ENGINE.lower()
206 if not texts.startswith("@"):
207 return "", engine, "", texts
208 if texts.startswith(("@男", "@male")):
209 info = get_random_one(sex="male")
210 model = random.choice(LIMIT_FOR_MODEL.get(info["name"], [""]))
211 return info["name"], info["engine"], model, texts.removeprefix("@男").removeprefix("@male").lstrip()
212 if texts.startswith(("@女", "@female")):
213 info = get_random_one(sex="female")
214 model = random.choice(LIMIT_FOR_MODEL.get(info["name"], [""]))
215 return info["name"], info["engine"], model, texts.removeprefix("@女").removeprefix("@female").lstrip()
216 if texts.lower().startswith("@gemini"):
217 info = get_random_one(engine="gemini")
218 model = random.choice(LIMIT_FOR_MODEL.get(info["name"], [""]))
219 return info["name"], info["engine"], model, texts[7:].lstrip()
220 if texts.lower().startswith("@qwen"):
221 info = get_random_one(engine="qwen")
222 model = random.choice(LIMIT_FOR_MODEL.get(info["name"], [""]))
223 return info["name"], info["engine"], model, texts[5:].lstrip()
224 if texts.lower().startswith("@sambert"):
225 info = get_random_one(engine="sambert")
226 model = random.choice(LIMIT_FOR_MODEL.get(info["name"], [""]))
227 return info["name"], info["engine"], model, texts[8:].lstrip()
228 if texts.lower().startswith("@edge"):
229 info = get_random_one(engine="edge")
230 model = random.choice(LIMIT_FOR_MODEL.get(info["name"], [""]))
231 return info["name"], info["engine"], model, texts[8:].lstrip()
232
233 texts = texts.removeprefix("@").lstrip()
234
235 for x in ENGINES:
236 if texts.lower().startswith(x["name"].lower()):
237 model = random.choice(LIMIT_FOR_MODEL.get(x["name"], [""]))
238 return x["name"], x["engine"], model, texts.removeprefix(x["name"]).lstrip()
239 return "", engine, "", texts