Browse Source

feat(翻译): 添加自动检测语种功能

实现文本翻译时的自动语种检测功能,当用户选择"自动检测"时,会先调用火山引擎API进行语种识别
在翻译服务中添加detectTextLanguage方法,支持频率控制和结果缓存
修改语言选择UI,在源语言列表中添加"自动检测"选项
newdev_shunjiawei
lxm 12 months ago
parent
commit
7bd6214742
  1. 177
      lib/data/services/volcano_translation_service.dart
  2. 45
      lib/modules/text_translation/controllers/text_translation_controller.dart

177
lib/data/services/volcano_translation_service.dart

@ -1,32 +1,42 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:math';
import 'dart:typed_data'; import 'dart:typed_data';
import '../../data/models/appconfig.dart';
import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../data/models/appconfig.dart';
import '../../core/utils/logger.dart'; import '../../core/utils/logger.dart';
import 'language_manager.dart'; import 'language_manager.dart';
import '../../modules/picture_translation/models/picture_translation_models.dart'; import '../../modules/picture_translation/models/picture_translation_models.dart';
//// 火山引擎机器翻译服务 //// 火山引擎机器翻译服务
/// 提供文本翻译功能
/// 提供图片翻译功能
/// 提供文本语种检测功能
class VolcanoTranslationService extends GetxService { class VolcanoTranslationService extends GetxService {
// API 配置 // API 配置
late String _baseUrl; late String _baseUrl;
final String _endpoint = '/'; // 修改为根路径 final String _endpoint = '/'; // 修改为根路径
late String _accessKey; late final String _accessKey;
late String _secretKey; late final String _secretKey;
late String _region; late final String _region;
late String _service; late final String _service;
// 初始化标志 // 初始化标志
bool _isInitialized = false; bool _isInitialized = false;
// 语言管理器 // 语言管理器
final LanguageManager _languageManager = Get.find<LanguageManager>(); final LanguageManager _languageManager = Get.find<LanguageManager>();
// final GetStorage _storage = GetStorage();
// 文本语种检测频率控制与缓存
DateTime? _lastDetectTime; // 上次检测时间
String? _lastDetectTextKey; // 上次检测的文本键
final Duration _detectMinInterval =
const Duration(milliseconds: 800); // 最小检测间隔,避免过频请求
final Map<String, String> _detectCache = {}; // 文本键 -> 检测到的短代码 缓存
// 构造函数 // 构造函数
VolcanoTranslationService(); VolcanoTranslationService();
@override @override
@ -46,25 +56,13 @@ class VolcanoTranslationService extends GetxService {
return true; return true;
} }
try { try {
// 优先使用后端 AppConfig,其次回退到 .env _accessKey = AppConfig.env('VOLCANO_TRANSLATION_ACCESS_KEY') ??
final bool appConfigReady = AppConfig.isInitialized();
final accessKeyFromConfig = appConfigReady
? AppConfig.env('VOLCANO_TRANSLATION_ACCESS_KEY')
: null;
final secretKeyFromConfig = appConfigReady
? AppConfig.env('VOLCANO_TRANSLATION_SECRET_KEY')
: null;
final regionFromConfig =
appConfigReady ? AppConfig.env('VOLCANO_TRANSLATION_REGION') : null;
_accessKey = accessKeyFromConfig ??
dotenv.env['VOLCANO_TRANSLATION_ACCESS_KEY'] ?? dotenv.env['VOLCANO_TRANSLATION_ACCESS_KEY'] ??
''; '';
_secretKey = secretKeyFromConfig ?? _secretKey = AppConfig.env('VOLCANO_TRANSLATION_SECRET_KEY') ??
dotenv.env['VOLCANO_TRANSLATION_SECRET_KEY'] ?? dotenv.env['VOLCANO_TRANSLATION_SECRET_KEY'] ??
''; '';
_region = regionFromConfig ?? _region = AppConfig.env('VOLCANO_TRANSLATION_REGION') ??
dotenv.env['VOLCANO_TRANSLATION_REGION'] ?? dotenv.env['VOLCANO_TRANSLATION_REGION'] ??
'cn-north-1'; 'cn-north-1';
@ -288,6 +286,136 @@ class VolcanoTranslationService extends GetxService {
} }
} }
/// 文本语种检测
/// 返回值为 ASR 代码(例如 `zh-CN` / `en-US`),若检测失败返回 null。
/// 频率控制:同一文本键在最小间隔内复用缓存结果,降低请求频率。
Future<String?> detectTextLanguage({
required String text,
}) async {
if (!_isInitialized) {
Logger.error('语种检测服务未初始化');
return null;
}
try {
// 构建文本键用于频率控制与缓存:去除首尾空格并截取前 256 字符
final normalized = text.trim();
if (normalized.isEmpty) {
return null;
}
final textKey =
normalized.length > 256 ? normalized.substring(0, 256) : normalized;
// 命中缓存直接返回,避免重复请求
final cachedShortCode = _detectCache[textKey];
if (cachedShortCode != null && cachedShortCode.isNotEmpty) {
final cachedAsr =
_languageManager.getAsrCodeByShortCode(cachedShortCode);
if (cachedAsr != null && cachedAsr.isNotEmpty) {
return cachedAsr;
}
}
// 频率控制:同一文本键在最小时间间隔内直接复用上次结果或跳过
final now = DateTime.now();
if (_lastDetectTime != null && _lastDetectTextKey == textKey) {
final delta = now.difference(_lastDetectTime!);
if (delta < _detectMinInterval) {
Logger.info('语种检测过于频繁,已短路本次请求');
return null;
}
}
_lastDetectTime = now;
_lastDetectTextKey = textKey;
// 构建请求体
final requestBody = {
'TextList': [normalized],
};
// 查询参数(官方文档:Action=LangDetect, Version=2020-06-01)
final queryParams = {
'Action': 'LangDetect',
'Version': '2020-06-01',
'Region': _region,
'Service': _service,
};
// 生成签名与头部
final headers = _generateSignature('POST', requestBody, queryParams);
// 拼接请求 URL
final queryString = Uri(queryParameters: queryParams).query;
final url = '$_baseUrl/?$queryString';
// 发送请求,增加网络超时保护
final response = await http
.post(
Uri.parse(url),
headers: headers,
body: json.encode(requestBody),
)
.timeout(const Duration(seconds: 8));
if (response.statusCode == 200) {
final responseData = json.decode(response.body);
// 有效性校验:必须存在 DetectedLanguageList 且包含 Language/Confidence
if (responseData is! Map ||
responseData['DetectedLanguageList'] is! List ||
(responseData['DetectedLanguageList'] as List).isEmpty) {
Logger.error('语种检测响应无有效结果: ${response.body}');
return null;
}
// 选择置信度最高的结果
String? bestShortCode;
double bestConfidence = -1;
for (final item in (responseData['DetectedLanguageList'] as List)) {
if (item is Map) {
final lang = item['Language']?.toString();
final conf = (item['Confidence'] is num)
? (item['Confidence'] as num).toDouble()
: null;
if (lang != null && lang.isNotEmpty && conf != null) {
if (conf > bestConfidence) {
bestConfidence = conf;
bestShortCode = lang;
}
}
}
}
// 置信度阈值校验:过低则视为失败
if (bestShortCode == null || bestConfidence < 0.6) {
Logger.info('语种检测置信度不足: code=$bestShortCode, conf=$bestConfidence');
return null;
}
// 映射为 ASR 代码
final asrCode = _languageManager.getAsrCodeByShortCode(bestShortCode);
if (asrCode == null || asrCode.isEmpty) {
Logger.error('语种检测返回短代码无法映射为ASR: $bestShortCode');
return null;
}
// 写入缓存(短代码缓存,便于后续复用)
_detectCache[textKey] = bestShortCode;
Logger.info(
'lxm---语种检测成功: $bestShortCode (conf=$bestConfidence) -> $asrCode');
return asrCode;
}
Logger.error(
'语种检测HTTP错误: status=${response.statusCode}, body=${response.body}');
return null;
} catch (e) {
Logger.error('语种检测请求异常: $e');
return null;
}
}
/// 图片翻译(整图识别+回贴翻译) /// 图片翻译(整图识别+回贴翻译)
Future<ImageTranslationResult?> translateImage({ Future<ImageTranslationResult?> translateImage({
required Uint8List imageBytes, required Uint8List imageBytes,
@ -345,7 +473,8 @@ class VolcanoTranslationService extends GetxService {
responseData['ResponseMetadata'] is Map && responseData['ResponseMetadata'] is Map &&
(responseData['ResponseMetadata']['Error'] != null)) { (responseData['ResponseMetadata']['Error'] != null)) {
final err = responseData['ResponseMetadata']['Error']; final err = responseData['ResponseMetadata']['Error'];
Logger.error('图片翻译错误: code=${err['Code']}, message=${err['Message']}'); Logger.error(
'图片翻译错误: code=${err['Code']}, message=${err['Message']}');
return null; return null;
} }

45
lib/modules/text_translation/controllers/text_translation_controller.dart

@ -129,10 +129,14 @@ class TextTranslationController extends GetxController {
if (language != sourceLanguageChineseName.value) { if (language != sourceLanguageChineseName.value) {
sourceLanguageChineseName.value = language; sourceLanguageChineseName.value = language;
// 同时更新语言代码 // 同时更新语言代码;当选择“自动检测”时,将源语言代码置空,后续翻译流程会进行语种检测
final asrCode = _languageManager.getAsrCodeByChineseName(language); if (language == '自动检测') {
if (asrCode != null) { sourceLanguageCode.value = '';
sourceLanguageCode.value = asrCode; } else {
final asrCode = _languageManager.getAsrCodeByChineseName(language);
if (asrCode != null) {
sourceLanguageCode.value = asrCode;
}
} }
_saveLanguageSettings(); _saveLanguageSettings();
@ -242,7 +246,28 @@ class TextTranslationController extends GetxController {
Logger.info('调用翻译服务...'); Logger.info('调用翻译服务...');
// 调用翻译服务,直接使用维护的语言代码 // 若选择了“自动检测”,先进行语种检测
if (sourceLanguageChineseName.value == '自动检测' ||
sourceLanguageCode.value.isEmpty) {
Logger.info('源语言为自动检测,开始语种检测…');
final detectedAsrCode =
await _translationService.detectTextLanguage(text: text);
if (detectedAsrCode == null || detectedAsrCode.isEmpty) {
Logger.error('语种检测失败');
Get.snackbar(
'错误', // 翻译键代表的中文文本内容
'语种检测失败,请手动选择源语言或稍后重试', // 翻译键代表的中文文本内容
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.9),
colorText: Colors.white,
);
return;
}
sourceLanguageCode.value = detectedAsrCode;
Logger.info('语种检测结果: $detectedAsrCode');
}
// 调用翻译服务,使用检测或手动选择的源语言代码
final result = await _translationService.translateText( final result = await _translationService.translateText(
text: text, text: text,
sourceLanguageCode: sourceLanguageCode.value, sourceLanguageCode: sourceLanguageCode.value,
@ -328,9 +353,15 @@ class TextTranslationController extends GetxController {
const Divider(height: 1), const Divider(height: 1),
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(
itemCount: supportedLanguages.length, itemCount: (isSourceLanguage
? (supportedLanguages.length + 1)
: supportedLanguages.length),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final language = supportedLanguages[index]; final language = isSourceLanguage
? (index == 0
? '自动检测'
: supportedLanguages[index - 1])
: supportedLanguages[index];
final isSelected = isSourceLanguage final isSelected = isSourceLanguage
? language == sourceLanguageChineseName.value ? language == sourceLanguageChineseName.value
: language == targetLanguageChineseName.value; : language == targetLanguageChineseName.value;

Loading…
Cancel
Save