From bf3a9ebc2d5b26939e341965f12a3937fd23257b Mon Sep 17 00:00:00 2001 From: tanlongsheng <252620078@qq.com> Date: Fri, 5 Dec 2025 19:57:26 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=AE=AF=E9=A3=9E=E8=AF=AD?= =?UTF-8?q?=E9=9F=B3=E8=AF=86=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../azure_speech/XunFeiAsrHelper.kt | 209 ++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/XunFeiAsrHelper.kt diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/XunFeiAsrHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/XunFeiAsrHelper.kt new file mode 100644 index 000000000..5cae21a89 --- /dev/null +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/XunFeiAsrHelper.kt @@ -0,0 +1,209 @@ +package com.yunqiinnovation.azure_speech + +import android.content.Context +import android.util.Base64 +import android.util.Log +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.WebSocket +import okhttp3.WebSocketListener +import okio.ByteString +import org.json.JSONObject +import java.io.File +import java.io.FileInputStream +import java.net.URLEncoder +import java.nio.charset.StandardCharsets +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.TimeZone +import java.util.UUID +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec + +class XunFeiAsrHelper(private val context: Context) { + private val tag = "XunFeiAsrHelper" + + private val appId = "43d87d22" + private val accessKeyId = "2bd01ce06ac35957fc0c082f9b6284ec" + + private val audioEncode = "pcm_s16le" + private val lang = "autodialect" + private val sampleRate = "16000" + private val audioFrameSize = 1280 + private val frameIntervalMs = 40 + private val baseWsUrl = "wss://office-api-ast-dx.iflyaisol.com/ast/communicate/v1" + private val client = OkHttpClient.Builder().retryOnConnectionFailure(true).build() + + private var webSocket: WebSocket? = null + private val isConnected = AtomicBoolean(false) + private val isSendingAudio = AtomicBoolean(false) + private var sessionId: String? = null + private var audioFileSize: Long = 0 + + private fun getAudioFileSize(audioPath: String): Long { + val file = File(audioPath) + return if (file.exists() && file.isFile) file.length() else 0L + } + + private fun getUtcTime(): String { + val sdf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US) + sdf.timeZone = TimeZone.getTimeZone("GMT+8") + return sdf.format(Date()) + } + + private fun calculateSignature(params: Map, accessKeySecret: String): String { + val baseStr = buildString { + var first = true + for ((key, value) in params.toSortedMap()) { + if (key == "signature") continue + if (value.isBlank()) continue + if (!first) append("&") + append(URLEncoder.encode(key, StandardCharsets.UTF_8.name())) + append("=") + append(URLEncoder.encode(value, StandardCharsets.UTF_8.name())) + first = false + } + } + val mac = Mac.getInstance("HmacSHA1") + val keySpec = SecretKeySpec(accessKeySecret.toByteArray(StandardCharsets.UTF_8), "HmacSHA1") + mac.init(keySpec) + val signBytes = mac.doFinal(baseStr.toByteArray(StandardCharsets.UTF_8)) + return Base64.encodeToString(signBytes, Base64.NO_WRAP) + } + + private fun buildParamsString(params: Map): String { + val sb = StringBuilder() + var first = true + for ((key, value) in params) { + if (!first) sb.append("&") + sb.append(URLEncoder.encode(key, StandardCharsets.UTF_8.name())) + sb.append("=") + sb.append(URLEncoder.encode(value, StandardCharsets.UTF_8.name())) + first = false + } + return sb.toString() + } + + fun connect(appId: String, accessKeyId: String, accessKeySecret: String): Boolean { + return try { + val params = mutableMapOf() + params["audio_encode"] = audioEncode + params["lang"] = lang + params["samplerate"] = sampleRate + params["accessKeyId"] = accessKeyId + params["appId"] = appId + params["uuid"] = UUID.randomUUID().toString().replace("-", "") + params["utc"] = getUtcTime() + params["signature"] = calculateSignature(params, accessKeySecret) + val fullUrl = "$baseWsUrl?${buildParamsString(params)}" + + val request = Request.Builder().url(fullUrl).build() + val latch = CountDownLatch(1) + val listener = object : WebSocketListener() { + override fun onOpen(ws: WebSocket, response: okhttp3.Response) { + webSocket = ws + isConnected.set(true) + latch.countDown() + } + + override fun onMessage(ws: WebSocket, text: String) { + try { + val json = JSONObject(text) + if (json.optString("msg_type") == "action") { + val data = json.optJSONObject("data") + val sid = data?.optString("sessionId") + if (!sid.isNullOrEmpty()) { + sessionId = sid + } + } + } catch (e: Exception) { + Log.d(tag, "onMessage parse error: ${e.message}") + } + } + + override fun onClosed(ws: WebSocket, code: Int, reason: String) { + isConnected.set(false) + } + + override fun onFailure(ws: WebSocket, t: Throwable, response: okhttp3.Response?) { + isConnected.set(false) + Log.e(tag, "WebSocket failure: ${t.message}") + } + } + client.newWebSocket(request, listener) + latch.await(15, TimeUnit.SECONDS) + isConnected.get() + } catch (e: Exception) { + Log.e(tag, "connect failed: ${e.message}") + false + } + } + + fun sendAudio(audioPath: String): Boolean { + if (!isConnected.get() || webSocket == null || isSendingAudio.get()) { + Log.d(tag, "sendAudio invalid state") + return false + } + isSendingAudio.set(true) + audioFileSize = getAudioFileSize(audioPath) + return try { + FileInputStream(audioPath).use { fis -> + val buffer = ByteArray(audioFrameSize) + var frameIndex = 0 + var startTime: Long? = null + while (true) { + val bytesRead = fis.read(buffer) + if (bytesRead == -1) break + val frameData = if (bytesRead == audioFrameSize) buffer else buffer.copyOf(bytesRead) + if (startTime == null) startTime = System.currentTimeMillis() + val expectedSendTime = startTime!! + frameIndex * frameIntervalMs + val now = System.currentTimeMillis() + val diff = expectedSendTime - now + if (diff > 1) Thread.sleep(diff) + webSocket?.send(ByteString.of(frameData, 0, frameData.size)) + frameIndex++ + } + val endMsg = JSONObject() + endMsg.put("end", true) + sessionId?.let { endMsg.put("sessionId", it) } + webSocket?.send(endMsg.toString()) + true + } + } catch (e: Exception) { + Log.e(tag, "sendAudio error: ${e.message}") + false + } finally { + isSendingAudio.set(false) + } + } + + fun runDemo(appId: String, accessKeyId: String, accessKeySecret: String, audioPath: String) { + try { + if (!connect(appId, accessKeyId, accessKeySecret)) return + if (!sendAudio(audioPath)) return + val estimatedDurationSec = (((audioFileSize / audioFrameSize) * frameIntervalMs) / 1000).toInt() + val waitTime = estimatedDurationSec + 5 + for (i in 0..waitTime) { + if (!isConnected.get()) break + Thread.sleep(1000) + } + } catch (_: InterruptedException) { + } finally { + close() + } + } + + fun close() { + try { + isConnected.set(false) + webSocket?.close(1000, "normal") + } catch (_: Exception) { + } finally { + webSocket = null + } + } +}