Browse Source

Merge branch 'new_dev' of https://github.com/deepcloud2048/deep_voice into liwei

weicu
liwei1dao 6 months ago
parent
commit
6f7fff916d
  1. 2
      .metadata
  2. 97
      doc/en/member_service_agreement.html
  3. 97
      doc/zh_cn/member_service_agreement.html
  4. 7
      lib/core/bindings/initial_binding.dart
  5. 3
      lib/core/services/floating_window_manager.dart
  6. 6
      lib/core/translations/language/en_us.dart
  7. 6
      lib/core/translations/language/zh_cn.dart
  8. 6
      lib/core/translations/language/zh_hk.dart
  9. 6
      lib/core/translations/language/zh_tw.dart
  10. 2
      lib/core/utils/app_lifecycle_manager.dart
  11. 30
      lib/core/utils/doc_url_helper.dart
  12. 270
      lib/core/utils/logger.dart
  13. 3
      lib/data/services/asr_service.dart
  14. 327
      lib/data/services/ble_manager.dart
  15. 170
      lib/data/services/microsoft_translation_service.dart
  16. 14
      lib/data/services/speech_impl/azure_asr_service.dart
  17. 4
      lib/data/services/speech_impl/azure_ast_service.dart
  18. 1
      lib/data/services/speech_impl/volcano_asr_api_service.dart
  19. 1
      lib/data/services/speech_impl/volcano_asr_service.dart
  20. 10
      lib/data/services/usage_stats_service.dart
  21. 18
      lib/data/services/volcano_translation_service.dart
  22. 155
      lib/modules/common/widgets/member_service_agreement_dialog.dart
  23. 10
      lib/modules/goods/controllers/goods_base_controller.dart
  24. 36
      lib/modules/home/controllers/home_controller.dart
  25. 4
      lib/modules/home/views/home_view.dart
  26. 22
      lib/modules/meeting/controllers/meeting_details_controller.dart
  27. 51
      lib/modules/translation/controllers/translation_controller.dart
  28. 13
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  29. 12
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt
  30. 16
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  31. 63
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt
  32. 25
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
  33. 18
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift
  34. 47
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift
  35. 12
      local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift
  36. 22
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/OpusAudioManager.kt
  37. 8
      local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift
  38. 63
      local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift
  39. 35
      local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoAsrHelper.kt
  40. 2
      pubspec.yaml

2
.metadata

@ -15,7 +15,7 @@ migration:
- platform: root
create_revision: 30ade5e937e6afbd2d2c38ec53175b89b556447e
base_revision: 30ade5e937e6afbd2d2c38ec53175b89b556447e
- platform: android
- platform: ios
create_revision: 30ade5e937e6afbd2d2c38ec53175b89b556447e
base_revision: 30ade5e937e6afbd2d2c38ec53175b89b556447e

97
doc/en/member_service_agreement.html

