first commit
This commit is contained in:
277
apps/miniprogram/utils/asrGateway.js
Normal file
277
apps/miniprogram/utils/asrGateway.js
Normal file
@@ -0,0 +1,277 @@
|
||||
/**
|
||||
* 小程序语音网关客户端:
|
||||
* 1. 对齐 Web 端 /ws/asr 协议(ready/result/round_end/error);
|
||||
* 2. 控制帧走 JSON,音频分片走二进制帧。
|
||||
*/
|
||||
const READY_TIMEOUT_MS = 7000;
|
||||
const CLOSE_TIMEOUT_MS = 2600;
|
||||
|
||||
function buildAsrWsUrl(rawGatewayUrl, token) {
|
||||
const input = String(rawGatewayUrl || "").trim();
|
||||
if (!input) {
|
||||
throw new Error("网关地址为空");
|
||||
}
|
||||
let base = input;
|
||||
if (base.startsWith("http://")) base = `ws://${base.slice(7)}`;
|
||||
if (base.startsWith("https://")) base = `wss://${base.slice(8)}`;
|
||||
if (!base.startsWith("ws://") && !base.startsWith("wss://")) {
|
||||
base = `wss://${base}`;
|
||||
}
|
||||
base = base.replace(/\/+$/, "");
|
||||
const safeToken = encodeURIComponent(String(token || ""));
|
||||
return `${base}/ws/asr?token=${safeToken}`;
|
||||
}
|
||||
|
||||
function createAsrGatewayClient(options) {
|
||||
const config = options || {};
|
||||
const endpoint = buildAsrWsUrl(config.gatewayUrl, config.gatewayToken);
|
||||
let socketTask = null;
|
||||
let ready = false;
|
||||
let closed = false;
|
||||
let stopCloseTimer = null;
|
||||
let readyTimer = null;
|
||||
let readyResolve = null;
|
||||
let readyReject = null;
|
||||
|
||||
function clearReadyTimer() {
|
||||
if (readyTimer) {
|
||||
clearTimeout(readyTimer);
|
||||
readyTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function clearStopCloseTimer() {
|
||||
if (stopCloseTimer) {
|
||||
clearTimeout(stopCloseTimer);
|
||||
stopCloseTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function rejectPendingReady(error) {
|
||||
if (typeof readyReject === "function") {
|
||||
readyReject(error);
|
||||
}
|
||||
readyResolve = null;
|
||||
readyReject = null;
|
||||
}
|
||||
|
||||
function resolvePendingReady() {
|
||||
if (typeof readyResolve === "function") {
|
||||
readyResolve();
|
||||
}
|
||||
readyResolve = null;
|
||||
readyReject = null;
|
||||
}
|
||||
|
||||
function sendJson(frame) {
|
||||
if (!socketTask || closed) return;
|
||||
socketTask.send({ data: JSON.stringify(frame) });
|
||||
}
|
||||
|
||||
function connect() {
|
||||
if (socketTask && !closed) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
closed = false;
|
||||
ready = false;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
readyResolve = resolve;
|
||||
readyReject = reject;
|
||||
socketTask = wx.connectSocket({ url: endpoint, timeout: READY_TIMEOUT_MS });
|
||||
clearReadyTimer();
|
||||
readyTimer = setTimeout(() => {
|
||||
if (ready) return;
|
||||
close("ready_timeout");
|
||||
rejectPendingReady(new Error("语音网关连接超时"));
|
||||
}, READY_TIMEOUT_MS);
|
||||
|
||||
socketTask.onOpen(() => {
|
||||
if (typeof config.onOpen === "function") {
|
||||
config.onOpen();
|
||||
}
|
||||
});
|
||||
|
||||
socketTask.onMessage((event) => {
|
||||
const raw = event && event.data;
|
||||
if (typeof raw !== "string") {
|
||||
return;
|
||||
}
|
||||
let frame = null;
|
||||
try {
|
||||
frame = JSON.parse(raw);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
if (frame.type === "ready") {
|
||||
ready = true;
|
||||
clearReadyTimer();
|
||||
resolvePendingReady();
|
||||
if (typeof config.onReady === "function") {
|
||||
config.onReady(frame.payload || {});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (frame.type === "result") {
|
||||
if (typeof config.onResult === "function") {
|
||||
config.onResult(frame.payload || {});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (frame.type === "round_end") {
|
||||
clearStopCloseTimer();
|
||||
if (typeof config.onRoundEnd === "function") {
|
||||
config.onRoundEnd(frame.payload || {});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (frame.type === "pong") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (frame.type === "error") {
|
||||
const message = String((frame.payload && frame.payload.message) || "语音网关异常");
|
||||
const error = new Error(message);
|
||||
if (!ready) {
|
||||
clearReadyTimer();
|
||||
rejectPendingReady(error);
|
||||
}
|
||||
if (typeof config.onError === "function") {
|
||||
config.onError(error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
socketTask.onError((event) => {
|
||||
const detail = event && event.errMsg ? String(event.errMsg) : "";
|
||||
const error = new Error(detail ? `语音网关连接失败: ${detail}` : "语音网关连接失败");
|
||||
if (!ready) {
|
||||
clearReadyTimer();
|
||||
rejectPendingReady(error);
|
||||
}
|
||||
if (typeof config.onError === "function") {
|
||||
config.onError(error);
|
||||
}
|
||||
});
|
||||
|
||||
socketTask.onClose((event) => {
|
||||
const wasReady = ready;
|
||||
ready = false;
|
||||
closed = true;
|
||||
clearReadyTimer();
|
||||
clearStopCloseTimer();
|
||||
socketTask = null;
|
||||
if (!wasReady) {
|
||||
const code = event && typeof event.code === "number" ? event.code : "";
|
||||
const reason = event && event.reason ? String(event.reason) : "";
|
||||
rejectPendingReady(
|
||||
new Error(`语音连接已关闭${code ? `(${code})` : ""}${reason ? `: ${reason}` : ""}`)
|
||||
);
|
||||
}
|
||||
if (typeof config.onClose === "function") {
|
||||
config.onClose(event || {});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function startRound(payload) {
|
||||
if (!ready) {
|
||||
throw new Error("语音网关未就绪");
|
||||
}
|
||||
sendJson({
|
||||
type: "start",
|
||||
payload:
|
||||
payload && typeof payload === "object"
|
||||
? payload
|
||||
: {
|
||||
audio: {
|
||||
format: "pcm",
|
||||
codec: "raw",
|
||||
rate: 16000,
|
||||
bits: 16,
|
||||
channel: 1
|
||||
},
|
||||
request: {
|
||||
model_name: "bigmodel",
|
||||
enable_itn: true,
|
||||
enable_punc: true,
|
||||
result_type: "full"
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function sendAudio(buffer) {
|
||||
if (!ready || !socketTask || closed) {
|
||||
return;
|
||||
}
|
||||
if (!(buffer instanceof ArrayBuffer) || buffer.byteLength <= 0) {
|
||||
return;
|
||||
}
|
||||
socketTask.send({ data: buffer });
|
||||
}
|
||||
|
||||
function stopRound() {
|
||||
if (!ready) return;
|
||||
sendJson({ type: "stop" });
|
||||
clearStopCloseTimer();
|
||||
stopCloseTimer = setTimeout(() => {
|
||||
close("round_timeout");
|
||||
}, CLOSE_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
function cancelRound() {
|
||||
if (!ready) {
|
||||
close("cancel_before_ready");
|
||||
return;
|
||||
}
|
||||
sendJson({ type: "cancel" });
|
||||
close("cancel");
|
||||
}
|
||||
|
||||
function ping() {
|
||||
if (!ready) return;
|
||||
sendJson({ type: "ping" });
|
||||
}
|
||||
|
||||
function close(reason) {
|
||||
clearReadyTimer();
|
||||
clearStopCloseTimer();
|
||||
closed = true;
|
||||
ready = false;
|
||||
if (!socketTask) return;
|
||||
try {
|
||||
socketTask.close({ code: 1000, reason: reason || "client_close" });
|
||||
} catch (error) {
|
||||
console.warn("[asrGateway.close]", error);
|
||||
}
|
||||
socketTask = null;
|
||||
}
|
||||
|
||||
return {
|
||||
connect,
|
||||
startRound,
|
||||
sendAudio,
|
||||
stopRound,
|
||||
cancelRound,
|
||||
ping,
|
||||
close,
|
||||
isReady() {
|
||||
return ready;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
READY_TIMEOUT_MS,
|
||||
CLOSE_TIMEOUT_MS,
|
||||
buildAsrWsUrl,
|
||||
createAsrGatewayClient
|
||||
};
|
||||
Reference in New Issue
Block a user