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