Browse Source

修改讯飞语音识别

newdev_shunjiawei
tanlongsheng 11 months ago
parent
commit
9c3f926b4a
  1. 281
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/XunFeiAsrHelper.kt

281
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/XunFeiAsrHelper.kt

@ -3,24 +3,12 @@ package com.yunqiinnovation.azure_speech
import android.content.Context import android.content.Context
import android.util.Base64 import android.util.Base64
import android.util.Log import android.util.Log
import okhttp3.OkHttpClient import okhttp3.*
import okhttp3.Request
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import okio.ByteString import okio.ByteString
import org.json.JSONObject import org.json.JSONObject
import java.io.File
import java.io.FileInputStream
import java.net.URLEncoder import java.net.URLEncoder
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.text.SimpleDateFormat import java.security.MessageDigest
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.Mac
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
@ -30,180 +18,147 @@ class XunFeiAsrHelper(private val context: Context) {
private val appId = "43d87d22" private val appId = "43d87d22"
private val accessKeyId = "2bd01ce06ac35957fc0c082f9b6284ec" 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 var webSocket: WebSocket? = null
private val isConnected = AtomicBoolean(false) private val client = OkHttpClient()
private val isSendingAudio = AtomicBoolean(false)
private var sessionId: String? = null // Status: "UNDEFINED", "CONNECTING", "OPEN", "CLOSING", "CLOSED"
private var audioFileSize: Long = 0 private var btnStatus = "UNDEFINED"
private var resultText = ""
var onResult: ((String) -> Unit)? = null
var onError: ((String) -> Unit)? = null
var onStatusChange: ((String) -> Unit)? = null
fun connectWebSocket() {
val url = getWebSocketUrl()
val request = Request.Builder().url(url).build()
private fun getAudioFileSize(audioPath: String): Long { changeBtnStatus("CONNECTING")
val file = File(audioPath)
return if (file.exists() && file.isFile) file.length() else 0L webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
Log.d(tag, "WebSocket Opened")
changeBtnStatus("OPEN")
} }
private fun getUtcTime(): String { override fun onMessage(webSocket: WebSocket, text: String) {
val sdf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US) renderResult(text)
sdf.timeZone = TimeZone.getTimeZone("GMT+8")
return sdf.format(Date())
} }
private fun calculateSignature(params: Map<String, String>, accessKeySecret: String): String { override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
val baseStr = buildString { Log.d(tag, "WebSocket Closing: $code / $reason")
var first = true changeBtnStatus("CLOSING")
for ((key, value) in params.toSortedMap()) { webSocket.close(1000, null)
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
} }
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
Log.d(tag, "WebSocket Closed: $code / $reason")
changeBtnStatus("CLOSED")
} }
val mac = Mac.getInstance("HmacSHA1")
val keySpec = SecretKeySpec(accessKeySecret.toByteArray(StandardCharsets.UTF_8), "HmacSHA1") override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
mac.init(keySpec) Log.e(tag, "WebSocket Error", t)
val signBytes = mac.doFinal(baseStr.toByteArray(StandardCharsets.UTF_8)) changeBtnStatus("CLOSED")
return Base64.encodeToString(signBytes, Base64.NO_WRAP) onError?.invoke(t.message ?: "WebSocket connection failed")
}
private fun buildParamsString(params: Map<String, String>): 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<String, String>()
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}") fun sendAudio(frameBuffer: ByteArray) {
webSocket?.let { ws ->
ws.send(ByteString.of(frameBuffer, 0, frameBuffer.size))
} }
} }
override fun onClosed(ws: WebSocket, code: Int, reason: String) { fun close() {
isConnected.set(false) webSocket?.let { ws ->
ws.send("{\"end\": true}")
changeBtnStatus("CLOSING")
}
} }
override fun onFailure(ws: WebSocket, t: Throwable, response: okhttp3.Response?) { private fun changeBtnStatus(status: String) {
isConnected.set(false) btnStatus = status
Log.e(tag, "WebSocket failure: ${t.message}") if (status == "CONNECTING") {
resultText = ""
} }
onStatusChange?.invoke(status)
} }
client.newWebSocket(request, listener)
latch.await(15, TimeUnit.SECONDS) private fun renderResult(resultData: String) {
isConnected.get() try {
} catch (e: Exception) { val jsonData = JSONObject(resultData)
Log.e(tag, "connect failed: ${e.message}") val action = jsonData.optString("action")
false
} when (action) {
} "started" -> {
Log.d(tag, "Handshake success")
fun sendAudio(audioPath: String): Boolean { }
if (!isConnected.get() || webSocket == null || isSendingAudio.get()) {
Log.d(tag, "sendAudio invalid state") "result" -> {
return false val dataStr = jsonData.optString("data")
} val data = JSONObject(dataStr)
isSendingAudio.set(true)
audioFileSize = getAudioFileSize(audioPath) val cn = data.optJSONObject("cn") ?: return
return try { val st = cn.optJSONObject("st") ?: return
FileInputStream(audioPath).use { fis -> val rt = st.optJSONArray("rt") ?: return
val buffer = ByteArray(audioFrameSize)
var frameIndex = 0 var resultTextTemp = ""
var startTime: Long? = null
while (true) { for (i in 0 until rt.length()) {
val bytesRead = fis.read(buffer) val j = rt.getJSONObject(i)
if (bytesRead == -1) break val ws = j.optJSONArray("ws") ?: continue
val frameData = if (bytesRead == audioFrameSize) buffer else buffer.copyOf(bytesRead) for (k in 0 until ws.length()) {
if (startTime == null) startTime = System.currentTimeMillis() val kObj = ws.getJSONObject(k)
val expectedSendTime = startTime!! + frameIndex * frameIntervalMs val cw = kObj.optJSONArray("cw") ?: continue
val now = System.currentTimeMillis() for (l in 0 until cw.length()) {
val diff = expectedSendTime - now val lObj = cw.getJSONObject(l)
if (diff > 1) Thread.sleep(diff) resultTextTemp += lObj.optString("w")
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) { if (st.optInt("type") == 0) {
try { resultText += resultTextTemp
if (!connect(appId, accessKeyId, accessKeySecret)) return onResult?.invoke(resultText)
if (!sendAudio(audioPath)) return } else {
val estimatedDurationSec = (((audioFileSize / audioFrameSize) * frameIntervalMs) / 1000).toInt() onResult?.invoke(resultText + resultTextTemp)
val waitTime = estimatedDurationSec + 5 }
for (i in 0..waitTime) { }
if (!isConnected.get()) break
Thread.sleep(1000) "error" -> {
Log.e(tag, "Error: $resultData")
onError?.invoke(jsonData.toString())
} }
} catch (_: InterruptedException) { }
} finally { } catch (e: Exception) {
close() Log.e(tag, "Parse error", e)
} }
} }
fun close() { private fun getWebSocketUrl(): String {
try { val url = "wss://rtasr.xfyun.cn/v1/ws"
isConnected.set(false) val ts = (System.currentTimeMillis() / 1000).toString()
webSocket?.close(1000, "normal") val baseString = appId + ts
} catch (_: Exception) { val signa = md5(baseString)
} finally { val signatureSha = hmacSha1(signa, accessKeyId)
webSocket = null val signature = Base64.encodeToString(signatureSha, Base64.NO_WRAP)
val encodedSignature = URLEncoder.encode(signature, "UTF-8")
return "$url?appid=$appId&ts=$ts&signa=$encodedSignature"
} }
private fun md5(string: String): String {
val md = MessageDigest.getInstance("MD5")
val digest = md.digest(string.toByteArray(StandardCharsets.UTF_8))
return digest.joinToString("") { "%02x".format(it) }
}
private fun hmacSha1(data: String, key: String): ByteArray {
val secretKeySpec = SecretKeySpec(key.toByteArray(StandardCharsets.UTF_8), "HmacSHA1")
val mac = Mac.getInstance("HmacSHA1")
mac.init(secretKeySpec)
return mac.doFinal(data.toByteArray(StandardCharsets.UTF_8))
} }
} }

Loading…
Cancel
Save