@ -0,0 +1,97 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Voitrans Membership Service Agreement</title>
<style>
:root { color-scheme: light dark; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; line-height: 1.65; max-width: 720px; margin: 0 auto; padding: 24px 16px 48px; color: #1a1a1a; }
@media (prefers-color-scheme: dark) { body { background: #111; color: #eee; } a { color: #8ab4f8; } }
h1 { font-size: 1.35rem; margin: 0 0 0.5em; }
.meta { font-size: 0.85rem; color: #666; margin-bottom: 1.5em; }
h2 { font-size: 1.05rem; margin: 1.4em 0 0.5em; }
p, li { font-size: 0.95rem; margin: 0.5em 0; }
ol { padding-left: 1.25em; }
.note { font-size: 0.85rem; color: #666; margin-top: 2em; padding-top: 1em; border-top: 1px solid #ddd; }
</style>
</head>
<body>
<h1>Voitrans Membership Service Agreement</h1>
<p class="meta">Last updated: April 22, 2026<br />Effective date: April 22, 2026</p>
<p>Welcome to the membership services provided by the Voitrans application (the "App"). Before purchasing, subscribing to, or using any membership service, please carefully read and fully understand this Agreement, in particular the clauses regarding fees, automatic renewal, refunds, limitations of liability, and dispute resolution. By tapping "Agree", completing payment, or otherwise accepting the membership service, you are deemed to have read and agreed to be bound by this Agreement.</p>
<p>If you do not agree to this Agreement, please do not purchase or use the membership service.</p>
<h2>1. Definitions</h2>
<ol>
<li><strong>Operator</strong>: refers to Shenzhen Liangchuang Times Technology Co., Ltd. ("Liangchuang"), the developer and operator of the App and the membership service.</li>
<li><strong>Membership Service</strong>: refers to the paid member benefits provided by the Operator, including but not limited to higher quotas for voice transcription, voice translation and AI assistant usage, exclusive features, priority customer support, and so on. The actual benefits are subject to the purchase page, your order, and any in-app description.</li>
<li><strong>User</strong>: refers to the natural person or duly authorized entity who has registered to use the App and purchased the membership service.</li>
</ol>
<h2>2. Service Content</h2>
<p>The specific content, term, and scope of the membership service are subject to the in-app display, the order confirmation, and any then-effective description. The Operator may, based on business planning, compliance requirements, or technical conditions, optimize, adjust, or discontinue certain member benefits, and will notify you via in-app announcements, pop-ups, or other reasonable means. If any adjustment materially adversely affects benefits you have already paid for, the Operator will, to the extent permitted by law, provide reasonable compensation or alternative arrangements.</p>
<h2>3. Fees and Payment</h2>
<ol>
<li>The membership service is a paid service. Prices are displayed on the purchase page and may vary by promotion, region, or distribution channel.</li>
<li>When you pay through a third-party payment channel (such as the Apple App Store, Google Play, WeChat Pay, Alipay, etc.), you must also comply with the rules of the corresponding payment channel. Payments are completed by the partner third-party payment institutions, which may use biometric information for payment verification. Please review the user agreement and privacy policy of the relevant third party.</li>
<li>If your membership includes <strong>automatic renewal</strong>, the renewal amount and cycle will be presented to you before each renewal in accordance with platform rules or applicable law. You may cancel automatic renewal at any time via the relevant platform (such as Apple App Store subscription management, Google Play subscription management) or via in-app instructions. After cancellation, your benefits within the current membership term will not be affected; no further charges will be made after the term ends.</li>
</ol>
<h2>4. Membership Account</h2>
<ol>
<li>The membership service is bound to your Voitrans account, which can be registered via mobile phone number, email, or WeChat authorization.</li>
<li>Member benefits are for the account holder's personal use only and may not be transferred, gifted, lent, rented, sold, or otherwise made available to any third party.</li>
<li>You are responsible for safeguarding your account and password. Any loss caused by leakage of your account credentials shall be borne by you.</li>
</ol>
<h2>5. Acceptable Use</h2>
<p>You shall use the membership service lawfully and in good faith, and shall not use it for any of the following:</p>
<ol>
<li>Any act that violates laws or regulations or infringes the lawful rights of others;</li>
<li>Reverse engineering, decompiling, disassembling, or otherwise attempting to obtain the source code of the App;</li>
<li>Any act that endangers network security;</li>
<li>Bypassing the usage limits of the membership service or unlawfully obtaining member benefits through technical means;</li>
<li>Using the membership service to send bulk advertising or spam information;</li>
<li>Any other act that violates the App's User Agreement and related rules.</li>
</ol>
<p>The Operator may, in accordance with this Agreement and related rules, take measures such as restricting features, suspending services, or terminating membership in response to violations.</p>
<h2>6. Refunds and Termination</h2>
<ol>
<li>Once a digital service has been activated, no-reason refunds are generally not supported, except as required by laws and regulations, by the relevant app store policies, or as separately committed by the Operator. If you experience issues such as system failure or duplicate charges that are not caused by you, you may contact customer support via [Settings] - [User Feedback] in the App for verification and handling.</li>
<li>If your account or service is restricted or terminated due to your breach of this Agreement or related rules, fees already paid will not be refunded.</li>
<li>After your membership expires or is terminated, you will no longer be entitled to member benefits. The retention period of membership-related data is subject to the App's Privacy Policy.</li>
<li>If you delete your Voitrans account, the membership service will be terminated at the same time. Unused member benefits cannot be restored, and fees already paid will not be refunded (unless otherwise required by law).</li>
</ol>
<h2>7. Privacy</h2>
<p>We value the protection of your personal information. The manner and scope in which we collect and use your personal information during the membership service are subject to the Voitrans Privacy Policy. Voice data, transcripts, translation records, and other data generated while using the membership service will be protected and processed in accordance with that Privacy Policy.</p>
<h2>8. Disclaimer and Limitation of Liability</h2>
<ol>
<li>The membership service depends on networks, devices, and third-party services (including but not limited to third-party AI model services and speech recognition services). The Operator will use commercially reasonable efforts to maintain stable service but is not liable for service interruption or data loss caused by force majeure, network failures, third parties, or other causes outside the Operator's reasonable control.</li>
<li>The output of features such as voice transcription, voice translation, and the AI assistant is provided for reference only. The Operator does not guarantee its accuracy, completeness, or timeliness, and is not responsible for any action taken by you based on such output.</li>
<li>To the maximum extent permitted by law, the Operator's total liability arising out of or in connection with the membership service shall not exceed the amount you actually paid for the relevant order.</li>
</ol>
<h2>9. Changes to this Agreement</h2>
<p>The Operator may revise this Agreement to reflect changes in laws, regulations, or business needs. Revisions will be notified via in-app announcements or other appropriate means. If you continue to use the membership service after the revision, you are deemed to accept the revised Agreement; if you do not agree, please stop using the service and may, where permitted, cancel automatic renewal.</p>
<h2>10. Governing Law and Dispute Resolution</h2>
<p>The conclusion, validity, interpretation, and dispute resolution of this Agreement shall be governed by the laws of the mainland of the People's Republic of China. Any dispute arising out of this Agreement shall first be resolved through friendly negotiation. If negotiation fails, either party may file a lawsuit with a court of competent jurisdiction at the Operator's location (Shenzhen).</p>
<h2>11. Miscellaneous</h2>
<ol>
<li>This Agreement constitutes the complete agreement between you and the Operator regarding the membership service, and applies together with the App's User Agreement and Privacy Policy. In the event of a conflict between this Agreement and the User Agreement on matters concerning the membership service, this Agreement shall prevail.</li>
<li>If any provision of this Agreement is held invalid or unenforceable, the validity of the remaining provisions shall not be affected.</li>
<li>The headings in this Agreement are for convenience only and do not affect the meaning or interpretation of any clause.</li>
</ol>
<h2>12. Contact Us</h2>
<p>If you have any questions about this Agreement or the membership service, you may contact us via:<br />
In-app feedback: Voitrans APP [Settings] - [User Feedback]<br />
Support email: tech@voitrans.com</p>
</body>
</html>

97
doc/zh_cn/member_service_agreement.html

@ -0,0 +1,97 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Voitrans会员服务协议</title>
<style>
:root { color-scheme: light dark; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Microsoft YaHei", sans-serif; line-height: 1.65; max-width: 720px; margin: 0 auto; padding: 24px 16px 48px; color: #1a1a1a; }
@media (prefers-color-scheme: dark) { body { background: #111; color: #eee; } a { color: #8ab4f8; } }
h1 { font-size: 1.35rem; margin: 0 0 0.5em; }
.meta { font-size: 0.85rem; color: #666; margin-bottom: 1.5em; }
h2 { font-size: 1.05rem; margin: 1.4em 0 0.5em; }
p, li { font-size: 0.95rem; margin: 0.5em 0; }
ol { padding-left: 1.25em; }
.note { font-size: 0.85rem; color: #666; margin-top: 2em; padding-top: 1em; border-top: 1px solid #ddd; }
</style>
</head>
<body>
<h1>Voitrans会员服务协议</h1>
<p class="meta">更新日期:2026年4月22日<br />生效日期:2026年4月22日</p>
<p>欢迎您使用Voitrans应用程序(下称"本应用")提供的会员服务。请您在购买、订阅或使用会员服务前,仔细阅读并充分理解本协议全部条款,特别是涉及费用、自动续费、退款、责任限制及争议解决的条款。您点击同意、完成支付或以其他方式接受会员服务的,即视为您已阅读并同意受本协议约束。</p>
<p>若您不同意本协议,请勿购买或使用会员服务。</p>
<h2>一、定义</h2>
<ol>
<li><strong>运营方</strong>:指深圳市良闯时代科技有限公司(下称"良闯"),是本应用及会员服务的开发者和运营者。</li>
<li><strong>会员服务</strong>:指运营方向用户提供的付费会员权益,包括但不限于更高的语音转写时长额度、语音翻译时长额度、AI智能助手使用额度、专属功能、优先客服等,具体以购买页面、订单及本应用内说明为准。</li>
<li><strong>用户</strong>:指注册并使用本应用,且购买会员服务的自然人或经合法授权的主体。</li>
</ol>
<h2>二、服务内容</h2>
<p>会员服务的具体内容、有效期限、使用范围以本应用内展示、订单确认信息及届时有效的说明为准。运营方有权根据业务规划、合规要求、技术条件等对会员权益进行优化、调整或下线部分功能,并将通过应用内公告、弹窗或其他合理方式提示;若调整对您已付费权益产生重大不利影响的,运营方将在法律允许的范围内提供合理补偿或替代方案。</p>
<h2>三、费用与支付</h2>
<ol>
<li>会员服务为收费服务,价格以购买页面展示为准,可能因营销活动、地区或渠道不同而存在差异。</li>
<li>您通过第三方支付渠道(如Apple App Store、Google Play、微信支付、支付宝等)完成支付的,应同时遵守相应支付渠道的规则。支付通过合作的第三方支付机构提供的支付服务完成,第三方支付服务有可能使用生物识别信息用于付款验证,请阅读该第三方机构的用户协议及隐私政策。</li>
<li>若会员包含<strong>自动续费</strong>,我们将在续费前按平台规则或法律规定向您展示续费金额与周期;您可在相应平台(如Apple App Store的订阅管理、Google Play的订阅管理)或本应用内按指引关闭自动续费。关闭自动续费后,当前会员有效期内的权益不受影响,到期后将不再自动扣费。</li>
</ol>
<h2>四、会员账号</h2>
<ol>
<li>会员服务与您的Voitrans账号绑定,您可通过手机号、电子邮箱或微信授权方式注册的账号使用会员服务。</li>
<li>会员权益仅限账号持有人本人使用,不得转让、赠与、借用、租用、售卖或以其他方式提供给第三方使用。</li>
<li>您应妥善保管账号及密码信息,因账号信息泄露导致的损失由您自行承担。</li>
</ol>
<h2>五、使用规范</h2>
<p>您应合法、善意地使用会员服务,不得利用会员服务从事以下行为:</p>
<ol>
<li>违反法律法规、侵犯他人合法权益的行为;</li>
<li>对本应用进行反向工程、反编译、反汇编或其他试图获取源代码的行为;</li>
<li>利用会员服务从事任何危害网络安全的行为;</li>
<li>通过技术手段绕过会员服务的使用限制或非法获取会员权益;</li>
<li>利用会员服务批量发布、传送广告信息及垃圾信息;</li>
<li>其他违反本应用《用户协议》及相关规则的行为。</li>
</ol>
<p>运营方有权依据本协议及相关规则对违规行为采取限制功能、中止服务、终止会员资格等措施。</p>
<h2>六、退款与终止</h2>
<ol>
<li>数字服务一经开通,除法律法规规定、应用商店政策或运营方另行承诺的情形外,原则上不支持无理由退款。若因系统故障、重复扣费等非用户过错导致的问题,您可通过本应用【设置】-【用户反馈】联系客服核实处理。</li>
<li>因您违反本协议或相关规则导致账号或服务被限制、终止的,已支付费用不予退还。</li>
<li>会员到期或终止后,您将无法继续享有会员权益。账号下与会员相关的数据保存期限以本应用《隐私政策》的规定为准。</li>
<li>如您注销Voitrans账号,会员服务将同时终止,未使用的会员权益将无法恢复,已支付费用不予退还(法律法规另有规定的除外)。</li>
</ol>
<h2>七、隐私保护</h2>
<p>我们重视您的个人信息保护。在会员服务过程中,我们收集和使用您个人信息的方式和范围,以本应用《Voitrans隐私政策》为准。您使用会员服务时所产生的语音信息、转写文稿、翻译记录等数据,我们将按照隐私政策的规定进行保护和处理。</p>
<h2>八、免责声明与责任限制</h2>
<ol>
<li>会员服务依赖网络、设备、第三方服务(包括但不限于第三方AI模型服务、语音识别服务等)等因素,运营方将尽力保障服务稳定,但不对因不可抗力、网络故障、第三方原因或非运营方可控原因导致的服务中断、数据丢失承担责任。</li>
<li>语音转写、语音翻译、AI智能助手等功能的输出结果仅供参考,运营方不保证其准确性、完整性和及时性,不对用户基于上述结果做出的任何行为承担责任。</li>
<li>在法律允许的范围内,运营方对会员服务所承担的赔偿责任以您就该笔订单实际支付的金额为上限。</li>
</ol>
<h2>九、协议变更</h2>
<p>运营方可根据法律法规变化及业务需要修订本协议。修订后将通过应用内公告或其他适当方式通知;若您继续使用会员服务,即视为接受修订后的协议;若您不同意,应停止使用并可在规则允许范围内申请关闭自动续费。</p>
<h2>十、适用法律与争议解决</h2>
<p>本协议的订立、效力、解释及争议解决均适用中华人民共和国大陆地区法律。因本协议引起的争议,双方应友好协商;协商不成的,任何一方均可向运营方所在地(深圳市)有管辖权的人民法院提起诉讼。</p>
<h2>十一、其他</h2>
<ol>
<li>本协议构成您与运营方之间关于会员服务的完整协议,与本应用《用户协议》《隐私政策》共同适用。如本协议与《用户协议》存在冲突,就会员服务相关事项以本协议为准。</li>
<li>本协议中任何条款被认定为无效或不可执行的,不影响其他条款的效力。</li>
<li>本协议的标题仅为方便阅读而设,不影响条款的含义或解释。</li>
</ol>
<h2>十二、联系我们</h2>
<p>如您对本协议或会员服务有任何疑问,可通过以下方式联系:<br />
应用内反馈:Voitrans APP【设置】-【用户反馈】<br />
客服邮箱:tech@voitrans.com</p>
</body>
</html>

7
lib/core/bindings/initial_binding.dart

@ -14,6 +14,7 @@ import '../../data/services/meeting/meeting_upload_service.dart';
import '../../data/services/novice_guide_service.dart';
import '../../data/services/speech_factory.dart';
import '../../data/services/volcano_translation_service.dart';
import '../../data/services/microsoft_translation_service.dart';
import '../../data/services/volcano_ai_service.dart';
import '../../data/services/language_manager.dart';
import '../../core/utils/logger.dart';
@ -43,9 +44,13 @@ class InitialBinding extends Bindings {
// 注册 AST 服务
Get.lazyPut<AstService>(() => AzureAstService(), fenix: true);
// 火山翻译服务
// 火山翻译服务(图片翻译 / 文本翻译模块仍在使用)
Get.lazyPut<VolcanoTranslationService>(() => VolcanoTranslationService(),
fenix: true);
// 微软翻译服务(同声翻译 / 面对面翻译 / 音视频翻译 使用)
Get.lazyPut<MicrosoftTranslationService>(
() => MicrosoftTranslationService(),
fenix: true);
// 蓝牙服务
Get.lazyPut<BluetoothManager>(() => BluetoothManager(), fenix: true);

3
lib/core/services/floating_window_manager.dart

@ -165,9 +165,8 @@ class FloatingWindowManager extends GetxController {
try {
await _api.updateContent(data.toMap());
Logger.info('悬浮窗内容已更新');
} catch (e) {
Logger.error('更新悬浮窗内容失败: $e');
Logger.e('FloatingWindow', '更新悬浮窗内容失败: $e');
}
}

6
lib/core/translations/language/en_us.dart

@ -114,6 +114,12 @@ const Map<String, String> enUS = {
". By clicking \"Agree and Enter\", you indicate that you have fully read and agree to the above agreements. Clicking \"Disagree\" will exit the application.", // ,点击"同意并进入"表示您已充分阅读并同意上述协议。点击"不同意"将退出应用。
"disagree": "Disagree", // 不同意
"agreeAndEnter": "Agree and Enter", // 同意并进入
"agreeAndContinue": "Agree and Continue",
"memberServiceAgreement": "Membership Service Agreement",
"memberServiceAgreementTitle": "Membership Service Agreement",
"memberServiceAgreementIntro": "To provide you with better membership services, please read carefully before subscribing: ",
"memberServiceAgreementOutro": ". By clicking \"Agree and Continue\", you confirm that you have fully read and accept the above agreement.",
"memberAgreementRejectedTip": "You must agree to the Membership Service Agreement to continue.",
// 首页
"deviceStatus": "Device Status",

6
lib/core/translations/language/zh_cn.dart

@ -110,6 +110,12 @@ const Map<String, String> zhCN = {
",点击'同意并进入'表示您已充分阅读并同意上述协议。点击'不同意'将退出应用。", // ,点击"同意并进入"表示您已充分阅读并同意上述协议。点击"不同意"将退出应用。
"disagree": "不同意", // 不同意
"agreeAndEnter": "同意并进入", // 同意并进入
"agreeAndContinue": "同意并继续",
"memberServiceAgreement": "《会员服务协议》",
"memberServiceAgreementTitle": "会员服务协议",
"memberServiceAgreementIntro": "为给您提供更优质的会员服务,请您在订阅会员前仔细阅读",
"memberServiceAgreementOutro": "。点击\"同意并继续\"表示您已充分阅读并同意上述协议。",
"memberAgreementRejectedTip": "需同意《会员服务协议》后才能继续支付",
// 首页
"deviceStatus": "设备状态",

6
lib/core/translations/language/zh_hk.dart

@ -110,6 +110,12 @@ const Map<String, String> zhHK = {
"點擊'同意並進入'表示您已充分閱讀並同意上述協議。點擊'不同意'將退出應用。", // ,点击"同意并进入"表示您已充分阅读并同意上述协议。点击"不同意"将退出应用。
"disagree": "不同意", // 不同意
"agreeAndEnter": "同意並進入", // 同意并进入
"agreeAndContinue": "同意並繼續",
"memberServiceAgreement": "《會員服務協議》",
"memberServiceAgreementTitle": "會員服務協議",
"memberServiceAgreementIntro": "為向您提供更優質的會員服務,請您在訂閱會員前仔細閱讀",
"memberServiceAgreementOutro": "。點擊\"同意並繼續\"表示您已充分閱讀並同意上述協議。",
"memberAgreementRejectedTip": "需同意《會員服務協議》後才能繼續支付",
// 首页
"deviceStatus": "設備狀態",

6
lib/core/translations/language/zh_tw.dart

@ -110,6 +110,12 @@ const Map<String, String> zhTW = {
"點擊'同意並進入'表示您已充分閱讀並同意上述協議。點擊'不同意'將退出應用。", // ,点击"同意并进入"表示您已充分阅读并同意上述协议。点击"不同意"将退出应用。
"disagree": "不同意", // 不同意
"agreeAndEnter": "同意並進入", // 同意并进入
"agreeAndContinue": "同意並繼續",
"memberServiceAgreement": "《會員服務協議》",
"memberServiceAgreementTitle": "會員服務協議",
"memberServiceAgreementIntro": "為向您提供更優質的會員服務,請您在訂閱會員前仔細閱讀",
"memberServiceAgreementOutro": "。點擊\"同意並繼續\"表示您已充分閱讀並同意上述協議。",
"memberAgreementRejectedTip": "需同意《會員服務協議》後才能繼續支付",
// 首页
"deviceStatus": "設備狀態",

2
lib/core/utils/app_lifecycle_manager.dart

@ -79,7 +79,7 @@ class AppLifecycleManager extends GetxService with WidgetsBindingObserver {
_lastCanWakeUp = canWakeUp;
try {
Logger.info('lxm---设置是否可以ble唤醒agent: $canWakeUp');
Logger.i('AppLifecycle', '设置是否可以ble唤醒agent: $canWakeUp');
await AgentService.setCanWakeUp(canWakeUp);
} catch (e) {
Logger.warning('setCanWakeUp失败: $e');

30
lib/core/utils/doc_url_helper.dart

@ -1,4 +1,5 @@
import 'package:get/get.dart';
import '../../data/services/network/dio_manager.dart';
/// 根据当前语言返回 doc 目录名,如 "zh_cn"、"en"、"fr"
String docLangPath() {
@ -18,10 +19,27 @@ String docLangPath() {
return supported.contains(lang) ? lang : 'en';
}
/// 构建隐私政策 URL
String privacyPolicyUrl() =>
'https://www.voitrans.net/doc/${docLangPath()}/privacy_policy.html';
/// 业务服务器上的法律文档目录前缀(按 docLangPath() 完整语言命中)
String _legalDocBase() => '${DioManager().baseUrl}/docs/legal/${docLangPath()}';
/// 构建用户协议 URL
String userAgreementUrl() =>
'https://www.voitrans.net/doc/${docLangPath()}/user_agreement.html';
/// 会员协议仅有 zh_cn / zh_hk / zh_tw / en 四种文件,其余语言一律回退英文
String _memberAgreementLangDir() {
final lang = Get.locale?.languageCode ?? 'en';
if (lang == 'zh') {
final country = Get.locale?.countryCode?.toUpperCase() ?? '';
if (country == 'CN') return 'zh_cn';
if (country == 'HK') return 'zh_hk';
return 'zh_tw';
}
return 'en';
}
/// 构建隐私政策 URL(托管在业务服务器上)
String privacyPolicyUrl() => '${_legalDocBase()}/privacy_policy.html';
/// 构建用户协议 URL(托管在业务服务器上)
String userAgreementUrl() => '${_legalDocBase()}/user_agreement.html';
/// 构建会员服务协议 URL(托管在业务服务器上)
String memberServiceAgreementUrl() =>
'${DioManager().baseUrl}/docs/legal/${_memberAgreementLangDir()}/member_service_agreement.html';

270
lib/core/utils/logger.dart

@ -1,6 +1,7 @@
import 'dart:developer' as developer;
import 'dart:io';
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';
import 'package:intl/intl.dart';
@ -14,10 +15,19 @@ enum LogLevel {
/// 日志工具类
///
/// 提供统一的日志记录接口,支持控制台输出和文件存储
/// 特性:
/// - 写日志只入内存队列,由后台 Timer 定时批量刷盘,避免主 Isolate 同步 I/O。
/// - Release 包默认 [LogLevel.warning],Debug 包默认 [LogLevel.debug]。
/// - `error` 级别写入后强制立即 flush,崩溃前关键错误不丢。
/// - 支持 `dLazy/iLazy/wLazy` 闭包式 API,级别被过滤时完全不执行字符串插值。
/// - 时间戳格式化按秒缓存,避免每条日志重复 `DateFormat`。
class Logger {
/// 当前日志级别,低于此级别的日志不会被输出
static LogLevel _currentLevel = LogLevel.debug;
/// 当前日志级别
static LogLevel _currentLevel =
kReleaseMode ? LogLevel.warning : LogLevel.debug;
/// 控制台输出开关(Release 默认关闭以减少 developer.log 开销)
static bool _consoleEnabled = !kReleaseMode;
/// 默认标签
static const String _defaultTag = 'App';
@ -27,30 +37,44 @@ class Logger {
static File? _currentLogFile;
static bool _fileLogEnabled = false;
static String? _logDirPath;
static final DateFormat _dateFormat = DateFormat('yyyy-MM-dd HH:mm:ss.SSS');
static final DateFormat _fileDateFormat = DateFormat('yyyy-MM-dd');
/// 单行最大字符数,超过则截断(避免日志被过长字符串拖慢)
static const int _maxLineLength = 4000;
/// 最大单个日志文件大小 (2MB)
static const int _maxFileSize = 2 * 1024 * 1024;
/// 最多保留日志文件数量
static const int _maxLogFiles = 7;
/// 写入计数器,用于节流轮转检查和刷盘
static int _writeCount = 0;
/// 内存队列:写日志只入队,由定时器批量落盘
static final List<String> _queue = <String>[];
/// 队列上限。超过后丢弃最旧条目(保留最新),防止内存无限增长。
static const int _maxQueueSize = 5000;
/// 轮转检查间隔(每 100 次写入检查一次)
static const int _rotationCheckInterval = 100;
/// 定时刷盘间隔
static const Duration _flushInterval = Duration(milliseconds: 500);
static Timer? _flushTimer;
/// 刷盘间隔(每 50 次写入刷一次)
static const int _flushInterval = 50;
/// 轮转检查间隔
static const Duration _rotationInterval = Duration(seconds: 30);
static Timer? _rotationTimer;
/// 轮转锁,防止并发轮转
static bool _isRotating = false;
/// 当前日志文件的日期标识,用于快速判断是否需要换天
/// 正在刷盘标记,防止定时器重入
static bool _isFlushing = false;
/// 当前日志文件的日期标识
static String _currentFileDate = '';
/// 时间戳缓存:同一秒内复用
static int _cachedTsEpochSecond = 0;
static String _cachedTsString = '';
/// 初始化文件日志
static Future<void> initFileLog() async {
try {
@ -62,7 +86,13 @@ class Logger {
}
await _openLogFile();
_fileLogEnabled = true;
Logger.i('Logger', '文件日志初始化成功: $_logDirPath');
// 启动后台刷盘与轮转定时器
_flushTimer ??= Timer.periodic(_flushInterval, (_) => _drainQueue());
_rotationTimer ??=
Timer.periodic(_rotationInterval, (_) => _checkRotation());
Logger.i('Logger', '文件日志初始化成功: $_logDirPath level=$_currentLevel');
} catch (e) {
developer.log('[Logger] 文件日志初始化失败: $e');
}
@ -73,24 +103,47 @@ class Logger {
final today = _fileDateFormat.format(DateTime.now());
final filePath = '$_logDirPath/app_$today.log';
_currentLogFile = File(filePath);
// 先关闭旧 sink,必须 await 确保关闭完成
await _logSink?.flush();
await _logSink?.close();
_logSink = _currentLogFile!.openWrite(mode: FileMode.append);
_currentFileDate = today;
}
/// 检查日志文件是否需要轮转(受节流和锁保护)
/// 把队列里的所有条目一次性写到 sink
static Future<void> _drainQueue() async {
if (_isFlushing) return;
if (!_fileLogEnabled || _logSink == null) return;
if (_queue.isEmpty) return;
_isFlushing = true;
try {
// O(1) 取出全部
final batch = List<String>.unmodifiable(_queue);
_queue.clear();
// 单次 write 减少 syscall 次数
_logSink!.write(batch.join('\n'));
_logSink!.write('\n');
await _logSink!.flush();
} catch (e) {
developer.log('[Logger] drain 失败: $e');
} finally {
_isFlushing = false;
}
}
/// 检查日志文件是否需要轮转(后台定时调度,不在写路径执行)
static Future<void> _checkRotation() async {
if (_isRotating || _currentLogFile == null) return;
_isRotating = true;
try {
// 检查日期是否变更
// 先把队列里的内容落盘,以免轮转后丢
await _drainQueue();
// 日期变更 → 换新文件
final today = _fileDateFormat.format(DateTime.now());
if (_currentFileDate != today) {
await _openLogFile();
}
// 检查文件大小
// 文件过大 → 切分
if (await _currentLogFile!.exists()) {
final size = await _currentLogFile!.length();
if (size > _maxFileSize) {
@ -104,9 +157,9 @@ class Logger {
_logSink = _currentLogFile!.openWrite(mode: FileMode.append);
}
}
// 清理旧日志文件
await _cleanOldLogs();
} catch (_) {} finally {
} catch (_) {
} finally {
_isRotating = false;
}
}
@ -131,40 +184,38 @@ class Logger {
} catch (_) {}
}
/// 单行最大字符数,超过则分批写入
static const int _maxLineLength = 4000;
/// 写入日志到文件
static void _writeToFile(String level, String tag, String message) {
if (!_fileLogEnabled || _logSink == null) return;
try {
final timestamp = _dateFormat.format(DateTime.now());
if (message.length <= _maxLineLength) {
_logSink!.writeln('$timestamp [$level][$tag] $message');
} else {
final totalParts = (message.length + _maxLineLength - 1) ~/ _maxLineLength;
for (var i = 0; i < totalParts; i++) {
final start = i * _maxLineLength;
final end = start + _maxLineLength > message.length ? message.length : start + _maxLineLength;
final chunk = message.substring(start, end);
_logSink!.writeln('$timestamp [$level][$tag] [${i + 1}/$totalParts] $chunk');
}
}
_writeCount++;
// 定期刷盘,确保数据写入磁盘
if (_writeCount % _flushInterval == 0) {
_logSink!.flush();
}
/// 快速时间戳(到秒级缓存 + 毫秒拼接)
static String _formatTimestamp(DateTime now) {
final epochSec = now.millisecondsSinceEpoch ~/ 1000;
if (epochSec != _cachedTsEpochSecond) {
_cachedTsEpochSecond = epochSec;
// yyyy-MM-dd HH:mm:ss
final y = now.year.toString().padLeft(4, '0');
final mo = now.month.toString().padLeft(2, '0');
final d = now.day.toString().padLeft(2, '0');
final h = now.hour.toString().padLeft(2, '0');
final mi = now.minute.toString().padLeft(2, '0');
final s = now.second.toString().padLeft(2, '0');
_cachedTsString = '$y-$mo-$d $h:$mi:$s';
}
final ms = now.millisecond.toString().padLeft(3, '0');
return '$_cachedTsString.$ms';
}
// 定期检查轮转(节流,不是每次写入都检查)
if (_writeCount % _rotationCheckInterval == 0) {
_checkRotation();
}
} catch (e) {
// 文件写入失败不应影响业务逻辑(尤其是 Dio 拦截器链)
developer.log('[Logger] 文件写入失败: $e');
/// 入队(不触发 I/O)
static void _enqueue(String level, String tag, String message) {
if (!_fileLogEnabled) return;
final ts = _formatTimestamp(DateTime.now());
String msg = message;
if (msg.length > _maxLineLength) {
msg =
'${msg.substring(0, _maxLineLength)}… (truncated ${message.length - _maxLineLength}字符)';
}
// 溢出保护:保留最新
if (_queue.length >= _maxQueueSize) {
_queue.removeRange(0, _queue.length - _maxQueueSize + 1);
}
_queue.add('$ts [$level][$tag] $msg');
}
/// 获取日志目录路径
@ -180,12 +231,13 @@ class Logger {
.where((e) => e is File && e.path.endsWith('.log'))
.cast<File>()
.toList();
files.sort((a, b) => b.path.compareTo(a.path)); // 最新在前
files.sort((a, b) => b.path.compareTo(a.path));
return files;
}
/// 刷新缓冲区(导出前调用)
/// 强制把队列刷到磁盘(导出前调用)
static Future<void> flush() async {
await _drainQueue();
await _logSink?.flush();
}
@ -194,86 +246,114 @@ class Logger {
_currentLevel = level;
}
/// 输出调试日志 (不带标签版本)
/// 开关控制台输出(默认 Release 关闭)
static void setConsoleEnabled(bool enabled) {
_consoleEnabled = enabled;
}
/// 当前级别是否允许某级别输出
static bool isLoggable(LogLevel level) =>
level.index >= _currentLevel.index;
// ========== 普通 API ==========
static void debug(String message) {
if (_currentLevel.index <= LogLevel.debug.index) {
_log('DEBUG', _defaultTag, message);
if (LogLevel.debug.index >= _currentLevel.index) {
_log('DEBUG', _defaultTag, message, false);
}
}
/// 输出调试日志
static void d(String tag, String message) {
if (_currentLevel.index <= LogLevel.debug.index) {
_log('DEBUG', tag, message);
if (LogLevel.debug.index >= _currentLevel.index) {
_log('DEBUG', tag, message, false);
}
}
/// 输出信息日志 (不带标签版本)
static void info(String message) {
if (_currentLevel.index <= LogLevel.info.index) {
_log('INFO', _defaultTag, message);
if (LogLevel.info.index >= _currentLevel.index) {
_log('INFO', _defaultTag, message, false);
}
}
/// 输出信息日志
static void i(String tag, String message) {
if (_currentLevel.index <= LogLevel.info.index) {
_log('INFO', tag, message);
if (LogLevel.info.index >= _currentLevel.index) {
_log('INFO', tag, message, false);
}
}
/// 输出警告日志 (不带标签版本)
static void warning(String message) {
if (_currentLevel.index <= LogLevel.warning.index) {
_log('WARNING', _defaultTag, message);
if (LogLevel.warning.index >= _currentLevel.index) {
_log('WARN', _defaultTag, message, false);
}
}
/// 输出警告日志
static void w(String tag, String message) {
if (_currentLevel.index <= LogLevel.warning.index) {
_log('WARNING', tag, message);
if (LogLevel.warning.index >= _currentLevel.index) {
_log('WARN', tag, message, false);
}
}
/// 输出错误日志 (不带标签版本)
static void error(String message, [dynamic error, StackTrace? stackTrace]) {
if (_currentLevel.index <= LogLevel.error.index) {
_log('ERROR', _defaultTag, message);
if (error != null) {
_log('ERROR', _defaultTag, 'Original error: $error');
}
if (LogLevel.error.index >= _currentLevel.index) {
_log('ERROR', _defaultTag, message, true);
if (error != null) _log('ERROR', _defaultTag, 'Original error: $error', true);
if (stackTrace != null) {
_log('ERROR', _defaultTag, 'Stack trace: $stackTrace');
_log('ERROR', _defaultTag, 'Stack trace: $stackTrace', true);
}
}
}
/// 输出错误日志
static void e(String tag, String message, [dynamic error, StackTrace? stackTrace]) {
if (_currentLevel.index <= LogLevel.error.index) {
_log('ERROR', tag, message);
if (error != null) {
_log('ERROR', tag, 'Original error: $error');
}
static void e(String tag, String message,
[dynamic error, StackTrace? stackTrace]) {
if (LogLevel.error.index >= _currentLevel.index) {
_log('ERROR', tag, message, true);
if (error != null) _log('ERROR', tag, 'Original error: $error', true);
if (stackTrace != null) {
_log('ERROR', tag, 'Stack trace: $stackTrace');
_log('ERROR', tag, 'Stack trace: $stackTrace', true);
}
}
}
// ========== 闭包式 API(高频热点推荐) ==========
/// Debug 级别懒求值:级别未放行时连字符串都不会构造。
static void dLazy(String tag, String Function() messageBuilder) {
if (LogLevel.debug.index >= _currentLevel.index) {
_log('DEBUG', tag, messageBuilder(), false);
}
}
static void iLazy(String tag, String Function() messageBuilder) {
if (LogLevel.info.index >= _currentLevel.index) {
_log('INFO', tag, messageBuilder(), false);
}
}
static void wLazy(String tag, String Function() messageBuilder) {
if (LogLevel.warning.index >= _currentLevel.index) {
_log('WARN', tag, messageBuilder(), false);
}
}
/// 内部日志输出方法
static void _log(String level, String tag, String message) {
if (message.length <= _maxLineLength) {
developer.log('[$tag]: $message');
} else {
final totalParts = (message.length + _maxLineLength - 1) ~/ _maxLineLength;
for (var i = 0; i < totalParts; i++) {
final start = i * _maxLineLength;
final end = start + _maxLineLength > message.length ? message.length : start + _maxLineLength;
developer.log('[$tag]: [${i + 1}/$totalParts] ${message.substring(start, end)}');
///
/// [forceFlush] = true 时(error 级别)立即触发一次 drain,
/// 保证崩溃前关键错误一定落盘。
static void _log(String level, String tag, String message, bool forceFlush) {
if (_consoleEnabled) {
if (message.length <= _maxLineLength) {
developer.log('[$tag]: $message');
} else {
developer.log(
'[$tag]: ${message.substring(0, _maxLineLength)}… (+${message.length - _maxLineLength}字符)');
}
}
_writeToFile(level, tag, message);
_enqueue(level, tag, message);
if (forceFlush) {
// fire-and-forget;_drainQueue 自带并发保护
// 不 await 以免阻塞调用方
// ignore: discarded_futures
_drainQueue();
}
}
}

3
lib/data/services/asr_service.dart

@ -27,10 +27,13 @@ abstract class AsrService {
/// 开始连续语音识别
///
/// 返回一个包含识别事件的流
/// [skipAsrForCallMode] true 时仅占位,不真正启动本地 ASR(用于通话翻译 call 模式:
/// AST 端到端服务已自行处理识别+翻译+TTS)。多媒体翻译等 external 场景保持 false。
Future<bool> startContinuousRecognition(
bool audioSourceType, {
bool isRemoveFirstPunctuation = true,
String mode = "normal",
bool skipAsrForCallMode = false,
});
/// 停止连续语音识别

327
lib/data/services/ble_manager.dart

@ -259,180 +259,119 @@ class BleManager extends GetxService {
/// 设置BLE事件监听
void _setupBleListener() {
//Logger.info('lxm--_setupBleListener被调用,开始设置BLE事件监听');
_statusSubscription?.cancel();
_dataSubscription?.cancel();
try {
Logger.info('lxm--开始监听BLE状态更新流');
// 监听BLE状态更新
_statusSubscription = _bleService.onStatusUpdated.listen(
(event) {
Logger.info('lxm--收到BLE状态更新事件: $event');
if (event is Map) {
Logger.info('lxm--事件是Map类型,开始处理');
// 处理连接状态更新
if (event['type'] == 'connectionStateChanged') {
Logger.info('lxm--处理连接状态变化事件');
final state = event['state'] as int;
final oldState = _connectionState.value;
_connectionState.value = state;
Logger.d('BleManager', '🔄 BLE状态更新: $oldState → $state');
Logger.d('BleManager', '📡 通知监听器状态变化: ${_connectionState.value}');
// 连接状态转换
if (state == BleConnectionState.connected) {
Logger.info('lxm--BLE设备已连接');
_isConnected.value = true;
// 设备信息(含唤醒状态)由 HomeController.fetchDeviceInfo() 统一查询
} else if (state == BleConnectionState.disconnected ||
state == BleConnectionState.error) {
Logger.info('lxm--BLE设备已断开或发生错误');
_isConnected.value = false;
_awaitingCodecOpen = false;
_awaitingCodecClose = false;
_isCodecActive.value = false;
_voiceWakeupSupported.value = false;
_voiceWakeupEnabled.value = true;
Logger.info('[VoiceWakeup] 断开连接,重置 supported=false, enabled=true');
Logger.info('lxm--断开连接时重置设备信息为0');
_updateDeviceInfoIfNeeded(
batteryLevel: 0,
leftEarBattery: 0,
rightEarBattery: 0,
caseBattery: 0,
leftEarCharging: false,
rightEarCharging: false,
caseCharging: false,
firmwareVersion: '',
firmwareProductId: '',
firmwareProductColor: '',
wakeupLeftVersion: '',
wakeupRightVersion: '',
);
// 断开连接时不清除设备信息,保留最后的电量信息
// 只有在需要完全清除设备时才重置信息
}
}
if (event['type'] == 'wakeupVoiceUpgradeProgress') {
// 唤醒语音升级进度
Logger.info('lxm--处理唤醒语音升级进度事件: $event');
_wakeupVoiceUpgradeProgress.value =
event['progress'] as int? ?? 0;
_wakeupVoiceUpgradeStatus.value = 1; // 升级中
Logger.info(
'lxm--唤醒语音升级进度: ${_wakeupVoiceUpgradeProgress.value}%');
}
if (event['type'] == 'wakeupVoiceUpgradeSuccess') {
// 唤醒语音升级状态
Logger.info('lxm--处理唤醒语音升级状态事件: $event');
_wakeupVoiceUpgradeStatus.value = 2; // 升级成功
Logger.info('lxm--唤醒语音升级状态: ${_wakeupVoiceUpgradeStatus.value}');
if (event is! Map) return;
if (event['type'] == 'connectionStateChanged') {
final state = event['state'] as int;
final oldState = _connectionState.value;
_connectionState.value = state;
Logger.i('BleManager', 'BLE状态更新: $oldState → $state');
if (state == BleConnectionState.connected) {
_isConnected.value = true;
} else if (state == BleConnectionState.disconnected ||
state == BleConnectionState.error) {
_isConnected.value = false;
_awaitingCodecOpen = false;
_awaitingCodecClose = false;
_isCodecActive.value = false;
_voiceWakeupSupported.value = false;
_voiceWakeupEnabled.value = true;
Logger.i('BleManager',
'[VoiceWakeup] 断开连接,重置 supported=false, enabled=true');
_updateDeviceInfoIfNeeded(
batteryLevel: 0,
leftEarBattery: 0,
rightEarBattery: 0,
caseBattery: 0,
leftEarCharging: false,
rightEarCharging: false,
caseCharging: false,
firmwareVersion: '',
firmwareProductId: '',
firmwareProductColor: '',
wakeupLeftVersion: '',
wakeupRightVersion: '',
);
}
}
if (event['type'] == 'wakeupVoiceUpgradeFailure') {
// 唤醒语音升级状态
Logger.info('lxm--处理唤醒语音升级状态事件: $event');
_wakeupVoiceUpgradeStatus.value = 3; // 升级失败
Logger.info('lxm--唤醒语音升级状态: ${_wakeupVoiceUpgradeStatus.value}');
}
if (event['type'] == 'wakeupVoiceUpgradeProgress') {
_wakeupVoiceUpgradeProgress.value = event['progress'] as int? ?? 0;
_wakeupVoiceUpgradeStatus.value = 1;
}
if (event['type'] == 'wakeupVoiceUpgradeSuccess') {
_wakeupVoiceUpgradeStatus.value = 2;
Logger.i('BleManager', '唤醒语音升级成功');
}
Logger.info('lxm--准备调用_processDeviceInfo处理设备信息');
// 处理设备信息
_processDeviceInfo(event);
} else {
Logger.info('lxm--事件不是Map类型: ${event.runtimeType}');
if (event['type'] == 'wakeupVoiceUpgradeFailure') {
_wakeupVoiceUpgradeStatus.value = 3;
Logger.w('BleManager', '唤醒语音升级失败');
}
_processDeviceInfo(event);
},
onError: (error) {
Logger.error('lxm--BLE状态事件流错误: $error');
Logger.e('BleManager', 'BLE状态事件流错误: $error');
},
);
Logger.info('lxm--BLE状态监听器设置完成');
// 监听BLE数据流
} catch (e) {
Logger.error('lxm--设置BLE事件监听器失败: ${e.toString()}');
Logger.e('BleManager', '设置BLE事件监听器失败: ${e.toString()}');
}
}
/// 处理来自原生层的设备信息
void _processDeviceInfo(Map<dynamic, dynamic> event) {
try {
Logger.info('lxm--_processDeviceInfo被调用');
Logger.info('lxm--接收到的事件数据: $event');
// 检查是否是设备信息响应
if (event.containsKey('command') && event.containsKey('success')) {
Logger.info('lxm--事件包含command和success字段');
final bool success = event['success'] as bool;
Logger.info('lxm--命令执行成功状态: $success');
if (!success) {
Logger.warning('lxm--设备命令执行失败: ${event['command']}');
return;
}
if (!event.containsKey('command') || !event.containsKey('success')) {
return;
}
// 根据命令类型处理不同信息
final command = event['command'];
Logger.info('lxm--命令类型: $command (${command.runtimeType})');
if (command is int) {
Logger.info('lxm--基于命令ID处理: $command');
// 基于命令ID处理
_processDeviceInfoByCommandId(command, event);
} else if (command is String) {
Logger.info('lxm--基于命令名称处理: $command');
// 基于命令名称处理
_processDeviceInfoByCommandName(command, event);
} else {
Logger.warning('lxm--未知的命令类型: ${command.runtimeType}');
}
final bool success = event['success'] as bool;
if (!success) {
Logger.w('BleManager', '设备命令执行失败: ${event['command']}');
return;
}
final command = event['command'];
if (command is int) {
_processDeviceInfoByCommandId(command, event);
} else if (command is String) {
_processDeviceInfoByCommandName(command, event);
} else {
Logger.info('lxm--事件不包含command或success字段,跳过处理');
Logger.info('lxm--事件包含的键: ${event.keys.toList()}');
Logger.w('BleManager', '未知的命令类型: ${command.runtimeType}');
}
} catch (e) {
Logger.error('lxm--处理设备信息异常: ${e.toString()}');
Logger.error('lxm--异常堆栈: ${StackTrace.current}');
} catch (e, s) {
Logger.e('BleManager', '处理设备信息异常: $e', e, s);
}
}
/// 根据命令ID处理设备信息
void _processDeviceInfoByCommandId(
int commandId, Map<dynamic, dynamic> data) {
Logger.info('lxm--_processDeviceInfoByCommandId被调用,命令ID: $commandId');
Logger.info('lxm--命令数据: $data');
switch (commandId) {
case BleCommands.GET_VERSION:
Logger.info('lxm--处理版本信息命令');
_updateVersionInfoFromData(data);
break;
case BleCommands.GET_PRODUCT_ID:
Logger.info('lxm--处理产品ID命令');
_updateProductIdFromData(data);
break;
case BleCommands.GET_PRODUCT_COLOR:
Logger.info('lxm--处理产品颜色命令(暂未实现)');
break;
case BleCommands.GET_BATTERY_INFO:
Logger.info('lxm--处理电池信息命令,准备调用_updateBatteryInfoFromData');
_updateBatteryInfoFromData(data);
Logger.info('lxm--电池信息处理完成');
break;
case BleCommands.CONTROL_CODEC:
Logger.info('lxm--处理编解码控制命令');
// 处理通话状态
print(" CONTROL_CODEC: $commandId,$data");
final bool codecSuccess = _isCodecControlSuccess(data);
if (_awaitingCodecOpen) {
//如果正在等待打开编解码器
_isCodecActive.value = codecSuccess; // 只在等待打开时更新状态
_isCodecActive.value = codecSuccess;
_awaitingCodecOpen = false;
} else if (_awaitingCodecClose) {
if (codecSuccess) {
@ -440,31 +379,21 @@ class BleManager extends GetxService {
}
_awaitingCodecClose = false;
}
if (codecSuccess) {
print("lxm--编解码控制成功 (rawData 第二字节非 0)");
Logger.info('lxm--编解码控制成功 (rawData 第二字节非 0)');
} else {
print("lxm--编解码控制失败 (rawData 为 [162, 0] 或无法解析)");
Logger.warning('lxm--编解码控制失败 (rawData 为 [162, 0] 或无法解析)');
if (!codecSuccess) {
Logger.w('BleManager', '编解码控制失败');
}
break;
case BleCommands.GET_WAKEUP_VERSION:
Logger.info('lxm--处理唤醒语音版本命令');
_updateWakeupVersionFromData(data);
break;
case BleCommands.GET_VOICE_WAKEUP_STATUS:
Logger.info('[VoiceWakeup] 收到查询响应(0x14), data: $data');
_updateVoiceWakeupStatusFromData(data);
break;
case BleCommands.SET_VOICE_WAKEUP_STATUS:
Logger.info('[VoiceWakeup] 收到设置响应(0x15), data: $data');
_updateVoiceWakeupSetResultFromData(data);
break;
default:
Logger.warning('lxm--未处理的命令ID: $commandId,数据: $data');
print(" 未处理的命令ID: $commandId,$data");
Logger.w('BleManager', '未处理的命令ID: $commandId');
}
}
@ -484,82 +413,46 @@ class BleManager extends GetxService {
case 'productColor':
break;
case 'Error':
print(" Error: $commandName,$data");
Logger.w('BleManager', '收到 Error 命令');
break;
default:
print(" 未处理的命令名称: $commandName,$data");
Logger.info('未处理的命令名称: $commandName');
Logger.w('BleManager', '未处理的命令名称: $commandName');
}
}
/// 更新电池信息
void _updateBatteryInfoFromData(Map<dynamic, dynamic> data) {
try {
Logger.info('lxm--_updateBatteryInfoFromData被调用');
Logger.info('lxm--接收到的原始数据: $data');
// 确保数据中包含电池信息
if (!data.containsKey('leftEarBattery') ||
!data.containsKey('rightEarBattery') ||
!data.containsKey('caseBattery')) {
Logger.warning('lxm--电池信息数据不完整,缺少必要字段');
Logger.warning('lxm--数据包含的键: ${data.keys.toList()}');
Logger.w('BleManager', '电池信息数据不完整');
return;
}
Logger.info('lxm--原始电池数据解析:');
Logger.info(
'lxm-- leftEarBattery原始值: ${data['leftEarBattery']} (类型: ${data['leftEarBattery'].runtimeType})');
Logger.info(
'lxm-- rightEarBattery原始值: ${data['rightEarBattery']} (类型: ${data['rightEarBattery'].runtimeType})');
Logger.info(
'lxm-- caseBattery原始值: ${data['caseBattery']} (类型: ${data['caseBattery'].runtimeType})');
Logger.info(
'lxm-- leftEarCharging原始值: ${data['leftEarCharging']} (类型: ${data['leftEarCharging'].runtimeType})');
Logger.info(
'lxm-- rightEarCharging原始值: ${data['rightEarCharging']} (类型: ${data['rightEarCharging'].runtimeType})');
Logger.info(
'lxm-- caseCharging原始值: ${data['caseCharging']} (类型: ${data['caseCharging'].runtimeType})');
Logger.info(
'lxm-- batteryLevel原始值: ${data['batteryLevel']} (类型: ${data['batteryLevel']?.runtimeType})');
// 提取电池信息
final int leftBattery = (data['leftEarBattery'] as int? ?? 0) == 127
? -1 // 特殊值127转换为-1
? -1
: (data['leftEarBattery'] as int? ?? 0) > 100
? 0 // 大于100的值重置为0
: (data['leftEarBattery'] as int? ?? 0); // 正常值
? 0
: (data['leftEarBattery'] as int? ?? 0);
final int rightBattery = (data['rightEarBattery'] as int? ?? 0) == 127
? -1 // 特殊值127转换为-1
? -1
: (data['rightEarBattery'] as int? ?? 0) > 100
? 0 // 大于100的值重置为0
: (data['rightEarBattery'] as int? ?? 0); // 正常值
? 0
: (data['rightEarBattery'] as int? ?? 0);
final int caseBattery = (data['caseBattery'] as int? ?? 0) == 127
? -1 // 特殊值127转换为-1
? -1
: (data['caseBattery'] as int? ?? 0) > 100
? 0 // 大于100的值重置为0
: (data['caseBattery'] as int? ?? 0); // 正常值
? 0
: (data['caseBattery'] as int? ?? 0);
final bool leftCharging = data['leftEarCharging'] as bool? ?? false;
final bool rightCharging = data['rightEarCharging'] as bool? ?? false;
final bool caseCharging = data['caseCharging'] as bool? ?? false;
// 获取整体电量(原生层使用左耳电量作为主要指示)
final int overallBattery = data['batteryLevel'] as int? ?? leftBattery;
Logger.info('lxm--处理后的电池数据:');
Logger.info('lxm-- leftBattery: $leftBattery');
Logger.info('lxm-- rightBattery: $rightBattery');
Logger.info('lxm-- caseBattery: $caseBattery');
Logger.info('lxm-- leftCharging: $leftCharging');
Logger.info('lxm-- rightCharging: $rightCharging');
Logger.info('lxm-- caseCharging: $caseCharging');
Logger.info('lxm-- overallBattery: $overallBattery');
Logger.info('lxm--准备调用_updateDeviceInfoIfNeeded');
// 更新设备信息
_updateDeviceInfoIfNeeded(
batteryLevel: overallBattery,
leftEarBattery: leftBattery,
@ -569,12 +462,8 @@ class BleManager extends GetxService {
rightEarCharging: rightCharging,
caseCharging: caseCharging,
);
Logger.info(
'lxm--设备电池信息已更新: 电量=$overallBattery%, 左耳=$leftBattery%, 右耳=$rightBattery%, 充电仓=$caseBattery%');
} catch (e) {
Logger.error('lxm--更新电池信息失败: ${e.toString()}');
Logger.error('lxm--错误堆栈: ${StackTrace.current}');
} catch (e, s) {
Logger.e('BleManager', '更新电池信息失败: $e', e, s);
}
}
@ -718,18 +607,8 @@ class BleManager extends GetxService {
String? wakeupLeftVersion,
String? wakeupRightVersion,
}) {
Logger.info('lxm--_updateDeviceInfoIfNeeded被调用');
Logger.info(
'lxm--传入参数: batteryLevel=$batteryLevel, leftEarBattery=$leftEarBattery, rightEarBattery=$rightEarBattery, caseBattery=$caseBattery');
Logger.info(
'lxm--充电状态: leftEarCharging=$leftEarCharging, rightEarCharging=$rightEarCharging, caseCharging=$caseCharging');
Logger.info('lxm--当前deviceInfo.value是否为null: ${deviceInfo.value == null}');
// 如果设备信息已存在,则更新
if (deviceInfo.value != null) {
Logger.info('lxm--设备信息已存在,进行更新操作');
Logger.info('lxm--更新前的设备信息: ${deviceInfo.value}');
deviceInfo.value = deviceInfo.value!.copyWith(
batteryLevel: batteryLevel ?? deviceInfo.value?.batteryLevel,
leftEarBattery: _getValidBatteryValue(
@ -751,20 +630,17 @@ class BleManager extends GetxService {
wakeupRightVersion:
wakeupRightVersion ?? deviceInfo.value!.wakeupRightVersion,
);
Logger.info('lxm--更新后的设备信息: ${deviceInfo.value}');
}
// 创建设备信息
// 如果设备信息不存在,但有地址和名称,则创建新的设备信息
else if (deviceInfo.value == null &&
_isConnected.value &&
(firmwareVersion != null || batteryLevel != null)) {
Logger.info('lxm--设备信息不存在,创建新的设备信息对象');
// 使用保存的设备信息创建新的DeviceInfo对象
String deviceAddress = currentDeviceAddress ?? "";
String deviceName = _currentDeviceName ?? "";
Logger.info('lxm--创建新的设备信息对象: address=$deviceAddress, name=$deviceName');
Logger.i('BleManager',
'创建新的设备信息对象: address=$deviceAddress, name=$deviceName');
deviceInfo.value = DeviceInfo(
address: deviceAddress,
name: deviceName,
@ -781,11 +657,6 @@ class BleManager extends GetxService {
wakeupLeftVersion: wakeupLeftVersion ?? '',
wakeupRightVersion: wakeupRightVersion ?? '',
);
Logger.info('lxm--新创建的设备信息: ${deviceInfo.value}');
} else {
Logger.info(
'lxm--不满足更新条件: deviceInfo.value == null: ${deviceInfo.value == null}, _isConnected.value: ${_isConnected.value}, firmwareVersion: $firmwareVersion, batteryLevel: $batteryLevel');
}
}
@ -1053,7 +924,7 @@ class BleManager extends GetxService {
);
stsCmei = matchedDevice.cmei;
} catch (_) {}
Logger.info('lxm--当前设备 CMEI: $stsCmei (MAC=$mac)');
Logger.i('BleManager', '当前设备 CMEI: $stsCmei (MAC=$mac)');
final config = <String, dynamic>{
// STS 模式路由
@ -1139,35 +1010,17 @@ class BleManager extends GetxService {
});
}
Logger.info('lxm--准备调用AgentService.startAgentService启动BLE服务');
final result = await AgentService.startAgentService(config);
Logger.info('lxm--AgentService.startAgentService调用结果: $result');
Logger.i('BleManager', 'AgentService.startAgentService 结果: $result');
if (result) {
Logger.info('lxm--BLE服务启动成功');
// 添加延迟以等待设备连接
Logger.info('lxm--等待2秒让设备连接');
//await Future.delayed(const Duration(seconds: 2));
// 检查连接状态
Logger.info('lxm--检查设备连接状态');
final isConnected = await _bleService.isConnected();
Logger.info('lxm--设备连接状态检查结果: $isConnected');
if (isConnected) {
Logger.info('lxm--设备已连接,更新连接状态');
_isConnected.value = true;
_connectionState.value = BleConnectionState.connected;
// 如果设备信息存在,则获取详细信息
Logger.info('lxm--准备获取设备详细信息');
if (deviceInfo.value != null) {
// 获取详细设备信息
// fetchDeviceInfo();
}
}
} else {
Logger.error('BLE服务启动失败');
Logger.e('BleManager', 'BLE服务启动失败');
}
return result;

170
lib/data/services/microsoft_translation_service.dart

@ -0,0 +1,170 @@
import 'dart:convert';
import 'package:get/get.dart';
import 'package:http/http.dart' as http;
import '../../core/utils/logger.dart';
import '../../data/models/appconfig.dart';
import 'language_configs/azure_language_config.dart';
import 'language_manager.dart';
/// 微软 Azure Translator REST 翻译服务
/// 端点:https://api.cognitive.microsofttranslator.com
/// 文档:https://learn.microsoft.com/azure/ai-services/translator/reference/v3-0-translate
class MicrosoftTranslationService extends GetxService {
static const String _defaultEndpoint =
'https://api.cognitive.microsofttranslator.com';
static const String _apiPath = '/translate';
static const String _apiVersion = '3.0';
late final String _subscriptionKey;
late final String _region;
late final String _endpoint;
bool _isInitialized = false;
bool get isInitialized => _isInitialized;
final LanguageManager _languageManager = Get.find<LanguageManager>();
/// shortCode -> 微软 translationCode 缓存(由 getAzureTranslationLanguageSpecs 构建)
final Map<String, String> _shortCodeToTranslationCode = {};
@override
Future<void> onInit() async {
await initialize();
super.onInit();
}
Future<bool> initialize() async {
if (_isInitialized) return true;
try {
_subscriptionKey = AppConfig.env('AZURE_TRANSLATION_KEY') ?? '';
_region = AppConfig.env('AZURE_TRANSLATION_REGION') ?? '';
_endpoint =
AppConfig.env('AZURE_TRANSLATION_ENDPOINT') ?? _defaultEndpoint;
if (_subscriptionKey.isEmpty) {
print(
'🟢[MS-TRANS-DBG] AZURE_TRANSLATION_KEY 未配置,服务无法初始化');
Logger.error('微软翻译服务初始化失败:AZURE_TRANSLATION_KEY 为空');
return false;
}
for (final spec in getAzureTranslationLanguageSpecs()) {
final short = spec['shortCode'];
final code = spec['translationCode'];
if (short != null && code != null) {
_shortCodeToTranslationCode[short] = code;
}
}
_isInitialized = true;
print(
'🟢[MS-TRANS-DBG] 微软翻译服务初始化成功: endpoint=$_endpoint, region=$_region, keyLen=${_subscriptionKey.length}, specCount=${_shortCodeToTranslationCode.length}');
Logger.info('微软翻译服务初始化成功 region=$_region');
return true;
} catch (e) {
print('🟢[MS-TRANS-DBG] 微软翻译服务初始化异常: $e');
Logger.error('微软翻译服务初始化异常: $e');
return false;
}
}
/// 把 ASR 代码(如 `zh-CN`)映射为微软 Translator 语言码(如 `zh-Hans`)
/// 找不到返回 null
String? _toTranslationCode(String asrCode) {
if (asrCode.isEmpty) return null;
final short = _languageManager.getShortCodeByAsrCode(asrCode);
if (short == null) return null;
return _shortCodeToTranslationCode[short];
}
/// 文本翻译
/// [text] 待翻译文本
/// [sourceLanguageCode] 源语言 ASR 码(传空串则让微软自动检测)
/// [targetLanguageCode] 目标语言 ASR 码
Future<String?> translateText({
required String text,
required String sourceLanguageCode,
required String targetLanguageCode,
}) async {
if (!_isInitialized) {
print('🟢[MS-TRANS-DBG] 未初始化,返回 null');
Logger.error('微软翻译服务未初始化');
return null;
}
if (text.isEmpty) return null;
try {
final String? targetCode = _toTranslationCode(targetLanguageCode);
if (targetCode == null) {
print(
'🟢[MS-TRANS-DBG] 目标语言码映射失败: targetAsr=$targetLanguageCode,返回 null');
Logger.error('微软翻译失败:目标语言 $targetLanguageCode 不在映射表');
return null;
}
final String? sourceCode = _toTranslationCode(sourceLanguageCode);
final queryParams = <String, String>{
'api-version': _apiVersion,
'to': targetCode,
};
if (sourceCode != null && sourceCode.isNotEmpty) {
queryParams['from'] = sourceCode;
}
final uri = Uri.parse('$_endpoint$_apiPath')
.replace(queryParameters: queryParams);
final body = json.encode([
{'text': text}
]);
final headers = {
'Content-Type': 'application/json',
'Ocp-Apim-Subscription-Key': _subscriptionKey,
'Ocp-Apim-Subscription-Region': _region,
'X-ClientTraceId': _newTraceId(),
};
print(
'🟢[MS-TRANS-DBG] 发起翻译请求: uri=$uri, from=$sourceCode, to=$targetCode, text=$text');
final response =
await http.post(uri, headers: headers, body: body).timeout(
const Duration(seconds: 10),
onTimeout: () => http.Response('{"error":"timeout"}', 408),
);
print(
'🟢[MS-TRANS-DBG] 翻译响应: status=${response.statusCode}, bodyLen=${response.body.length}, body=${response.body.length > 500 ? response.body.substring(0, 500) : response.body}');
if (response.statusCode != 200) {
Logger.error(
'微软翻译 HTTP 错误: status=${response.statusCode}, body=${response.body}');
return null;
}
final decoded = json.decode(response.body);
if (decoded is! List || decoded.isEmpty) {
print('🟢[MS-TRANS-DBG] 响应非 List 或为空,解析失败');
return null;
}
final first = decoded.first;
if (first is! Map) return null;
final translations = first['translations'];
if (translations is! List || translations.isEmpty) return null;
final translated = translations.first;
if (translated is! Map) return null;
final result = translated['text'];
if (result is! String) return null;
print('🟢[MS-TRANS-DBG] 翻译成功: $result');
return result;
} catch (e, st) {
print('🟢[MS-TRANS-DBG] 翻译请求异常: $e\n$st');
Logger.error('微软翻译请求异常: $e');
return null;
}
}
String _newTraceId() {
final now = DateTime.now().microsecondsSinceEpoch;
return 'ms-trans-$now';
}
}

14
lib/data/services/speech_impl/azure_asr_service.dart

@ -202,6 +202,7 @@ class AzureAsrService extends GetxService implements AsrService {
bool audioSourceType, {
bool isRemoveFirstPunctuation = true,
String mode = "normal",
bool skipAsrForCallMode = false,
}) async {
// 防抖判断:短时间内重复调用直接拦截
final now = DateTime.now();
@ -225,6 +226,7 @@ class AzureAsrService extends GetxService implements AsrService {
'audioSourceType': audioSourceType,
'isRemoveFirstPunctuation': isRemoveFirstPunctuation,
'mode': mode,
'skipAsrForCallMode': skipAsrForCallMode,
});
if (!result) {
@ -305,29 +307,21 @@ class AzureAsrService extends GetxService implements AsrService {
switch (eventType) {
case 'audioData':
final Uint8List data = eventMap['data'] as Uint8List? ?? Uint8List(0);
final double timestamp =
(eventMap['timestamp'] as num?)?.toDouble() ?? 0.0;
final int size = eventMap['size'] as int? ?? 0;
print('接收到音频数据: 大小=${size}字节, 时间戳=${timestamp}');
Logger.debug('接收到音频数据: 大小=${size}字节, 时间戳=${timestamp}');
// 发送到音频数据流
_audioDataStreamController?.add(data);
break;
}
}
/// 获取音频数据流
Stream<Uint8List>? getAudioDataStream() {
print("getAudioDataStream");
_audioDataStreamController ??= StreamController<Uint8List>.broadcast();
return _audioDataStreamController?.stream;
}
/// 处理来自原生端的识别事件
void _handleRecognitionEvent(dynamic event) {
if (event is! Map || _eventStreamController == null) return;
if (event is! Map) return;
if (_eventStreamController == null) return;
final Map<dynamic, dynamic> eventMap = event;
final String eventType = eventMap['type'] as String? ?? '';

4
lib/data/services/speech_impl/azure_ast_service.dart

@ -122,7 +122,7 @@ class AzureAstService extends GetxService implements AstService {
final String utteranceId = eventMap['utteranceId'] as String? ?? '';
final String text = eventMap['text'] as String? ?? '';
final String detectedLanguage = eventMap['language'] as String? ?? '';
Logger.info('[STS] 识别中 [$serviceId]: text=$text');
Logger.d('AzureAST', '[STS] 识别中 [$serviceId]: text=$text');
_eventStreamController?.add(ASTEvent(
type: ASTEventType.intermediateResult,
serviceId: serviceId,
@ -137,7 +137,7 @@ class AzureAstService extends GetxService implements AstService {
final String text = eventMap['translatedText'] as String? ?? '';
final String detectedLanguage =
eventMap['targetLanguage'] as String? ?? '';
Logger.info('[STS] 翻译中 [$serviceId]: text=$text');
Logger.d('AzureAST', '[STS] 翻译中 [$serviceId]: text=$text');
_eventStreamController?.add(ASTEvent(
type: ASTEventType.translationInterim,
serviceId: serviceId,

1
lib/data/services/speech_impl/volcano_asr_api_service.dart

@ -869,6 +869,7 @@ class VolcanoAsrApiService implements AsrService {
bool audioSourceType, {
bool isRemoveFirstPunctuation = true,
String mode = "normal",
bool skipAsrForCallMode = false,
}) {
// TODO: implement startContinuousRecognition
throw UnimplementedError();

1
lib/data/services/speech_impl/volcano_asr_service.dart

@ -404,6 +404,7 @@ class VolcanoAsrService extends GetxService implements AsrService {
bool audioSourceType, {
bool isRemoveFirstPunctuation = true,
String mode = "normal",
bool skipAsrForCallMode = false,
}) {
// TODO: implement startContinuousRecognition
throw UnimplementedError();

10
lib/data/services/usage_stats_service.dart

@ -33,7 +33,7 @@ class UsageStatsService extends GetxService {
/// 初始化服务
void _initializeService() {
_loadTodayTranslationStats();
Logger.info('lxm--使用量统计服务初始化完成');
Logger.i('UsageStats', '使用量统计服务初始化完成');
}
/// 加载当日翻译统计数据
@ -243,11 +243,11 @@ class UsageStatsService extends GetxService {
},
};
Logger.info('lxm---MEET用量上报参数: $params');
Logger.d('UsageStats', 'MEET用量上报参数: $params');
final response = await Api.usagesResPoints(params);
Logger.info('lxm---MEET用量上报响应: $response');
Logger.d('UsageStats', 'MEET用量上报响应: $response');
_applyUsageResponse(response);
} catch (e) {
@ -420,11 +420,11 @@ class UsageStatsService extends GetxService {
},
};
Logger.info('lxm---ASR用量上报参数: $params');
Logger.d('UsageStats', 'ASR用量上报参数: $params');
final response = await Api.usagesResPoints(params);
Logger.info('lxm---ASR用量上报响应: $response');
Logger.d('UsageStats', 'ASR用量上报响应: $response');
_applyUsageResponse(response);
} catch (e) {

18
lib/data/services/volcano_translation_service.dart

@ -76,11 +76,10 @@ class VolcanoTranslationService extends GetxService {
}
_isInitialized = true;
Logger.info(
'lxm---Volcano Translation Service initialized成功, accessKey: $_accessKey, region: $_region, service: $_service,secretKey: $_secretKey');
Logger.i('VolcanoTrans', 'Volcano Translation Service initialized 成功');
return true;
} catch (e) {
Logger.error('lxm---Volcano Translation Service initialized失败,$e');
Logger.e('VolcanoTrans', 'Volcano Translation Service 初始化失败: $e');
return false;
}
}
@ -177,6 +176,7 @@ class VolcanoTranslationService extends GetxService {
required String targetLanguageCode,
}) async {
if (!_isInitialized) {
print('🟡[VOLC-DBG] 翻译服务未初始化,返回 null');
Logger.error('翻译服务未初始化');
return null;
}
@ -200,6 +200,7 @@ class VolcanoTranslationService extends GetxService {
if (targetShortCode != null) {
targetCode = targetShortCode;
}
print('🟡[VOLC-DBG] translateText: srcAsr=$sourceLanguageCode->srcShort=$sourceCode, tgtAsr=$targetLanguageCode->tgtShort=$targetCode, text=$text');
// 构建请求体
final requestBody = {
@ -223,11 +224,13 @@ class VolcanoTranslationService extends GetxService {
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);
@ -279,13 +282,20 @@ class VolcanoTranslationService extends GetxService {
}
// 返回提取到的翻译结果
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) {
} catch (e, st) {
print('🟡[VOLC-DBG] 翻译请求异常: $e\n$st');
Logger.error('翻译请求异常: $e');
return null;
}

155
lib/modules/common/widgets/member_service_agreement_dialog.dart

@ -0,0 +1,155 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../webview_page.dart';
import '../../../core/utils/doc_url_helper.dart';
/// 会员服务协议确认弹窗
/// 在用户进入支付(充值/购买会员)前必须显式同意
class MemberServiceAgreementDialog extends StatelessWidget {
final VoidCallback? onAccept; // 同意回调
final VoidCallback? onReject; // 拒绝回调
final bool canDismiss; // 是否可通过返回键关闭
const MemberServiceAgreementDialog({
super.key,
this.onAccept,
this.onReject,
this.canDismiss = true,
});
/// 显示会员服务协议弹窗,返回值为 true 表示同意,false / null 表示拒绝或取消
static Future<bool> show({bool canDismiss = true}) async {
final result = await Get.dialog<bool>(
MemberServiceAgreementDialog(
canDismiss: canDismiss,
onAccept: () => Get.back(result: true),
onReject: () => Get.back(result: false),
),
barrierDismissible: canDismiss,
);
return result == true;
}
@override
Widget build(BuildContext context) {
final bool isDarkMode = Get.theme.brightness == Brightness.dark;
return PopScope(
canPop: canDismiss,
child: Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
backgroundColor: isDarkMode ? Colors.grey[800] : Colors.white,
child: Container(
padding: EdgeInsets.all(20.w),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'memberServiceAgreementTitle'.tr, // 会员服务协议
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black,
),
),
SizedBox(height: 16.h),
Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: Text.rich(
TextSpan(
children: [
TextSpan(
text: 'memberServiceAgreementIntro'.tr,
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode
? Colors.white70
: Colors.black87,
),
),
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: GestureDetector(
onTap: () {
Get.to(() => WebViewPage(
url: memberServiceAgreementUrl(),
title: 'memberServiceAgreement'.tr,
));
},
child: Text(
'memberServiceAgreement'.tr, // 《会员服务协议》
style: TextStyle(
fontSize: 14.sp,
color: const Color(0xFF3B82F6),
),
),
),
),
TextSpan(
text: 'memberServiceAgreementOutro'.tr,
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white70 : Colors.black87,
),
),
],
),
textAlign: TextAlign.center,
),
),
SizedBox(height: 20.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
TextButton(
onPressed: () {
if (onReject != null) {
onReject!();
} else {
Get.back(result: false);
}
},
style: TextButton.styleFrom(
foregroundColor: isDarkMode
? Colors.grey[400]
: Colors.black54,
),
child: Text(
'disagree'.tr, // 不同意
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.black54,
),
),
),
ElevatedButton(
onPressed: () {
if (onAccept != null) {
onAccept!();
} else {
Get.back(result: true);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0066CC),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
elevation: isDarkMode ? 4 : 2,
padding: EdgeInsets.symmetric(
horizontal: 16.w, vertical: 10.h),
),
child: Text('agreeAndContinue'.tr), // 同意并继续
),
],
),
],
),
),
),
);
}
}

10
lib/modules/goods/controllers/goods_base_controller.dart

@ -10,6 +10,7 @@ import '../../../data/services/network/api.dart';
import 'package:flutter/foundation.dart';
import 'dart:async';
import 'package:app_links/app_links.dart';
import '../../common/widgets/member_service_agreement_dialog.dart';
/// goods 模块基础控制器,封装通用的商品加载与支付流程
abstract class GoodsBaseController extends GetxController {
@ -97,6 +98,15 @@ abstract class GoodsBaseController extends GetxController {
method = enabled.first;
}
// 3.5 充值前必须显式同意《会员服务协议》(合规要求)
final agreed = await MemberServiceAgreementDialog.show();
if (!agreed) {
Get.snackbar('tip'.tr, 'memberAgreementRejectedTip'.tr,
snackPosition: SnackPosition.BOTTOM,
margin: const EdgeInsets.all(12));
return;
}
// 4. 根据选择的支付方式执行支付
bool ok = false;
String failMsg = 'paymentFailedTryAgain'.tr; // 支付失败,请重试

36
lib/modules/home/controllers/home_controller.dart

@ -55,22 +55,6 @@ class HomeController extends GetxController {
final firmwareVersion = ''.obs;
void updateDeviceInfo(DeviceInfo? newDeviceInfo) {
Logger.d(_tag, 'lxm--updateDeviceInfo被调用');
Logger.d(_tag, 'lxm--接收到的DeviceInfo: $newDeviceInfo');
if (newDeviceInfo != null) {
Logger.d(_tag, 'lxm--原始数据 - 左耳电量: ${newDeviceInfo.leftEarBattery}');
Logger.d(_tag, 'lxm--原始数据 - 右耳电量: ${newDeviceInfo.rightEarBattery}');
Logger.d(_tag, 'lxm--原始数据 - 充电仓电量: ${newDeviceInfo.caseBattery}');
Logger.d(_tag, 'lxm--原始数据 - 左耳充电: ${newDeviceInfo.leftEarCharging}');
Logger.d(_tag, 'lxm--原始数据 - 右耳充电: ${newDeviceInfo.rightEarCharging}');
Logger.d(_tag, 'lxm--原始数据 - 充电仓充电: ${newDeviceInfo.caseCharging}');
Logger.d(_tag, 'lxm--原始数据 - 固件版本: ${newDeviceInfo.firmwareVersion}');
} else {
Logger.d(_tag, 'lxm--接收到的DeviceInfo为null');
}
// 只有当新数据不为0时才更新电池值,否则保持原值
leftEarBattery.value = newDeviceInfo?.leftEarBattery ?? 0;
rightEarBattery.value = newDeviceInfo?.rightEarBattery ?? 0;
caseBattery.value = newDeviceInfo?.caseBattery ?? 0;
@ -78,16 +62,6 @@ class HomeController extends GetxController {
rightEarCharging.value = newDeviceInfo?.rightEarCharging ?? false;
caseCharging.value = newDeviceInfo?.caseCharging ?? false;
firmwareVersion.value = newDeviceInfo?.firmwareVersion ?? '';
Logger.d(_tag, 'lxm--更新后的值 - 左耳电量: ${leftEarBattery.value}');
Logger.d(_tag, 'lxm--更新后的值 - 右耳电量: ${rightEarBattery.value}');
Logger.d(_tag, 'lxm--更新后的值 - 充电仓电量: ${caseBattery.value}');
Logger.d(_tag, 'lxm--更新后的值 - 左耳充电: ${leftEarCharging.value}');
Logger.d(_tag, 'lxm--更新后的值 - 右耳充电: ${rightEarCharging.value}');
Logger.d(_tag, 'lxm--更新后的值 - 充电仓充电: ${caseCharging.value}');
Logger.d(_tag, 'lxm--更新后的值 - 固件版本: ${firmwareVersion.value}');
// rightEarCharging.refresh();
//update();
}
Worker? _deviceInfoWorker;
@ -138,15 +112,7 @@ class HomeController extends GetxController {
void _setupStateListeners() {
_deviceInfoWorker = ever(_bleManager.deviceInfo, (DeviceInfo? info) {
Logger.d(_tag, 'lxm--_deviceInfoWorker监听器被触发');
Logger.d(_tag, 'lxm--从BleManager接收到的DeviceInfo: $info');
if (info != null) {
Logger.d(_tag, 'lxm--DeviceInfo不为null,调用updateDeviceInfo');
updateDeviceInfo(info);
} else {
Logger.d(_tag, 'lxm--DeviceInfo为null,跳过更新');
}
//update();
if (info != null) updateDeviceInfo(info);
});
_classicBluetoothWorker =

4
lib/modules/home/views/home_view.dart

@ -519,10 +519,6 @@ class HomeView extends GetView<HomeController> {
final caseCharging = controller.caseCharging.value;
final isConnected = controller.isFullyConnectedRx.value;
print(
'lxm--电池信息UI - 左耳电量: $leftBattery, 右耳电量: $rightBattery, 充电仓电量: $caseBattery, '
'充电(L/R/C): $leftEarCharging/$rightEarCharging/$caseCharging, 已连接: $isConnected');
// 判断是否显示充电盒:充电盒电量大于0且设备已连接
final shouldShowCase = caseBattery > 0 && isConnected;

22
lib/modules/meeting/controllers/meeting_details_controller.dart

@ -170,12 +170,22 @@ class MeetingDetailsController extends GetxController
if (!await file.exists()) {
Logger.error('音频文件不存在: $filepath');
isPlayerReady.value = false;
Get.snackbar(
'error'.tr,
'audioFileNotFound'.tr, // 音频文件不存在
snackPosition: SnackPosition.BOTTOM,
duration: const Duration(seconds: 3),
);
// 本地文件缺失:若云端存在音频,清空本地路径并持久化,
// 让 UI 自动切换为"下载"按钮,下载完成后即可播放;
// 仅当云端也没有音频时才提示文件不存在
if (meetingData.value.audiourl.isNotEmpty) {
Logger.d('MeetingDetails', '本地音频缺失,切换为下载态(云端URL可用)');
meetingData.value.filepath = '';
meetingData.refresh();
await SqfliteApi.editMeetingTitle(id, {'filePath': ''});
} else {
Get.snackbar(
'error'.tr,
'audioFileNotFound'.tr, // 音频文件不存在
snackPosition: SnackPosition.BOTTOM,
duration: const Duration(seconds: 3),
);
}
return;
}

51
lib/modules/translation/controllers/translation_controller.dart

@ -46,6 +46,8 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
// ==================== 服务依赖 ====================
final AsrService _asrService = Get.find<AsrService>();
// 同声翻译 / 面对面翻译 / 音视频翻译走火山翻译(REST)
// 通话翻译(call 模式)不使用该字段,改走 _astService(AST 端到端)
final VolcanoTranslationService _translationService =
Get.find<VolcanoTranslationService>();
final TtsService _ttsService = Get.find<TtsService>();
@ -1038,16 +1040,16 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
return;
}
if (currentMode.value == 'simultaneous') {
Logger.info('liwei-------- 开始语音识别,模式:同声传译,ASR phone_call');
Logger.i('Translation', '开始语音识别,模式:同声传译,ASR phone_call');
await _startAsrService('phone_call');
} else if (currentMode.value == 'faceToFace') {
Logger.info('iwei-------- 开始语音识别,模式:面对面,ASR push_to_talk');
Logger.i('Translation', '开始语音识别,模式:面对面,ASR push_to_talk');
await _startAsrService('push_to_talk');
} else if (currentMode.value == 'call') {
Logger.info('iwei-------- 通话模式:AST端到端服务已启动,跳过Azure ASR');
Logger.i('Translation', '通话模式:AST端到端服务已启动,跳过Azure ASR');
} else {
Logger.info(
'iwei-------- 开始语音识别,模式:${currentMode.value},ASR 模式:normal');
Logger.i('Translation',
'开始语音识别,模式:${currentMode.value},ASR 模式:normal');
await _startAsrService('normal');
}
await _finalizeRecognitionStart();
@ -1172,9 +1174,10 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
/// 处理语音识别事件
/// [event] 识别事件
void _handleRecognitionEvent(RecognitionEvent event) {
// Logger.info("处理识别事件:${event}");
final String asrProvider = event.provider;
if (asrProvider.isNotEmpty) Logger.info('ASR provider: $asrProvider');
if (asrProvider.isNotEmpty) {
Logger.d('Translation', 'ASR provider: $asrProvider');
}
switch (event.type) {
case RecognitionEventType.finalResult:
if (event.text.isEmpty) break;
@ -1182,12 +1185,11 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
detectedLanguageCode = currentMode.value == 'call'
? targetLanguageCode.value
: event.detectedLanguage;
Logger.info("处理识别完成:${event.text}");
Logger.d('Translation', '处理识别完成: ${event.text}');
if (currentMode.value != 'faceToFace') {
handleFinalResult(event.text);
} else {
faceToFaceRecognitionText.value += event.text;
Logger.info('lxm-面对面翻译识别文本更新: ${faceToFaceRecognitionText.value}');
}
if (currentMode.value == 'faceToFace' && activeSpeaker.value == 0) {
handleFinalResult(faceToFaceRecognitionText.value);
@ -1200,7 +1202,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
if (event.text.isEmpty) break;
//通话模式下:己方语言即为源语言,无需判断
detectedLanguageCode = sourceLanguageCode.value;
Logger.info("lxm---处理己方识别完成:${event.text}");
Logger.d('Translation', '处理己方识别完成: ${event.text}');
handleFinalResult(event.text);
break;
@ -1210,7 +1212,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
detectedLanguageCode = currentMode.value == 'call'
? targetLanguageCode.value
: event.detectedLanguage;
Logger.info("处理识别中事件:${event.text}");
Logger.d('Translation', '处理识别中: ${event.text}');
handleIntermediateResult(event.text);
break;
@ -1244,7 +1246,8 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
/// 处理 AST(语音识别+翻译一体)事件
/// 按 serviceId 区分双路(A=己方, B=对方),用 utteranceId 匹配同一句话的事件
void _handleAstEvent(ASTEvent event) {
Logger.info('[STS] _handleAstEvent 收到: type=${event.type}, serviceId=${event.serviceId}, text=${event.text}');
Logger.d('Translation',
'[STS] _handleAstEvent: type=${event.type}, serviceId=${event.serviceId}');
// 根据 serviceId 确定语言方向
final String srcCode = event.serviceId == "A"
? sourceLanguageCode.value
@ -1256,7 +1259,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
switch (event.type) {
case ASTEventType.intermediateResult:
if (event.text.isEmpty) break;
Logger.info('AST 识别中 [${event.serviceId}]: ${event.text}');
Logger.d('Translation', 'AST 识别中 [${event.serviceId}]: ${event.text}');
currentSessionId ??= DateTime.now().millisecondsSinceEpoch.toString();
// 按 utteranceId 查找已有项
@ -1298,7 +1301,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
case ASTEventType.finalResult:
if (event.text.isEmpty) break;
Logger.info('AST 识别完成 [${event.serviceId}]: ${event.text}');
Logger.i('Translation', 'AST 识别完成 [${event.serviceId}]: ${event.text}');
currentSessionId ??= DateTime.now().millisecondsSinceEpoch.toString();
TranslationItem? target;
@ -1349,7 +1352,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
case ASTEventType.translationInterim:
if (event.text.isEmpty) break;
Logger.info('AST 翻译中 [${event.serviceId}]: ${event.text}');
Logger.d('Translation', 'AST 翻译中 [${event.serviceId}]: ${event.text}');
currentSessionId ??= DateTime.now().millisecondsSinceEpoch.toString();
TranslationItem? target;
@ -1387,7 +1390,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
case ASTEventType.translationResult:
if (event.text.isEmpty) break;
Logger.info('AST 翻译完成 [${event.serviceId}]: ${event.text}');
Logger.i('Translation', 'AST 翻译完成 [${event.serviceId}]: ${event.text}');
currentSessionId ??= DateTime.now().millisecondsSinceEpoch.toString();
TranslationItem? target;
@ -1608,8 +1611,8 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
await _updateTranslationHistory(
sourceText, translationResult, timestamp, languageCodes, isFinal);
}
} catch (e) {
Logger.error('翻译失败: ${e.toString()}');
} catch (e, st) {
Logger.e('Translation', '翻译失败: $e', e, st);
} finally {
isTranslating.value = false;
}
@ -1723,18 +1726,10 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
await _updateFloatingWindowWithTranslation(translationHistory[index]);
}
// 添加TranslationItem日志打印
if (isFinal) {
final item = translationHistory[index];
Logger.info('lxm-翻译完成 - TranslationItem详情: '
'源文本: ${item.sourceText}, '
'翻译文本: ${item.translatedText}, '
'源语言: ${item.sourceLanguageCode}, '
'目标语言: ${item.targetLanguageCode}, '
'时间戳: ${item.timestamp.toIso8601String()}, '
'会话ID: ${item.sessionId}, '
'是否会话首项: ${item.isFirstInSession}, '
'是否中间结果: ${item.isIntermediate}');
Logger.i('Translation',
'翻译完成: ${item.sourceLanguageCode}→${item.targetLanguageCode} ${item.sourceText.length}字符');
}
if (isFinal) {

13
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt

@ -857,16 +857,12 @@ object AgentService : CoroutineScope {
stopIdleCheck() // 先停止现有的检查
if (!isRecognitionActive) return
Log.d(TAG, "启动空闲检测,超时时间: ${idleSeconds}秒,模式: $currentRecognitionMode")
idleCheckJob = launch {
try {
delay(idleSeconds * 1000L)
Log.d(TAG, "空闲检测触发,isActive: $isActive, isRecognitionActive: $isRecognitionActive, isTtsSpeaking: $isTtsSpeaking, isAiStreaming: $isAiStreaming, mode: $mode")
if (isActive && isRecognitionActive) {
if (!isTtsSpeaking && !isAiStreaming) {
// 条件满足:TTS未播放且AI未流式输出,执行自动停止
Log.d(TAG, "空闲检测触发自动停止,模式: $currentRecognitionMode")
Log.i(TAG, "空闲检测触发自动停止,模式: $currentRecognitionMode")
BleService.closeCodec()
if (currentRecognitionMode != "phone_call"){
stopRecognition()
@ -878,8 +874,6 @@ object AgentService : CoroutineScope {
)
)
} else {
// 条件不满足:TTS或AI仍在工作中,重新调度计时器等待下一轮检查
Log.d(TAG, "空闲检测条件不满足(tts=$isTtsSpeaking, ai=$isAiStreaming),重新调度")
startIdleCheck(mode, idleSeconds)
}
}
@ -897,7 +891,6 @@ object AgentService : CoroutineScope {
private fun stopIdleCheck() {
idleCheckJob?.cancel()
idleCheckJob = null
Log.d(TAG, "空闲检测已停止")
}
/**
@ -1009,16 +1002,13 @@ object AgentService : CoroutineScope {
fun recognizeCallback(): Boolean {
try {
Log.d(TAG, "recognizeCallback")
val success = azureAsrHelper?.setupEventListeners(object :
AzureAsrHelper.ContinuousRecognizeCallback {
override fun onRecognizing(sessionid:String, recognizing: String, detectedLanguage: String) {
Log.d(TAG, "liwei-------- onRecognizing: $sessionid")
pendingProcessJob?.cancel()
if (recognizing.isNotEmpty()&&isKeepResult==true) {
// 检测到语音,更新状态
_hasSpeechDetected.set(true)
Log.d(TAG, "ai识别中重置定时器")
restartIdleCheck()
if(currsessionAsrId.isEmpty()) {
currsessionAsrId = sessionid
@ -1147,7 +1137,6 @@ object AgentService : CoroutineScope {
if (currentRecognitionMode == "push_to_talk") { //按住说话
if (recognitionResult.isEmpty()){
Log.d(TAG, "liwei-------- onSessionStopped:currentRecognitionResult $currentRecognitionResult")
recognitionResult+= currentRecognitionResult
currentRecognitionResult = ""
}

12
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt

@ -64,7 +64,7 @@ object BleAgent : BleService.Callback, AgentServiceListener {
* 设置是否可以唤醒
*/
fun setCanWakeUp(canWakeUp: Boolean) {
Log.d(TAG, "liwei--------------设置是否可以唤醒: $canWakeUp")
Log.i(TAG, "设置是否可以唤醒: $canWakeUp")
isCanWakeUp = canWakeUp
}
@ -153,19 +153,17 @@ object BleAgent : BleService.Callback, AgentServiceListener {
}
override fun onAudioDataReceived(data: ByteArray, channel: Int) {
// Log.d(TAG, "onAudioDataReceived, data: ${data.size}")
AgentService.pushAudioData(data)
// 可选:处理音频数据
}
/**
* 处理唤醒信号
* 在收到唤醒信号时启动语音识别
*/
override fun onWakeupSignalReceived() {
Log.d(TAG, "liwei-------------- 收到唤醒信号,启动语音识别${AgentService.isRecognitionActive}")
Log.i(TAG, "收到唤醒信号,启动语音识别 active=${AgentService.isRecognitionActive}")
if (!isCanWakeUp) {
Log.d(TAG, "liwei-------------- 当前不可以唤醒")
Log.i(TAG, "当前不可以唤醒")
return
}
// 停止当前TTS,避免冲突
@ -184,12 +182,12 @@ object BleAgent : BleService.Callback, AgentServiceListener {
* 处理开始 AI 单次对话
*/
override fun onStartSingleDialogReceived() {
Log.d(TAG, "liwei-------------- 收到开始 AI 单次对话指令")
Log.i(TAG, "收到开始 AI 单次对话指令")
startSingleDialog()
}
override fun onEndSingleDialogReceived() {
Log.d(TAG, "liwei-------------- 收到结束 AI 单次对话指令")
Log.i(TAG, "收到结束 AI 单次对话指令")
// 停止语音识别
stopListening()
}

16
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift

@ -2623,18 +2623,20 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
sendEvent(name: "recognition_stopped", data: ["sessionid":currsessionAsrId,"status": "stopped"])
}else{
sendEvent(name: "recognition_stopped", data: ["sessionid":sessionid,"status": "stopped"])
isRecognizing = false
hasSpeechDetected = false
stopIdleCheck()
if (currentRecognitionMode == "ble_wakeup") {
audioPlayer?.playStopSound()
}
tryResumeExternalMusicIfIdle(reason: "onSessionStopped")
}
// 统一复位 ASR 状态 + 触发外部音乐恢复判断:
// 之前 PTT 分支漏掉了这几步,isRecognizing 卡在 true,
// 后续 AI/TTS 完成回调里 tryResumeExternalMusicIfIdle 的 recogBlocking 守卫永远为 true,
// 导致 scheduleDebouncedDeactivate 永远进不去、外部音乐收不到 notifyOthersOnDeactivation。
isRecognizing = false
hasSpeechDetected = false
stopIdleCheck()
_log("INFO", "[Agent] onSessionStopped mode=\(currentRecognitionMode) tryResume")
tryResumeExternalMusicIfIdle(reason: "onSessionStopped")
}
func onCanceled(sessionid:String ,_ reason: String, _ errorDetails: String) {

63
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt

@ -121,11 +121,8 @@ class IntegratedSpeechTranslationService(
val sendLen = (ttsdata.size / multiple) * multiple
if (sendLen > 0) {
val chunk = ttsdata.copyOfRange(0, sendLen)
Log.d(TAG, "按${multiple}字节对齐发送音频: ${chunk.size} 字节,缓存剩余: ${ttsdata.size - sendLen}")
eventCallback?.onSynthesisAudioGenerated(utteranceId, text, chunk)
ttsdata = ttsdata.copyOfRange(sendLen, ttsdata.size)
} else {
Log.d(TAG, "暂未够${multiple}字节,当前缓存: ${ttsdata.size}")
}
}
/**
@ -397,10 +394,6 @@ class IntegratedSpeechTranslationService(
isInUtterance.set(true)
}
val uttId = currentUtteranceId ?: nextUtteranceId()
Log.d(
TAG,
"识别中事件[${uttId}]: ${event.result.text},serviceConfig.sourceLanguage:${serviceConfig.sourceLanguage}"
)
eventCallback?.onRecognizing(
uttId,
event.result.text,
@ -413,7 +406,6 @@ class IntegratedSpeechTranslationService(
// 取消上一次“识别中翻译”任务,避免旧翻译结果覆盖新结果
recognizingTranslationJob?.cancel()
recognizingTranslationJob = launch {
Log.d(TAG, "识别中翻译触发: ${event.result.text}")
processTranslationOnly(uttId, event.result.text)
}
}
@ -433,23 +425,15 @@ class IntegratedSpeechTranslationService(
serviceConfig.sourceLanguage,
confidence
)
Log.d(
TAG,
"识别完成事件[${uttId}]: ${event.result.text},serviceConfig.sourceLanguage:${serviceConfig.sourceLanguage}"
)
Log.i(TAG, "识别完成[${uttId}]: ${event.result.text}")
// 只有在服务仍在运行时才触发翻译流程
if (serviceState.isRecognizing.get()) {
isInUtterance.set(false)
currentUtteranceId = null
launch {
// 识别完成时,马上打断上一次“识别中”的翻译,但不要在主线程 join
cancelRecognizingTranslationJobSafely()
Log.d(TAG, "触发最终翻译流程: ${event.result.text}")
processTranslationAndSynthesis(uttId, event.result.text)
}
} else {
Log.d(TAG, "服务已停止,跳过翻译流程: ${event.result.text}")
}
}
}
@ -524,8 +508,7 @@ class IntegratedSpeechTranslationService(
// 合成开始事件
SynthesisStarted.addEventListener { _, _ ->
Log.d(TAG, "合成开始事件")
ttsdata = ByteArray(0)
ttsdata = ByteArray(0)
serviceState.isSynthesizing.set(true)
val uttId = currentSynthesisId ?: ""
val text = currentSynthesisText ?: ""
@ -533,11 +516,9 @@ class IntegratedSpeechTranslationService(
eventCallback?.onStateChanged("Synthesis", true)
}
// 合成进行中事件
// 合成进行中事件(高频:每音频包触发)
Synthesizing.addEventListener { _, event ->
Log.d(TAG, "合成进行中事件")
// 计算进度(简化版)
val progress = 0.5f // 实际应用中可以根据音频数据计算
val progress = 0.5f
val uttId = currentSynthesisId ?: ""
val text = currentSynthesisText ?: ""
eventCallback?.onSynthesisProgress(uttId, text, progress)
@ -548,7 +529,6 @@ class IntegratedSpeechTranslationService(
// 合成完成事件
SynthesisCompleted.addEventListener { _, event ->
Log.d(TAG, "合成完成事件")
val uttId = currentSynthesisId ?: ""
val text = currentSynthesisText ?: ""
eventCallback?.onSynthesisCompleted(uttId, text)
@ -559,7 +539,7 @@ class IntegratedSpeechTranslationService(
// 合成取消事件
SynthesisCanceled.addEventListener { _, event ->
Log.d(TAG, "合成取消事件")
Log.w(TAG, "合成取消事件")
ttsdata = ByteArray(0)
serviceState.isSynthesizing.set(false)
val reason = event.result.reason
@ -744,7 +724,6 @@ class IntegratedSpeechTranslationService(
serviceState.isTranslating.set(true)
eventCallback?.onTranslationStarted(targetUtteranceId, targetText)
eventCallback?.onStateChanged("Translation", true)
Log.d(TAG, "翻译开始${targetText}")
try {
val srcLang = if (serviceConfig.translationSourceLanguage.isNotEmpty()) serviceConfig.translationSourceLanguage else serviceConfig.sourceLanguage
val tgtLang = if (serviceConfig.translationTargetLanguage.isNotEmpty()) serviceConfig.translationTargetLanguage else serviceConfig.targetLanguage
@ -767,7 +746,6 @@ class IntegratedSpeechTranslationService(
when {
translationResult?.success == true && !translationResult.translatedText.isNullOrEmpty() -> {
Log.d(TAG, "翻译成功${translationResult.translatedText}")
eventCallback?.onTranslated(
targetUtteranceId,
targetText,
@ -816,7 +794,6 @@ class IntegratedSpeechTranslationService(
}
try {
Log.d(TAG, "开始语音合成: $text")
serviceState.isSynthesizing.set(true)
// 在开始时记录当前合成上下文,并抛出开始事件
@ -830,16 +807,9 @@ class IntegratedSpeechTranslationService(
val result = synthesizer?.SpeakSsmlAsync(ssml)?.get()
if (result?.reason == ResultReason.SynthesizingAudioCompleted) {
val playbackStatus =
if (serviceConfig.enableAudioPlayback) "音频已播放" else "音频已合成但未播放"
Log.d(TAG, "语音合成成功,$playbackStatus")
// 获取原始音频数据
val rawAudioData = result.audioData
Log.d(TAG, "语音合成成功,发送音频: ${ttsdata.size} 字节,缓存剩余: ${ttsdata.size}")
eventCallback?.onSynthesisAudioGenerated(utteranceId, text, ttsdata)
ttsdata = ByteArray(0)
eventCallback?.onSynthesisAudioGenerated(utteranceId, text, ttsdata)
ttsdata = ByteArray(0)
return rawAudioData
} else {
Log.e(TAG, "语音合成失败: ${result?.reason}")
@ -875,22 +845,12 @@ class IntegratedSpeechTranslationService(
* - text: 待翻译的源文本
*/
private suspend fun processTranslationOnly(utteranceId: String, text: String) {
// 若服务已停止,直接跳过
if (!serviceState.isRecognizing.get()) {
Log.d(TAG, "服务已停止,取消识别中翻译流程: $text")
return
}
// 若已有翻译在进行,直接退出(避免并发)
if (serviceState.isTranslating.get()) {
Log.d(TAG, "已有翻译进行中,识别中翻译跳过: $text")
return
}
if (!serviceState.isRecognizing.get()) return
if (serviceState.isTranslating.get()) return
serviceState.isTranslating.set(true)
eventCallback?.onTranslationStarted(utteranceId, text)
eventCallback?.onStateChanged("Translation", true)
Log.d(TAG, "识别中翻译开始: $text")
try {
val srcLang = if (serviceConfig.translationSourceLanguage.isNotEmpty()) serviceConfig.translationSourceLanguage else serviceConfig.sourceLanguage
@ -905,8 +865,6 @@ class IntegratedSpeechTranslationService(
when {
translationResult?.success == true && !translationResult.translatedText.isNullOrEmpty() -> {
Log.d(TAG, "识别中翻译成功: ${translationResult.translatedText}")
// 使用“识别中”专用的临时翻译回调,区分最终翻译
eventCallback?.onInterimTranslated(
utteranceId,
text,
@ -916,18 +874,15 @@ class IntegratedSpeechTranslationService(
}
translationResult?.error != null -> {
Log.w(TAG, "识别中翻译失败: ${translationResult.error}")
eventCallback?.onTranslationFailed(utteranceId, text, translationResult.error)
}
else -> {
Log.w(TAG, "识别中翻译返回空结果")
eventCallback?.onTranslationFailed(utteranceId, text, "翻译服务返回空结果")
}
}
} catch (e: CancellationException) {
// 被识别完成事件或新识别中事件取消,不做失败提示
Log.d(TAG, "识别中翻译任务已取消: $text")
} catch (e: TimeoutCancellationException) {
Log.w(TAG, "识别中翻译超时: $text")
eventCallback?.onTranslationFailed(utteranceId, text, "翻译超时")

25
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift

@ -849,15 +849,12 @@ public class AzureAsrHelper: NSObject {
return currentLanguage
}
print("result: \(result)")
// 备用方案:从属性中获取语言
if let properties = result.properties,
let language = properties.getPropertyByName(
"speechServiceResponse_RecognitionLanguage"),
!language.isEmpty
{
print("从属性中检测到语言: \(language)")
return language
}
@ -865,13 +862,12 @@ public class AzureAsrHelper: NSObject {
do {
let langResult = try SPXAutoDetectSourceLanguageResult(result)
if let language = langResult.language, !language.isEmpty {
print("SDK方法检测到语言: \(language)")
return language
}
} catch {
print("SDK语言检测失败: \(error.localizedDescription)")
os_log("SDK语言检测失败: %{public}@", log: log, type: .error,
error.localizedDescription)
}
print("无法检测语言,使用默认语言: \(currentLanguage)")
return currentLanguage
}
@ -916,8 +912,6 @@ public class AzureAsrHelper: NSObject {
* 设置事件监听器
*/
public func setupEventListeners(callback: ContinuousRecognizeCallback) -> Bool {
print("设置ssssss监听器:${speechConfig}")
// 保存回调引用用于网络事件
self.continuousCallback = callback
@ -935,9 +929,6 @@ public class AzureAsrHelper: NSObject {
// 使用现有的 getDetectedLanguage 方法替代 result.language
let detectedLanguage = self.getDetectedLanguage(from: result)
let text = result.text
print("正在识别事件=检测到语言: \(detectedLanguage), 识别中: \(text)")
DispatchQueue.main.async {
callback.onRecognizing(
@ -953,42 +944,34 @@ public class AzureAsrHelper: NSObject {
!(result.text?.isEmpty ?? true)
else { return }
// 使用现有的 getDetectedLanguage 方法替代 result.language
let detectedLanguage = self.getDetectedLanguage(from: result)
let text = result.text
print("识别完成事件=检测到语言: \(detectedLanguage), 识别中: \(text)")
os_log("识别完成: lang=%{public}@ text=%{public}@", log: self.log, type: .info,
detectedLanguage, text ?? "")
// 在主线程回调结果
DispatchQueue.main.async {
callback.onResult(
sessionid: self.currsessionId, result.text ?? "", detectedLanguage)
self.currsessionId = UUID().uuidString
}
}
// 会话开始事件
recognizer.addSessionStartedEventHandler { (sender, event) in
// 直接在当前线程调用回调
self.currsessionId = UUID().uuidString
callback.onSessionStarted(sessionid: self.currsessionId)
print("会话开始事件:")
}
// 会话结束事件
recognizer.addSessionStoppedEventHandler { [weak self] (sender, event) in
guard let self = self else { return }
print("会话结束事件:")
// 直接在当前线程调用回调
callback.onSessionStopped(sessionid: self.currsessionId)
self.audioStream?.isContinuousRecognitionActive = false
//self.stopAudioProcessing()
}
// 取消事件
recognizer.addCanceledEventHandler { [weak self] (_, event) in
guard let self = self else { return }
print("取消事件:")
let errorDetails = event.errorDetails ?? "未知错误"
let reason = String(describing: event.reason.rawValue)

18
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift

@ -415,17 +415,21 @@ private func sendAudioDataEvent(_ event: [String: Any]) {
let removeFirstPunctuation = args["isRemoveFirstPunctuation"] as? Bool ?? true
let mode = args["mode"] as? String ?? "normal"
// 通话翻译 call 模式由 Dart 侧显式传 true:AST 端到端服务已跑,占位跳过 ASR。
// 缺省 false,保证同声/面对面/多媒体翻译(external 音频)都能正常启动 ASR。
let skipAsrForCallMode = args["skipAsrForCallMode"] as? Bool ?? false
// 根据参数确定音频源类型
let audioSourceType = isExternalActive ?
AzureAsrHelper.AudioSourceType.external :
let audioSourceType = isExternalActive ?
AzureAsrHelper.AudioSourceType.external :
AzureAsrHelper.AudioSourceType.microphone
os_log("startContinuousRecognition: mode=%@, audioSourceType=%@, isExternal=%d, astProvider=%@",
log: ctLog, type: .info, mode, String(describing: audioSourceType), isExternalActive ? 1 : 0, currentAstProvider)
os_log("startContinuousRecognition: mode=%@, audioSourceType=%@, isExternal=%d, skipAsr=%d, astProvider=%@",
log: ctLog, type: .info, mode, String(describing: audioSourceType), isExternalActive ? 1 : 0, skipAsrForCallMode ? 1 : 0, currentAstProvider)
// 通话翻译模式(external):AST 端到端服务已处理识别+翻译+TTS,无需启动 ASR
if isExternalActive {
// 仅当 Dart 显式标记为 call 模式下的占位调用时才跳过 ASR;
// 之前仅凭 isExternalActive 判断会误伤多媒体翻译(external 音频但需要启动 ASR)。
if isExternalActive && skipAsrForCallMode {
os_log("startContinuousRecognition: 通话模式,AST端到端服务已激活(provider=%@),跳过ASR", log: ctLog, type: .info, currentAstProvider)
result(true)
} else {

47
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift

@ -168,8 +168,7 @@ public class AzureTtsHelper: NSObject, ITtsService, AVAudioPlayerDelegate {
*/
public func startspeak(sessionid: String) -> Bool {
self.sessionid = sessionid
print("startspeak sessionid \(sessionid)")
// os_log("调用链: startspeak 设置session=%{public}@", log: log, type: .info, sessionid)
os_log("startspeak session=%{public}@", log: log, type: .info, sessionid)
// 重置当前会话的文本计数
self.currentSessionTextCount = 0
self.pendingTextCount = 0
@ -1581,8 +1580,8 @@ private func applyAudioSessionForTTS() -> Bool {
|| currentRecognitionMode == "normal"
|| currentRecognitionMode == "ble_wakeup")
if internalSessionActive && !audioInterrupted && isBlePlaybackMode
&& sessionSnapshot.category == .playback
&& sessionSnapshot.mode == .spokenAudio {
&& sessionSnapshot.category == .playAndRecord
&& sessionSnapshot.mode == .default {
return true
}
dlog("INFO", "applyAudioSessionForTTS 入口 mode=\(currentRecognitionMode) otherAudioPlaying=\(sessionSnapshot.isOtherAudioPlaying) category=\(sessionSnapshot.category.rawValue) sessionMode=\(sessionSnapshot.mode.rawValue) internalActive=\(internalSessionActive)")
@ -1628,9 +1627,13 @@ private func applyAudioSessionForTTS() -> Bool {
self.micCapture?.stopCapture()
Thread.sleep(forTimeInterval: 0.05)
}
desiredCategory = .playback
desiredMode = .spokenAudio
// 不再 mixWithOthers:唤醒/TTS 期间需要打断外部和内部其他音频
// 和 ASR 阶段保持同 category(.playAndRecord):
// 换成 .playback + .spokenAudio 会让 iOS 给外部 App 发一次 "interruption ended (no shouldResume)",
// 最终 setActive(false, notifyOthers) 也没办法让它恢复。
// 整轮对话只用一种 category,iOS 视角就是单一 interruption,释放时才能正确触发 auto-resume。
desiredCategory = .playAndRecord
desiredMode = .default
desiredOptions = [.defaultToSpeaker, .allowBluetoothA2DP]
} else if isRecording {
desiredCategory = .playAndRecord
desiredMode = .default
@ -1755,14 +1758,15 @@ private func deactivateAudioSessionAfterTTSIfIdle() {
dlog("INFO", "deactivateAudioSessionAfterTTSIfIdle 入口")
let work = { [weak self] in
guard let self = self else { return }
// 注:这里刻意不再用 externalBusy skip。
// 之前试过"AI 还在 streaming 时段间 skip"——会导致整轮对话只在最终发一次
// notifyOthersOnDeactivation,有些外部 App(尤其第三方音乐)一次信号来不
// 及/会被吃掉,表现为"完全不恢复"。
// 放行后段间会反复 deactivate(notifyOthers),外部 App 能持续收到 ended,
// 虽然有段间短暂冒音的副作用,但能稳定恢复。
// AI/ASR 仍忙时 skip deactivate:
// 实测 PTT 模式多轮工具调用(tool_call → TTS → tool_call → TTS)里,如果段间放行
// deactivate,外部 App(Apple Music/Spotify 等)会在 1s 内经历 resume→interrupt→resume
// 的快速抖动,最后一次 resume 会被部分 App 判为异常并忽略,表现为"彻底不恢复"。
// 直接 skip 后等 aiOnComplete/onSessionStopped 的 tryResumeExternalMusicIfIdle
// 重新 scheduleDebouncedDeactivate,保证整轮对话只经历一次 pause→resume。
if self.externalBusyProvider?() == true {
self.dlog("INFO", "external busy (AI/ASR 仍忙)——仍允许 deactivate,外部 App 稳定恢复优先")
self.dlog("INFO", "deactivate skip: external busy (AI/ASR 仍忙),等回合结束再释放")
return
}
if self.micCapture?.isCapturing == true {
self.dlog("INFO", "deactivate skip: mic capturing,排重试")
@ -1833,6 +1837,21 @@ private func deactivateAudioSessionAfterTTSIfIdle() {
// "setActive 成功后清零 + 下轮 startRecognition 读到 isOtherAudioPlaying=false 覆盖"
// 组合攻击擦掉,导致外部音乐恢复不了
let opts: AVAudioSession.SetActiveOptions = .notifyOthersOnDeactivation
// 释放前先把 category 切到 .ambient:让 iOS 明确收到"我们彻底不占 session 了"的信号,
// shouldResume 提示才能可靠送达 Apple Music / Spotify 等外部 App。
// 多个外部 App 对"仍处于 .playAndRecord 的 deactivate"会默认不恢复(视为潜在录音/通话),
// 切到 .ambient 后再 deactivate 是 Siri 以外多数语音助手实测通用的套路。
let prevCategory = session.category
let prevMode = session.mode
let prevOptions = session.categoryOptions
if prevCategory != .ambient {
do {
try session.setCategory(.ambient, mode: .default, options: [])
self.dlog("INFO", "deactivate 前先切到 .ambient(原 category=\(prevCategory.rawValue) mode=\(prevMode.rawValue))")
} catch {
self.dlog("WARNING", "切到 .ambient 失败: \(error.localizedDescription),继续原 category 释放")
}
}
self.dlog("INFO", "准备 setActive(false) flagHint=\(self.notifyOthersOnNextDeactivate) otherAudioPlaying=\(otherAudioPlaying) category=\(session.category.rawValue) mode=\(session.mode.rawValue)")
do {
try session.setActive(false, options: opts)

12
local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift

@ -177,14 +177,18 @@ public class SimpleAudioReceiver: NSObject {
desiredOptions = [.allowBluetooth, .defaultToSpeaker]
}
} else { // normal, push_to_talk, ble_wakeup 等:仅录音,不需要同时播报
desiredCategory = .record
// 不使用 .record:该 category 的 interruption 被外部音乐 App(Apple Music/Spotify 等)
// 归为"Siri/录音类打断",.notifyOthersOnDeactivation 拿不到 auto-resume。
// 改用 .playAndRecord,和 phone_call 对齐,iOS 会把这类打断归为"媒体打断",
// 外部 App 在 TTS 结束 session 释放时才能正常恢复。
desiredCategory = .playAndRecord
desiredMode = .default
desiredOptions = []
desiredOptions = [.defaultToSpeaker, .allowBluetoothA2DP]
}
// 如果在真实通话中,强制使用更适合的录音设置
if isTelephonyActive() {
desiredCategory = .record
desiredCategory = .playAndRecord
desiredMode = .voiceChat
desiredOptions = [.allowBluetooth]
}

22
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/OpusAudioManager.kt

@ -300,16 +300,10 @@ class OpusAudioManager {
* @param data 音频数据 (Opus编码)
*/
fun processAudioData(data: ByteArray) {
if (!isInitialized) {
Log.w(TAG, "OpusAudioManager未初始化,忽略音频数据")
return
}
if (!isInitialized) return
if (decodeOpusManager?.isDecodeStream == true) {
// 将数据加入队列进行异步处理
audioDataQueue.offer(data)
} else {
Log.d(TAG, "Opus解码流未启动,忽略音频数据")
}
}
@ -344,17 +338,8 @@ class OpusAudioManager {
Log.d(TAG, "准备开始Opus数据流编码")
encodeOpusManager?.startEncodeStream(object : OnEncodeStreamCallback {
override fun onEncodeStream(data: ByteArray?) {
Log.d(TAG, "Opus编码回调收到数据,大小: ${data?.size ?: 0} bytes")
if (data != null) {
// 编码完成的数据处理:
// 1. 通知回调
callback?.onAudioDataEncoded(data)
// // 2. 将编码后的数据加入发送队列进行分块发送
// addAudioDataToSendQueue(data)
} else {
Log.w(TAG, "编码回调收到空数据")
}
}
@ -411,13 +396,8 @@ class OpusAudioManager {
return
}
// 如果已经在编码流中,直接写入数据
if (encodeOpusManager?.isEncodeStream == true) {
Log.d(TAG, "正在进行Opus编码流,写入音频数据")
// 将外部音频数据写入编码流
encodeOpusManager?.writeEncodeStream(data)
} else {
Log.w(TAG, "Opus编码流未启动,无法写入音频数据")
}
}

8
local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift

@ -680,12 +680,8 @@ public class BleService: NSObject {
// MARK: - 编解码控制
public func writeExternalAudioData(data: Data) {
print("pcm音频进入编码", data.count)
// 确保Opus编码器处于活跃状态(如果不是则启动)
// 将外部音频数据写入编码流,每次处理1280字节
opusProcessor?.encodePCMData(data)
// 将外部音频数据写入编码流
opusProcessor?.encodePCMData(data)
}
/// 打开编码器

63
local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift

@ -443,26 +443,14 @@ class SwiftOpusAudioProcessor: NSObject {
/// 编码PCM音频数据
/// - Parameter pcmData: PCM音频数据
func encodePCMData(_ pcmData: Data) {
guard isEncodingActive, let encoder = jlOpusEncoder else {
print("编码器未启动或不可用")
return
}
guard !pcmData.isEmpty else {
print("PCM数据为空")
return
}
print("接收PCM数据 - 数据大小: \(pcmData.count) bytes")
guard isEncodingActive, let encoder = jlOpusEncoder else { return }
guard !pcmData.isEmpty else { return }
// 将新数据加入队列
queueLock.lock()
pcmDataQueue.append(pcmData)
let currentQueueSize = pcmDataQueue.count
queueLock.unlock()
print("PCM数据已加入队列 - 当前队列大小: \(currentQueueSize) bytes")
// 异步处理队列中的数据
processQueuedData()
}
@ -479,57 +467,33 @@ class SwiftOpusAudioProcessor: NSObject {
}
processingQueue?.async { [weak self, weak encoder] in
guard let self = self, let encoder = encoder else {
print("编码器或处理器已释放")
return
}
guard let self = self, let encoder = encoder else { return }
while self.isEncodingActive {
var chunkData: Data?
// 从队列中取出数据
self.queueLock.lock()
if self.pcmDataQueue.count >= self.chunkSize {
// 取出640字节数据
chunkData = self.pcmDataQueue.subdata(in: 0..<self.chunkSize)
self.pcmDataQueue.removeSubrange(0..<self.chunkSize)
} else if self.pcmDataQueue.count > 0 {
// 不足640字节,补齐静音数据
let availableBytes = self.pcmDataQueue.count
let silentBytesNeeded = self.chunkSize - availableBytes
// 创建静音数据(16位PCM静音为0)
let silentData = Data(repeating: 0, count: silentBytesNeeded)
// 组合实际数据和静音数据
chunkData = Data()
chunkData?.append(self.pcmDataQueue)
chunkData?.append(silentData)
// 清空队列
self.pcmDataQueue.removeAll()
print("数据不足640字节,已补齐静音数据 - 实际数据: \(availableBytes) bytes, 静音数据: \(silentBytesNeeded) bytes")
}
self.queueLock.unlock()
// 如果有数据需要编码
if let dataToEncode = chunkData {
guard self.isEncodingActive else {
print("编码已停止,退出处理循环")
break
}
// 编码数据块
guard self.isEncodingActive else { break }
encoder.opusEncode(dataToEncode)
print("已编码音频块 - 数据大小: \(dataToEncode.count) bytes")
} else {
// 队列为空,退出循环
break
}
}
print("队列处理完成")
}
}
@ -563,13 +527,8 @@ extension SwiftOpusAudioProcessor: JLOpusEncoderDelegate {
print("Opus编码错误: \(error.localizedDescription)")
return
}
guard let opusData = data, !opusData.isEmpty else {
print("编码后的Opus数据为空")
return
}
print("Opus编码成功 - 数据大小: \(opusData.count) bytes")
guard let opusData = data, !opusData.isEmpty else { return }
// 回调编码后的数据到主线程
DispatchQueue.main.async { [weak self] in

35
local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoAsrHelper.kt

@ -636,27 +636,17 @@ class VolcanoAsrHelper(private val context: Context) {
}
SpeechEngineDefines.MESSAGE_TYPE_PARTIAL_RESULT -> {
// 中间识别结果
Log.i(TAG, "中间识别结果: $data")
// 中间识别结果(高频,降级为 debug)
try {
val json = JSONObject(data)
if (!json.has("result")) {
Log.w(TAG, "中间结果没有 result 字段: $data")
return
}
// if (json.has("definite")) {
// Log.i(TAG, "utterances: ${json.getString("utterances")}")
// }
// 从多种可能的格式中获取文本
val text = extractTextFromResult(json)
// 获取语言信息,如果没有则使用默认语言
val detectedLanguage = json.optString("lang", language)
if (text.isNotEmpty()) {
mainHandler.post {
Log.i(TAG, "提取到中间文本: $text")
// 中间结果
continuousCallback?.onRecognizing(text, detectedLanguage)
}
}
@ -664,34 +654,25 @@ class VolcanoAsrHelper(private val context: Context) {
Log.e(TAG, "解析中间识别结果异常: ${e}")
}
}
SpeechEngineDefines.MESSAGE_TYPE_FINAL_RESULT -> {
Log.i(TAG, "最终识别结果: $data")
// 最终识别结果
try {
val json = JSONObject(data)
if (!json.has("result")) {
Log.w(TAG, "最终结果没有 result 字段: $data")
Log.w(TAG, "最终结果没有 result 字段")
return
}
// 从多种可能的格式中获取文本
val text = extractTextFromResult(json)
// 获取语言信息,如果没有则使用默认语言
val detectedLanguage = json.optString("lang", language)
if (text.isNotEmpty()) {
mainHandler.post {
if (isContinuousMode) {
Log.i(TAG, "发送最终识别结果: $text (连续模式)")
Log.i(TAG, "最终识别结果(连续): $text")
continuousCallback?.onResult(text, detectedLanguage)
// 在连续模式下,不需要手动触发下一轮识别,引擎会自动继续监听
// 但如果需要明确标记新一轮开始,可以在此处添加代码
} else {
Log.i(TAG, "发送最终识别结果: $text (单次模式)")
Log.i(TAG, "最终识别结果(单次): $text")
recognizeCallback?.onResult(text, detectedLanguage)
}
}

2
pubspec.yaml

@ -1,7 +1,7 @@
name: voitrans
description: "Voitrans - AI Voice Assistant."
publish_to: "none"
version: 1.0.26+101
version: 1.0.27+101
environment:
sdk: ">=3.3.0 <4.0.0"

Loading…
Cancel
Save