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.
152 lines
4.5 KiB
152 lines
4.5 KiB
import 'dart:math';
|
|
|
|
import 'package:dio/dio.dart';
|
|
|
|
import '../../core/utils/logger.dart';
|
|
import '../models/appconfig.dart';
|
|
import 'ai_vendor_lock.dart';
|
|
|
|
/// 微软文本翻译。同声 / 面对面 / 音视频里扩展语种走这条,不经服务端阿里。
|
|
///
|
|
/// 密钥与通话翻译同一套:`mt_azure` 的 subscription_key / region。
|
|
/// 请求形状与原生 `MicrosoftTranslationServiceImpl` 一致。
|
|
class MicrosoftTextTranslation {
|
|
static const defaultEndpoint =
|
|
'https://api.cognitive.microsofttranslator.com';
|
|
|
|
final Dio _dio = Dio(BaseOptions(
|
|
connectTimeout: const Duration(seconds: 10),
|
|
sendTimeout: const Duration(seconds: 10),
|
|
receiveTimeout: const Duration(seconds: 10),
|
|
));
|
|
|
|
Future<String?> translate({
|
|
required String text,
|
|
required String from,
|
|
required String to,
|
|
CancelToken? cancelToken,
|
|
}) async {
|
|
final src = text.trim();
|
|
if (src.isEmpty) return null;
|
|
if (aiOnlyIflytek) {
|
|
Logger.w('MsTrans', '测试服只允许科大讯飞,不调用微软翻译');
|
|
return null;
|
|
}
|
|
final key =
|
|
AppConfig.cred('mt_azure', 'subscription_key', 'AZURE_TRANSLATION_KEY');
|
|
if (key == null || key.isEmpty) {
|
|
Logger.w('MsTrans', '微软翻译密钥未下发,跳过 $from→$to');
|
|
return null;
|
|
}
|
|
final region = AppConfig.cred('mt_azure', 'region', 'AZURE_TRANSLATION_REGION');
|
|
final endpoint =
|
|
AppConfig.cred('mt_azure', 'endpoint', 'AZURE_TRANSLATION_ENDPOINT') ??
|
|
defaultEndpoint;
|
|
final headers = <String, String>{
|
|
'Ocp-Apim-Subscription-Key': key,
|
|
'Content-Type': 'application/json',
|
|
'X-ClientTraceId': _traceId(),
|
|
};
|
|
if (region != null && region.isNotEmpty) {
|
|
headers['Ocp-Apim-Subscription-Region'] = region;
|
|
}
|
|
try {
|
|
final query = <String, String>{
|
|
'api-version': '3.0',
|
|
'to': to,
|
|
};
|
|
if (from.isNotEmpty) query['from'] = from;
|
|
final response = await _dio.post<dynamic>(
|
|
'${endpoint.replaceAll(RegExp(r'/+$'), '')}/translate',
|
|
queryParameters: query,
|
|
data: [
|
|
{'text': src},
|
|
],
|
|
options: Options(headers: headers),
|
|
cancelToken: cancelToken,
|
|
);
|
|
final out = microsoftTranslationText(response.data);
|
|
if (out == null) {
|
|
Logger.w('MsTrans', '微软翻译返回空: $from→$to');
|
|
}
|
|
return out;
|
|
} on DioException catch (e) {
|
|
if (e.type == DioExceptionType.cancel) return null;
|
|
Logger.w('MsTrans', '微软翻译失败($from→$to): $e');
|
|
return null;
|
|
} catch (e) {
|
|
Logger.w('MsTrans', '微软翻译失败($from→$to): $e');
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 阿里机器翻译盖不住的语种:繁体会被收成简体,乌克兰语和克罗地亚语直接被拒。
|
|
bool microsoftMtRequired(String from, String to) {
|
|
bool hit(String raw) {
|
|
switch (raw.trim().toLowerCase()) {
|
|
case 'zh-hant':
|
|
case 'zh-hant-hk':
|
|
case 'zh-tw':
|
|
case 'zh-hk':
|
|
case 'uk':
|
|
case 'uk-ua':
|
|
case 'hr':
|
|
case 'hr-hr':
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return hit(from) || hit(to);
|
|
}
|
|
|
|
/// 收成微软翻译认的码。空串留给自动检测,请求里不带 from。
|
|
String microsoftMtCode(String raw) {
|
|
final code = raw.trim();
|
|
if (code.isEmpty) return '';
|
|
switch (code.toLowerCase()) {
|
|
case 'zh-hant':
|
|
case 'zh-hant-hk':
|
|
case 'zh-tw':
|
|
case 'zh-hk':
|
|
return 'zh-Hant';
|
|
case 'zh':
|
|
case 'zh-cn':
|
|
case 'zh-hans':
|
|
return 'zh-Hans';
|
|
case 'uk':
|
|
case 'uk-ua':
|
|
return 'uk';
|
|
case 'hr':
|
|
case 'hr-hr':
|
|
return 'hr';
|
|
default:
|
|
final dash = code.indexOf('-');
|
|
if (dash > 0) return code.substring(0, dash).toLowerCase();
|
|
return code.toLowerCase();
|
|
}
|
|
}
|
|
|
|
/// 解析微软翻译 3.0 的响应。失败或空译文返回 null。
|
|
String? microsoftTranslationText(dynamic data) {
|
|
if (data is! List || data.isEmpty) return null;
|
|
final first = data.first;
|
|
if (first is! Map) return null;
|
|
final translations = first['translations'];
|
|
if (translations is! List || translations.isEmpty) return null;
|
|
final item = translations.first;
|
|
if (item is! Map) return null;
|
|
final text = item['text'];
|
|
if (text is! String) return null;
|
|
final out = text.trim();
|
|
return out.isEmpty ? null : out;
|
|
}
|
|
|
|
String _traceId() {
|
|
final rand = Random();
|
|
String hex(int n) =>
|
|
List.generate(n, (_) => rand.nextInt(16).toRadixString(16)).join();
|
|
return '${hex(8)}-${hex(4)}-4${hex(3)}-${hex(4)}-${hex(12)}';
|
|
}
|
|
|