Browse Source

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

newdev_shunjiawei
lxm 1 year ago
parent
commit
8a353ae1a2
  1. 51
      lib/core/widgets/intro_overlay_builder.dart
  2. 12
      lib/modules/agent/views/agent_view.dart
  3. 176
      lib/modules/home/views/home_view.dart
  4. 2
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  5. 188
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  6. 176
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt
  7. 139
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/NetworkStateMonitor.kt
  8. 202
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
  9. 6
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift
  10. 105
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/IntegratedSpeechTranslationService.swift
  11. 70
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/VolcanoTranslationServiceImpl.swift
  12. 177
      local_plugins/azure_speech/ios/azure_speech/Sources/tools/NetworkStateMonitor.swift

51
lib/core/widgets/intro_overlay_builder.dart

@ -4,6 +4,18 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:lottie/lottie.dart'; import 'package:lottie/lottie.dart';
class IntroOverlayBuilder { class IntroOverlayBuilder {
static OverlayPosition overlayPosition({
required Offset offset,
required Size screenSize,
required Size size,
}) {
return OverlayPosition(
top: ScreenUtil().statusBarHeight,
width: screenSize.width,
crossAxisAlignment: CrossAxisAlignment.center,
);
}
static Widget homeBuilder( static Widget homeBuilder(
StepWidgetParams params, StepWidgetParams params,
bool isWakeup, { bool isWakeup, {
@ -12,6 +24,18 @@ class IntroOverlayBuilder {
}) { }) {
return Column( return Column(
children: [ children: [
Container(
padding: EdgeInsetsGeometry.all(12.w),
alignment: Alignment.centerRight,
child: _button(
'跳过',
() {
params.onFinish();
},
isSkip: true,
),
),
100.verticalSpace,
isWakeup isWakeup
? _AnimatedLottieWidget() ? _AnimatedLottieWidget()
: SizedBox( : SizedBox(
@ -31,27 +55,12 @@ class IntroOverlayBuilder {
), ),
), ),
if (onNext != null) if (onNext != null)
Row( _button(
mainAxisAlignment: MainAxisAlignment.center, '下一步',
children: [ () {
Padding( params.onNext!();
padding: EdgeInsets.only(right: 50.w), onNext();
child: _button( },
'下一步',
() {
params.onNext!();
onNext();
},
),
),
_button(
'跳过',
() {
params.onFinish();
},
isSkip: true,
),
],
), ),
], ],
); );

12
lib/modules/agent/views/agent_view.dart

@ -196,17 +196,7 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
IntroStepBuilder( IntroStepBuilder(
group: 'ai', group: 'ai',
order: 3, order: 3,
getOverlayPosition: ({ getOverlayPosition: IntroOverlayBuilder.overlayPosition,
required Offset offset,
required Size screenSize,
required Size size,
}) {
return OverlayPosition(
top: 200.h,
width: screenSize.width,
crossAxisAlignment: CrossAxisAlignment.end,
);
},
overlayBuilder: (params) { overlayBuilder: (params) {
return Obx( return Obx(
() => IntroOverlayBuilder.homeBuilder( () => IntroOverlayBuilder.homeBuilder(

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

@ -72,6 +72,11 @@ class HomeView extends GetView<HomeController> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// 预缓存图片
precacheImage(
const AssetImage('assets/images/headphone_new3.png'), context);
precacheImage(
const AssetImage('assets/images/headphone_new4.png'), context);
final bool isDarkMode = Theme.of(context).brightness == Brightness.dark; final bool isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold( return Scaffold(
backgroundColor: isDarkMode ? Colors.black : const Color(0xFFF9FAFC), backgroundColor: isDarkMode ? Colors.black : const Color(0xFFF9FAFC),
@ -83,77 +88,69 @@ class HomeView extends GetView<HomeController> {
children: [ children: [
// 顶部标题栏 // 顶部标题栏
_buildAppBar(isDarkMode), _buildAppBar(isDarkMode),
// 可变高度的顶部图片区域 - 使用Expanded让它占用剩余空间
// 可变高度的顶部图片区域
Obx(() => _buildAdaptiveEarbudsImage()),
SizedBox(height: 20.h),
// 设备状态卡片 - 使用 AnimatedContainer 实现平滑过渡
IntroStepBuilder(
group: 'home',
order: 1,
onWidgetLoad: () {
controller.noviceGuideService.startHomeGuide();
},
overlayBuilder: (params) {
return IntroOverlayBuilder.aiBuilder(
params,
'欢迎使用Voitrans!首先,让我们唤醒你的专属 AI—— 小言',
onNext: () {
controller.noviceGuideService.startWakeupListen();
},
);
},
builder: (BuildContext context, GlobalKey key) => SizedBox(
key: key,
child: _buildAnimatedDeviceStatusCard(isDarkMode),
),
),
SizedBox(height: 20.h),
IntroStepBuilder(
group: 'home',
order: 2,
getOverlayPosition: ({
required Offset offset,
required Size screenSize,
required Size size,
}) {
return OverlayPosition(
top: 200.h,
width: screenSize.width,
crossAxisAlignment: CrossAxisAlignment.end,
);
},
overlayBuilder: (params) {
return Obx(
() => IntroOverlayBuilder.homeBuilder(
params,
controller.noviceGuideService.isWakeup.value,
onNext: () {
controller.noviceGuideService.closeWakeupListen();
},
),
);
},
builder: (BuildContext context, GlobalKey key) => SizedBox(
key: key,
),
),
// 热门AI体验 - 使用 Expanded 确保始终在可视范围内
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildSectionTitle(isDarkMode), Obx(() => _buildAdaptiveEarbudsImage()),
SizedBox(height: 16.h), SizedBox(height: 20.h),
Expanded(child: _buildPremiumAILayout()), // 设备状态卡片
IntroStepBuilder(
group: 'home',
order: 1,
onWidgetLoad: () {
controller.noviceGuideService.startHomeGuide();
},
overlayBuilder: (params) {
return IntroOverlayBuilder.aiBuilder(
params,
'欢迎使用Voitrans!首先,让我们唤醒你的专属 AI—— 小言',
onNext: () {
controller.noviceGuideService.startWakeupListen();
},
);
},
builder: (BuildContext context, GlobalKey key) =>
SizedBox(
key: key,
child: _buildAnimatedDeviceStatusCard(isDarkMode),
),
),
SizedBox(height: 20.h),
IntroStepBuilder(
group: 'home',
order: 2,
getOverlayPosition: IntroOverlayBuilder.overlayPosition,
overlayBuilder: (params) {
return Obx(
() => IntroOverlayBuilder.homeBuilder(
params,
controller.noviceGuideService.isWakeup.value,
onNext: () {
controller.noviceGuideService.closeWakeupListen();
},
),
);
},
builder: (BuildContext context, GlobalKey key) =>
SizedBox(
key: key,
),
),
], ],
), ),
), ),
// 热门AI体验 - 固定在底部,基于内容的固定高度
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, // 重要:让高度基于内容
children: [
_buildSectionTitle(isDarkMode),
SizedBox(height: 16.h),
// 给AI布局一个固定的高度,或者让它基于内容自适应
_buildPremiumAILayout(),
],
),
], ],
), ),
), ),
@ -203,40 +200,39 @@ class HomeView extends GetView<HomeController> {
); );
} }
// 自适应高度的耳机图片 - 根据设备连接状态动态调整高度 // 自适应高度的耳机图片 - 根据设备连接状态动态调整高度
// 在自适应图片组件中添加RepaintBoundary
Widget _buildAdaptiveEarbudsImage() { Widget _buildAdaptiveEarbudsImage() {
final bool isDarkMode = Get.isDarkMode; final bool isDarkMode = Get.isDarkMode;
// 根据设备连接状态调整图片高度 final double imageHeight =
// 已连接时高度较小,为设备信息让出空间;未连接时高度较大 controller.isFullyConnectedRx.value ? 160.h : 220.h;
final double imageHeight = controller.isFullyConnectedRx.value ? 160.h : 220.h;
return RepaintBoundary(
return AnimatedContainer( // 添加重绘边界,优化动画性能
duration: const Duration(milliseconds: 500), // 平滑过渡动画 child: AnimatedContainer(
curve: Curves.easeInOut, duration: const Duration(milliseconds: 500),
height: imageHeight, curve: Curves.easeInOut,
child: PageView( height: imageHeight,
children: [ child: PageView(
Center( children: [
child: ClipRRect( Center(
borderRadius: BorderRadius.circular(16.r), child: ClipRRect(
child: Image.asset( borderRadius: BorderRadius.circular(16.r),
isDarkMode child: Image.asset(
? 'assets/images/headphone_new3.png' isDarkMode
: 'assets/images/headphone_new4.png', ? 'assets/images/headphone_new3.png'
height: imageHeight, : 'assets/images/headphone_new4.png',
fit: BoxFit.contain, height: imageHeight,
fit: BoxFit.contain,
),
), ),
), ),
), ],
], ),
), ),
); );
} }
// 带动画的设备状态卡片 - 实现平滑的高度变化过渡 // 带动画的设备状态卡片 - 实现平滑的高度变化过渡
Widget _buildAnimatedDeviceStatusCard(bool isDarkMode) { Widget _buildAnimatedDeviceStatusCard(bool isDarkMode) {
return Obx(() { return Obx(() {

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

@ -1727,7 +1727,7 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
stopIdleCheck() stopIdleCheck()
} }
func onError(_ error: String) { func onError(_ errorCode: Int, _ error: String) {
let data: [String: Any] = ["message": error.isEmpty ? "未知错误" : error] let data: [String: Any] = ["message": error.isEmpty ? "未知错误" : error]
sendEvent(name: "error", data: data) sendEvent(name: "error", data: data)
isRecognizing = false isRecognizing = false

188
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt

@ -2,31 +2,17 @@ package com.yunqiinnovation.azure_speech
import android.content.Context import android.content.Context
import android.media.AudioFormat
import android.media.AudioRecord
import android.media.MediaRecorder
import android.media.audiofx.AcousticEchoCanceler
import android.media.audiofx.NoiseSuppressor
import android.media.audiofx.AutomaticGainControl
import android.util.Log import android.util.Log
import com.microsoft.cognitiveservices.speech.* import com.microsoft.cognitiveservices.speech.*
import com.microsoft.cognitiveservices.speech.util.EventHandler import com.microsoft.cognitiveservices.speech.util.EventHandler
import com.yunqiinnovation.azure_speech.tools.SimpleAudioReceiver import com.yunqiinnovation.azure_speech.tools.SimpleAudioReceiver
import android.media.AudioManager
import android.media.AudioAttributes
import android.media.AudioFocusRequest
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import com.microsoft.cognitiveservices.speech.audio.PullAudioInputStreamCallback
import com.microsoft.cognitiveservices.speech.audio.AudioStreamFormat
import java.util.concurrent.BlockingQueue
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import com.yunqiinnovation.azure_speech.tools.RecordFile import com.yunqiinnovation.azure_speech.tools.RecordFile
import com.microsoft.cognitiveservices.speech.audio.AudioConfig import com.microsoft.cognitiveservices.speech.audio.AudioConfig
import com.microsoft.cognitiveservices.speech.audio.AudioInputStream
import com.microsoft.cognitiveservices.speech.audio.PushAudioInputStream
import java.util.concurrent.atomic.AtomicBoolean
/** /**
@ -35,11 +21,6 @@ import java.util.concurrent.atomic.AtomicBoolean
* 针对回音消除的场景,使用拉流模式 (PullAudioInputStreamCallback) 实现,避免复杂的协程处理 * 针对回音消除的场景,使用拉流模式 (PullAudioInputStreamCallback) 实现,避免复杂的协程处理
* 通过检测扬声器和耳机状态,自动决定是否启用回音消除 * 通过检测扬声器和耳机状态,自动决定是否启用回音消除
*/ */
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Build
class AzureAsrHelper(private val context: Context) { class AzureAsrHelper(private val context: Context) {
private val tag = "AzureAsrHelper" private val tag = "AzureAsrHelper"
@ -66,10 +47,8 @@ class AzureAsrHelper(private val context: Context) {
// 音频处理 // 音频处理
var audioStream: SimpleAudioReceiver? = null var audioStream: SimpleAudioReceiver? = null
// 网络状态监听相关 // 网络状态监听
private var connectivityManager: ConnectivityManager? = null private var networkMonitor: NetworkStateMonitor = NetworkStateMonitor(context)
private var networkCallback: ConnectivityManager.NetworkCallback? = null
private var isNetworkAvailable = true
private var isNetworkRecovering = false private var isNetworkRecovering = false
@ -150,10 +129,16 @@ class AzureAsrHelper(private val context: Context) {
// 设置分段策略为时间模式 // 设置分段策略为时间模式
setProperty("Speech_SegmentationStrategy", "Time") setProperty("Speech_SegmentationStrategy", "Time")
} }
// 初始化网络监听
networkMonitor.initialize(object : NetworkStateMonitor.NetworkStateListener {
// 初始化网络监听 override fun onNetworkAvailable() {
initNetworkMonitoring() handleNetworkAvailable()
}
override fun onNetworkLost() {
handleNetworkLost()
}
})
return true return true
} catch (e: Exception) { } catch (e: Exception) {
@ -289,30 +274,7 @@ class AzureAsrHelper(private val context: Context) {
} }
} }
/**
* 检查当前网络状态
* @return true表示网络可用,false表示网络不可用
*/
private fun checkNetworkStatus(): Boolean {
try {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val network = connectivityManager.activeNetwork ?: return false
val networkCapabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
return networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
} else {
@Suppress("DEPRECATION")
val networkInfo = connectivityManager.activeNetworkInfo
return networkInfo?.isConnectedOrConnecting == true
}
} catch (e: Exception) {
Log.e(tag, "检查网络状态失败: ${e.message}")
return false
}
}
/** /**
* 开始连续语音识别 * 开始连续语音识别
@ -544,13 +506,8 @@ class AzureAsrHelper(private val context: Context) {
// recognizer?.stopContinuousRecognitionAsync()?.get(1000, TimeUnit.MILLISECONDS) // recognizer?.stopContinuousRecognitionAsync()?.get(1000, TimeUnit.MILLISECONDS)
// 停止音频处理 // 停止音频处理
stopAudioProcessing() stopAudioProcessing()
// 清理网络监听
// 清理网络监听 networkMonitor.dispose()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && networkCallback != null) {
connectivityManager?.unregisterNetworkCallback(networkCallback!!)
networkCallback = null
}
connectivityManager = null
// 停止录音 // 停止录音
if (audioStream!!.recordfile != null) { if (audioStream!!.recordfile != null) {
@ -766,80 +723,34 @@ class AzureAsrHelper(private val context: Context) {
*/ */
fun onError(code: Int,error: String) fun onError(code: Int,error: String)
} }
/**
* 检查当前网络状态
* @return true表示网络可用,false表示网络不可用
*/
private fun checkNetworkStatus(): Boolean {
return networkMonitor.checkNetworkStatus()
}
/** /**
* 检查是否是网络相关错误 * 检查是否是网络相关错误
*/ */
private fun isNetworkRelatedError(reason: String, errorDetails: String): Boolean { private fun isNetworkRelatedError(reason: String, errorDetails: String): Boolean {
val networkErrorKeywords = arrayOf( return networkMonitor.isNetworkRelatedError(reason, errorDetails)
"network", "connection", "timeout", "unreachable",
"dns", "socket", "ssl", "tls", "certificate",
"网络", "连接", "超时", "不可达"
)
val combinedText = "$reason $errorDetails".lowercase()
return networkErrorKeywords.any { keyword ->
combinedText.contains(keyword.lowercase())
}
} }
/**
* 初始化网络状态监听
*/
private fun initNetworkMonitoring() {
try {
connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
super.onAvailable(network)
Log.d(tag, "网络连接可用")
handleNetworkAvailable()
}
override fun onLost(network: Network) {
super.onLost(network)
Log.d(tag, "网络连接丢失")
handleNetworkLost()
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
super.onCapabilitiesChanged(network, networkCapabilities)
val hasInternet = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
Log.d(tag, "网络能力变化,是否有网络: $hasInternet")
if (hasInternet && !isNetworkAvailable) {
handleNetworkAvailable()
} else if (!hasInternet && isNetworkAvailable) {
handleNetworkLost()
}
}
}
val networkRequest = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()
connectivityManager?.registerNetworkCallback(networkRequest, networkCallback!!)
}
} catch (e: Exception) {
Log.e(tag, "初始化网络监听失败: ${e.message}")
}
}
/** /**
* 处理网络连接可用 * 处理网络连接可用
*/ */
private fun handleNetworkAvailable() { private fun handleNetworkAvailable() {
if (!isNetworkAvailable) { if (!isNetworkRecovering) {
isNetworkAvailable = true
isNetworkRecovering = true isNetworkRecovering = true
Log.d(tag, "网络恢复,准备重新启动识别") Log.d(tag, "网络恢复,准备重新启动识别")
// 如果之前在进行连续识别,重新启动 // 如果之前在进行连续识别,重新启动
Handler(Looper.getMainLooper()).postDelayed({
Handler(Looper.getMainLooper()).postDelayed({ restartContinuousRecognition()
restartContinuousRecognition() }, 1000) // 延迟1秒确保网络稳定
}, 1000) // 延迟1秒确保网络稳定
} }
} }
@ -847,14 +758,8 @@ class AzureAsrHelper(private val context: Context) {
* 处理网络连接丢失 * 处理网络连接丢失
*/ */
private fun handleNetworkLost() { private fun handleNetworkLost() {
if (isNetworkAvailable) { Log.d(tag, "网络断开,停止音频处理")
isNetworkAvailable = false stopAudioProcessingImmediately()
Log.d(tag, "网络断开,停止音频处理")
stopAudioProcessingImmediately()
}
} }
/** /**
@ -863,7 +768,7 @@ class AzureAsrHelper(private val context: Context) {
private fun stopAudioProcessingImmediately() { private fun stopAudioProcessingImmediately() {
try { try {
Log.d(tag, "立即停止音频处理") Log.d(tag, "立即停止音频处理")
stopContinuousRecognition() stopContinuousRecognition()
// 不改变isContinuousRecognitionActive状态,保持识别意图 // 不改变isContinuousRecognitionActive状态,保持识别意图
} catch (e: Exception) { } catch (e: Exception) {
Log.e(tag, "立即停止音频处理失败: ${e.message}") Log.e(tag, "立即停止音频处理失败: ${e.message}")
@ -874,23 +779,18 @@ class AzureAsrHelper(private val context: Context) {
* 重新启动连续识别(网络恢复时使用) * 重新启动连续识别(网络恢复时使用)
*/ */
private fun restartContinuousRecognition() { private fun restartContinuousRecognition() {
try { try {
Log.d(tag, "重新启动连续识别") Log.d(tag, "重新启动连续识别")
// 检查continuousCallback是否为空 // 检查continuousCallback是否为空
val callback = continuousCallback val callback = continuousCallback
if (callback == null) { if (callback == null) {
Log.e(tag, "无法重新启动连续识别:回调为空") Log.e(tag, "无法重新启动连续识别:回调为空")
return return
} }
setupEventListeners(callback) setupEventListeners(callback)
isNetworkRecovering = false isNetworkRecovering = false
Log.d(tag, "连续识别重新启动成功") Log.d(tag, "连续识别重新启动成功")
// 通知上层网络已恢复
//continuousCallback?.onError("网络已恢复")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(tag, "重新启动连续识别失败: ${e.message}") Log.e(tag, "重新启动连续识别失败: ${e.message}")
isNetworkRecovering = false isNetworkRecovering = false

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

@ -644,19 +644,54 @@ class IntegratedSpeechTranslationService(
private fun getVoiceForLanguage(language: String): String { private fun getVoiceForLanguage(language: String): String {
return voiceCache.getOrPut(language) { return voiceCache.getOrPut(language) {
when (language) { when (language) {
"zh-CN" -> "zh-CN-XiaoxiaoNeural" // 中文相关
"zh-TW" -> "zh-TW-HsiaoChenNeural" "zh-CN" -> "zh-CN-XiaoxiaoNeural"
"en-US" -> "en-US-AriaNeural" "zh-TW" -> "zh-TW-HsiaoChenNeural"
"en-GB" -> "en-GB-SoniaNeural" "zh-HK" -> "zh-HK-HiuMaanNeural"
"ja-JP" -> "ja-JP-NanamiNeural"
"ko-KR" -> "ko-KR-SunHiNeural" // 英语相关
"fr-FR" -> "fr-FR-DeniseNeural" "en-US" -> "en-US-AriaNeural"
"de-DE" -> "de-DE-KatjaNeural"
"es-ES" -> "es-ES-ElviraNeural" // 亚洲语言
"ru-RU" -> "ru-RU-SvetlanaNeural" "ja-JP" -> "ja-JP-NanamiNeural"
"ar-SA" -> "ar-SA-ZariyahNeural" "ko-KR" -> "ko-KR-SunHiNeural"
"pt-BR" -> "pt-BR-FranciscaNeural" "th-TH" -> "th-TH-PremwadeeNeural"
"it-IT" -> "it-IT-ElsaNeural" "vi-VN" -> "vi-VN-HoaiMyNeural"
"id-ID" -> "id-ID-GadisNeural"
"hi-IN" -> "hi-IN-SwaraNeural"
"ms-MY" -> "ms-MY-YasminNeural"
"bn-IN" -> "bn-IN-TanishaaNeural"
"ta-IN" -> "ta-IN-PallaviNeural"
"te-IN" -> "te-IN-ShrutiNeural"
"mr-IN" -> "mr-IN-AarohiNeural"
"ur-IN" -> "ur-IN-GulNeural"
"tr-TR" -> "tr-TR-EmelNeural"
"fa-IR" -> "fa-IR-DilaraNeural"
// 欧洲语言
"fr-FR" -> "fr-FR-DeniseNeural"
"es-ES" -> "es-ES-ElviraNeural"
"pt-BR" -> "pt-BR-FranciscaNeural"
"it-IT" -> "it-IT-ElsaNeural"
"de-DE" -> "de-DE-KatjaNeural"
"ru-RU" -> "ru-RU-SvetlanaNeural"
"pl-PL" -> "pl-PL-AgnieszkaNeural"
"nl-NL" -> "nl-NL-ColetteNeural"
"sv-SE" -> "sv-SE-SofieNeural"
"cs-CZ" -> "cs-CZ-VlastaNeural"
"el-GR" -> "el-GR-AthinaNeural"
"ro-RO" -> "ro-RO-AlinaNeural"
"hu-HU" -> "hu-HU-NoemiNeural"
"uk-UA" -> "uk-UA-PolinaNeural"
"da-DK" -> "da-DK-ChristelNeural"
"fi-FI" -> "fi-FI-NooraNeural"
"nb-NO" -> "nb-NO-IselinNeural"
"hr-HR" -> "hr-HR-GabrijelaNeural"
"ca-ES" -> "ca-ES-JoanaNeural"
// 阿拉伯语和非洲语言
"ar-EG" -> "ar-EG-SalmaNeural"
"sw-KE" -> "sw-KE-ZuriNeural"
else -> "en-US-AriaNeural" else -> "en-US-AriaNeural"
}.also { }.also {
serviceConfig.currentVoice = it serviceConfig.currentVoice = it
@ -830,13 +865,45 @@ fun setAudioOutputDevice(device: com.deep_voice.speech.tts.AudioOutputDevice) {
/** /**
* 获取当前支持的语言列表 * 获取当前支持的语言列表
*/ */
fun getSupportedLanguages(): List<String> {
return listOf( fun getSupportedLanguages(): List<String> {
"zh-CN", "zh-TW", "en-US", "en-GB", "ja-JP", "ko-KR", return listOf(
"fr-FR", "de-DE", "es-ES", "ru-RU", "ar-SA", "pt-BR", // 亚洲语言
"it-IT", "th-TH", "vi-VN", "hi-IN" "zh-CN", "zh-TW", "zh-HK", "ja-JP", "ko-KR",
) "hi-IN", "ta-IN", "te-IN", "bn-IN", "pa-IN",
} "ur-PK", "th-TH", "vi-VN", "ms-MY", "id-ID",
"fil-PH", "km-KH", "my-MM", "lo-LA", "ne-NP",
// 欧洲语言
"en-US", "en-GB", "en-CA", "en-AU", "fr-FR",
"fr-CA", "de-DE", "es-ES", "es-MX", "it-IT",
"nl-NL", "pl-PL", "ru-RU", "tr-TR", "uk-UA",
"cs-CZ", "hu-HU", "sv-SE", "fi-FI", "da-DK",
"no-NO", "ro-RO", "el-GR", "bg-BG", "hr-HR",
"sr-RS", "sk-SK", "sl-SI", "lt-LT", "lv-LV",
"et-EE", "is-IS", "ga-IE",
// 中东/非洲语言
"ar-SA", "ar-EG", "he-IL", "fa-IR", "ps-AF",
"ku-TR", "sw-KE", "am-ET", "yo-NG", "zu-ZA",
"xh-ZA", "af-ZA", "st-ZA", "tn-ZA", "ha-NG",
"ig-NG", "mg-MG", "rw-RW", "so-SO", "ti-ER",
// 美洲/大洋洲
"pt-BR", "pt-PT", "qu-PE", "gn-PY", "ay-BO",
"mi-NZ", "haw-US", "sm-WS", "to-TO", "fj-FJ",
// 其他重要语言
"as-IN", "or-IN", "kn-IN", "ml-IN", "gu-IN",
"mr-IN", "sa-IN", "sd-IN", "bo-CN", "ug-CN",
"ii-CN", "mn-MN", "jv-ID", "su-ID", "ceb-PH",
"gl-ES", "eu-ES", "ca-ES", "gd-GB", "cy-GB",
"br-FR", "fy-NL", "lb-LU", "mt-MT", "sq-AL",
"hy-AM", "ka-GE", "be-BY", "kk-KZ", "uz-UZ",
"ky-KG", "tg-TJ", "tk-TM", "tt-RU", "cv-RU"
)
}
/** /**
* 设置识别参数 * 设置识别参数
@ -1219,24 +1286,57 @@ class VolcanoTranslationServiceImpl :
.build() .build()
// 语言代码映射 // 语言代码映射
private val languageCodeMap = mapOf( private val languageCodeMap = mapOf(
"zh-CN" to "zh", // 亚洲
"zh-TW" to "zh-Hant", "zh-CN" to "zh", // 简体中文(中国大陆)
"en-US" to "en", "zh-TW" to "zh-Hant", // 繁体中文(台湾)
"en-GB" to "en", "zh-HK" to "zh-Hant", // 繁体中文(香港)
"ja-JP" to "ja", "ja-JP" to "ja", // 日语(日本)
"ko-KR" to "ko", "ko-KR" to "ko", // 韩语(韩国)
"fr-FR" to "fr", "hi-IN" to "hi", // 印地语(印度)
"de-DE" to "de", "ta-IN" to "ta", // 泰米尔语(印度)
"es-ES" to "es", "te-IN" to "te", // 泰卢固语(印度)
"ru-RU" to "ru", "bn-IN" to "bn", // 孟加拉语(印度)
"ar-SA" to "ar", "pa-IN" to "pa", // 旁遮普语(印度)
"pt-BR" to "pt", "ur-PK" to "ur", // 乌尔都语(巴基斯坦)
"it-IT" to "it", "th-TH" to "th", // 泰语(泰国)
"th-TH" to "th", "vi-VN" to "vi", // 越南语(越南)
"vi-VN" to "vi", "ms-MY" to "ms", // 马来语(马来西亚)
"hi-IN" to "hi" "id-ID" to "id", // 印尼语(印尼)
) "fil-PH" to "tl", // 菲律宾语(菲律宾)
// 欧洲
"en-US" to "en", // 英语(美国)
"en-GB" to "en", // 英语(英国)
"fr-FR" to "fr", // 法语(法国)
"fr-CA" to "fr", // 法语(加拿大)
"de-DE" to "de", // 德语(德国)
"es-ES" to "es", // 西班牙语(西班牙)
"es-MX" to "es", // 西班牙语(墨西哥)
"it-IT" to "it", // 意大利语(意大利)
"nl-NL" to "nl", // 荷兰语(荷兰)
"pl-PL" to "pl", // 波兰语(波兰)
"ru-RU" to "ru", // 俄语(俄罗斯)
"tr-TR" to "tr", // 土耳其语(土耳其)
// 中东
"ar-SA" to "ar", // 阿拉伯语(沙特)
"ar-EG" to "ar", // 阿拉伯语(埃及)
"he-IL" to "he", // 希伯来语(以色列)
// 非洲
"sw-KE" to "sw", // 斯瓦希里语(肯尼亚)
"am-ET" to "am", // 阿姆哈拉语(埃塞俄比亚)
"yo-NG" to "yo", // 约鲁巴语(尼日利亚)
// 其他
"pt-BR" to "pt", // 葡萄牙语(巴西)
"pt-PT" to "pt", // 葡萄牙语(葡萄牙)
"fa-IR" to "fa", // 波斯语(伊朗)
"el-GR" to "el", // 希腊语(希腊)
"cs-CZ" to "cs", // 捷克语(捷克)
"uk-UA" to "uk", // 乌克兰语(乌克兰)
)
override suspend fun initialize(config: Map<String, String>): Boolean { override suspend fun initialize(config: Map<String, String>): Boolean {
return try { return try {

139
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/NetworkStateMonitor.kt

@ -0,0 +1,139 @@
// NetworkStateMonitor.kt
package com.yunqiinnovation.azure_speech
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Build
import android.util.Log
/**
* 网络状态监听器,负责监控网络连接状态变化
*/
class NetworkStateMonitor(private val context: Context) {
private val tag = "NetworkStateMonitor"
private var connectivityManager: ConnectivityManager? = null
private var networkCallback: ConnectivityManager.NetworkCallback? = null
private var isNetworkAvailable = true
private var networkStateListener: NetworkStateListener? = null
/**
* 网络状态监听接口
*/
interface NetworkStateListener {
fun onNetworkAvailable()
fun onNetworkLost()
}
/**
* 初始化网络状态监听
*/
fun initialize(listener: NetworkStateListener? = null) {
networkStateListener = listener
initNetworkMonitoring()
}
/**
* 检查当前网络状态
* @return true表示网络可用,false表示网络不可用
*/
fun checkNetworkStatus(): Boolean {
try {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val network = connectivityManager.activeNetwork ?: return false
val networkCapabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
return networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
} else {
@Suppress("DEPRECATION")
val networkInfo = connectivityManager.activeNetworkInfo
return networkInfo?.isConnectedOrConnecting == true
}
} catch (e: Exception) {
Log.e(tag, "检查网络状态失败: ${e.message}")
return false
}
}
/**
* 初始化网络状态监听
*/
private fun initNetworkMonitoring() {
try {
connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
super.onAvailable(network)
Log.d(tag, "网络连接可用")
isNetworkAvailable = true
networkStateListener?.onNetworkAvailable()
}
override fun onLost(network: Network) {
super.onLost(network)
Log.d(tag, "网络连接丢失")
isNetworkAvailable = false
networkStateListener?.onNetworkLost()
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
super.onCapabilitiesChanged(network, networkCapabilities)
val hasInternet = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
Log.d(tag, "网络能力变化,是否有网络: $hasInternet")
if (hasInternet && !isNetworkAvailable) {
isNetworkAvailable = true
networkStateListener?.onNetworkAvailable()
} else if (!hasInternet && isNetworkAvailable) {
isNetworkAvailable = false
networkStateListener?.onNetworkLost()
}
}
}
val networkRequest = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()
connectivityManager?.registerNetworkCallback(networkRequest, networkCallback!!)
}
} catch (e: Exception) {
Log.e(tag, "初始化网络监听失败: ${e.message}")
}
}
/**
* 释放资源
*/
fun dispose() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && networkCallback != null) {
connectivityManager?.unregisterNetworkCallback(networkCallback!!)
networkCallback = null
}
connectivityManager = null
networkStateListener = null
}
/**
* 检查是否是网络相关错误
*/
fun isNetworkRelatedError(reason: String, errorDetails: String): Boolean {
val networkErrorKeywords = arrayOf(
"network", "connection", "timeout", "unreachable",
"dns", "socket", "ssl", "tls", "certificate",
"网络", "连接", "超时", "不可达"
)
val combinedText = "$reason $errorDetails".lowercase()
return networkErrorKeywords.any { keyword ->
combinedText.contains(keyword.lowercase())
}
}
}

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

@ -3,7 +3,8 @@ import AVFoundation
import MicrosoftCognitiveServicesSpeech import MicrosoftCognitiveServicesSpeech
import speech import speech
import os.log import os.log
import Network // 移除 Network 导入,因为我们将使用独立的 NetworkStateMonitor
// import Network
// MARK: - 基于微软Azure语音服务的ASR实现 // MARK: - 基于微软Azure语音服务的ASR实现
/** /**
@ -22,12 +23,10 @@ public class AzureAsrHelper: NSObject {
private var recognizer: SPXSpeechRecognizer? private var recognizer: SPXSpeechRecognizer?
private var audioConfig: SPXAudioConfiguration? private var audioConfig: SPXAudioConfiguration?
// 网络监听相关 // 网络监听相关 - 使用独立的NetworkStateMonitor
private var networkMonitor: NWPathMonitor? private var networkMonitor: NetworkStateMonitor = NetworkStateMonitor()
private var networkQueue: DispatchQueue?
private var isNetworkAvailable = true
private var continuousCallback: ContinuousRecognizeCallback? private var continuousCallback: ContinuousRecognizeCallback?
private var isNetworkRecovering = false
// 状态管理 // 状态管理
private var _isContinuousRecognitionActive = false private var _isContinuousRecognitionActive = false
@ -117,7 +116,7 @@ public class AzureAsrHelper: NSObject {
// 预初始化音频组件 // 预初始化音频组件
preInitializeAudioComponents() preInitializeAudioComponents()
// 初始化网络监听 // 初始化网络监听 - 使用独立的NetworkStateMonitor
initNetworkMonitoring() initNetworkMonitoring()
return true return true
@ -126,77 +125,77 @@ public class AzureAsrHelper: NSObject {
return false return false
} }
} }
/**
* 初始化网络监听 /**
*/ * 初始化网络监听
private func initNetworkMonitoring() { * 使用独立的NetworkStateMonitor类
networkQueue = DispatchQueue(label: "NetworkMonitor") */
networkMonitor = NWPathMonitor() private func initNetworkMonitoring() {
networkMonitor.initialize(listener: self)
networkMonitor?.pathUpdateHandler = { [weak self] path in os_log("网络监听已初始化", log: log, type: .info)
guard let self = self else { return } }
let wasAvailable = self.isNetworkAvailable /**
self.isNetworkAvailable = (path.status == .satisfied) * 处理网络可用事件
*/
if wasAvailable && !self.isNetworkAvailable { private func handleNetworkAvailable() {
// 网络断开 os_log("网络已恢复", log: log, type: .info)
self.handleNetworkLost()
} else if !wasAvailable && self.isNetworkAvailable { DispatchQueue.main.async { [weak self] in
// 网络恢复 guard let self = self else { return }
self.handleNetworkAvailable() if !self.isNetworkRecovering {
} self.isNetworkRecovering = true
} os_log("网络恢复,准备重新启动识别", log: self.log, type: .info)
if let queue = networkQueue {
networkMonitor?.start(queue: queue)
}
os_log("网络监听已初始化", log: log, type: .info)
}
/**
* 处理网络可用事件
*/
private func handleNetworkAvailable() {
os_log("网络已恢复", log: log, type: .info)
DispatchQueue.main.async { [weak self] in
// self?.continuousCallback?.onNetworkRestored()
}
}
/**
* 处理网络丢失事件
*/
private func handleNetworkLost() {
os_log("网络连接丢失", log: log, type: .error)
// 立即停止音频处理
stopAudioProcessingImmediately()
DispatchQueue.main.async { [weak self] in // 如果之前在进行连续识别,重新启动
// self?.continuousCallback?.onNetworkError("网络连接已断开") DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.restartContinuousRecognition()
} }
} }
}
/** }
* 立即停止音频处理
*/ /**
private func stopAudioProcessingImmediately() { * 重新启动连续识别(网络恢复时使用)
audioStream?.stopMicrophoneCapture() */
private func restartContinuousRecognition() {
if _isContinuousRecognitionActive { os_log("重新启动连续识别", log: log, type: .info)
do {
try recognizer?.stopContinuousRecognition() // 检查continuousCallback是否为空
_isContinuousRecognitionActive = false guard let callback = continuousCallback else {
os_log("因网络问题强制停止识别", log: log, type: .info) os_log("连续识别回调为空,无法重新启动", log: log, type: .info)
} catch { return
_isContinuousRecognitionActive = false }
os_log("强制停止识别失败: %@", log: log, type: .error, error.localizedDescription)
} do {
} _ = setupEventListeners(callback: callback)
isNetworkRecovering = false
os_log("连续识别重新启动成功", log: log, type: .info)
} catch {
os_log("重新启动连续识别失败: %@", log: log, type: .error, error.localizedDescription)
isNetworkRecovering = false
}
}
/**
* 立即停止音频处理
*/
private func stopAudioProcessingImmediately() {
audioStream?.stopMicrophoneCapture()
if _isContinuousRecognitionActive {
do {
try recognizer?.stopContinuousRecognition()
_isContinuousRecognitionActive = false
os_log("因网络问题强制停止识别", log: log, type: .info)
} catch {
_isContinuousRecognitionActive = false
os_log("强制停止识别失败: %@", log: log, type: .error, error.localizedDescription)
} }
}
}
/** /**
* 预初始化音频组件 * 预初始化音频组件
* 在初始化阶段就准备好音频流和相关组件,减少首次启动延迟 * 在初始化阶段就准备好音频流和相关组件,减少首次启动延迟
@ -352,6 +351,8 @@ public class AzureAsrHelper: NSObject {
// 停止音频处理 // 停止音频处理
audioStream?.releaseAudioResources() audioStream?.releaseAudioResources()
// 释放网络监听资源
networkMonitor.dispose()
// 释放资源 // 释放资源
recognizer = nil recognizer = nil
speechConfig = nil speechConfig = nil
@ -614,7 +615,7 @@ public class AzureAsrHelper: NSObject {
} }
// 取消事件 // 取消事件
recognizer.addCanceledEventHandler { [weak self] (sender, event) in recognizer.addCanceledEventHandler { [weak self] (_, event) in
guard let self = self else { return } guard let self = self else { return }
print("取消事件:") print("取消事件:")
let errorDetails = event.errorDetails ?? "未知错误" let errorDetails = event.errorDetails ?? "未知错误"
@ -624,7 +625,13 @@ public class AzureAsrHelper: NSObject {
// 检查是否为网络相关错误 // 检查是否为网络相关错误
if self.isNetworkRelatedError(errorDetails) { if self.isNetworkRelatedError(errorDetails) {
callback.onError("网络连接问题: \(errorDetails)") os_log("检测到网络相关错误,停止音频处理", log: self.log, type: .info)
self.stopAudioProcessingImmediately()
// 如果不是正在恢复网络,通知上层网络错误
if !self.isNetworkRecovering {
callback.onError(1000,"启动连续识别失败,网络连接已断开")
}
} else { } else {
callback.onCanceled(reason, errorDetails) callback.onCanceled(reason, errorDetails)
} }
@ -795,24 +802,19 @@ public class AzureAsrHelper: NSObject {
* *
* @param error 错误信息 * @param error 错误信息
*/ */
func onError(_ error: String) func onError(_ code: Int,_ error: String)
} }
/** /**
* 检查网络状态 * 检查网络状态
* @return 网络是否可用 * @return 网络是否可用
*/ */
private func checkNetworkStatus() -> Bool { private func checkNetworkStatus() -> Bool {
guard let monitor = networkMonitor else { // 直接使用 NetworkStateMonitor 的 checkNetworkStatus 方法
os_log("网络监听器未初始化,假设网络可用", log: log) let isAvailable = networkMonitor.checkNetworkStatus()
return true
}
let currentPath = monitor.currentPath
let isAvailable = currentPath.status == .satisfied
if !isAvailable { if !isAvailable {
os_log("网络不可用,无法启动语音识别", log: log, type: .error) os_log("网络不可用,无法启动语音识别", log: log, type: .error)
continuousCallback?.onError("网络连接不可用,请检查网络设置") continuousCallback?.onError(1000,"网络连接不可用,请检查网络设置")
} }
return isAvailable return isAvailable
@ -835,7 +837,35 @@ private func isNetworkRelatedError(_ errorDetails: String) -> Bool {
} }
} }
// MARK: - NetworkStateListener实现
extension AzureAsrHelper: NetworkStateMonitor.NetworkStateListener {
/**
* 处理网络可用事件
*/
func onNetworkAvailable() {
os_log("网络已恢复", log: log, type: .info)
DispatchQueue.main.async { [weak self] in
self?.handleNetworkAvailable()
}
}
/**
* 处理网络丢失事件
*/
func onNetworkLost() {
os_log("网络连接丢失", log: log, type: .error)
// 立即停止音频处理
stopAudioProcessingImmediately()
DispatchQueue.main.async { [weak self] in
self?.continuousCallback?.onError(1000,"网络连接已断开")
}
}
}
// //ios 语言检测 // //ios 语言检测
// /** // /**
// * 检测输入文本的语言,从支持的语言列表中选择最匹配的 // * 检测输入文本的语言,从支持的语言列表中选择最匹配的

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

@ -1057,12 +1057,14 @@ private class AsrCallbackWrapper: AzureAsrHelper.ContinuousRecognizeCallback {
/** /**
* 错误回调 * 错误回调
* @param errorCode 错误码
* @param error 错误信息 * @param error 错误信息
*/ */
func onError(_ error: String) { func onError(_ errorCode: Int, _ error: String) {
plugin?.sendAsrEvent([ plugin?.sendAsrEvent([
"type": "error", "type": "error",
"error": error "code": errorCode,
"message": error
]) ])
} }
} }

105
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/IntegratedSpeechTranslationService.swift

@ -212,7 +212,12 @@ import os.log
serviceConfig: ServiceConfiguration? = nil, serviceConfig: ServiceConfiguration? = nil,
callback: ServiceEventCallback callback: ServiceEventCallback
) async -> Bool { ) async -> Bool {
// 重新初始化前先清理
if (serviceState.isInitialized) {
os_log("检测到服务已初始化,先进行清理", log: log, type: .info)
dispose()
}
do { do {
self.eventCallback = callback self.eventCallback = callback
@ -285,19 +290,10 @@ import os.log
config.speechRecognitionLanguage = serviceConfig.sourceLanguage config.speechRecognitionLanguage = serviceConfig.sourceLanguage
config.speechSynthesisVoiceName = getVoiceForLanguage(serviceConfig.targetLanguage) config.speechSynthesisVoiceName = getVoiceForLanguage(serviceConfig.targetLanguage)
config.setSpeechSynthesisOutputFormat(.riff16Khz16BitMonoPcm) config.setSpeechSynthesisOutputFormat(.riff16Khz16BitMonoPcm)
// 优化配置
config.setPropertyTo(Self.END_SILENCE_TIMEOUT, byName: "SpeechServiceConnection_EndSilenceTimeoutMs")
config.setPropertyTo(Self.SEGMENTATION_SILENCE_TIMEOUT, byName: "Speech_SegmentationSilenceTimeoutMs")
config.setPropertyTo(Self.INITIAL_SILENCE_TIMEOUT, byName: "SpeechServiceConnection_InitialSilenceTimeoutMs")
config.setPropertyTo("INTERACTIVE", byName: "SpeechServiceConnection_RecoMode")
// 启用详细结果
config.setPropertyTo("true", byName: "SpeechServiceResponse_RequestDetailedResultTrueFalse")
// 自动语言检测 // 自动语言检测
if serviceConfig.enableAutoLanguageDetection { if serviceConfig.enableAutoLanguageDetection {
config.setPropertyTo("Continuous", byName: "SpeechServiceConnection_LanguageIdMode") config.setPropertyTo("Continuous", by: SPXPropertyId.speechServiceConnectionLanguageIdMode)
} }
os_log("Azure语音服务配置完成", log: log, type: .info) os_log("Azure语音服务配置完成", log: log, type: .info)
@ -767,24 +763,73 @@ import os.log
} }
let voice: String let voice: String
switch language { switch language {
case "zh-CN": // 亚洲地区
voice = "zh-CN-XiaoxiaoNeural" case "zh-CN":
case "en-US": voice = "zh-CN-XiaoxiaoNeural"
voice = "en-US-AriaNeural" case "zh-TW":
case "ja-JP": voice = "zh-TW-HsiaoNeural"
voice = "ja-JP-NanamiNeural" case "zh-HK":
case "ko-KR": voice = "zh-HK-HiuGaaiNeural"
voice = "ko-KR-SunHiNeural" case "ja-JP":
case "fr-FR": voice = "ja-JP-NanamiNeural"
voice = "fr-FR-DeniseNeural" case "ko-KR":
case "de-DE": voice = "ko-KR-SunHiNeural"
voice = "de-DE-KatjaNeural" case "hi-IN":
case "es-ES": voice = "hi-IN-KalpanaNeural"
voice = "es-ES-ElviraNeural" case "ta-IN":
default: voice = "ta-IN-SundarNeural"
voice = "en-US-AriaNeural" case "th-TH":
} voice = "th-TH-PattaraNeural"
case "vi-VN":
voice = "vi-VN-AnhNeural"
// 欧洲地区
case "en-US":
voice = "en-US-AriaNeural"
case "en-GB":
voice = "en-GB-SusanNeural"
case "fr-FR":
voice = "fr-FR-DeniseNeural"
case "de-DE":
voice = "de-DE-KatjaNeural"
case "es-ES":
voice = "es-ES-ElviraNeural"
case "it-IT":
voice = "it-IT-DonatellaNeural"
case "pl-PL":
voice = "pl-PL-JozefinaNeural"
case "ru-RU":
voice = "ru-RU-DariaNeural"
// 中东/非洲
case "ar-SA":
voice = "ar-SA-MajdNeural"
case "he-IL":
voice = "he-IL-NoaNeural"
case "sw-KE":
voice = "sw-KE-SabinaNeural"
// 美洲/大洋洲
case "pt-BR":
voice = "pt-BR-ManuelaNeural"
case "es-MX":
voice = "es-MX-CarlaNeural"
// 其他重要语言
case "tr-TR":
voice = "tr-TR-SerifeNeural"
case "uk-UA":
voice = "uk-UA-OksanaNeural"
case "fa-IR":
voice = "fa-IR-SaraNeural"
case "nl-NL":
voice = "nl-NL-LotteNeural"
default:
voice = "en-US-AriaNeural" // 默认回退英语
}
voiceCache[language] = voice voiceCache[language] = voice
return voice return voice

70
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/VolcanoTranslationServiceImpl.swift

@ -34,24 +34,58 @@ public class VolcanoTranslationServiceImpl: IntegratedSpeechTranslationService.T
}() }()
// 语言代码映射 // 语言代码映射
private let languageCodeMap: [String: String] = [ private let languageCodeMap: [String: String] = [
"zh-CN": "zh", // 亚洲
"zh-TW": "zh-Hant", "zh-CN": "zh", // 简体中文(中国大陆)
"en-US": "en", "zh-TW": "zh-Hant", // 繁体中文(台湾)
"en-GB": "en", "zh-HK": "zh-Hant", // 繁体中文(香港)
"ja-JP": "ja", "ja-JP": "ja", // 日语(日本)
"ko-KR": "ko", "ko-KR": "ko", // 韩语(韩国)
"fr-FR": "fr", "hi-IN": "hi", // 印地语(印度)
"de-DE": "de", "ta-IN": "ta", // 泰米尔语(印度)
"es-ES": "es", "te-IN": "te", // 泰卢固语(印度)
"ru-RU": "ru", "bn-IN": "bn", // 孟加拉语(印度)
"ar-SA": "ar", "pa-IN": "pa", // 旁遮普语(印度)
"pt-BR": "pt", "ur-PK": "ur", // 乌尔都语(巴基斯坦)
"it-IT": "it", "th-TH": "th", // 泰语(泰国)
"th-TH": "th", "vi-VN": "vi", // 越南语(越南)
"vi-VN": "vi", "ms-MY": "ms", // 马来语(马来西亚)
"hi-IN": "hi" "id-ID": "id", // 印尼语(印尼)
] "fil-PH": "tl", // 菲律宾语(菲律宾)
// 欧洲
"en-US": "en", // 英语(美国)
"en-GB": "en", // 英语(英国)
"fr-FR": "fr", // 法语(法国)
"fr-CA": "fr", // 法语(加拿大)
"de-DE": "de", // 德语(德国)
"es-ES": "es", // 西班牙语(西班牙)
"es-MX": "es", // 西班牙语(墨西哥)
"it-IT": "it", // 意大利语(意大利)
"nl-NL": "nl", // 荷兰语(荷兰)
"pl-PL": "pl", // 波兰语(波兰)
"ru-RU": "ru", // 俄语(俄罗斯)
"tr-TR": "tr", // 土耳其语(土耳其)
// 中东
"ar-SA": "ar", // 阿拉伯语(沙特)
"ar-EG": "ar", // 阿拉伯语(埃及)
"he-IL": "he", // 希伯来语(以色列)
// 非洲
"sw-KE": "sw", // 斯瓦希里语(肯尼亚)
"am-ET": "am", // 阿姆哈拉语(埃塞俄比亚)
"yo-NG": "yo", // 约鲁巴语(尼日利亚)
// 其他
"pt-BR": "pt", // 葡萄牙语(巴西)
"pt-PT": "pt", // 葡萄牙语(葡萄牙)
"fa-IR": "fa", // 波斯语(伊朗)
"el-GR": "el", // 希腊语(希腊)
"cs-CZ": "cs", // 捷克语(捷克)
"uk-UA": "uk", // 乌克兰语(乌克兰)
]
// MARK: - TranslationServiceInterface Implementation // MARK: - TranslationServiceInterface Implementation

