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.util.Base64
import android.util.Log
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import okhttp3.*
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 java.security.MessageDigest
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
@ -30,180 +18,147 @@ class XunFeiAsrHelper(private val context: Context) {
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 val client = OkHttpClient()
// Status: "UNDEFINED", "CONNECTING", "OPEN", "CLOSING", "CLOSED"
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 {
val file = File(audioPath)
return if (file.exists() && file.isFile) file.length() else 0L
changeBtnStatus("CONNECTING")
webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
Log.d(tag, "WebSocket Opened")
changeBtnStatus("OPEN")
}
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())
override fun onMessage(webSocket: WebSocket, text: String) {
renderResult(text)
}
private fun calculateSignature(params: Map<String, String>, 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
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")
}
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, 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
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
Log.e(tag, "WebSocket Error", t)
changeBtnStatus("CLOSED")
onError?.invoke(t.message ?: "WebSocket connection failed")
}
})
}
} 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) {
isConnected.set(false)
fun close() {
webSocket?.let { ws ->
ws.send("{\"end\": true}")
changeBtnStatus("CLOSING")
}
}
override fun onFailure(ws: WebSocket, t: Throwable, response: okhttp3.Response?) {
isConnected.set(false)
Log.e(tag, "WebSocket failure: ${t.message}")
private fun changeBtnStatus(status: String) {
btnStatus = status
if (status == "CONNECTING") {
resultText = ""
}
onStatusChange?.invoke(status)
}
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
private fun renderResult(resultData: String) {
try {
val jsonData = JSONObject(resultData)
val action = jsonData.optString("action")
when (action) {
"started" -> {
Log.d(tag, "Handshake success")
}
"result" -> {
val dataStr = jsonData.optString("data")
val data = JSONObject(dataStr)
val cn = data.optJSONObject("cn") ?: return
val st = cn.optJSONObject("st") ?: return
val rt = st.optJSONArray("rt") ?: return
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 (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)
if (st.optInt("type") == 0) {
resultText += resultTextTemp
onResult?.invoke(resultText)
} else {
onResult?.invoke(resultText + resultTextTemp)
}
}
"error" -> {
Log.e(tag, "Error: $resultData")
onError?.invoke(jsonData.toString())
}
} catch (_: InterruptedException) {
} finally {
close()
}
} catch (e: Exception) {
Log.e(tag, "Parse error", e)
}
}
fun close() {
try {
isConnected.set(false)
webSocket?.close(1000, "normal")
} catch (_: Exception) {
} finally {
webSocket = null
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