You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

878 lines
29 KiB

import 'package:get/get.dart';
import '../../core/utils/logger.dart';
import '../models/user_Info.dart';
import '../services/speech_factory.dart';
import 'language_configs/azure_language_config.dart';
import 'language_configs/google_language_config.dart';
import 'language_configs/iflytek_language_config.dart';
import 'language_configs/volcano_language_config.dart';
import 'language_configs/alibaba_language_config.dart';
import 'language_configs/alibaba35_language_config.dart';
import 'language_configs/ast_config.dart';
/// 语言信息类,包含语言的各种表示形式
class LanguageInfo {
/// 短代码,如 'zh', 'en'
final String shortCode;
/// 中文名称
final String chineseName;
/// 英文名称
final String englishName;
/// Azure ASR 语言代码,如 'zh-CN', 'en-US'
final String asrCode;
/// Azure TTS 语音名称(女声)
final String ttsVoiceName;
/// Azure TTS 语音名称(男声)
final String ttsVoiceNameMale;
/// 备注信息
final String? note;
LanguageInfo({
required this.shortCode,
required this.chineseName,
required this.englishName,
required this.asrCode,
required this.ttsVoiceName,
required this.ttsVoiceNameMale,
this.note,
});
}
/// 语言类型管理类,提供语言信息的查询和转换功能
class LanguageManager extends GetxService {
// 单例实例
static LanguageManager? _instance;
// 单例工厂方法
factory LanguageManager() {
_instance ??= LanguageManager._internal();
return _instance!;
}
// 私有构造函数
LanguageManager._internal() {
_initLanguageMap();
}
// 语言信息映射表,以 ASR 代码为键
final Map<String, LanguageInfo> _languageMap = {};
// 中文名称到 ASR 代码的映射
final Map<String, String> _chineseNameToAsrCodeMap = {};
// 英文名称到 ASR 代码的映射
final Map<String, String> _englishNameToAsrCodeMap = {};
// 短代码到 ASR 代码的映射
final Map<String, String> _shortCodeToAsrCodeMap = {};
/// 初始化语言映射表
void _initLanguageMap() {
// 中文相关
_addLanguage(
shortCode: 'zh',
chineseName: '中文(简体)',
englishName: 'Chinese (simplified)',
asrCode: 'zh-CN',
ttsVoiceName: 'zh-CN-XiaoxiaoNeural',
ttsVoiceNameMale: 'zh-CN-YunxiNeural',
);
_addLanguage(
shortCode: 'zh-Hant',
chineseName: '中文(繁体)',
englishName: 'Chinese (traditional)',
asrCode: 'zh-TW',
ttsVoiceName: 'zh-TW-HsiaoChenNeural',
ttsVoiceNameMale: 'zh-TW-YunJheNeural',
);
_addLanguage(
shortCode: 'zh-Hant-hk',
chineseName: '中文(香港繁体)',
englishName: 'Chinese (Hongkong traditional)',
asrCode: 'zh-HK',
ttsVoiceName: 'zh-HK-HiuMaanNeural',
ttsVoiceNameMale: 'zh-HK-WanLungNeural',
);
// 英语相关
_addLanguage(
shortCode: 'en',
chineseName: '英语',
englishName: 'English',
asrCode: 'en-US',
ttsVoiceName: 'en-US-AriaNeural',
ttsVoiceNameMale: 'en-US-GuyNeural',
);
// 日语相关
_addLanguage(
shortCode: 'ja',
chineseName: '日语',
englishName: 'Japanese',
asrCode: 'ja-JP',
ttsVoiceName: 'ja-JP-NanamiNeural',
ttsVoiceNameMale: 'ja-JP-KeitaNeural',
);
// 韩语相关
_addLanguage(
shortCode: 'ko',
chineseName: '韩语',
englishName: 'Korean',
asrCode: 'ko-KR',
ttsVoiceName: 'ko-KR-SunHiNeural',
ttsVoiceNameMale: 'ko-KR-InJoonNeural',
);
// 欧洲语言
_addLanguage(
shortCode: 'fr',
chineseName: '法语',
englishName: 'French',
asrCode: 'fr-FR',
ttsVoiceName: 'fr-FR-DeniseNeural',
ttsVoiceNameMale: 'fr-FR-HenriNeural',
);
_addLanguage(
shortCode: 'es',
chineseName: '西班牙语',
englishName: 'Spanish',
asrCode: 'es-ES',
ttsVoiceName: 'es-ES-ElviraNeural',
ttsVoiceNameMale: 'es-ES-AlvaroNeural',
);
_addLanguage(
shortCode: 'pt',
chineseName: '葡萄牙语',
englishName: 'Portuguese',
asrCode: 'pt-BR',
ttsVoiceName: 'pt-BR-FranciscaNeural',
ttsVoiceNameMale: 'pt-BR-AntonioNeural',
);
_addLanguage(
shortCode: 'it',
chineseName: '意大利语',
englishName: 'Italian',
asrCode: 'it-IT',
ttsVoiceName: 'it-IT-ElsaNeural',
ttsVoiceNameMale: 'it-IT-DiegoNeural',
);
_addLanguage(
shortCode: 'de',
chineseName: '德语',
englishName: 'German',
asrCode: 'de-DE',
ttsVoiceName: 'de-DE-KatjaNeural',
ttsVoiceNameMale: 'de-DE-ConradNeural',
);
_addLanguage(
shortCode: 'ru',
chineseName: '俄语',
englishName: 'Russian',
asrCode: 'ru-RU',
ttsVoiceName: 'ru-RU-SvetlanaNeural',
ttsVoiceNameMale: 'ru-RU-DmitryNeural',
);
_addLanguage(
shortCode: 'pl',
chineseName: '波兰语',
englishName: 'Polish',
asrCode: 'pl-PL',
ttsVoiceName: 'pl-PL-AgnieszkaNeural',
ttsVoiceNameMale: 'pl-PL-MarekNeural',
);
_addLanguage(
shortCode: 'nl',
chineseName: '荷兰语',
englishName: 'Dutch',
asrCode: 'nl-NL',
ttsVoiceName: 'nl-NL-ColetteNeural',
ttsVoiceNameMale: 'nl-NL-MaartenNeural',
);
_addLanguage(
shortCode: 'sv',
chineseName: '瑞典语',
englishName: 'Swedish',
asrCode: 'sv-SE',
ttsVoiceName: 'sv-SE-SofieNeural',
ttsVoiceNameMale: 'sv-SE-MattiasNeural',
);
_addLanguage(
shortCode: 'cs',
chineseName: '捷克语',
englishName: 'Czech',
asrCode: 'cs-CZ',
ttsVoiceName: 'cs-CZ-VlastaNeural',
ttsVoiceNameMale: 'cs-CZ-AntoninNeural',
);
_addLanguage(
shortCode: 'el',
chineseName: '现代希腊语',
englishName: 'Modern Greek',
asrCode: 'el-GR',
ttsVoiceName: 'el-GR-AthinaNeural',
ttsVoiceNameMale: 'el-GR-NestorasNeural',
);
_addLanguage(
shortCode: 'ro',
chineseName: '罗马尼亚语',
englishName: 'Romanian',
asrCode: 'ro-RO',
ttsVoiceName: 'ro-RO-AlinaNeural',
ttsVoiceNameMale: 'ro-RO-EmilNeural',
);
_addLanguage(
shortCode: 'hu',
chineseName: '匈牙利语',
englishName: 'Hungarian',
asrCode: 'hu-HU',
ttsVoiceName: 'hu-HU-NoemiNeural',
ttsVoiceNameMale: 'hu-HU-TamasNeural',
);
_addLanguage(
shortCode: 'uk',
chineseName: '乌克兰语',
englishName: 'Ukrainian',
asrCode: 'uk-UA',
ttsVoiceName: 'uk-UA-PolinaNeural',
ttsVoiceNameMale: 'uk-UA-OstapNeural',
);
_addLanguage(
shortCode: 'da',
chineseName: '丹麦语',
englishName: 'Danish',
asrCode: 'da-DK',
ttsVoiceName: 'da-DK-ChristelNeural',
ttsVoiceNameMale: 'da-DK-JeppeNeural',
);
_addLanguage(
shortCode: 'fi',
chineseName: '芬兰语',
englishName: 'Finnish',
asrCode: 'fi-FI',
ttsVoiceName: 'fi-FI-NooraNeural',
ttsVoiceNameMale: 'fi-FI-HarriNeural',
);
_addLanguage(
shortCode: 'no',
chineseName: '挪威语',
englishName: 'Norwegian',
asrCode: 'nb-NO',
ttsVoiceName: 'nb-NO-IselinNeural',
ttsVoiceNameMale: 'nb-NO-FinnNeural',
);
_addLanguage(
shortCode: 'hr',
chineseName: '克罗地亚语',
englishName: 'Croatian',
asrCode: 'hr-HR',
ttsVoiceName: 'hr-HR-GabrijelaNeural',
ttsVoiceNameMale: 'hr-HR-SreckoNeural',
);
_addLanguage(
shortCode: 'ca',
chineseName: '加泰隆语',
englishName: 'Catalan',
asrCode: 'ca-ES',
ttsVoiceName: 'ca-ES-JoanaNeural',
ttsVoiceNameMale: 'ca-ES-EnricNeural',
);
// 亚洲语言
_addLanguage(
shortCode: 'ar',
chineseName: '阿拉伯语',
englishName: 'Arabic',
asrCode: 'ar-EG',
ttsVoiceName: 'ar-EG-SalmaNeural',
ttsVoiceNameMale: 'ar-EG-ShakirNeural',
);
_addLanguage(
shortCode: 'th',
chineseName: '泰语',
englishName: 'Thai',
asrCode: 'th-TH',
ttsVoiceName: 'th-TH-PremwadeeNeural',
ttsVoiceNameMale: 'th-TH-NiwatNeural',
);
_addLanguage(
shortCode: 'vi',
chineseName: '越南语',
englishName: 'Vietnamese',
asrCode: 'vi-VN',
ttsVoiceName: 'vi-VN-HoaiMyNeural',
ttsVoiceNameMale: 'vi-VN-NamMinhNeural',
);
_addLanguage(
shortCode: 'id',
chineseName: '印尼语',
englishName: 'Indonesian',
asrCode: 'id-ID',
ttsVoiceName: 'id-ID-GadisNeural',
ttsVoiceNameMale: 'id-ID-ArdiNeural',
);
_addLanguage(
shortCode: 'hi',
chineseName: '印地语',
englishName: 'Hindi',
asrCode: 'hi-IN',
ttsVoiceName: 'hi-IN-SwaraNeural',
ttsVoiceNameMale: 'hi-IN-MadhurNeural',
);
_addLanguage(
shortCode: 'tr',
chineseName: '土耳其语',
englishName: 'Turkish',
asrCode: 'tr-TR',
ttsVoiceName: 'tr-TR-EmelNeural',
ttsVoiceNameMale: 'tr-TR-AhmetNeural',
);
_addLanguage(
shortCode: 'ms',
chineseName: '马来语',
englishName: 'Malay',
asrCode: 'ms-MY',
ttsVoiceName: 'ms-MY-YasminNeural',
ttsVoiceNameMale: 'ms-MY-OsmanNeural',
);
_addLanguage(
shortCode: 'bn',
chineseName: '孟加拉语',
englishName: 'Bengali',
asrCode: 'bn-IN',
ttsVoiceName: 'bn-IN-TanishaaNeural',
ttsVoiceNameMale: 'bn-IN-BashkarNeural',
);
_addLanguage(
shortCode: 'ta',
chineseName: '泰米尔语',
englishName: 'Tamil',
asrCode: 'ta-IN',
ttsVoiceName: 'ta-IN-PallaviNeural',
ttsVoiceNameMale: 'ta-IN-ValluvarNeural',
);
_addLanguage(
shortCode: 'te',
chineseName: '泰卢固语',
englishName: 'Telugu',
asrCode: 'te-IN',
ttsVoiceName: 'te-IN-ShrutiNeural',
ttsVoiceNameMale: 'te-IN-MohanNeural',
);
_addLanguage(
shortCode: 'mr',
chineseName: '马拉提语',
englishName: 'Marathi',
asrCode: 'mr-IN',
ttsVoiceName: 'mr-IN-AarohiNeural',
ttsVoiceNameMale: 'mr-IN-ManoharNeural',
);
_addLanguage(
shortCode: 'ur',
chineseName: '乌尔都语',
englishName: 'Urdu',
asrCode: 'ur-IN',
ttsVoiceName: 'ur-IN-GulNeural',
ttsVoiceNameMale: 'ur-IN-SalmanNeural',
);
_addLanguage(
shortCode: 'fa',
chineseName: '波斯语',
englishName: 'Persian',
asrCode: 'fa-IR',
ttsVoiceName: 'fa-IR-DilaraNeural',
ttsVoiceNameMale: 'fa-IR-FaridNeural',
);
// 非洲语言
_addLanguage(
shortCode: 'sw',
chineseName: '斯瓦希里语',
englishName: 'Swahili',
asrCode: 'sw-KE',
ttsVoiceName: 'sw-KE-ZuriNeural',
ttsVoiceNameMale: 'sw-KE-RafikiNeural',
);
Logger.info('语言管理器初始化完成,共加载 ${_languageMap.length} 种语言');
}
/// 添加语言信息到映射表
void _addLanguage({
required String shortCode,
required String chineseName,
required String englishName,
required String asrCode,
required String ttsVoiceName,
required String ttsVoiceNameMale,
String? note,
}) {
final languageInfo = LanguageInfo(
shortCode: shortCode,
chineseName: chineseName,
englishName: englishName,
asrCode: asrCode,
ttsVoiceName: ttsVoiceName,
ttsVoiceNameMale: ttsVoiceNameMale,
note: note,
);
_languageMap[asrCode] = languageInfo;
_chineseNameToAsrCodeMap[chineseName] = asrCode;
_englishNameToAsrCodeMap[englishName] = asrCode;
_shortCodeToAsrCodeMap[shortCode] = asrCode;
}
/// 获取所有支持的语言信息
Map<String, LanguageInfo> getAllLanguages() {
return Map.from(_languageMap);
}
/// 获取所有支持的语言的中文名称
List<String> getAllChineseNames() {
return _chineseNameToAsrCodeMap.keys.toList();
}
/// 获取所有支持的语言的英文名称
List<String> getAllEnglishNames() {
return _englishNameToAsrCodeMap.keys.toList();
}
/// 根据 ASR 代码获取语言信息
LanguageInfo? getLanguageByAsrCode(String asrCode) {
return _languageMap[asrCode];
}
/// 根据中文名称获取语言信息
LanguageInfo? getLanguageByChineseName(String chineseName) {
final asrCode = _chineseNameToAsrCodeMap[chineseName];
if (asrCode == null) return null;
return _languageMap[asrCode];
}
/// 根据英文名称获取语言信息
LanguageInfo? getLanguageByEnglishName(String englishName) {
final asrCode = _englishNameToAsrCodeMap[englishName];
if (asrCode == null) return null;
return _languageMap[asrCode];
}
/// 根据短代码获取语言信息
LanguageInfo? getLanguageByShortCode(String shortCode) {
final asrCode = _shortCodeToAsrCodeMap[shortCode];
if (asrCode == null) return null;
return _languageMap[asrCode];
}
/// 根据中文名称获取 ASR 代码
String? getAsrCodeByChineseName(String chineseName) {
return _chineseNameToAsrCodeMap[chineseName];
}
/// 根据英文名称获取 ASR 代码
String? getAsrCodeByEnglishName(String englishName) {
return _englishNameToAsrCodeMap[englishName];
}
/// 根据短代码获取 ASR 代码
String? getAsrCodeByShortCode(String shortCode) {
return _shortCodeToAsrCodeMap[shortCode];
}
/// 根据 ASR 代码获取中文名称
String? getChineseNameByAsrCode(String asrCode) {
final languageInfo = _languageMap[asrCode];
return languageInfo?.chineseName;
}
/// 根据 ASR 代码获取英文名称
String? getEnglishNameByAsrCode(String asrCode) {
final languageInfo = _languageMap[asrCode];
return languageInfo?.englishName;
}
/// 根据 ASR 代码获取短代码
String? getShortCodeByAsrCode(String asrCode) {
final languageInfo = _languageMap[asrCode];
return languageInfo?.shortCode;
}
/// 根据 shortCode 获取性别适配的 TTS 音色(用于 findBestMatchingProvider)
String? _getGenderAwareTtsCode(String shortCode) {
final asrCode = _shortCodeToAsrCodeMap[shortCode];
if (asrCode == null) return null;
return _getVoiceByGender(_languageMap[asrCode]);
}
/// 根据用户性别获取对应的 TTS 语音名称
/// gender: 0=未设置(默认女声), 1=男, 2=女
String? _getVoiceByGender(LanguageInfo? info) {
if (info == null) return null;
try {
final gender = User.instance.gender;
final voice = gender == 1 ? info.ttsVoiceNameMale : info.ttsVoiceName;
Logger.debug('TTS音色选择: gender=$gender, voice=$voice');
return voice;
} catch (_) {
Logger.debug('TTS音色选择: 用户未初始化,使用默认女声');
return info.ttsVoiceName;
}
}
/// 根据 ASR 代码获取 TTS 语音名称(根据用户性别自动选择)
String? getTtsVoiceNameByAsrCode(String asrCode) {
return _getVoiceByGender(_languageMap[asrCode]);
}
/// 根据中文名称获取 TTS 语音名称(根据用户性别自动选择)
String? getTtsVoiceNameByChineseName(String chineseName) {
final asrCode = _chineseNameToAsrCodeMap[chineseName];
if (asrCode == null) return null;
return _getVoiceByGender(_languageMap[asrCode]);
}
/// 根据短代码获取中文名称
String? getChineseNameByShortCode(String shortCode) {
final asrCode = _shortCodeToAsrCodeMap[shortCode];
if (asrCode == null) return null;
return _languageMap[asrCode]?.chineseName;
}
/// 根据短代码获取英文名称
String? getEnglishNameByShortCode(String shortCode) {
final asrCode = _shortCodeToAsrCodeMap[shortCode];
if (asrCode == null) return null;
return _languageMap[asrCode]?.englishName;
}
/// 根据短代码获取 TTS 语音名称(根据用户性别自动选择)
String? getTtsVoiceNameByShortCode(String shortCode) {
final asrCode = _shortCodeToAsrCodeMap[shortCode];
if (asrCode == null) return null;
return _getVoiceByGender(_languageMap[asrCode]);
}
/// 获取中文名称到 ASR 代码的映射
Map<String, String> getChineseNameToAsrCodeMap() {
return Map.from(_chineseNameToAsrCodeMap);
}
/// 获取英文名称到 ASR 代码的映射
Map<String, String> getEnglishNameToAsrCodeMap() {
return Map.from(_englishNameToAsrCodeMap);
}
/// 获取短代码到 ASR 代码的映射
Map<String, String> getShortCodeToAsrCodeMap() {
return Map.from(_shortCodeToAsrCodeMap);
}
/// 返回当前所有的AST语言配置(中文名称)
List<String> getAllAstLanguageNames() {
return getAstLanguageConfigs()
.map((e) => e['chineseName'] ?? '')
.where((e) => e.isNotEmpty)
.toList();
}
/// 查找同时支持源语言和目标语言的最佳提供商(通过 shortCode 匹配)
///
/// ⚠️ **通话翻译不再走豆包(火山)端到端**(2026-08-31 改):
/// 原来 zh/en/ja/ko 这几对语言优先命中豆包,现统一改走阿里
/// (通义 qwen3-livetranslate 端到端)。豆包覆盖的 {zh,en,ja,ko} 是阿里
/// 翻译表(zh/en/ja/ko/fr/de/es/it/pt/ru)的真子集,所以摘掉豆包不会
/// 留下任何没人接的语言对。**别把 `SpeechServiceType.volcano` 加回
/// providers 列表**——加回去它排在前面就会又把中英通话翻译抢走。
/// (原生侧 DoubaoE2ETranslateHelper 仍在,只是不再被这里选中;
/// 本函数是通话翻译选 provider 的唯一入口,见
/// TranslationController._initializeCallModeTranslationService。)
///
/// 路由策略(2026-09-01 起,四档降级):
/// 1. **阿里 3.5**(qwen3.5-livetranslate-flash-realtime,带实时声音复刻):
/// 源/目标都在它的「音频+文本输出」29 语内。这一档译文用**说话人本人
/// 的声音**,男女音色天然对得上,不需要猜性别。
/// 2. **阿里旧版**(qwen3-livetranslate-flash-realtime):3.5 出不了音频的
/// 语种落这里。旧版 18 语里有 16 语被 3.5 覆盖,所以实际只有
/// **yue(粤语)/ el(希腊语)** 会走到——它俩在 3.5 里是「仅文本输出」。
/// 3. **微软**:上面两档都不覆盖的语种(app 一共开放 80 个语种,
/// 阿里两档合起来 31 个,剩下约 50 个都靠微软)。
/// 4. **讯飞**:兜底。
///
/// ⚠️ 「3.5 支持 60 语」这个宣传数字**不能直接拿来当路由依据**:其中 31 语
/// 是仅文本输出。通话翻译必须拿到 TTS PCM 回灌耳机,没音频等于功能不成立,
/// 所以第 1 档卡的是 29 语的音频档,不是 60。
BestProviderResult? findBestMatchingProvider(
String sourceShortCode, String targetShortCode) {
final providers = [
SpeechServiceType.alibaba35,
SpeechServiceType.alibaba,
SpeechServiceType.azure,
SpeechServiceType.iflytek,
];
Logger.info(
'[TRANS-INIT] 开始匹配提供商: $sourceShortCode -> $targetShortCode, 策略=阿里3.5(声音复刻)>阿里旧版>微软>讯飞(豆包已停用)');
for (final provider in providers) {
final providerName = _providerStringFromType(provider);
final transSpecs = _getTranslationSpecs(provider);
Map<String, String>? sourceTransSpec;
Map<String, String>? targetTransSpec;
for (final spec in transSpecs) {
if (spec['shortCode'] == sourceShortCode) {
sourceTransSpec ??= {};
sourceTransSpec["translationCode"] = spec["translationCode"]!;
}
if (spec['shortCode'] == targetShortCode) {
targetTransSpec ??= {};
targetTransSpec["translationCode"] = spec["translationCode"]!;
}
}
if (sourceTransSpec == null || targetTransSpec == null) {
Logger.info(
'[TRANS-INIT] 跳过 ${providerDisplayName(providerName)}: 翻译表缺少${sourceTransSpec == null ? " 源=$sourceShortCode" : ""}${targetTransSpec == null ? " 目标=$targetShortCode" : ""}');
continue;
}
// 阿里端到端 (qwen3-livetranslate): 覆盖主流 10 语(zh/en/ja/ko/fr/de/es/it/pt/ru),
// 两端都在阿里翻译表内即走阿里;稀有语种交给微软兜底。
// 这里不加额外白名单——上面 transSpecs 的匹配检查已经做了筛选。
// 对于非端到端服务(Azure, Iflytek),还需要检查 ASR 和 TTS 支持
if (!_isAlibaba(provider)) {
final asrSpecs = _getAsrSpecs(provider);
final ttsSpecs = _getTtsSpecs(provider);
for (final spec in asrSpecs) {
if (spec['shortCode'] == sourceShortCode) {
sourceTransSpec["asrCode"] = spec["asrCode"]!;
}
if (spec['shortCode'] == targetShortCode) {
targetTransSpec["asrCode"] = spec["asrCode"]!;
}
}
for (final spec in ttsSpecs) {
if (spec['shortCode'] == sourceShortCode) {
sourceTransSpec["ttsCode"] =
_getGenderAwareTtsCode(spec['shortCode']!) ?? spec["ttsVoiceName"]!;
}
if (spec['shortCode'] == targetShortCode) {
targetTransSpec["ttsCode"] =
_getGenderAwareTtsCode(spec['shortCode']!) ?? spec["ttsVoiceName"]!;
}
}
} else if (_isAlibaba(provider)) {
// 阿里端到端的音色是**按路**定的,两路含义完全不同,别给成同一个值:
//
// A 路 = 己方麦克风 → 译成对方语言 → **对方听见的"我"**
// 原生读 supportedLanguages[5],即这里的 targetSpec['ttsCode']
// B 路 = 对端通话音 → 译成己方语言 → **我听见的"对方"**
// 原生读 supportedLanguages[4],即这里的 sourceSpec['ttsCode']
//
// 映射见 AzureSpeechPlugin(两端一致):
// bailianConfigA.voice = ttsLang1 / bailianConfigB.voice = ttsLang0。
// 改这两行前先回去核一眼,写反了就变成「对方说话用我的性别、
// 我说话用默认女声」,且不会报任何错。
//
// 对方是男是女**无从得知**:qwen3-livetranslate 的 voice 是 session 级
// 参数,握手时定死且中途改不了,模型也不做说话人性别识别。
// 所以 B 路一律女声;A 路代表用户本人,跟随用户设置的性别
// (未设置同样落女声,见 getAlibabaVoiceForUser)。
// 每一路的音色跟的是**这一路要播出来的语言**:
// A 路播 targetShortCode,B 路播 sourceShortCode。
final String userVoice = getAlibabaVoiceForUser(targetShortCode);
final String peerVoice = getAlibabaVoiceForPeer(sourceShortCode);
final ttsSpecs = _getTtsSpecs(provider);
for (final spec in ttsSpecs) {
// 仍然按 shortCode 命中才填:填不上会退到 translation_controller 里
// getTtsVoiceNameByAsrCode 的 Azure 音色名(zh-CN-XiaoxiaoNeural),
// 那对阿里是非法值。上面的 transSpecs 校验保证了这里一定命中。
if (spec['shortCode'] == sourceShortCode) {
sourceTransSpec["ttsCode"] = peerVoice;
}
if (spec['shortCode'] == targetShortCode) {
targetTransSpec["ttsCode"] = userVoice;
}
}
// ⚠️ 日志里别直接读 User.instance:未初始化时它是 throw 而不是返回 null,
// 异常会被上层 _initializeCallModeTranslationService 的 catch 吞成
// 「通话翻译初始化失败」,一句日志把整个功能带塌。
int? genderForLog;
try {
genderForLog = User.instance.gender;
} catch (_) {
genderForLog = null;
}
if (provider == SpeechServiceType.alibaba35) {
Logger.info(
'[TRANS-INIT] 阿里3.5 开启声音复刻:两路音色由原生强制写成 default,'
'译文用说话人本人的声音,不按性别选音色(gender=${genderForLog ?? "未初始化"} 仅记录)');
} else {
Logger.info(
'[TRANS-INIT] 阿里旧版音色: A路(我说话/对方听)=$userVoice, B路(对方说话/我听)=$peerVoice, gender=${genderForLog ?? "未初始化"}');
}
}
final providerString = _providerStringFromType(provider);
final isEndToEnd = _isAlibaba(provider);
// warn 级:这一行决定了「有没有声音复刻」——只有 alibaba35 那一档才有。
// 客户反馈「没被复刻」时,第一件事就是看这里命中的是哪一档。
Logger.warning(
'[TRANS-INIT] 命中 ${providerDisplayName(providerString)}[$providerString] for $sourceShortCode -> $targetShortCode (端到端=$isEndToEnd), transSrc=${sourceTransSpec["translationCode"]}, transTgt=${targetTransSpec["translationCode"]}, asrSrc=${sourceTransSpec["asrCode"]}, asrTgt=${targetTransSpec["asrCode"]}, ttsSrc=${sourceTransSpec["ttsCode"]}, ttsTgt=${targetTransSpec["ttsCode"]}');
return BestProviderResult(
providerString, sourceTransSpec, targetTransSpec);
}
Logger.warning(
'[TRANS-INIT] 所有提供商都不匹配 $sourceShortCode -> $targetShortCode,返回 null');
return null;
}
/// 两档阿里共用同一套端到端流水线(同一个原生 helper、同一套 WS 协议),
/// 差别只在 model 名与是否开声音复刻,所以凡是「是不是端到端阿里」的判断
/// 都走这里,别再逐个写 `== SpeechServiceType.alibaba`——漏掉 3.5 会让它
/// 掉进 Azure 那条「还要查 ASR/TTS 表」的分支里,直接匹配失败。
static bool _isAlibaba(SpeechServiceType t) =>
t == SpeechServiceType.alibaba || t == SpeechServiceType.alibaba35;
String _providerStringFromType(SpeechServiceType type) {
switch (type) {
case SpeechServiceType.iflytek:
return 'iflytek';
case SpeechServiceType.azure:
return 'azure';
case SpeechServiceType.google:
return 'google';
case SpeechServiceType.alibaba:
return 'alibaba';
case SpeechServiceType.alibaba35:
return 'alibaba35';
case SpeechServiceType.volcano:
return 'volcano';
case SpeechServiceType.flutter:
return 'flutter';
}
}
/// 将 provider 字符串映射为中文厂商名,用于日志可读性
static String providerDisplayName(String provider) {
switch (provider) {
case 'volcano':
return '豆包(火山)';
case 'alibaba':
return '阿里(通义·旧版)';
case 'alibaba35':
return '阿里(通义3.5·声音复刻)';
case 'azure':
return '微软(Azure)';
case 'iflytek':
return '讯飞';
case 'google':
return '谷歌';
case 'flutter':
return '本地(Flutter)';
default:
return provider;
}
}
// ===== 私有工具方法:加载不同提供商的语言规格 =====
List<Map<String, String>> _getAsrSpecs(SpeechServiceType provider) {
switch (provider) {
case SpeechServiceType.azure:
return getAzureASRLanguageSpecs();
case SpeechServiceType.google:
return getGoogleASRLanguageSpecs();
case SpeechServiceType.iflytek:
return getIflytekASRLanguageSpecs();
default:
return getAzureASRLanguageSpecs();
}
}
List<Map<String, String>> _getTranslationSpecs(SpeechServiceType provider) {
switch (provider) {
case SpeechServiceType.azure:
return getAzureTranslationLanguageSpecs();
case SpeechServiceType.google:
return getGoogleTranslationLanguageSpecs();
case SpeechServiceType.iflytek:
return getIflytekTranslationLanguageSpecs();
case SpeechServiceType.volcano:
return getVolcanoTranslationLanguageSpecs();
case SpeechServiceType.alibaba:
return getAlibabaTranslationLanguageSpecs();
case SpeechServiceType.alibaba35:
return getAlibaba35TranslationLanguageSpecs();
default:
return getAzureTranslationLanguageSpecs();
}
}
List<Map<String, String>> _getTtsSpecs(SpeechServiceType provider) {
switch (provider) {
case SpeechServiceType.azure:
return getAzureTTSLanguageSpecs();
case SpeechServiceType.google:
return getGoogleTTSLanguageSpecs();
case SpeechServiceType.iflytek:
return getIflytekTTSLanguageSpecs();
case SpeechServiceType.alibaba:
return getAlibabaTranslationLanguageSpecs();
case SpeechServiceType.alibaba35:
return getAlibaba35TranslationLanguageSpecs();
default:
return getAzureTTSLanguageSpecs();
}
}
}
class BestProviderResult {
final String provider;
final Map<String, String> sourceSpec;
final Map<String, String> targetSpec;
BestProviderResult(this.provider, this.sourceSpec, this.targetSpec);
}