177
local_plugins/azure_speech/ios/azure_speech/Sources/tools/NetworkStateMonitor.swift

@ -0,0 +1,177 @@
import Foundation
import SystemConfiguration
import Network
/// 网络状态监听器(iOS)
final class NetworkStateMonitor {
private let tag = "NetworkStateMonitor"
// iOS 12+
private var monitor: NWPathMonitor?
private let monitorQueue = DispatchQueue(label: "NetworkStateMonitor.queue")
// iOS 10–11 回退
private var reachability: SCNetworkReachability?
private var reachabilityQueue: DispatchQueue?
private(set) var isNetworkAvailable: Bool = true
private weak var networkStateListener: NetworkStateListener?
/// 网络状态监听接口
protocol NetworkStateListener: AnyObject {
func onNetworkAvailable()
func onNetworkLost()
}
/// 初始化网络状态监听
func initialize(listener: NetworkStateListener? = nil) {
networkStateListener = listener
initNetworkMonitoring()
}
/// 检查当前网络状态
/// - Returns: true 表示网络可用
func checkNetworkStatus() -> Bool {
if #available(iOS 12.0, *) {
// NWPathMonitor 只在回调里更新状态,因此这里返回缓存的 isNetworkAvailable
return isNetworkAvailable
} else {
// 使用 Reachability 即时检测
var zeroAddress = sockaddr_in()
zeroAddress.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
zeroAddress.sin_family = sa_family_t(AF_INET)
let ref = withUnsafePointer(to: &zeroAddress) {
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
SCNetworkReachabilityCreateWithAddress(nil, $0)
}
}
guard let reachRef = ref else { return false }
var flags = SCNetworkReachabilityFlags()
if !SCNetworkReachabilityGetFlags(reachRef, &flags) { return false }
return Self.isReachable(flags: flags)
}
}
/// 释放资源
func dispose() {
if #available(iOS 12.0, *) {
monitor?.cancel()
monitor = nil
} else {
if let reachability = reachability {
SCNetworkReachabilitySetDispatchQueue(reachability, nil)
SCNetworkReachabilitySetCallback(reachability, nil, nil)
}
reachability = nil
reachabilityQueue = nil
}
networkStateListener = nil
}
/// 判断是否为网络相关错误(关键字简单匹配)
func isNetworkRelatedError(reason: String, errorDetails: String) -> Bool {
let networkErrorKeywords = [
"network","connection","timeout","unreachable",
"dns","socket","ssl","tls","certificate",
"网络","连接","超时","不可达"
]
let combined = (reason + " " + errorDetails).lowercased()
return networkErrorKeywords.contains { combined.contains($0.lowercased()) }
}
}
// MARK: - Private
private extension NetworkStateMonitor {
func initNetworkMonitoring() {
if #available(iOS 12.0, *) {
let monitor = NWPathMonitor()
self.monitor = monitor
monitor.pathUpdateHandler = { [weak self] path in
guard let self = self else { return }
let hasInternet = (path.status == .satisfied)
// 只有状态变化时才回调
if hasInternet && !self.isNetworkAvailable {
self.isNetworkAvailable = true
self.networkStateListener?.onNetworkAvailable()
Self.log(self.tag, "网络连接可用")
} else if !hasInternet && self.isNetworkAvailable {
self.isNetworkAvailable = false
self.networkStateListener?.onNetworkLost()
Self.log(self.tag, "网络连接丢失")
} else {
Self.log(self.tag, "网络能力变化,是否有网络: \(hasInternet)")
}
}
monitor.start(queue: monitorQueue)
} else {
// iOS 10–11 使用 Reachability 回退
var zeroAddress = sockaddr_in()
zeroAddress.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
zeroAddress.sin_family = sa_family_t(AF_INET)
reachability = withUnsafePointer(to: &zeroAddress) {
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
SCNetworkReachabilityCreateWithAddress(nil, $0)
}
}
guard let reachability = reachability else {
Self.log(tag, "初始化网络监听失败: Reachability 创建失败")
return
}
let queue = DispatchQueue(label: "NetworkStateMonitor.reachability")
reachabilityQueue = queue
var context = SCNetworkReachabilityContext(
version: 0,
info: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()),
retain: nil,
release: nil,
copyDescription: nil
)
let callback: SCNetworkReachabilityCallBack = { (_, flags, info) in
guard let info = info else { return }
let monitor = Unmanaged<NetworkStateMonitor>.fromOpaque(info).takeUnretainedValue()
let hasInternet = NetworkStateMonitor.isReachable(flags: flags)
if hasInternet && !monitor.isNetworkAvailable {
monitor.isNetworkAvailable = true
monitor.networkStateListener?.onNetworkAvailable()
NetworkStateMonitor.log(monitor.tag, "网络连接可用")
} else if !hasInternet && monitor.isNetworkAvailable {
monitor.isNetworkAvailable = false
monitor.networkStateListener?.onNetworkLost()
NetworkStateMonitor.log(monitor.tag, "网络连接丢失")
} else {
NetworkStateMonitor.log(monitor.tag, "网络能力变化,是否有网络: \(hasInternet)")
}
}
if SCNetworkReachabilitySetCallback(reachability, callback, &context),
SCNetworkReachabilitySetDispatchQueue(reachability, queue) {
// 触发一次初始查询
var flags = SCNetworkReachabilityFlags()
if SCNetworkReachabilityGetFlags(reachability, &flags) {
self.isNetworkAvailable = Self.isReachable(flags: flags)
}
} else {
Self.log(tag, "初始化网络监听失败: 设置回调/队列失败")
SCNetworkReachabilitySetCallback(reachability, nil, nil)
SCNetworkReachabilitySetDispatchQueue(reachability, nil)
self.reachability = nil
}
}
}
static func isReachable(flags: SCNetworkReachabilityFlags) -> Bool {
// 经典 Reachability 判定
let reachable = flags.contains(.reachable)
let requiresConnection = flags.contains(.connectionRequired)
let canConnectAutomatically = flags.contains(.connectionOnTraffic) || flags.contains(.connectionOnDemand)
let canConnectWithoutUser = canConnectAutomatically && !flags.contains(.interventionRequired)
return reachable && (!requiresConnection || canConnectWithoutUser)
}
static func log(_ tag: String, _ message: String) {
#if DEBUG
print("[\(tag)] \(message)")
#endif
}
}
Loading…
Cancel
Save