import 'dart:convert'; import 'dart:typed_data'; import 'package:http/http.dart' as http; import 'package:crypto/crypto.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:get/get.dart'; import '../../data/models/appconfig.dart'; import '../../core/utils/logger.dart'; import 'language_manager.dart'; import '../../modules/picture_translation/models/picture_translation_models.dart'; //// 火山引擎机器翻译服务 /// 提供文本翻译功能 /// 提供图片翻译功能 /// 提供文本语种检测功能 class VolcanoTranslationService extends GetxService { // API 配置 late String _baseUrl; final String _endpoint = '/'; // 修改为根路径 late final String _accessKey; late final String _secretKey; late final String _region; late final String _service; // 初始化标志 bool _isInitialized = false; // 语言管理器 final LanguageManager _languageManager = Get.find(); // 文本语种检测频率控制与缓存 DateTime? _lastDetectTime; // 上次检测时间 String? _lastDetectTextKey; // 上次检测的文本键 final Duration _detectMinInterval = const Duration(milliseconds: 800); // 最小检测间隔,避免过频请求 final Map _detectCache = {}; // 文本键 -> 检测到的短代码 缓存 // 构造函数 VolcanoTranslationService(); @override Future onInit() async { await initialize(); super.onInit(); } // 对外暴露初始化状态,便于控制器判断 bool get isInitialized => _isInitialized; // 初始化服务 Future initialize() async { // 已初始化则直接返回,避免重复初始化 if (_isInitialized) { Logger.info('翻译服务已初始化,跳过重复初始化'); return true; } try { _accessKey = AppConfig.env('VOLCANO_TRANSLATION_ACCESS_KEY') ?? dotenv.env['VOLCANO_TRANSLATION_ACCESS_KEY'] ?? ''; _secretKey = AppConfig.env('VOLCANO_TRANSLATION_SECRET_KEY') ?? dotenv.env['VOLCANO_TRANSLATION_SECRET_KEY'] ?? ''; _region = AppConfig.env('VOLCANO_TRANSLATION_REGION') ?? dotenv.env['VOLCANO_TRANSLATION_REGION'] ?? 'cn-north-1'; _service = 'translate'; _baseUrl = 'https://translate.volcengineapi.com'; if (_accessKey.isEmpty || _secretKey.isEmpty) { Logger.error( 'lxm---Volcano Translation Service initialized失败,accessKey或secretKey为空'); return false; } _isInitialized = true; Logger.i('VolcanoTrans', 'Volcano Translation Service initialized 成功'); return true; } catch (e) { Logger.e('VolcanoTrans', 'Volcano Translation Service 初始化失败: $e'); return false; } } // 生成签名 Map _generateSignature(String method, Map requestBody, Map queryParams) { // 1. 准备时间相关参数 final now = DateTime.now().toUtc(); final date = now.toIso8601String().split('T')[0].replaceAll('-', ''); final timestamp = now .toIso8601String() .replaceAll(':', '') .replaceAll('-', '') .split('.')[0] + 'Z'; // 2. 构建规范查询字符串 - 按照参数名ASCII排序 final sortedParams = Map.fromEntries( queryParams.entries.toList()..sort((a, b) => a.key.compareTo(b.key))); final List queryParts = []; sortedParams.forEach((key, value) { // 使用RFC 3986编码,确保特殊字符被正确处理 final encodedKey = Uri.encodeComponent(key).replaceAll('+', '%20'); final encodedValue = Uri.encodeComponent(value).replaceAll('+', '%20'); queryParts.add('$encodedKey=$encodedValue'); }); final canonicalQueryString = queryParts.join('&'); // 3. 创建规范请求 final contentType = 'application/json'; final payloadHash = sha256.convert(utf8.encode(json.encode(requestBody))).toString(); final host = 'translate.volcengineapi.com'; // 确保头部按照字典序排序,且键名小写 // 注意:只包含host和x-date头部,与示例保持一致 final canonicalHeaders = 'host:$host\nx-date:$timestamp\n'; final signedHeaders = 'host;x-date'; // 构建规范请求,格式必须与示例完全一致 final canonicalRequest = '$method\n$_endpoint\n$canonicalQueryString\n$canonicalHeaders\n$signedHeaders\n$payloadHash'; // 4. 创建待签字符串 final algorithm = 'HMAC-SHA256'; final credentialScope = '$date/$_region/$_service/request'; final stringToSign = '$algorithm\n$timestamp\n$credentialScope\n${sha256.convert(utf8.encode(canonicalRequest)).toString()}'; // 5. 计算签名 - 按照文档正确派生密钥 // 注意:这里是关键修复点,确保密钥派生过程完全符合文档 final kSecret = utf8.encode(_secretKey); // 直接使用原始密钥,不添加前缀 // kDate = HMAC(kSecret, Date) var kDate = Hmac(sha256, kSecret).convert(utf8.encode(date)).bytes; // kRegion = HMAC(kDate, Region) var kRegion = Hmac(sha256, kDate).convert(utf8.encode(_region)).bytes; // kService = HMAC(kRegion, Service) var kService = Hmac(sha256, kRegion).convert(utf8.encode(_service)).bytes; // kSigning = HMAC(kService, "request") var kSigning = Hmac(sha256, kService).convert(utf8.encode('request')).bytes; // 使用派生的签名密钥计算最终签名 final signature = Hmac(sha256, kSigning).convert(utf8.encode(stringToSign)).toString(); // 6. 添加签名到请求头 - 格式必须与示例完全一致 final authorization = '$algorithm Credential=$_accessKey/$credentialScope, SignedHeaders=$signedHeaders, Signature=$signature'; // 只返回必要的头部,与示例保持一致 return { 'Content-Type': contentType, 'X-Date': timestamp, 'Authorization': authorization, 'Host': host, }; } // 文本翻译 /// [text] 待翻译的文本 /// [sourceLanguageCode] 源语言的ASR代码 /// [targetLanguageCode] 目标语言的ASR代码 /// [return] 翻译后的文本 /// 计费标准:每月200万字符免费,超出部分按49元/百万字符计费 Future translateText({ required String text, required String sourceLanguageCode, required String targetLanguageCode, }) async { if (!_isInitialized) { print('🟡[VOLC-DBG] 翻译服务未初始化,返回 null'); Logger.error('翻译服务未初始化'); return null; } try { // 获取源语言和目标语言的短代码 String sourceCode = ''; String targetCode = 'zh'; // 从 ASR 代码获取短代码 if (sourceLanguageCode.isNotEmpty) { final sourceShortCode = _languageManager.getShortCodeByAsrCode(sourceLanguageCode); if (sourceShortCode != null) { sourceCode = sourceShortCode; } } final targetShortCode = _languageManager.getShortCodeByAsrCode(targetLanguageCode); if (targetShortCode != null) { targetCode = targetShortCode; } print('🟡[VOLC-DBG] translateText: srcAsr=$sourceLanguageCode->srcShort=$sourceCode, tgtAsr=$targetLanguageCode->tgtShort=$targetCode, text=$text'); // 构建请求体 final requestBody = { 'SourceLanguage': sourceCode, 'TargetLanguage': targetCode, 'TextList': [text], }; // 添加必要的查询参数 - 确保包含所有必需参数 final queryParams = { 'Action': 'TranslateText', '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'; print('🟡[VOLC-DBG] 发起翻译请求: url=$url, body=${json.encode(requestBody)}'); final response = await http.post( Uri.parse(url), headers: headers, body: json.encode(requestBody), ); print('🟡[VOLC-DBG] 翻译响应: status=${response.statusCode}, bodyLen=${response.body.length}, body=${response.body.length > 500 ? response.body.substring(0, 500) : response.body}'); if (response.statusCode == 200) { final responseData = json.decode(response.body); // 尝试从不同的响应结构中提取翻译结果 String? translation; // 1. 标准响应结构 if (responseData['TranslationList'] != null && responseData['TranslationList'] is List && responseData['TranslationList'].isNotEmpty) { final translationItem = responseData['TranslationList'][0]; if (translationItem is Map && translationItem.containsKey('Translation')) { translation = translationItem['Translation']; } } // 2. 检查是否有错误响应 if (translation == null && responseData['ResponseMetadata'] != null && responseData['ResponseMetadata'] is Map && responseData['ResponseMetadata']['Error'] != null) { return null; } // 3. 尝试其他可能的响应结构 if (translation == null) { if (responseData.containsKey('Translation')) { translation = responseData['Translation']; } else if (responseData.containsKey('Result') && responseData['Result'] is Map && responseData['Result'].containsKey('Translation')) { translation = responseData['Result']['Translation']; } else if (responseData.containsKey('Data') && responseData['Data'] is Map) { final data = responseData['Data']; if (data.containsKey('Translation')) { translation = data['Translation']; } else if (data.containsKey('TranslationList') && data['TranslationList'] is List && data['TranslationList'].isNotEmpty) { final item = data['TranslationList'][0]; if (item is Map && item.containsKey('Translation')) { translation = item['Translation']; } } } } // 返回提取到的翻译结果 if (translation == null) { print('🟡[VOLC-DBG] HTTP 200 但未能解析出翻译字段,responseData=$responseData'); } else { print('🟡[VOLC-DBG] 翻译成功: $translation'); } return translation; } print('🟡[VOLC-DBG] HTTP 错误: status=${response.statusCode}, body=${response.body}'); Logger.error( '翻译HTTP错误: status=${response.statusCode}, body=${response.body}'); return null; } catch (e, st) { print('🟡[VOLC-DBG] 翻译请求异常: $e\n$st'); Logger.error('翻译请求异常: $e'); return null; } } /// 文本语种检测 /// 返回值为 ASR 代码(例如 `zh-CN` / `en-US`),若检测失败返回 null。 /// 频率控制:同一文本键在最小间隔内复用缓存结果,降低请求频率。 Future 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; } } /// 图片翻译(整图识别+回贴翻译) /// [imageBytes] 待翻译的图片二进制数据 /// [targetLanguageCode] 目标语言的ASR代码 /// [return] 翻译结果 /// 计费标准:每月100张免费,超出部分按0.04元/张计费 Future translateImage({ required Uint8List imageBytes, required String targetLanguageCode, // ASR代码,如 zh-CN / en-US }) async { if (!_isInitialized) { Logger.error('图片翻译服务未初始化'); return null; } try { // 目标语言短代码(例如 zh / en) String targetCode = 'zh'; final targetShortCode = _languageManager.getShortCodeByAsrCode(targetLanguageCode); if (targetShortCode != null && targetShortCode.isNotEmpty) { targetCode = targetShortCode; } // 将图片二进制转为base64字符串(不带前缀) final imgBase64 = base64.encode(imageBytes); // 构建请求体(根据官方文档) final requestBody = { 'Image': imgBase64, 'TargetLanguage': targetCode, }; // 查询参数(官方示例仅包含 Action/Version;为保持与文本翻译一致,保留 Region/Service) final queryParams = { 'Action': 'TranslateImage', 'Version': '2020-07-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), ); if (response.statusCode == 200) { final responseData = json.decode(response.body); // 错误元数据检测 if (responseData is Map && responseData['ResponseMetadata'] is Map && (responseData['ResponseMetadata']['Error'] != null)) { final err = responseData['ResponseMetadata']['Error']; Logger.error( '图片翻译错误: code=${err['Code']}, message=${err['Message']}'); return null; } // 解析回贴图片 Uint8List? translatedImage; if (responseData['Image'] is String) { try { translatedImage = base64.decode(responseData['Image']); } catch (_) { translatedImage = null; } } // 解析文本块 final List blocks = []; if (responseData['TextBlocks'] is List) { for (final item in (responseData['TextBlocks'] as List)) { if (item is Map) { blocks.add(ImageTextBlock.fromJson(item)); } else if (item is Map) { blocks.add(ImageTextBlock.fromJson( item.map((k, v) => MapEntry(k.toString(), v)))); } } } // 提取一个整体检测语种(若存在文本块) String? detectedLang; if (blocks.isNotEmpty && blocks.first.detectedLanguage.isNotEmpty) { detectedLang = blocks.first.detectedLanguage; } return ImageTranslationResult( translatedImageBytes: translatedImage, blocks: blocks, detectedLanguage: detectedLang, ); } Logger.error( '图片翻译HTTP错误: status=${response.statusCode}, body=${response.body}'); return null; } catch (e) { Logger.error('图片翻译请求异常: $e'); return null; } } }