Browse Source

修改讯飞语音识别

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

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

@ -3,207 +3,162 @@ 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
class XunFeiAsrHelper(private val context: Context) { class XunFeiAsrHelper(private val context: Context) {
private val tag = "XunFeiAsrHelper" 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 { private val appId = "43d87d22"
val sdf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US) private val accessKeyId = "2bd01ce06ac35957fc0c082f9b6284ec"
sdf.timeZone = TimeZone.getTimeZone("GMT+8")
return sdf.format(Date())
}
private fun calculateSignature(params: Map<String, String>, accessKeySecret: String): String { private var webSocket: WebSocket? = null
val baseStr = buildString { private val client = OkHttpClient()
var first = true
for ((key, value) in params.toSortedMap()) { // Status: "UNDEFINED", "CONNECTING", "OPEN", "CLOSING", "CLOSED"
if (key == "signature") continue private var btnStatus = "UNDEFINED"
if (value.isBlank()) continue
if (!first) append("&") private var resultText = ""
append(URLEncoder.encode(key, StandardCharsets.UTF_8.name()))
append("=") var onResult: ((String) -> Unit)? = null
append(URLEncoder.encode(value, StandardCharsets.UTF_8.name())) var onError: ((String) -> Unit)? = null
first = false var onStatusChange: ((String) -> Unit)? = null
}
} fun connectWebSocket() {
val mac = Mac.getInstance("HmacSHA1") val url = getWebSocketUrl()
val keySpec = SecretKeySpec(accessKeySecret.toByteArray(StandardCharsets.UTF_8), "HmacSHA1") val request = Request.Builder().url(url).build()
mac.init(keySpec)
val signBytes = mac.doFinal(baseStr.toByteArray(StandardCharsets.UTF_8)) changeBtnStatus("CONNECTING")
return Base64.encodeToString(signBytes, Base64.NO_WRAP)
webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
Log.d(tag, "WebSocket Opened")
changeBtnStatus("OPEN")
}
override fun onMessage(webSocket: WebSocket, text: String) {
renderResult(text)
}
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
Log.d(tag, "WebSocket Closing: $code / $reason")
changeBtnStatus("CLOSING")
webSocket.close(1000, null)
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
Log.d(tag, "WebSocket Closed: $code / $reason")
changeBtnStatus("CLOSED")
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
Log.e(tag, "WebSocket Error", t)
changeBtnStatus("CLOSED")
onError?.invoke(t.message ?: "WebSocket connection failed")
}
})
}
fun sendAudio(frameBuffer: ByteArray) {
webSocket?.let { ws ->
ws.send(ByteString.of(frameBuffer, 0, frameBuffer.size))
} }
}
private fun buildParamsString(params: Map<String, String>): String { fun close() {
val sb = StringBuilder() webSocket?.let { ws ->
var first = true ws.send("{\"end\": true}")
for ((key, value) in params) { changeBtnStatus("CLOSING")
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 { private fun changeBtnStatus(status: String) {
return try { btnStatus = status
val params = mutableMapOf<String, String>() if (status == "CONNECTING") {
params["audio_encode"] = audioEncode resultText = ""
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
}
} }
onStatusChange?.invoke(status)
}
fun sendAudio(audioPath: String): Boolean { private fun renderResult(resultData: String) {
if (!isConnected.get() || webSocket == null || isSendingAudio.get()) { try {
Log.d(tag, "sendAudio invalid state") val jsonData = JSONObject(resultData)
return false val action = jsonData.optString("action")
}
isSendingAudio.set(true) when (action) {
audioFileSize = getAudioFileSize(audioPath) "started" -> {
return try { Log.d(tag, "Handshake success")
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) { "result" -> {
try { val dataStr = jsonData.optString("data")
if (!connect(appId, accessKeyId, accessKeySecret)) return val data = JSONObject(dataStr)
if (!sendAudio(audioPath)) return
val estimatedDurationSec = (((audioFileSize / audioFrameSize) * frameIntervalMs) / 1000).toInt() val cn = data.optJSONObject("cn") ?: return
val waitTime = estimatedDurationSec + 5 val st = cn.optJSONObject("st") ?: return
for (i in 0..waitTime) { val rt = st.optJSONArray("rt") ?: return
if (!isConnected.get()) break
Thread.sleep(1000) var resultTextTemp = ""
for (i in 0 until rt.length()) {
val j = rt.getJSONObject(i)
val ws = j.optJSONArray("ws") ?: continue
for (k in 0 until ws.length()) {
val kObj = ws.getJSONObject(k)
val cw = kObj.optJSONArray("cw") ?: continue
for (l in 0 until cw.length()) {
val lObj = cw.getJSONObject(l)
resultTextTemp += lObj.optString("w")
}
} }
} catch (_: InterruptedException) { }
} finally {
close() if (st.optInt("type") == 0) {
resultText += resultTextTemp
onResult?.invoke(resultText)
} else {
onResult?.invoke(resultText + resultTextTemp)
}
} }
}
fun close() { "error" -> {
try { Log.e(tag, "Error: $resultData")
isConnected.set(false) onError?.invoke(jsonData.toString())
webSocket?.close(1000, "normal")
} catch (_: Exception) {
} finally {
webSocket = null
} }
}
} catch (e: Exception) {
Log.e(tag, "Parse error", e)
} }
}
private fun getWebSocketUrl(): String {
val url = "wss://rtasr.xfyun.cn/v1/ws"
val ts = (System.currentTimeMillis() / 1000).toString()
val baseString = appId + ts
val signa = md5(baseString)
val signatureSha = hmacSha1(signa, accessKeyId)
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