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.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
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 val tag = "XunFeiAsrHelper"
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 val appId = "43d87d22"
private val accessKeyId = "2bd01ce06ac35957fc0c082f9b6284ec"
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
}
}
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 var webSocket: WebSocket? = null
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()
changeBtnStatus("CONNECTING")
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 {
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 close() {
webSocket?.let { ws ->
ws.send("{\"end\": true}")
changeBtnStatus("CLOSING")
}
}
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}")
}
}
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
}
private fun changeBtnStatus(status: String) {
btnStatus = status
if (status == "CONNECTING") {
resultText = ""
}
onStatusChange?.invoke(status)
}
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)
private fun renderResult(resultData: String) {
try {
val jsonData = JSONObject(resultData)
val action = jsonData.optString("action")
when (action) {
"started" -> {
Log.d(tag, "Handshake success")
}
}
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)
"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 (_: InterruptedException) {
} finally {
close()
}
if (st.optInt("type") == 0) {
resultText += resultTextTemp
onResult?.invoke(resultText)
} else {
onResult?.invoke(resultText + resultTextTemp)
}
}
}
fun close() {
try {
isConnected.set(false)
webSocket?.close(1000, "normal")
} catch (_: Exception) {
} finally {
webSocket = null
"error" -> {
Log.e(tag, "Error: $resultData")
onError?.invoke(jsonData.toString())
}
}
} 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